14 集群:哪些环节会存在性能瓶颈和数据可靠性风险?
你好,我是文强。
在基础篇的课程中,我们学习了最简单的消息队列的构建过程和底层原理。接下来我们将开始进阶篇的学习,进阶篇将从集群构建、性能、可靠性、数据安全、可观测性几个方面展开。总结来说, 我们将把单机的消息队列架构扩展成为分布式的高可靠、高性能的完整集群。
这节课我们会先分析消息队列集群中哪些环节可能存在性能瓶颈和可靠性风险,以便让你对消息队列的集群有一个整体认识。
从技术上看,消息队列的性能和可靠性由 生产者、 Broker 集群、 消费者 三方共同保障,而不只是服务端的工作。通常衡量集群性能的一个重要指标是全链路耗时,即客户端发出一条消息到消费者消费到这条消息的时间差,先来看一张图。
这是在展示当生产者发送一条消息到Broker存储,消费者消费这条数据全流程所涉及到的组件。接下来我们就将围绕着这个链路展开分析,就从生产者开始吧。
生产者的性能和可靠性
我们先来画一张图,梳理下在生产者客户端中需要关注的几个点,主要分为客户端SDK和网络两大部分。
网络层面
在网络层面,对性能和可靠性的影响主要包括 连接协议、 传输加密、 网路稳定性、 网络延时、 网络带宽 五个方面。
我们在 第 07 讲 讲过,生产者客户端会先和 Broker 建立并保持 TCP 长连接,而不是在每次发送数据时都重新连接,以确保通信的性能。这也是默认情况下不用HTTP协议的原因。
在数据传输过程中,为了避免数据包被篡改、窃取,就需要进行传输加密。因为网络质量不稳定,传输过程中可能也存在丢包的情况,此时就需要依赖TCP的重传机制来解决问题。但当出现大量网络重传时,就会极大地影响性能,导致集群的吞吐下降和耗时上升。这也是我们在系统运营过程需要监控网络包重传率的原因。
另外当启用加密传输后,数据的传输性能会下降,这也是我们启用加密传输时一个需要考虑的点。我们在第19讲还会详细分析消息队列的安全机制,这里先不展开。
在性能部分,客户端和服务端的网络耗时是绕不过去的。特别在流量大、高吞吐的场景下,网络耗时对数据传输性能的影响更大。一般情况下,公有云内网耗时在1ms以内,跨可用区通信的网络延时会增加23ms。如果是跨地域通信,此时的网络延时可能会增加至少510ms,更远的话甚至几十几百ms。所以,在客户端的部署上,为了保证延时和吞吐,就需要尽量将客户端和服务端部署在同一个可用区内网中,以避免网络链路带来的影响。
在网络部分,我们还需要关注网络带宽。如下图所示, 一般我们会关注客户端节点网卡、中间网络链路、Broker 节点的网卡三个部分的带宽容量。 客户端和 Broker 的网卡使用情况比较好发现和分析,经常忽略且不容易分析的是中间网络带宽的使用情况,比如在跨地域传输、跨云传输的场景下,中间网络带宽很容易成为瓶颈。
SDK层面
在 SDK 层面,对性能和可靠性的影响主要包括 发送模式、 批量语义、 异常处理、 生产者数量 四个方面。
在生产端,一般支持发送即忘、同步发送、异步发送三种发送模式,发送模式的设计思想是希望在性能和可靠性之间寻找平衡。
发送即忘 是指调用 send() 函数后,不用等待服务端的返回结果,因此可以不断地发送数据。这种模式的性能是最高的,可靠性是最低的,因为数据发送失败后没有任何后续的容错处理。
同步发送 是指调用 send() 函数后,业务代码同步等待服务端的返回,优点是能保证发送消息的顺序性,这种模式的性能是最低的。其性能高度依赖Broker和服务端之间的网络延时,以及Broker的处理耗时。
异步发送 是指调用 send() 函数后,使用异步线程回调的方式发送数据。即在不阻碍主线程的情况下发送数据,此时业务可以一直不停地发送数据。但是如果 send() 速度大于底层发送给Broker的速度,当SDK底层的线程池用完后,发送数据也会阻塞。
总结一下,从性能上来看,发送即忘 > 异步发送 > 同步发送。从可靠性来看,异步发送 = 同步发送 > 发送即忘。同步发送可以保证顺序,异步发送因为重传机制的存在,会无法保证顺序。
批量发送是指生产端是否支持 Batch 语义。Batch 语义就是 第 07 讲 讲到的批量发送,批量比非批量的吞吐性能高。从全链路延时来看,因为批量发送需要在生产者客户端本地等待聚合数据,所以非批量发送的全链路耗时会比批量发送的全链路延时低。
当客户端接收到服务端的报错时,如果没有正确地捕获异常,进行重试和记录,就有可能出现数据发送失败我们却不知道的情况,最终导致数据丢失。一般会在数据发送流程中,做好异常捕获、重试的逻辑,并对发送结果进行记录。比如发送失败的异常信息,发送时候记录消息的ID或者能唯一标识消息的信息,从而做到发送数据的可追溯。
最后,因为单个生产者和单个TCP连接是有性能瓶颈的,在业务中我建议你建立多个生产端实例同时来写入数据,这样可以提高生产者的性能。
接下来我们来看一下 Broker 的性能和可靠性。
Broker 的性能和可靠性
Broker的性能和可靠性分为单机和集群两个维度。单机维度属于垂直扩容,集群维度属于水平扩容。我们先来看一下单机维度的性能和可靠性。
单机维度
在单机维度,Broker 的性能和可靠性提升可以拆成应用程序、操作系统、物理硬件三个层面。
应用程序 是指消息队列的应用程序本身,比如 Java 编写的 Kafka Broker 程序、Erlang 编写的 RabbitMQ Broker等等。
如下图所示,应用程序的优化主要分为网络层、逻辑层、存储层三个模块。其中网络层、存储层的性能和可靠性是最重要的,至于如何优化我们在基础篇已经讲过,这里就不再重复了。
除了网络层和存储层,逻辑层的处理速度也很重要。比如在生产流程中,需要进行数据解析、数据校验、数据重组、协议转换等操作,这些操作都需要通过编码实现,编码质量的好坏会严重影响该流程的性能。此时就需要用到相关语言的编程技巧,来提升逻辑层的处理速度和稳定性。我会在第 18 讲分享一些常用的 Java 编码技巧,可以期待一下。
另一方面,应用程序的性能还受限于编程语言虚拟机,比如Java 虚拟机。合理的对语言虚拟机调优可以极大地提高程序的处理性能。比如 Java 虚拟机调优一般会关注 JDK 版本、垃圾回收策略、堆大小等等几个方面。
操作系统 是指通过调整操作系统参数来提高性能。在消息队列中,我一般会建议你调整系统最大FD、PageCache刷盘策略、Socket最大缓冲区大小、进程可映射的内存区域数量等等配置来提高性能。从实际运营来看,大部分情况下默认的系统参数都够用,对这些参数的调优一般在需要深度优化挖掘单节点性能的场景中会用到。如果你想对 Kafka 集群的操作系统进行调优,还可以参考 Kafka 操作系统调优。
物理硬件 是指节点的CPU、内存、网卡、硬盘等物理资源 。 我们都知道用更大规格的物理资源,性能肯定会更高。从实际运营的角度,通过升级物理硬件来提高性能,也是最简单、最直接的方式。
但是你知道你的集群应该升配哪类物理资源规格吗?是CPU、内存、网卡、硬盘都要一起升吗?要升配到什么规格呢?接下来我们来看一下消息队列对于这四个指标的的需求情况。
-
CPU:因为消息队列并没有很复杂的计算逻辑,核心流程是接收返回数据,所以大部分情况下消息队列对 CPU 的需求并不高。只有在比如开启压缩、TCP连接数很高、高频率GC的场景中会消耗较多 CPU。所以如果一开始就上大规格的CPU节点,就会存在浪费,我们现网就有大量的 2C8G 的机器在运行且状态良好。
-
内存:内存是 Broker 很依赖的资源,因为数据的写入、消费的热读等等都需要依赖内存。比如 Broker 保存数据,是先将数据写入到 PageCache 再刷新到硬盘的。消费时如果命中内存中的数据,性能会高很多。另外较大的内存也就可以配置更大的堆,从而避免GC带来的CPU损耗。消息队列一般也会用到堆外内存,比如 PageCache 就是堆外内存,所以即使没有配置给堆的内存也会被使用到。
-
网卡:网卡我们只需要关注是否被打满就行,打满了系统就会异常。大部分业务中网卡容量都是够的,比如某个云上虚拟机的网卡最小为 1.5Gbps(187MB/s)。 对于一些大流量的业务,就需要重点观察网卡的流量。网卡打满的时候,数据一致性、客户端写入都会受到影响。网卡和性能其实没有关系,只要网卡没有被打满,可以认为性能都是一样的。
-
硬盘:消息队列是一个非常依赖硬盘性能的产品。要满足低延时、高吞吐的需求,硬盘的吞吐就非常重要。硬盘的三个衡量指标是IOPS、吞吐、延时。用高性能的硬盘会给集群带来很大的性能提升。关于硬盘的性能提升,你可以回看 第 06 讲 存储的性能优化。
总结来讲,单机就是垂直深度挖掘单机的的性能。接下来看一下集群维度的可靠性和性能。
集群维度
如下图所示,我们都知道集群的核心思想就是水平扩容,即通过水平扩容添加节点,让集群拥有更强的处理能力。在消息队列集群中,性能和可靠性是通过创建更多分区、多个副本,并将分区和副本分配到多个节点上来实现的。
所以消息队列集群维度性能和可靠性的核心,包括分区、副本、集群构建、数据一致性、集群可靠性和性能等等几个方面,我们会在后面几节课展开来讲,这里先不展开。接下来我们来看看消费者的性能和可靠性。
消费者的性能和可靠性
从技术上来看,消费者的性能和可靠性也分为客户端SDK和网络两个部分。其中网络部分和生产者是一样的,我们不再重复。我们重点来看一下消费者SDK的性能和可靠性。
在消费性能方面, 我们主要关注延时和堆积两个指标。延时是指Broker保存一条消息后,这条消息被客户端消费到的时间差。堆积是指Broker堆积很多消息没有被及时消费。
继续来看一张图,消费者的性能和可靠性主要跟 消费模型、 消费重平衡、 消费模式、 位点提交 四个方面有关。
为了提高消息消费的及时性,最好是选择Push模型,即服务端有消息后主动Push给多个客户端,此时的消费的延时是最低的。从提高吞吐来看,为了避免服务端堆积,主流消息队列都是通过客户端主动批量Pull数据来提高吞吐、避免堆积。 一般情况下,Pull 模型都是默认的消费模型。
消息队列一般是通过消费分组(或订阅)消费数据,以便能自动分配消费关系和保存消费进度。此时当消费重平衡时,为了重新分配消费关系,所有的消费都会暂停,从而会影响到消费性能。如果重平衡次数较多,问题就会更加严重。所以,像Flink等流式计算引擎,都会绕过消费分组,指定分区进行消费,以避免重平衡带来的性能下降。而 RocketMQ 为了解决重平衡问题,就将重平衡移动到了 Broker 端,尽量降低消费重平衡带来的性能影响。
在分配消费关系的时候,如果以分区粒度将分区分配给一个消费者,此时当消费者性能有差别时,就会出现消费倾斜,导致分区堆积,从而影响性能。而如果是以消息粒度投递数据,即一个分区的数据能够投递给不同的消费者,此时就不会出现性能问题,性能是更高的,但是消息数据的顺序性无法保证。所以为了提高消费性能,我们可以选择合适的消费模式,消费模式可以参考 第08讲 和 第09讲。
从可靠性来看,消费端是不存在丢数据的情况的。但是客户端如果存在错误提交消费位点(Offset)的情况,比如应该提交 Offset 却没有提交,就会导致重复消费;或者不应该提交Offset 却提交了Offset,就会导致消费者没有消费到应该消费的数据,从而导致下游认为数据丢失。此时从代码上来看,建议是手动提交Offset(或ACK),即消费到数据,并且业务逻辑处理成功后,才执行ACK或者提交Offset。
当消费完成,需要提交位点后才能消费下一份数据,此时如果提交位点的请求过慢,也会影响消费的性能。有的消息队列会同时支持单条ACK和批量ACK的特性,正常来讲批量ACK的性能更高。
在消费端,对性能影响最大的是网络链路的耗时。网络耗时会极大影响消费的性能,特别是在跨区、跨地域消费的情况下,问题会更加明显。所以我依旧建议你就近部署和消费。
总结
这节课我们了解了消息队列性能和生产者、Broker、消费者三者相关。下面我们从这三个维度来总结一下可能影响性能和可靠性的关键点。
思考题
根据二八原则,集群中应该有某些点对于集群的性能和可靠性影响很大,你认为主要有哪些?
期待你的思考,如果觉得有收获,也欢迎你把这节课分享给身边的朋友。我们下节课再见!
上节课思考闭环
Pulsar为什么会对ZooKeeper造成很大的压力?
压力的来源主要是 Pulsar 在 ZooKeeper 中存储的元数据的大小。
Pulsar 需要在 ZooKeeper 中存储Topic、分区、Broker 负载、Bundle、BookKeeper Ledger等元数据信息,这些数据需要占用大量的节点和空间。而当ZooKeeper存储大量数据时,因为其底层存储结构,ZooKeeper的性能就会急速下降。
而且Pulsar 主打百万分区,分区越多,需要在ZooKeeper中保留的数据就越多,也就导致了ZooKeeper的负载会更高,压力的放大作用更明显。