临近双十一大家都免不了要对自己的业务系统进行压测。公司一个核心业务预计双十一会迎来数倍日常流量的业务高峰,该系统强依赖于 Kafka,Kafka 本身是分布式的系统,扩容比较方便。但是为了保证核心业务的稳定性和高可用,需要在机房故障的场景下核心业务快速恢复服务,因此 Kafka 需要跨机房热备机制。
一般情况的 Kafka 集群,都是在同一个 IDC 中的,跨 IDC 的热备在 Kafka 的使用场景叫做 Mirror,Kafka 内置的支持为 MirrorMaker,现在已经进化到了第二版,下文简称为 MM2.
经过前期技术调研,基本确定了 MM2 的 Kafka 热备机制。为了确定这套方案在双十一的流量高峰情况下,能满足消息系统热备的需求,需要提前做压测。
MM2 的工作原理可以简单理解为:在目的端机房消费源端机房需要复制的消息。因此 MM2 的压测,本质上还是 Kafka 的压测。而幸运的是 Kafka 作为高性能、广泛使用的消息队列,它本身就提供了 benchmark 工具,可以方便的使用内置的命令行进行压测。
基本的用法如下:
./bin/kafka-producer-perf-test.sh --topic test_perf --num-records 1000000 --record-size 15000 --throughput -1 --producer-props bootstrap.servers=10.xx.xx.1:9094,10.xx.xx.2:9094 acks=1
几个重要的参数解释如下:
消息体的大小,我们可以根据实际的业务场景构造合适的消息大小,例如统计一批消息的平均大小:
./bin/kafka-console-consumer.sh --bootstrap-server 10.xx.xx.1:9094,10.xx.xx.2:9094 --topic sdsoms --max-messages=100 | wc
acks 模式需要和实际生产环境保持一致即可。
确定了消息体大小,和 acks 之后,可以调整 num-records 和 throughput,保证有足够的消息数量和速率。
在具体压测实施过程中,遇到了不少问题。经过业务预估,双十一业务高峰时 Kafka 需要承受的 QPS 约为 x 万,但是初次压测下来的数据仅能到几千就出现了大量的跨机房复制消息延迟。
通过对机器负载分析发现:
部署消息迁移服务的机器 CPU/磁盘都没有到平静,实际上从 MM2 消息迁移的原理来看,它工作完全是在内存中完成,因此磁盘基本没有使用。
那么最有可能的问题就出在了网络上。
跨 IDC 的消息同步会面临几个问题:
- IDC 之间的网络延迟(RTT)会远高于 IDC 内的,典型的情况下机房内的延迟往往在 0.1ms 以内,而广州到上海的 IDC 延迟一般为 30ms。
- 带宽有限,且成本高。跨 IDC 的网络铺设成本高,总带宽有限,费用很高。机房内部双千兆一般都是标配,万兆也很普及,而且一般是免费使用。
经过一番分析和计算,在压测的业务场景中,消息体较大,达到了 15000Byte(典型的消息应用场景中消息体一般在几十到几百 Byte),这个大小在几千的 QPS 下,带宽就需要几百兆大小。
为了确定是带宽的原因,一方面同 OP 确定跨机房的带宽上限。另一方面在两个机房的机器上安装 iperf3 工具测试速度。
使用 iperf3 工具测试网络速度:
# MM2目的端的机器(也就是MM2服务部署机器)操作
iperf3 -s
# MM2源端的机器操作
iperf3 -c MM2_host_ip
综合测速结果和 OP 反馈的机房间带宽,发现确实到了带宽瓶颈。
所以向 OP 申请临时调高带宽,带宽调高了 X 倍。
本以为,机房间带宽增加之后,MM2 的性能应该也会有接近 X 倍的提升。但是实际情况却是:
在性能有小幅提升后,在 CPU/磁盘/网络依然没有到达瓶颈的情况下,再出出现了大量的跨机房消息复制延迟。
经过一番深入分析发现,在高延迟的网络环境中,打满带宽不是一件显而易见的事情:
我们知道 Kafka 使用的是 TCP 网络传输协议,TCP 在经过三次握手之后,开始进入的流量控制阶段,在这个阶段,会使用滑动窗口确定一个 RTT 时间范围内发送的数据量。在一个 TCP 连接中,如果网络延迟很大,那么它实际上不是那么容易能完全利用带宽。
这里引入一个概念:
带宽时延乘积,在数据通信中,带宽时延乘积(英语:bandwidth-delay product;或称带宽延时乘积、带宽延时积等)指的是一个数据链路的能力(每秒比特)与来回通信延迟(单位秒)的乘积。其结果是以比特(或字节)为单位的一个数据总量,等同在任何特定时间该网络线路上的最大数据量——已发送但尚未确认的数据。
当然,在现代的高延迟、大带宽网络中 TCP 提供了缩放因子来解决这个问题,可以避免 TCP 默认最大窗口 65535 带来的带宽利用不足问题。
沿着这个思路,我们对服务的网络相关参数做了一些调整,主要增加了 tcp 相关 buffer 大小,虽然带来了一些提升,但是离我们的目标还很远。这是因为,考虑一个线路的传输速率时,除了需要考虑 BDP 这个因素外,还需要考虑丢包率,滑动窗口增加,在增加发送速率的同时,也增加了丢包重传的成本。
在带宽上限提不上去的情况下,还想提升 QPS,那么可行的方案就是减小消息体大小。减小消息体有两个思路:
压缩就是在消息生产之后,进入 producer 时,进行压缩,之后的同集群落盘和跨集群 MM2 复制,就都是压缩后的消息了。producer 和 consumer 一般都内置了消息压缩的支持,Kafka 支持 GZIP、Snappy、LZ4 三种压缩算法,从 2.1.0 开始正式支持 zstd 算法。各种压缩算法之间的对比可以参考这个文章:http://zhongmingmao.me/2019/08/02/kafka-compression/
在我们的使用场景中,我们希望尽可能的提高压缩比,降低网络传输量,虽然 zstd 具有最高的压缩比,不幸的时我们使用的版本尚不支持,因此只能退而求其次选择了 GZIP 算法。但即使时 GZIP 也能获得很高的压缩比。
原来的消息体很大主要的原因是,很多数据其实不需要,但是为了简单方便,都放到了消息中,通过仔细分析消息字段,并去除不需要的字段,大幅减小了消息体大小。
通过上述两种优化的叠加,基本上达到了预期的压测目标,希望消息系统可以顺利通过双十一的考验。
[1] https://engineering.salesforce.com/mirrormaker-performance-tuning-63afaed12c21
[2] https://plantegg.github.io/2019/09/28/TCP--性能和发送接收 Buffer 的关系/就是要你懂
[3] http://www.dengshenyu.com/kafka-data-mirror/
[4] https://www.slideshare.net/JiangjieQin/producer-performance-tuning-for-apache-kafka-63147600