14 集群性能瓶颈和数据可靠性风险
消息队列的性能和可靠性由生产者、Broker 集群、消费者三方共同保障,而不只是服务端的工作。通常衡量集群性能的一个重要指标是全链路耗时,即客户端发出一条消息到消费者消费到这条消息的时间差
生产者的性能和可靠性
网络层面
网络层面,对性能和可靠性的影响主要包括连接协议、传输加密、网路稳定性、网络延时、网络带宽五个方面
生产者客户端会先和 Broker 建立并保持 TCP 长连接,而不是在每次发送数据时都重新连接,以确保通信的性能。这也是默认情况下不用 HTTP 协议的原因。
在数据传输过程中,为了避免数据包被篡改、窃取,就需要进行传输加密。因为网络质量不稳定,传输过程中可能也存在丢包的情况,此时就需要依赖 TCP 的重传机制来解决问题。但当出现大量网络重传时,就会极大地影响性能,导致集群的吞吐下降和耗时上升。这也是我们在系统运营过程需要监控网络包重传率的原因。
消息发送模式
Broker 的性能和可靠性
消费者的性能和可靠性
为了提高消息消费的及时性,最好是选择 Push 模型,即服务端有消息后主动 Push 给多个客户端,此时的消费的延时是最低的。从提高吞吐来看,为了避免服务端堆积,主流消息队列都是通过客户端主动批量 Pull 数据来提高吞吐、避免堆积。一般情况下,Pull 模型都是默认的消费模型。
消息队列一般是通过消费分组(或订阅)消费数据,以便能自动分配消费关系和保存消费进度。此时当消费重平衡时,为了重新分配消费关系,所有的消费都会暂停,从而会影响到消费性能。如果重平衡次数较多,问题就会更加严重。所以,像 Flink 等流式计算引擎,都会绕过消费分组,指定分区进行消费,以避免重平衡带来的性能下降。而 RocketMQ 为了解决重平衡问题,就将重平衡移动到了 Broker 端,尽量降低消费重平衡带来的性能影响。
从可靠性来看,消费端是不存在丢数据的情况的。但是客户端如果存在错误提交消费位点(Offset)的情况,比如应该提交 Offset 却没有提交,就会导致重复消费;或者不应该提交 Offset 却提交了 Offset,就会导致消费者没有消费到应该消费的数据,从而导致下游认为数据丢失。此时从代码上来看,建议是手动提交 Offset(或 ACK),即消费到数据,并且业务逻辑处理成功后,才执行 ACK 或者提交 Offset
15 分布式消息队列集群
有状态服务和无状态服务
我们经常听到有状态服务和无状态服务这两个词。二者之间最重要的一个区别在于:是否需要在本地存储持久化数据。需要在本地存储持久化数据的就是有状态服务,反之就是无状态服务。
HTTP Web 服务就是典型的无状态服务。在搭建 HTTP Web 集群的时候,我们经常会使用 Nginx 或者在其他网关后面挂一批 HTTP 节点,此时后端的这批 HTTP 服务节点就是一套集群。
消息队列是有状态服务。消息是和分片绑定,分片是和节点绑定。所以,当需要发送一个消息后,就需要发送到固定的节点,如果把消息发送到错误的节点,就会失败。所以,为了将消息发送到对的节点和从对的节点削峰数据,消息队列在消息的收发上,就有服务端转发和客户端寻址两种方案。
消息队列的集群设计思路
元数据存储
消息队列集群元数据是指集群中 Topic、分区、配置、节点、权限等信息。元数据必须保证可靠、高效地存储,不允许丢失。因为一旦元数据丢失,其实际的消息数据也会变得没有意义。
节点发现
节点探活
技术上一般分为主动上报和定时探测两种,这两种方式的主要区别在于心跳探活发起方的不同。从技术和实现上看,差别都不大,从稳定性来看,一般推荐主动上报。因为由中心组件主动发起探测,当节点较多时,中心组件可能会有性能瓶颈,所以目前业界主要的探活实现方式也是主动上报
从探测策略上看,基本都是基于 ping-pong 的方式来完成探活。心跳发起方一般会根据一定的时间间隔发起心跳探测。如果保活组件一段时间没有接收到心跳或者主动心跳探测失败,就会剔除这个节点。比如每 3 秒探测一次,连续 3 次探测失败就剔除节点。探测行为一般会设置较短的超时时间,以便尽快完成探测
主节点选举
节点启动
创建topic
Leader切换
16 分布式的消息队列集群
元数据存储服务设计
基础第三方存储
一般只要具备可靠存储能力的组件都可以当作第三方引擎。简单的可以是单机维度的内存、文件,或者单机维度的数据库、KV 存储,进一步可以是分布式的协调服务 ZooKeeper、etcd 等等。
集群内部自实现元数据存储
总结来看,在集群中实现元数据服务的优点是,后期架构会很简洁,不需要依赖第三方组件。缺点是需要自研实现,研发投入高。而如果使用独立的元数据服务,因为是现成的组件,产品成型就会很快,这也是当前主流消息队列都是依赖第三方组件来实现元数据存储的原因。所以当前主流消息队列的架构如下所示
如图所示,Broker 在启动或重连时,会根据配置中的 ZooKeeper 地址找到集群对应的 ZooKeeper 集群。然后会在 ZooKeeper 的 /broker/ids 目录中创建名称为自身 BrokerID 的临时节点,同时在节点中保存自身的 Broker IP 和 ID 等信息。当 Broker 宕机或异常时,TCP 连接就会断开或者超时,此时临时节点就会被删除
17 分布式集群可靠性
分区、副本和数据倾斜
副本间数据同步
ZooKeeper 需要保证数据的高可靠,不允许丢失。而在多数原则理论中,如果数据只写入到 Leader 和 Follower 中,此时这两台节点同时损坏或者集群发生异常时导致 Leader 频繁切换,数据就可能会损坏或丢失。为了解决这些复杂场景,Zab 协议定义了 Zxid、崩溃恢复等细节来保证数据不会丢失
18 Java分布式存储系统的编程技巧
PageCache 调优和 Direct IO
应用程序读取文件,会经过应用缓存、PageCache、DISK(硬盘)三层。即应用程序读取文件时,Linux 内核会把从硬盘中读取的文件页面缓存在内存一段时间,这个文件缓存被称为 PageCache。
可以绕过操作系统,直接使用通过自定义 Cache + Direct IO 来实现更细致、自定义的管理内存、命中和换页等操作,从而针对我们的业务场景来优化缓存策略,从而实现比 PageCache 更好的效果。从技术实现上看,它和我们使用 C++ 管理内存编程是一样的效果。
FileChannel 和 mmap
FileChannel 大多数时候是和 ByteBuffer 打交道的,你可以将 ByteBuffer 理解为一个 byte[] 的封装类。ByteBuffer 是在应用内存中的,它和硬盘之间还隔着一层 PageCache。
我们通过 filechannel.write 写入数据时,会将数据从应用内存写入到 PageCache,此时便认为完成了落盘操作。但实际上,操作系统最终帮我们将 PageCache 的数据自动刷到了硬盘。这也是 FileChannel 提供了一个 force() 方法来通知操作系统进行及时刷盘的原因。
预分配文件、预初始化、池化
在提高文件写入性能的时候,预分配文件是一个简单实用的优化技巧。比如前面讲过,消息队列的数据文件都是需要分段的,所以在创建分段文件的时候,可以预先写入空数据(比如 0)将文件预分配好。此时当我们真正写入业务数据的时候,速度就会快很多。
还一点就是对象池化,对象池化是指只要是需要反复 new 出来的东西都可以池化,以避免内存分配后再回收,造成额外的开销。Netty 中的 Recycler、RingBuffer 中预先分配的对象都是按照这个池化的思路来实现的
直接内存(堆外)和堆内内存
从应用程序的角度来看,可以通过批量同步刷盘的操作来提高性能。批量同步刷盘的核心思路是:每次刷盘尽量刷更多的数据到硬盘上。技术上是指通过收集多线程写过来的数据,汇总起来批量同步刷到硬盘中,从而提高数据同步刷盘的性能
业务线程 T1 到 T6 的数据通过内存将数据发送给 IO 线程。然后业务线程进入 await 状态,当 IO Thread 收集到一定的数据后,再一起将数据同步刷到硬盘中。最后唤醒 T1 到 T6 线程,返回写入成功。
新的存储AEP
线程绑核
19 安全 身份认证鉴权 加密传输
消息队列的系统安全由六部分组成:
网络隔离、传输安全、集群认证、资源授权、自我保护、数据加密
网络隔离安全性
虚拟网络(VPC)就是一个独立的子网。如果不做对外打通(比如开通公网、跟其他网络拉专线),它的数据在这个网络内就是安全的
数据传输加密
数据传输安全的核心是 SSL/TLS,你可以简单理解成,如果要保证传输过程中的数据安全,就要用 SSL/TLS。消息队列也是这个逻辑,几乎所有的消息队列产品,传输过程中的加密机制都是基于 SSL/TLS 实现的,你可以在它们的官方文档找到相应的资料,比如支持 TLS 的 Pulsar 或者 RabbitMQ,支持 SSL 的 Kafka。
SSL 和 TLS 是同一个东西。SSL 3.0 及之前的版本叫 SSL,3.0 之后叫做 TLS,TLS 是 SSL 的升级版
建立连接时的身份认证
我们可以通过配置开启认证。认证是客户端连接服务端的第一道门槛,主要是解决客户端连接到服务端时,是否允许建立连接的问题。
认证就是客户端携带认证信息(比如用户名 + 密码、Token、Auth 等)连接上服务端。服务端首先会判断当前开启或配置的认证类型,然后校验这些信息,如果通过,就允许建立连接,否则,就返回认证错误。
20 消息集群加密和限流方案
集群中的数据加密
当客户端发送消息 A(比如 hello world)到 Broker,Broker 保存到磁盘的数据是一串内容 A 加密后的字符串,当消费端消费数据时,Broker 将加密后的字符串解密成消息 A 返回给消费端。
消息队列限流机制
全局限流方案
21 分布式监控设计
OpenTelemetry 主要是解决可观测性数据的获取规范问题,类似消息队列领域的 AMQP 和 OpenMessaging,目的都是打造一个标准化规范。它系统地将可观测性分为指标(Metrics)、日志(Logs)、跟踪(Traces)三个方面。在消息队列领域,可观测性建设主要也是围绕着这三点展开。
单机维度指标
消息队列关键指标
22 可观测性 设计实现消息轨迹功能
消息唯一标识
一条消息的生命周期是从客户端发出来的时候开始算的,所以发送出来的时候,就应该赋予消息一个唯一 ID,这才能真正表达消息的唯一。一旦在服务端赋予唯一 ID,因为客户端可能会重复发送同一条数据,服务端就会认为是两条数据,生成两个 ID。所以,消息 ID 的生成一般是在客户端 SDK 自动生成或者业务手动生成指定的