月度归档:2015年05月

Borg

4月份Google发了一篇介绍大规模集群管理系统Borg的论文『Large-scale cluster management at Google with Borg』,因为我近期也在做相关方面的工程,所以就在第一时间读了一遍,对Borg的架构及其特性有了一个大概的了解。

论文大致可分为7个部分:

1. Introduction(User perspective):用户视角的Borg介绍
2. Borg architecture: Borg整体架构
3. Scalability:Borg系统可扩展性
4. Availability:Borg系统本身的高可用性和用户服务的高可用性
5. Utilization:资源利用率
6. Isolation:隔离性
7. Lessons:经验教训

1. Introduction(User perspective)

Borg是什么?

Borg是一个集群管理工具,可以管理、调度、启动、重启、监控应用程序。可以想象成它是一个集群操作系统,产品或服务无需关心硬件层的差异,只需声明所需的资源类型和配额即可。Borg有以下特性:

1) 物理资源利用率高。
2) 服务器共享,在进程级别做隔离。
3) 应用高可用,故障恢复时间短。
4) 调度策略灵活。
5) 应用接入和使用方便。提供了完备的Job描述语言,服务发现,实时状态监控和诊断工具。

对应用来说,它能带来的几点好处:

1) 对外隐藏底层资源管理和调度、故障处理等。
2) 实现应用的高可靠和高可用。
3) 足够弹性,支持应用跑在成千上万的机器上。

Borg定义了几个概念:

1) Workload

Borg将运行其上的服务(workload)分为两类:
* prod:在线任务,长期运行、对延时敏感、面向终端用户等,比如Gmail,Google Docs, Web Search服务等。
* non-prod:离线任务,也称为批处理任务(batch),比如一些分布式计算服务等。

2) Cell

Cell是一个逻辑上的概念,它是一系列服务器的集合。一个Cell上跑一个集群管理系统Borg。其实在Cell的概念之上还有一个Cluster的概念,一个Cluster可以有一个或多个Cell(规模、用途各异),并且归属于一个IDC。Cell的规模有大有小,一个中等规模的Cell大约含有1w台服务器。一般来说,Cell中的服务器都是异构的,它们可能有着不同的规格(CPU, RAM, disk, network)、处理器类型,有着性能和容量上的差异等。

通过定义Cell可以让Borg对服务器资源进行统一抽象,作为用户就无需知道自己的应用跑在哪台机器上,也不用关心资源分配、程序安装、依赖管理、健康检查及故障恢复等。

3) Job和Task

用户以Job的形式提交应用部署请求。一个Job包含一个或者多个相同的Task,每个Task运行相同的一份应用程序,Task数量就是应用的副本数。Job也是逻辑上的概念,Task才是Borg系统的可调度单元。每个Task可以对应到Linux上的一组进程,这组进程运行在LXC容器里,Google解释了采用容器技术的原因:1. 硬件虚拟化成本太高。2. Borg开始研发那会儿,虚拟化技术并不成熟。

每个Job可以定义一些属性、元信息和优先级,优先级涉及到抢占式调度过程。Borg对Job的定义了如下4个优先级:monitoring, production, batch, and best effort(test),其中monitoring和production属于prod workload,batch和test属于non-prod workload。高优先级可以抢占低优先级,相同优先级的Job之间不能互相抢占,另外,prod workload的Job不能被抢占,也就是说,能被抢占的只有低优先级的batch和test任务。

以下是Job和Task的生命周期状态图:
Job的生命周期

从状态图中可见,无论是pending状态还是running状态的任务,Borg都支持它们动态更新,实时生效,这依赖于描述Job和Task所采用的BCL语言(GCL的一个变种)特性,具体原理和实现不明。

4) Naming

Borg的服务发现通过BNS(Borg name service)来实现。Task调度和部署完毕后,Borg把Task的主机名和端口号写入Chubby(解决分布式环境下一致性问题的文件系统),服务调用方通过RPC系统去自动发现服务。举例说明一个Task在服务发现配置中心的命名规则:
50.jfoo.ubar.cc.borg.google.com可表示在一个名为cc的Cell中由用户ubar部署的一个名为jfoo的Job下的第50个Task。

有了服务发现,即使Task下线、被抢占、故障恢复等情况出现,整个服务仍然可用的,当然服务前提是多副本的。

2. Borg architecture

Borg的整体架构分成两部分:Borgmaster和Borglet。整体架构如下:
Borg架构图

1. Borgmaster:Borgmaster是整个Borg系统逻辑上的中心节点,它又分成两个部分,Borgmaster主进程和scheduler调度进程。职责划分如下:

1) Borgmaster主进程:
* 处理客户端RPC请求,比如创建Job,查询Job等。
* 维护系统组件和服务的状态,比如服务器、Task等。
* 负责与Borglet通信。

2) scheduler进程:

用户提交Job后,Borgmaster主进程将Job信息持久化到Paxos存储池中,并且将其Task丢到一个Pending队列里。由scheduler进程定期轮询Pending队列,对Task进行调度执行。调度策略为:从高到低,相同优先级采用round-robin策略。

调度过程分为两阶段:

阶段一: feasibility checking,初步的调度可行性分析,确定一批符合调度要求的服务器。
阶段二: scoring,根据一些算法对阶段一中筛选出的服务器进行打分,选出最适合调度的一台机器。scoring过程可考虑的因素较多,比如可以考虑怎么调度才能使被抢占的低优先级任务数量最少,考虑目标服务器已经存在目标任务的包,考虑目标任务的跨域(power and failure domain)部署,考虑在目标机器上进行高低优先级任务的混合部署。

Borg前后经历了三种scoring模型:
1) E-PVM: 对多维度的资源类型降维,最终得到一个一维的数值。这是一种"worst fit"模型,各服务器负载尽量平均,好处是能够容忍应用的峰值,但是会增加资源碎片,导致大型应用找不到合适服务器部署。
2) "best fit": 与第一类“worst fit”模型相反,它会尽量利用服务器资源,优先将已分配服务器资源跑满。这种模型的好处是不会产生很多资源碎片,坏处是要求应用申请时的配额一定要准确,否则可能被kill掉。另外这种模型对存在峰值的应用类型容忍能力有限。
3) hybrid: 它综合了"worst fit"和"best fit",Borg目前采用的是这种模型,它的优势在于避免产生无法利用的资源碎片。所谓资源碎片,是指服务器某类资源耗尽,但是其他类型资源充足。比如说内存已经分配完,但是CPU使用率仍旧很低,那么这部分CPU资源是无法被利用的,就产生了CPU碎片。

可见从理论上看Borg的调度算法很完善,但是过于完善却不利于实际应用,因为每次调度都会涉及大量计算。Google从实用型角度做了一些优化:

1) Score caching: 对一次完整计算过后结果进行缓存,当服务器或者任务的状态未发生变更或者变更很少时,直接采用缓存数据,避免重复计算。
2) Equivalence classes: 因为一个Job下的所有Task都是等价的,所以可以采用等价类的思路,无需为Job中每个Task进行feasibility checking和scoring,调度多个相同的Task只需计算一次。
3) Relaxed randomization: 引入一些随机性,即每次随机选择一些机器,只要符合需求的服务器数量达到一定值时,就可以停止计算,无需每次对Cell中所有服务器进行feasibility checking。

经过上述3点优化,实验结果表明:一次未经优化的调度过程需要3天,优化后只需几百秒!另外,在实际应用中,一个Job从用户提交,到最后得到调度和执行,平均消耗时间是25s,包安装占了其中的80%时间。

2. Borglet

Borglet是部署在所有服务器上的Agent,负责接收Borgmaster进程的指令。

3. Scalability

Google在Scalability部分低调地炫耀了一把,他们在论文中宣称,目前还不确定关于中心化架构的Borg系统的极限在哪里。关于“炫耀”,在论文中多处可见,比如作者多次提到Borg已稳定运行10多年,比如作者在介绍架构设计的时候提到虽然Borg不是业界第一个集群管理系统的解决方案,但在量级上却无人出其左右。目前,一个Borg系统能处理的服务器数量级在万台这个级别,每分钟处理的Task数量超过10000,部署Borgmaster的机器规格为10-14核、50GB内存。

扩展性从三个方面的设计上说明:

1). Borgmaster主进程有5个副本,一主四从。每个副本都在内存中维护一份整个集群的状态,另外,集群状态也会被持久化到一个分布式的高可用Paxos存储系统上。在Cell初始化或者主节点挂掉的情况下,存活的副本会进行Paxos选举,选举新的主节点。主节点会在Chubby(提供了分布式的锁服务)获得一把锁来标记自己的主节点身份。所有能涉及改变集群状态的操作都由主节点完成,比如用户提交Job、结束Task执行或者机器下线之类的操作,主节点可读可写,从节点只读。主节点选举和failover的过程大概会持续10秒,在一些大规格Cell中可能会需要1分钟。

2). scheduler进程也是多副本运行的设计,一主多从。副本个数没有说明,应该没有限制。scheduler进程做的事情是状态无关的,从节点定期从主节点上获取当前集群的状态变更部分(含已分配和待调度任务之类的信息),合并到本地的集群状态副本中,然后根据已有信息对任务进行调度计算,计算完毕将调度结果通知主节点,由主节点来决定是否进行调度。比如主节点发现scheduler副本本地缓存的集群状态已经过时,那么计算出来的调度结果也是无效的,那么就不会采纳。也就是说,scheduler从节点只计算,不进行实际调度。

3). Borgmaster采用轮询的方式定期从Borglet获取该节点的状态。之所以采用"拉"模型,而不是Borglet的“推”模型,Google的考虑是:在Borg管理的集群规模上,采用“推”模型将很难进行流量控制,但是“拉”模型可以通过动态改变轮询周期来进行控制,另外,在“推”模型中,当Borgmaster从故障中恢复时,会瞬间产生流量风暴。其实在我看来,采用“推”还是“拉”模型,可能更多的是由Borgmaster本身是否带状态来决定的。Borg采用的是带状态的master设计(区分主从),因此如果Borglet采用了“推”模型,当产生上述的流量风暴时便不可控,如果Borgmaster是无状态的,那么通过一些负载均衡技术就很容易化解所谓的recovery storms,便无需考虑流量控制。此外,每个Borgmaster从节点都有自己负责轮询和通信的固定数量的Borglet,这是通过进程内部的link shard模块实现的,Borglet每次都会上报自己的全量数据,Borgmaster从节点收到全量数据后,与自己本地缓存的集群状态进行比对,如果发现该Borglet有状态变化,则把状态变化的增量部分汇报给Borgmaster主节点,由其进行最终的集群状态变更操作,经过从节点对数据的预处理后,主节点能少处理很多信息。

4. Availability

高可用性设计分为两个方面:

1) 应用高可用

* 被抢占的non-prod任务放回pending queue,等待重新调度。
* 多副本应用跨故障域部署。所谓故障域有大有小,比如相同机器、相同机架或相同电源插座等,一挂全挂。
* 对于类似服务器或操作系统升级的维护操作,避免大量服务器同时进行。
* 支持幂等性,支持客户端重复操作。
* 当服务器状态变为不可用时,要控制重新调度任务的速率。因为Borg无法区分是节点故障还是出现了短暂的网络分区,如果是后者,静静地等待网络恢复更利于保障服务可用性。
* 当某种"任务@服务器"的组合出现故障时,下次重新调度时需避免这种组合再次出现,因为极大可能会再次出现相同故障。
* 记录详细的内部信息,便于故障排查和分析。

保障应用高可用的关键性设计原则是:无论何种原因,即使Borgmaster或者Borglet挂掉、失联,都不能杀掉正在运行的服务(Task)。

2) Borg系统高可用

实践中,Borgmaster的可用性达到了4个9(99.99%)。

* Borgmaster组件多副本设计。
* 采用一些简单的和底层(low-level)的工具来部署Borg系统实例,避免引入过多的外部依赖。
* 每个Cell的Borg均独立部署,避免不同Borg系统相互影响。

5. Utilization

解决资源利用率问题是类似Borg这样的集群管理系统存在的核心价值,从已有资料来看,只有Borg完全做到了,并且能给出数据佐证。百度的Matrix也号称做到了,具体成效待实践证明。要提升资源利用率,“混部”是一个很好的解决思路,Borg和Matrix也是采用这样的思路,通过将在线任务(prod)和离线任务(non-prod, batch)混合部署,空闲时,离线任务可以充分利用计算资源,繁忙时,在线任务通过抢占的方式保证优先得到执行,合理地利用资源。这背后需要一个很完备的调度算法做支撑,遗憾的时,Borg论文中并没有过多涉及相关的技术,只能从一些数据中了解Borg带来的收益:

* 98%的服务器实现了混部。
* 90%的服务器中跑了超过25个Task和4500个线程。
* 在一个中等规模的Cell里,在线任务和离线任务独立部署比混合部署所需的服务器数量多出约20%-30%。可以简单算一笔账,Google的服务器数量在千万级别,按20%算也是百万级别,大概能省下的服务器采购费用就是百亿级别了,这还不包括省下的机房等基础设施和电费等费用。

6. Isolation

隔离性从两方面讨论:

1. 安全性隔离:采用chroot jail实现。Borg时代还没有namespace技术,chroot可以认为是一个简略版的mnt namespace。
2. 性能隔离:采用基于cgroup的容器技术实现。前文已经提到,Borg将任务类型分成在线和离线两种,在线任务(prod)是延时敏感(latency-sensitive)型的,优先级高,而离线任务(non-prod,batch)优先级低,Borg通过不同优先级之间的抢占式调度来优先保障在线任务的性能,牺牲离线任务。另一方面,Borg将资源类型也分成两类,可压榨的(compressible)和不可压榨的(non-compressible)。compressible类型的资源比如CPU、磁盘IO带宽,当这类资源成为瓶颈时,Borg不会Kill掉相应的任务。non-compressible类型的资源比如内存、磁盘空间,当这类资源成为瓶颈时,Borg会Kill掉相应的任务。

7. Lessons

Google在论文中总结了Borg系统设计的优缺点,并在它的开源版本Kubernetes(k8s)中进行了传承和改善。

关于一些不好的设计:
* Job和Task之间的关系过于耦合,不能灵活指定Task。k8s中引入label机制,可以通过label指定任何Task(k8s中称为Pod)集合。
* 部署在同一台服务器上的Task共享一个IP,通过端口号区分彼此。这使得所有Task共享服务器上的端口空间,使得端口本身也成为一种竞争资源,成为调度的一个考虑因素。k8s中每个Pod独享IP和该IP上的整个端口空间。
* Borg系统中存在一些特权用户,使用法变得复杂。

关于一些好的设计:
* 对用户暴露内部的运行和调试日志,用户可以自行排查。
* 微服务架构设计。

虽说Google在k8s中重新设计了一些Borg中不好的地方,但那都是一些皮毛,真正核心的东西比如资源调度根本没在k8s中体现。

以上是我对Borg论文学习总结,中间夹带了一些自己的理解,如有错误,还请指正。

--EOF--

Docker集群性能数据监控与简单选型

Docker要在生产环境上使用,监控工具必不可少。传统物理机或着虚拟机通常可以通过部署一些agent收集数据,上报到中心节点进行数据聚合和存储,再通过一些UI工具进行可视化展现。与之相比,容器因为它的轻量级特性,一个容器内部通常只包含一个服务进程,通过在容器内部部署额外的性能数据采集agent非常不优雅;另一个原因,top、free、sysstat等常用性能诊断命令在容器中往往拿到的是宿主机的数据,无法准确获取容器数据。因此各种在宿主机上采集容器数据的工具(链)冒了出来。

市面上关于Docker的监控工具五花八门,但是相对成熟的却又很少。Google的cAdvisor已经是属于较成熟的一类,它会在每个宿主机节点上部署cAdvisor进程,默认对外暴露8080端口,用户可通过其提供的Web服务访问当前节点和各容器的性能数据(CPU、内存、网络、磁盘、文件系统等等),非常详细。展现方式有两种,一种是API,返回标准JSON格式数据,另一种是友好的图表方式,如下图:
cAdvisor

默认cAdvisor是将数据缓存在内存中,因此数据展示能力有限,当然它也提供不同的持久化存储后端:云端的Google BigQuery或者本地端的InfluxDB,通过-storage_driver启动参数指定。

当使用InfluxDB作为cAdvisor的存储后端时,cAdvisor会在InfluxDB中建立一个名为stats的series,所有容器性能数据都存储到这张表里,主要记录数据包括time、tx_errors、rx_bytes、container_name、rx_errors、fs_limit、memory_usage、memory_working_set、fs_device、cpu_cumulative_usage、machine、tx_bytes、fs_usage等等。

在一个大规模部署的Docker集群中,往往会选用各种集群管理和Docker编配工具,比如KubernetesFigSwarm等。网上各种Docker集群工具的选型文章也多得很,可以参考阅读。只能说,由Google在主推,并且有其闭源版本Borg在Google内部稳定运行10多年的口碑,Kubernetes(k8s)虽然出来得晚,但是社区发展得相当快,大有后来居上的势头。

那么假设选择了k8s作为Docker集群管理工具,容器性能数据监控如何选型?

k8s会在每个node(minor)节点上部署一个Kubelet,默认暴露10250端口,Kubelet主要负责容器的生命周期管理和状态维护,以及监控数据采集。实际上,Kubelet也是通过cAdvisor来采集容器性能数据的,所以需要在Kubelet的启动参数中增加--cadvisor_port参数,它表示本地的cAdvisor服务端口号。Google同时也提供了开源组件heapster,用作对Docker集群的监控,heapster原生支持k8s和CoreOS中容器的性能数据采集。当heapster配合k8s运行时,需要指定kubernetes_master的地址,heapster通过k8s得到所有node节点地址,然后通过访问对应的node ip和端口号(10250)来调用目标节点Kubelet的HTTP接口,再由Kubelet调用cAdvisor服务获取该节点上所有容器的性能数据,并依次返回到heapster进行数据聚合。heapster聚合的metric可分为以下几类:

uptime
cpu/usage
memory/usage
memory/page_faults
memory/working_set
network/rx
network/rx_errors
network/tx
network/tx_errors
filesystem/usage

同cAdvisor类似,heaspter也支持多种存储后端,比如默认的memory,表示存内存,另外还有influxdb、bigquery、gcm,可由-sink启动参数指定。如果持久化到InfluxDB,那么根据metric的分类,InfluxDB会生成以下series:

cpu/usage_ns_cumulative
filesystem/usage
memory/page_faults_gauge
memory/usage_bytes_gauge
memory/working_set_bytes_gauge
network/rx_bytes_cumulative
network/rx_errors_cumulative
network/tx_bytes_cumulative
network/tx_errors_cumulative
uptime_ms_cumulative

这里需要注意的是,metric的分类有两种,一种是类似cpu使用时间、网络流入流出量,聚合的是累计值,在serie名称中用cumulative表示,另一种是类似内存使用量,聚合的是瞬时值,在serie名称中用gauge表示。

有了InfluxDB的数据持久化存储之后,剩下的就是数据的展现功能,可以说,任何支持InfluxDB作为存储后端的UI系统(dashboard)都可以用来作为展现层,比如InfluxDB官方自带的图形界面,也可以对接Grafana,页面相当酷炫。作为一个平台管理人员,用heapster(cAdvisor) + InfluxDB + Grafana应该可以取得不错的用户体验,但是如果把k8s用在多租户的场景下,由于Grafana的可编程能力有限,对接起来不是很合适,较好地方式还是基于InfluxDB的原始数据(支持类SQL查询)自己实现UI,这样与容器集群管理平台的契合度也高一些。

--EOF--

RabbitMQ不同Confirm模式下的性能对比

前几天看到一篇文章『Evaluating persistent, replicated message queues』,作者比较客观地分析了多种分布式消息服务器间集群和消息可靠传输机制,比对了各自的性能情况,他的测试场景为:

1. 分布式队列,节点间数据复制(同步、异步)
2. 消息可靠性等级最高(持久化、ack等)

由于不同的消息服务器实现原理不同,会造成集群节点间数据复制代价和消息可靠性上的差异,最终文章给出的基准性能测试数据(消息吞吐量维度)总结如下图:
benchmark-summary

从上图来看,Kafka毫无争议的拥有最大的消息吞吐量。但是RabbitMQ的数据却是有点反直觉,因为之前给人的感觉RabbitMQ作为一款工业级的消息队列服务器,虽说不是靠高性能扬名,但也不至于让性能问题成为累赘。

网上的基准测试结果只能作为参考,不能作为技术选型的依据。当我们看到某类产品(Web服务器、缓存、队列、数据库等)的一组性能测试数据时,首先要了解以下三点:

1. 作者是否利益相关,利益相关往往导致给出的数据和结论偏向自家产品。
2. 作者是否能hold得住不同产品的技术细节,有时候一个参数值的优化会影响到产品的表现。
3. 具体的测试场景和参数。这个不用多解释了,每个产品都有自己擅长和不擅长的使用场景。

对于上述的第2点和第3点,本质上是由于信息不对称造成的。本文的目的不是为了质疑那篇文章的结论,而是借此分析一下是否可以通过一些客户端程序优化来提升RabbitMQ性能。

之前自己也做过RabbitMQ的性能测试,对于固定消息体大小和线程数,如果消息持久化、生产者confirm、消费者ack三个参数中开启消息持久化和生产者confirm,那么对性能影响相当致命,能够衰减一个数量级,吞吐量甚至会退化到几百msg/s。

消息持久化的优化没太好方法,用更好更快的物理存储(SAS,SSD,RAID卡)总会带来改善的。生产者confirm这一环节的优化则主要在于客户端程序的优化上。归纳起来,客户端实现生产者confirm有三种编程方式:

1. 普通confirm模式。每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了。
2. 批量confirm模式。每次发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。
3. 异步confirm模式。提供一个回调方法,服务器端confirm了一条(或多条)消息后SDK会回调这个方法。

从编程实现的复杂度上来看:

第1种普通confirm模式最简单,publish一条消息后,等待服务器端confirm,如果服务器端返回false或者超时时间内未返回,客户端进行消息重传。

第2种批量confirm模式稍微复杂一点,客户端程序需要定期(每x秒)或定量(每x条)或者两者结合来pubish消息,然后等待服务器端confirm。相比普通confirm模式,批量可以极大提升confirm效率,但是问题在于一旦出现confirm返回false或者超时的情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且,当消息经常丢失时,批量confirm性能应该是不升反降的。

第3种异步confirm模式的编程实现最复杂,Channel对象提供的ConfirmListener()回调方法只包含deliveryTag(当前Channel发出的消息序号),我们需要自己为每个Channel维护一个unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次handleAck方法,unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率角度上看,这个unconfirm集合最好采用有序集合SortedSet存储结构。实际上,SDK里的waitForConfirms()方法也是通过SortedSet维护消息序号的。

我写了一个简单的RabbitMQ生产者confirm环节性能测试程序放在了github上,它实现了上述三种confirm模式,并且有丰富的参数可以配置,比如生产者数量、消费者数量、消息体大小、消息持久化、生产者confirm、消费者ack等等,可以根据使用场景组合。以下的讨论都是基于这个测试程序跑出来的结果。

首先是测试环境:

OS: OSX 10.10, Darwin 14.0.0, x86_64
Erlang: R16B03-1 (erts-5.10.4)
RabbitMQ: 3.5.1
CPU: 2.5 GHz Intel Core i5
Disk: SATA
Message Size: 1000 Bytes

一、单线程,未开启消息持久化和消费者ack。

普通 批量,50 msg/批 批量,100 msg/批 批量,200 msg/批 异步
2931 msg/s 6581 msg/s 7019 msg/s 7563 msg/s 8550 msg/s

可见,单线程跑时批量和异步confirm甩开普通confirm一大截了。严格来讲,异步confirm不存在单线程模式,因为回调handleAck()方法的线程和publish消息的线程不是同一个。

二、多线程,开启消息持久化和消费者ack。

多线程下普通confirm模式:

100线程 500线程 800线程 1000线程
659 msg/s 2110 msg/s 2353 msg/s 2477 msg/s

多线程下批量confirm模式:

100线程,50 msg/批 100线程,100 msg/批 100线程,500 msg/批 500线程,100 msg/批
3828 msg/s 3551 msg/s 3567 msg/s 3829 msg/s

多线程下异步confirm模式:

50线程 100线程 200线程
3621 msg/s 3378 msg/s 2842 msg/s

以上是不同线程数量的维度下,相同confirm模式的性能数据,大致来看,遵循线程数越大,吞吐量越大的规律。当然,当线程数量达到一个阈值之后,吞吐量会下降。通过这些数据还能得到一个隐式的结论:不论哪种confirm模式,通过调整客户端线程数量,都可以达到一个最大吞吐量值。无非是达到这个最大值的代价不同,比如异步模式需要少量线程数就能达到,而普通模式需要大量线程数才能达到。

最后再从相同线程数量(100线程数)的维度下,分析下不同confirm模式的性能数据:

普通 批量,50 msg/批 批量,100 msg/批 批量,500 msg/批 异步
659 msg/s 3828 msg/s 3551 msg/s 3567 msg/s 3378 msg/s

由此可见,选取了一个典型的线程数量(100)后,普通confirm模式性能相比批量和异步模式,差了一个数量级。

从以上所有的数据分析来看,异步和批量confirm模式两者没有明显的性能差距,实际上他们的实现原理是一样,无非是客户端SDK进行了不同的封装而已。所以,只需从可编程性的角度选择异步或批量或者两者结合的模式即可。相比而言,选择普通confirm模式只剩编程简单这个理由了。

回到本文开头提到的不同队列服务之间的性能对比,实际上,我认为RabbitMQ最大的优势在于它提供了最灵活的消息路由策略、高可用和可靠性,可靠性又分为两部分(消息可靠性和软件可靠性),以及丰富的插件、平台支持和完善的文档。然而,由于AMQP协议本身的灵活性导致了它比较重量,所以造成了它相比某些队列服务(如Kafka)吞吐量处于下风。因此,当选择一个消息队列服务时,关键还是看需求上更看重消息吞吐量、消息堆积能力还是路由灵活性、高可用、可靠传输这些方面,只有先确定使用场景,根据使用场景对不同服务进行针对性的测试和分析,最终得到的结论才能成为技术选型的依据。

--EOF--