【Free Style】kafka 解密:破除单机topic数多性能下降魔咒 (上)

举报
步步清风 发表于 2017/11/24 17:56:30 2017/11/24
【摘要】 版权归PUMA项目组所有,转载请声明,多谢。 kakfa大规模集群能力在前面已给大家分享过,kafka作为消息总线,在支撑云千万tps上千节点的集群能力非常出色,本文继续对业界关于单机多topic的性能瓶颈点问题(比如:https://yq.aliyun.com/articles/62832?spm=5176.100239.blogcont25379.8.KMUH1L),国内某云使用Rock

版权归PUMA项目组所有,转载请声明,多谢。

kakfa大规模集群能力在前面已给大家分享过,kafka作为消息总线,在支撑云千万tps上千节点的集群能力非常出色,本文继续对业界关于单机多topic的性能瓶颈点问题(比如:https://yq.aliyun.com/articles/62832?spm=5176.100239.blogcont25379.8.KMUH1L),国内某云使用RocketMQ,性能对比唱衰kafka 64分区时出现性能拐点,为此我们表示不服,从理论到实测数据,一步步揭开所谓单机性能拐点的秘密及解决之道。

1     简介

本文分析了Kafka分区数量的支持情况,通过一系列的测试和分析需要确认分区的数目是否受到限制,并找到解决方案。

本文读者包含分布式消息总线系统的开发者、测试者,以及应用该系统的客户产品

硬件约束:2285服务器2

软件约束:无

2     需求背景

为什么要扩展Kafka分区数量呢?主要有以下几点:

1.    分区的数目关系消息队列的并行度,消息发送是往分区里发送的,每个消费者都只能对每个分区启动1个线程获取数据。所以分区是消息队列并行度的最大保证。

2.    消息总线需要多业务接入,每个业务都要创建自己的Topic和分区,必然对队列数目有需求(队列数目包含Topic和分区)

3.    终端有个IM的需求,是做一个类似一个微信和旺旺一样的聊天工具。如果用消息总线来做,那么要求消息队列为每个用户分配一个队列,这样至少要支持几亿的消息队列,这个在目前的消息队列中完全没办法支持。

4.    当消息队列作为公有云的服务提供的时候,我们分析每个用户会独占一个队列(分区或Topic),同样会对分区数量带来要求。

正是这些需求和原因导致我们将分区个数作为了MQ的一个度量指标。同时从网上的一些分析,Kafka最多支持64个分区性能就下降得很厉害,为了验证和解决分区数量限制的问题,我们就有本文档的分析。

3     分析

3.1   原理分析

3.1.1    基本概念

3.1.1.1 分区

分区是Kafka中的最重要的概念之一,消息发送和接收都是按照分区为单位来处理的。

image.png

分区的几个主要作用如下:

1、 Producer(消息发送者)的往消息Server的写入并发数与分区数成正比。

2、 Consumer(消息消费者)消费某个Topic的并行度与分区数保持一致,假设分区数是20,那么Consumer的消费并行度最大为20

3、 每个Topic由固定数量的分区数组成,分区数的多少决定了单台Broker能支持的Topic数量,Topic数量又决定了支持的业务数量。

简单的说,有多少分区,就有多少对应的文件读写,Kafka的每个分区对应一个文件:

image.png

每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高,经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证。

3.1.1.2 磁盘

在对消息存储和缓存时,Kafka使用了文件系统。

Kafka的设计基于一种非常简单的指导思想:不是要在内存中保存尽可能多的数据,在需要时将这些数据刷新(flush)到文件系统,而是要做完全相反的事情。所有数据都要立即写入文件系统中持久化的日志中,但不进行刷新数据的任何调用。实际中这样做意味着,数据被传输到OS内核的页面缓存中了,OS随后会将这些数据刷新到磁盘。

大家普遍为“磁盘很慢”,因而人们都对持久化(persistent structure)结构能够提供说得过去的性能抱有怀疑态度。实际上,同人们的期望值相比,磁盘可以说是既很慢又很快,这取决决于磁盘的使用方式。设计的很好的磁盘结构可以和网络一样快。在一个由6个7200rpm的SATA硬盘组成的RAID-5磁盘阵列上,线性写入(linear write)的速度大约是600MB/秒,但随机写入却只有100k/秒,其中的差距接近6000倍。

Kafka并没有在内存中创建缓冲区,然后再向磁盘write的方法,而是直接使用了PageCache。

OS在文件系统的读写上已经做了太多的优化,PageCache就是其中最重要的一种方法,详细的说明请看3.1.2章节。


直接使用PageCache有如下几个好处:

1)减少内存开销: Java对象的内存开销(overhead)非常大,往往是对象中存储的数据所占内存的两倍以上。

2)避免GC问题:Java中的内存垃圾回收会随着堆内数据不断增长而变得越来越不明确,回收所花费的代价也会越来越大。

3)简单可靠:OS会调用所有的空闲内存作为PageCache,并在其上做了大量的优化:预读,后写,flush管理等,这些都不用应用层操心,而是由OS自动完成。

由于这些因素,使用文件系统并依赖于PageCache页面缓存要优于自己在内存中维护一个缓存或者什么其他别的结构。

3.1.2    文件读写分析

Kafka借力于Linux内核的Page Cache,不(显式)用内存,胜用内存,完全没有别家那样要同时维护内存中数据、持久化数据的烦恼——只要内存足够,生产者与消费者的速度也没有差上太多,读写便都发生在Page Cache中,完全没有同步的磁盘访问。

 

image.png

3.1.2.1 读写空中接力

Linux总会把系统中还没被应用使用的内存挪来给Page Cache,在命令行输入free,或者cat /proc/meminfo"Cached"的部分就是Page Cache

Page Cache中每个文件是一棵Radix(基树),节点由4k大小的Page组成,可以通过文件的偏移量快速定位Page


当写操作发生时,它只是将数据写入Page Cache中,并将该页置上dirty标志。

当读操作发生时,它会首先在Page Cache中查找内容,如果有就直接返回了,没有的话就会从磁盘读取文件再写回Page Cache

 

可见,只要生产者与消费者的速度相差不大,消费者会直接读取之前生产者写入Page Cache的数据,大家在内存里完成接力,根本没有磁盘访问。

而比起在内存中维护一份消息数据的传统做法,这既不会重复浪费一倍的内存,Page Cache又不需要GC(可以放心使用大把内存了),而且即使Kafka重启了,Page Cache还依然在。

3.1.2.2 后台异步flush的策略

这是大家最需要关心的,因为不能及时flush的话,OS crash(不是应用crash) 可能引起数据丢失,Page Cache瞬间从朋友变魔鬼。

当然,Kafka不怕丢,因为它的持久性是靠replicate保证,重启后会从原来的replicate follower中拉缺失的数据。

内核线程pdflush负责将有dirty标记的页面,发送给IO调度层。内核会为每个磁盘起一条pdflush线程,每5秒(/proc/sys/vm/dirty_writeback_centisecs)唤醒一次,根据下面三个参数来决定行为:

1.    /proc/sys/vm/dirty_expire_centiseconds:如果page dirty的时间超过了30(单位是10ms),就

会被刷到磁盘,所以crash时最多丢30秒左右的数据。

2.    /proc/sys/vm/dirty_background_ratio:如果dirty page的总大小已经超过了10%的可用内存(cat

/proc/meminfo MemFree+ Cached - Mapped),则会在后台启动pdflush 线程写盘,但不影响

当前的write(2)操作。增减这个值是最主要的flush策略里调优手段。

3.     /proc/sys/vm/dirty_ratio:如果wrte(2)的速度太快,比pdflush还快,dirty page 迅速涨到 10%

的总内存(cat /proc/meminfo里的MemTotal),则此时所有应用的写操作都会被block,各自在自

己的时间片里去执行flush,因为操作系统认为现在已经来不及写盘了,如果crash会丢太多数据,

要让大家都冷静点。这个代价有点大,要尽量避免。在Redis2.8以前,Rewrite AOF就经常导致

这个大面积阻塞,现在已经改为Redis32Mb先主动flush()一下了。

详细的文章可以看:http://www.westnet.com/~gsmith/content/linux-pdflush.htm

3.1.2.3 主动flush的方式

对于重要数据,应用需要自己触发flush保证写盘。

1.    调用fsync()  fdatasync()

fsync(fd)将属于该文件描述符的所有dirty page的写入请求发送给IO调度层。

fsync()总是同时flush文件内容与文件元数据, fdatasync()flush文件内容与后续操作必须

的文件元数据。元数据含时间戳,大小等,大小可能是后续操作必须,而时间戳就不是必须的。

因为文件的元数据保存在另一个地方,所以fsync()总是触发两次IO,性能要差一点。

2.    打开文件时设置O_SYNCO_DSYNC标志或O_DIRECT标志

O_SYNCO_DSYNC标志表示每次write后要等到flush完成才返回,效果等同于write()后紧接一个fsync()fdatasync(),不过按APUE里的测试,因为OS做了优化,性能会比自己调write() + fsync()好一点,但与只是write相比就慢很多了。O_DIRECT标志表示直接IO,完全跳过Page Cache。不过这也放弃了读文件时的Cache,必须每次读取磁盘文件。而且要求所有IO请求长度,偏移都必须是底层扇区大小的整数倍。所以使用直接IO的时候一定要在应用层做好Cache
 

Kafka的默认机制中,fsync的间隔时间和消息个数都是最大值,所以基本上都是依赖OS层面的flush

3.1.2.4 Page Cache的清理策略

当内存满了,就需要清理Page Cache,或把应用占的内存swap到文件去。有一个swappiness的参数(/proc/sys/vm/swappiness)决定是swap还是清理page cache,值在0100之间,设为0表示尽量不要用swap,这也是很多优化指南让你做的事情,因为默认值居然是60Linux认为Page Cache更重要。

Page Cache的清理策略是LRU的升级版。如果简单用LRU,一些新读出来的但可能只用一次的数据会占满了LRU的头端。因此将原来一条LRU队列拆成了两条,一条放新的Page,一条放已经访问过好几次的PagePage刚访问时放在新LRU队列里,访问几轮了才升级到旧LRU队列(想想JVM Heap的新生代老生代)。清理时就从新LRU队列的尾端开始清理,直到清理出足够的内存。


Linux 提供了这样一个参数min_free_kbytes,用来确定系统开始回收内存的阀值,控制系统的空闲内存。值越高,内核越早开始回收内存,空闲内存越高。

 image.png

3.1.2.5 预读策略

根据清理策略,Apache Kafka里如果消费者太慢,堆积了几十G的内容,Cache还是会被清理掉的。这时消费者就需要读盘了。

内核这里又有个动态自适应的预读策略,每次读请求会尝试预读更多的内容(反正都是一次读操作)。内核如果发现一个进程一直使用预读数据,就会增加预读窗口的大小,否则会关掉预读窗口。连续读的文件,明显适合预读。

image.png

 

3.1.2.6 调度层优化

IO调度层主要做两个事情,合并和排序。

合并是将相同和相邻扇区(每个512字节)的操作合并成一个,比如现在要读扇区123,那可以合并成一个读扇区1-3的操作。

排序就是将所有操作按扇区方向排成一个队列,让磁盘的磁头可以按顺序移动,有效减少了机械硬盘寻址这个最慢最慢的操作。

排序看上去很美,但可能造成严重的不公平,比如某个应用在相邻扇区狂写盘,其他应用就都干等在那了,pdflush还好等等没所谓,读请求都是同步的,耗在那会很惨。所有又有多种算法来解决这个问题,其中内核2.6的默认算法是CFQ(完全公正排队)

3.1.3    原理分析结论

1Kafka使用文件系统来交换消息,性能是否比使用内存来交换消息的系统要低很多?

Apache Kafka里,消息的读写都发生在内存中(Pagecache),真正写盘的就是那条pdflush内核线程,根本不在Kafka的主流程中,读操作大多数会命中Pagecache,同时由于预读机制存在,所以性能非常好,从原理上有保证的。

2、 每个分区一个文件,那么多个分区会有多个文件同时读写,是否会极大的降低性能?

1)        首先,由于Kafka读写流程是发生在PageCache中,后台的flush不在主流程中触发,所以正常情况下理论上是没有影响的,除非PageCache占用内存过大,或是释放导致读写消耗Kafka进程的CPU时间。

2)        再次,文件都是顺序读写,OS层面有预读和后写机制,即使一台服务器上有多个Partition文件,经过合并和排序后都能获得很好的性能,不会出现文件多了变成随机读写的情况,但是当达到相当多的数量之后,也会存在一定的影响。

3)        PageCache过大,大量触发磁盘I/O的时候,超过了/proc/sys/vm/dirty_ratioFlush会占用各个应用自己的CPU时间,会对主流程产生影响,让主流程变慢。

3.2   性能测试

3.2.1    测试环境

image.png

同时启动消息发送和消费。

 image.png

指标说明:

TPS

在客户端侧由代码统计的每秒发送和接收到的消息个数。

wai

系统因为io导致的进程wait。再深一点讲就是:这时候系统在做io,导致没有进程在干活,cpu在执行idle进程空转,所以说iowait的产生要满足两个条件,一是进程在等io,二是等io时没有进程可运行。

%util

使用iostat测试得到。Percentage of CPU time during which I/O requests   were issued to the device (bandwidth utilization for the device). Device   saturation occurs when this value is close to 100%。一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间   I/O 队列是非空的。如果 %util 接近 100%,说明产生的I/O请求太多,I/O系统已经满负荷,该磁盘可能存在瓶颈。

IOPS

该设备每秒的传输次数(Indicate the number of   transfers per second that were issued to the device.)。“一次传输”意思是“一次I/O请求”。多个逻辑请求可能会被合并为“一次I/O请求”。“一次传输”请求的大小是未知的。IOPS=reads+writes

reads

在用例运行过程中,每秒读io的个数均值

writes

在用例运行过程中,每秒写io的个数均值

 从流程来看,影响性能的几个主要点为:客户端,网络,服务器端CPU,内存,文件系统(磁盘)。

3.2.2    单硬盘测试

当系统只有一个硬盘的时候,所有的分区文件集中在一个磁盘上,测试数据如下:

image.png

从数据来看,cpu,内存,网络问题都不大。

 image.png


随着分区数目的增加,吞吐量会下降。从图上来看,2000的分区下,生产者和消费者TPS下降幅度已经较大了。1000左右的分区TPS浮动不大。

 

备注:ConsumerTPS下降的原因,根据JProfile分析,主要是客户端问题,后面章节有专门分析。


image.png

从磁盘的IOPS来看,超过1000之后,会急剧上升,3000之后因为TPS的下降,反而有所下降。

特别注意的是:读操作一直为0,证明基本上所有的Consumer消息都是从PageCache中获取的。

 

image.png

接下来,看一下utilwai,超过1000之后,util会急剧上升,到了3000左右,基本满负荷了。

这个和TPS是能对上的。

 

根据以上的测试数据,我们可以得到一个初步的结论:

1、 当分区超过了2000之后,util急剧上升,磁盘性能会成为瓶颈,导致Producer TPS下降。

2、 由于Read操作一直为0,证明磁盘对Consumer的影响不大。ConsumerTPS下降是客户端原因。(后面的章节给出证据)

建议:单台服务器单硬盘下,分区数量不超过2000,推荐值在1000以下。

3.2.3    多硬盘测试

启动8个磁盘,1个磁盘跑zookeeperOS,其他7个磁盘分担所有的分区文件。

 image.png

UtilIOPS7个硬盘加起来的值,实际上每个硬盘的负荷是平均分担的。

 image.png


1、可以看到,3000分区下,ProducerTPS很平稳,实际上还要超过单硬盘下单分区的TPS

证明追加硬盘的方法可以明显的提高TPS

2Consumer TPS,在多硬盘下其实和单硬盘是一致的,超过2000分区之后,一直都是18w左右。证明硬盘并不是Consumer的瓶颈。

 image.png

image.png


磁盘的IOPSUtil是随分区增加而增加的,但是实际上被7个硬盘平均分担,每个硬盘的负荷除以7之后很小了。

当分区达到5000之后,TPS会下降,但是磁盘负荷还远没有达到瓶颈。

CPU,网络,内存也同样没有问题。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。