pagecache引起的生产速度变慢?

前言

去年在准备写书,很长一段时间没有更新文章,但最终还是因为自己的原因没有坚持写完,在这之后又有一段时间没有找到动力(实际上是想躺平~),所以没有任何输出。

最近突然看到自己的公众号开通了留言功能,所以还是打算重新进行一些整理输出(白话:想躺躺不平),一方面是方便自己回溯,另一方面是也可以就总结的内容进行交流,今天就来分享下最近深入分析的一个有关kafka的问题。

问题背景

在我们的实际使用场景中,在消息没有堆积的情况下(消费者速度能更上生产者的速度,即消费者组的lag几乎始终为0),生产和消费速度能同时达到一个较高的值。然而,当消息存在一定滞后时(例如先停止消费一段时间,再开启消费者),在很长一段时间内生产的速度是非常非常低的,这可能会导致消息在生产者客户端一侧因为不能及时发送而出现“丢”消息的情况。

pagecache的瓶颈?

对于生产者发送的消息,在kafka broker端的处理,就是调用系统的write接口,将消息写入操作系统的虚拟文件系统层(内部先将内容写入pagecache,然后再刷入到磁盘中);而对于消费者消费的消息,broker则是调用系统的read接口从虚拟文件系统读取(先判断所要读取的内容是否缓存在pagecache中,如果是则直接从pagecache读取,这也称之为缓存命中,如果没有则需要从磁盘读取并缓存到pagecache中)。

在无堆积的场景中,通过cachestat命令可以直观的看到系统pagecache的命中情况,基本维持在98%以上。

de6cfede22b6448d7c97294024c63048.jpeg

也就是说这种情况下, 消费者消费的消息都是缓存在pagecache中,fetch时直接从pagecache读取即可,整个过程没有磁盘的读IO。

a10f02b0f66dc2266fe4c1ed64a43877.jpeg

注:尽管broker内部用于存储消息的log文件是通过mmap进行映射的,但最终写入时依旧是将消息写入了pagecache。

了解到这些后,我们一开始的怀疑方向是:在有堆积的情况下, 由于大量滞后的消息从磁盘读取到pagecache中,是否pagecache的脏页dirty page存在瓶颈,会阻塞业务层的写操作,最终导致生产速度的变慢。

通过调整如下几个脏页相关的系统参数后:

// 开始触发脏页回写的阈值,当前设置为1280MB
vm.dirty_background_bytes = 1342177280
// 脏页的最大阈值,达到这个值后会阻塞io操作,当前设置为30GB
vm.dirty_bytes = 32212254720
// 数据在脏页中的过期时间,当前设置为30s
vm.dirty_expire_centisecs = 3000
// 检测周期,用于将过期数据回写磁盘,默认5s
vm.dirty_writeback_centisecs = 500

再次来测试,发现其生产速度并没有变化。同时,通过"sar -B"命令观察系统脏页情况,发现其脏页始终在1.5GB左右,远没有达到最大阈值,也就是说,不可能出现阻塞业务接口的情况,这也就能说明,在这个场景里,系统脏页大小与磁盘并非瓶颈所在。

接近真相

通过上面的测试证明了系统的pagecache与磁盘都不存在瓶颈,那么写入慢的原因只可能是在业务层,单位时间内写入的次数变少了,从而出现了整体生产速度变低的情况。为了验证这一点,我们分别在无堆积和有消息堆积的场景下,通过strace追踪broker进程内部对系统write的调用次数,与每次调用的耗时,其统计情况如下表所示:


无堆积
有堆积
单次write耗时0.000121397s0.000109609s
每秒write次数93986240

从单次write的耗时的对比也可以间接证明,pagecache层与磁盘是不存在瓶颈的。但从每秒的write次数则可以看出来,在堆积情况下的write次数远小于无堆积情况下的次数,这应该就是生产速度变慢的原因了。

找到了问题的方向后,我们又通过jmx分析了生产请求在broker内部的耗时情况,发现堆积情况下的生产请求总耗时明显大于无堆积情况下的耗时。

f86d86f5371073c4d3bcfe5f3699721d.jpeg

进一步分析生产请求在各个阶段的耗时之前,我们先通过一张图了解下,生产者请求在broker内部处理的大概逻辑。

8d47915794e36d3186087d921c00f8fc.jpeg

  • 当生产者或消费者的请求进入到kafka broker时,Processor线程负责从接收缓存区中获取请求并放入RequestQueue中,请求在RequestQueue中放置的时间为RequestQueueTime。

  • RequestHandler线程从RequestQueue中拉取请求并进行处理(将请求写入本地日志,或从本地日志读取内容),请求处理的耗时为LocalTime。

  • 当请求处理完毕后,会将请求传递给DelayedOperationPurgatory,满足指定条件后才发送请求响应,请求在本地处理完等待放到ResponseQueue的时间为RemoteTime。

  • 当满足条件后请求响应会放回到Processor的ResponseQueue中,请求响应在ResponseQueue中放置的时间为ResponseQueueTime。

  • 最后,Processor线程从ResponseQueue中拉取请求响应,并发送给生产者/消费者,其发送的时间为ResponseSendTime。

这几个时间加起来就是TotalTime。

具体来看下各阶段耗时的对比,如下图所示:

RequestQueueTime

757c630679d50fdd02e05e6e44481c2b.jpeg

堆积情况下,请求在队列中的耗时比无堆积情况下的要长,可以理解为无堆积情况下单位时间接收的请求要更多,因此在队列中等待的时间更长,可以说是符合预期的。

LocalTime

c535f28c0b115f3b5a02e5df5c7a77a8.jpeg

两种情况下, 本地的处理耗时几乎是一样的,再次证明在pagecache和磁盘层都不存在瓶颈。

ResponseQueueTime

359b51b7513ef594fb180fa9a5e1db14.jpeg

ResponseSendTime

012c2ca804ed76a4e9a44864b62a1569.jpeg

从这里可以看到, 请求响应在队列中等待的时间明显变长,同时请求响应发送的耗时也明显增加,因此,可以推导出结论,由于单位时间内请求响应等待发送与实际发送的耗时变长,从而导致整体生产速度降低。

问题原因

从JMX暴露的指标,我们看到了请求响应发送耗时明显增加导致了整体生产速度的降低,那为什么会出现这种现象呢?

我们又从jmx的指标里对比分析了两种情况下,单位时间内生产者请求数与消费者请求数:

c4261e89a5726fc33ef83b9ed679e29e.jpeg

08056660994e94832856da19b348c465.jpeg

从图中可以看到, 不管是生产的请求个数,还是消费请求个数,堆积场景下比无堆积情况下都是要少很多的。但是,堆积情况下的消费速度(吞吐量)和无堆积情况下的消费速度却是相差无几的。由此可以推断,在堆积场景下,单个消费请求获取到的数据量是要更多的,通过`wireshark`抓包,也证实了这一点:在无堆积情况下,单个请求响应大小在10KB到1MB之间波动(测试时单条消息在10KB左右),平均大小为几十KB,而在堆积情况下,请求响应大小几乎都是1MB。

而请求响应的大小和传输时间是正相关的,从抓包中可以看到,1MB的数据发送耗时为7ms左右,100KB的数据发送耗时为2ms左右。

在无堆积的情况下,消息几乎都是实时消费掉的,因此消费者每次`fetch`到的消息量是不大的,而堆积情况下, 每次`fetch`的消息是填满一整个`batch`,这就是两种场景下的区别,也就是发送耗时变长的原因。

但是,消费请求响应发送耗时的变大怎么影响到生产请求的呢?这就得梳理kafka broker内部Processor线程的处理逻辑了。

在Processor线程的处理逻辑中,关键在于,每次循环处理时,先处理请求响应:即先将responseQueue中的内容全部取出并发送给客户端(生产者/消费者),然后才从tcp连接的接收缓冲区中获取请求消息,并放到requestQueue中。

af4b1bde2bf8402f6131a103eaee3f38.jpeg

由于broker采用reactor的网络模型,即Acceptor接收到客户端新的tcp连接之后,是轮询将连接分配给Processor,负责后续tcp连接的数据交互,因此,同一个Processor线程中处理的连接可能同时包含生产者和消费者,当消费者fetch请求响应处理耗时变长,也就变相的拉低了整个生产者请求的处理速度。这也就是问题的真正原因。

分析出原因后,我们通过对消费者的速度进行了一定的限制(还是确保消费速度会大于生产速度,这样才能将滞后的消息消费掉),在堆积情况下, 生产的速度立马得到了提升。

小结

本文针对kafka在消息滞后消费(堆积)情况下,生产速度非常低的现象,从系统层到业务层,通过不同手段进行分析,定位到服务端网络处理方式引起的单位时间消费请求响应发送耗时变长变相拉低了生产者的速度。最后,通过对消费者消费速度进行一定的限制来保证堆积情况下具有一定的生产速度。

好了,这就是本文的全部内容,如果觉得本文对您有帮助,请点赞+转发,如果觉得有不正确的地方,欢迎留言交流~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值