查看原文
其他

基于eBPF监控和排查云原生环境中的磁盘IO性能问题

沈涛 eBay技术荟 2022-12-29


作者|沈涛

编辑|陈乐

供稿|ebay cloud team 

本文共5115字,预计阅读时间13分钟

更多干货请关注“eBay技术荟”公众号




背景

问题始于eBay Rheos Streaming团队的工程师发现他们的Kafka服务在有些时候follower追不上leader的数据。这种情况通常发生在某个kafka broker down或者partittion reassgin(例如系统定期检查发现有超过两个partittion部署在同一个rack上,需要在不同rack的broker上保留一个副本来维持系统可用性)的时候,此时kafka集群中的其他broker需要将数据相互sync来保证每个partittion至少有三个副本。如图所示,当第三台node crash 或者需要partittion reassign时,第四台node的broker或者原来down掉的broker重新调度到新的节点,为了保证三副本,follower需要从其他broker的partition leader中读取数据,此时如果单个broker的leader很多,那么read的压力就会很大,与此同时,leader还需要向用户提供写请求,很多写请求是需要保证数据sync到所有副本才会返回成功,因此follower追不上数据可能会直接影响应用。


问题分析

根据问题的描述,我们知道在发生问题的时候,kafka的读写行为发生了变化,在原本处于稳定的顺序写IO行为中,插入了大量非线性的读操作。这些读IO来自不同partittion,可能分布在磁盘的任何sector上,这种乱序的插入有可能造成磁头的大量偏移。为了优化寻址操作,内核既不会简单地按请求接收次序,也不会立即将其提交给磁盘。相反,它会在提交前,先执行合并与排序的预操作,这种预操作可以极大地提高系统的整体性能。在内核中负责提交IO请求的子系统称为IO调度程序。IO调度程序将磁盘IO资源分配给系统中所有挂起的块IO请求,这种资源分配是通过将请求队列中挂起的请求进行合并和排序来完成的。它决定队列中的请求排列顺序以及在什么时候派发请求到块设备。如果IO经过IO调度程序仍然没有有效地优化调度顺序和次数,就会加大磁头移动的负担,造成IO性能下降。因此,IO调度程序对磁盘寻道有着重要影响。

于是我们怀疑Kafka这种短期特殊读写行为,让当前调度器对读的调度性能下降,读请求没有得到有效的IO资源分配和调度。但遗憾的是,从传统的iops,io throughput 和 io latency 等这些指标,我们很难观测到Block层中调度器的调度性能差异。例如我们能够看到读的throughput不高,以及iops上不去,但却并不知道这背后的原因。借于此前我们已经通过eBPF帮助用户排查和解决了网络上的一些问题,我们再次想到了通过eBPF来解决磁盘IO的问题。如果我们可以观测到每次调度层的IO的磁盘分布,也许可以找到一些蛛丝马迹。


这里先补充一些eBPF的相关知识:

eBPF的观测过程

eBPF在内核中实现了一个虚拟机和一组用户的指令集,用户通过指令集分别编写自己的内核态和用户态ebpf程序,通过内核调用将内核态程序attach到某个内核事件上(如tracepoint),对事件的处理结果通过eBPF特定的数据结构map或者ringbuffer等传递到用户态程序。eBPF程序成为云原生基础设施的一部分,部署在集群和节点中,生成和汇报节点相关的指标信息,最终汇总到监控和日志相关的存储中。这是eBPF在云原生环境中的整体数据处理流程。


相比传统的观测方法,eBPF的诊断和观测方式主要拥有以下两个显著优势:

1.所有运行在Kubernetes中的Pod或容器内的一切行为都需要与内核交互,而eBPF能够订阅各种内核事件,如文件读写,磁盘IO,网络传输等,能够监控内核的各种行为,提供更为底层和详细的观测数据,甚至针对不同的应用程序,开发者可以编写不同的eBPF程序抓取自身想要的数据,以此对应用性能调优和debug提供帮助。

2.eBPF的方式是无侵入式的,不需要修改和重启用户的应用程序,特别是在生产环境,不需要走繁琐的发布流程,高度保存了事故现场,也不需要重新编译和重启内核,可以直接开箱即用地快速部署on-demand eBPF程序验证想法,拿到第一手诊断信息。


社区已经有不少eBPF开源项目,如bcc,ebpf-exporter,bpftrace,cilium等。在bcc中提供了不少bio相关的检测工具,我们发现了叫biopattern的工具,可以展示disk IO pattern。通过找到一个出现问题的磁盘,运行biopattern结果如下:

能够看到,biopattern可以返回单位时间中对磁盘的随机IO和顺序IO的比例以及总的读写数量。在上述结果中,显然有大量的随机IO进入,使得整体的IO性能遭到下降,在顺序IO升高的情况下,读写的bytes高很多,但当随机IO达到100%时,完成的读写bytes和counts都大幅下降。这说明,当前的biopattern使得磁盘整体的读写性能降低了。


但这个工具仍有几个不足:

1. 尽管知道随机IO的比例,但依然不清楚磁盘的分布情况;

2. 结果显示不够直观,需要同时结合多个数据观测;

3. biopattern是统计block_rq_complete tracepoint拿到的数据,缺少不同层次的比较。


于是我们改写了biopattern的代码,通过计算前后进入tracepoint时的sector差值,再取2的对数统计到不同slot中,核心代码如下:

改进的思路在于,原始的biopattern很难比较两个相同random百分比的情况下的性能差异,以及在random情况下的具体磁盘落点分布。通过计算每一次请求完成时的磁头偏移,得到磁盘整体的IO分布,这更加接近IO pattern的定义,能根据结果看到更加详细的IO变化。


为了让数据更容易被收集和可视化,我们借助于ebpf_exporter工具。在编写完ebpf程序之后,只需要编写简单的yaml文件,自定义展示的数据类型,ebpf_exporter就能在集群中不断收集到节点的磁盘IO数据并发送到promethues:

如果理解了eBPF的基本原理,那么整个eBPF程序开发和发布过程都非常顺滑流畅,真正做到了对应用和节点的无侵入,比编写kernel module等其他观测方式要轻量地多,展示出了eBPF强大的应用潜力。


在Grafana中我们将刚才通过ebpf_exporter收集到的数据通过热力图的方式展现出来:

在这个图中,横轴是时间,纵轴表示每一个slot,颜色越偏向红色,表示某个时间段内落在这个slot的数量越多。显然,slot的值越大,偏移量越大。


既然已经发现在问题发生的时候,用户的IO行为发生了变化,为了验证当前的IO调度程序是否能够很好的处理这种状况,我们给所在结点部署了新的biopattern并抓取了上述数据。Tess (eBay kubernetes 项目)线上集群已经升级到了5.4的kernel,kafka集群所在节点默认使用了mq-deadline[6]的IO调度器,官方推荐的hdd磁盘调度器为mq-deadline与bfq[4],两者都对hdd磁盘性能有着不少优化,都是基于多核多队列框架的IO调度器[2,3]。于是我们尝试将调度器切换到bfq[7],看一看各项metrics的变化。


如上图所示,在17:00 到17:50之间,我们切换到了bfq调度器,在17:50到18:10分,我们切换到了mq-deadline,随后继续切换到bfq,以此来纵向对比。可以看到在切换到bfq期间,尽管有一些IO落到了不同的slot,但大多数都集中在0,也即线性的sectors。但是在mq-deadline调度期间,0 slot的数量下降明显,并且  4-8 和 24-28的slot数量也有所增加。相同的数据我们还可查看p75的分布图:

在切换到mq-deadline期间,p75也有显著上升,这说明在这期间,调度器没有很好的将不同IO分配地足够线性,这加大了磁盘的寻道负担。从其他传统性能指标可以看到此时IO读的性能有所下降:

如前文所述,仅通过一个观测点拿到的数据缺乏纵向的解剖能力,于是通过在kafka集群执行blktrace, 并统计IO的执行过程,可以知道大量的IO都会经过以下几个阶段:

可见kafka的读和写都经过了Q-G-I-D-C的阶段,当然有些IO还有其他阶段,例如bio太大需要做切分的X阶段等,但总体来讲,block:block_bio_queue(Q: 将bio请求入队) -> block:block_getrq (G: 拿到一个空的request) -> block:block_rq_insert(I: 将request插入队列) -> block:block_rq_issue(D: request开始向设备驱动发起请求) -> block:block_rq_complete(C: 设备驱动完成IO请求) 的过程能够代表绝大部分的IO过程。于是我们在block:block_rq_complete之外,加入了block:block_bio_queue和block:block_rq_insert 两个观测点,前者用来观测从bio进入到Block层的初始状态,后者用来观察IO进入Scheduler层并插入request时的状态。与此同时,我们再分别给出各自观测点的IO数量和p75分布来协助观测,以此拿到整个IO过程较为完整的biopattern:


问题解决与后续

我们帮助用户部署了上述观测工具,并修改了所有kafka集群的调度器,在运行了一段时间之后,用户发现由读性能带来的问题大幅减少,follower可以较为快速的追上数据。并且由于观测了不同阶段的IO数量,我们还帮助用户发现了另一种特殊case,kafka节点IO不够但bio数量不足的问题,最后通过更改kafka读的吞吐量解决。


尽管问题解决,但我们依然需要为客户解释为何bfq比mq-deadlone能够更好应对大量随机读带来的性能问题。于是我们翻看了bfq和mq-deadline各自的代码和文档,并模拟出类似的场景,还原比较了两者之间的性能差异。以下是这部分的一些报告总结。


Mq-deadline[6]是deadline的多队列架构版本,本身和deadline调度器相似,它分别提供了读和写的队列,并设置了不同的过期时间:

static const int read_expire = HZ / 2;  /* max time before a read is submitted. */

static const int write_expire = 5 * HZ; /* ditto for writes, these limits are SOFT! */

它还为写IO设置了最大饥饿数来保证bio写不被饿死:

static const int writes_starved = 2;   /* max times reads can starve a write */

mq-deadline的策略非常简单,它按照FIFO的顺序派发IO,并通过红黑树快速找到合适的插入和合并的位置:

struct rb_root sort_list[2];

struct list_head fifo_list[2];


sort_list 用于排序和合并,fifo_list用于派发请求,而在派发之前,mq-deadline不但设置了过期时间,而且会优先让read先派发,这也是为什么要给write设置最大饥饿数:

所以mq-deadline就是一个具有merge和sort的fifo 队列,并分别给read和write队列都设置了不同的deadline以保证不被饿死,尤其对read做了一定的优化,优先让read先调度。那既然如此,为何随机读在与顺序写的竞争过程中依然性能没有bfq好呢?我们再看看bfq的实现。


bfq[7]来源于cfq,与此不同的是,cfq是按照时间片的公平调度策略,即给不同的队列设置时间片来保证绝对的公平。但这种调度策略的性能会比较差,因为很显然顺序IO比随机IO在相同时间内访问的磁道要高得多,所以顺序IO可能在cfq中读写更多的磁盘。于是在bfq中,不再按照时间片分配调度,而是分配budget,这里的budget通过sectors来计算,即给不同的队列分配不同的sectors,当当前队列消耗完分配的sectors,就切换到其他的队列,而每个队列又有自己的weight来决定调度的先后顺序,如图所示:

这种由基于时间到基于服务量的变化,让bfq可以根据需要在队列之间分配合适的吞吐量,在throughput波动或者磁盘设备内部排队的情况中不会失真。bfq使用名为B-WF2Q+[5]的中间调度器来给队列分配budget和weight从而控制整个调度策略,代码位置在block/bfq-wf2q.c.


bfq比较复杂,简单来说,bfq有以下一些规则:

  • 每一个正在磁盘上处理IO的计算单元(process)都有一个权重(weight)和队列(queue)

  • 任何时候设备只会有一个队列独占访问,访问模型是根据sectors计算出的budget. 每一次请求的派发(dispatch)都会让budget减小。队列只会在三种情况下让出调度资源:1)队列用完budget, 2) 队列为空,3)队列budget超时。budget超时是为了防止部分缓慢的随机IO长期占用磁盘引起整体吞吐量大幅下降。

  • 当同时有多个process竞争并且具备相同的weight,bfq会选择throughput更高的process调度。它根据这些process在曾经运行的过程中磁盘的空闲程度来判断。

  • bfq默认开启低延时模式(在/sys/block//queue/iosched/low_latency中可以配置), 这时bfq会启发式的检测交互式和软实时应用,从而减小他们的延时。最简单的减小延时方法便是提高他们的weight使得优先被调度。检测的代码实现在bfq_bfqq_softrt_next_start函数中。

  • bfq使用EQM(Early Queue Merge) 机制来进行队列的合并和插入,从而能够让随机IO更加线性,提高吞吐。 


以上只是列出了bfq的大体规则和框架,实际有更多细节,此处不再赘述,相关代码和文档位置在block/bfq-iosched.c/和Documentation/block/bfq-iosched.rst,可以自行查阅。


比较bfq和mq-deadline的各自实现,尽管mq-deadline在竞争的过程中给于read更高的优先级,并且过期的时间也比write更短,但mq-deadline依然是基于时间在分配读与写,并且也没有基于random IO做更多的优化,本质上是以总体吞吐量为第一位来设计的调度器。而bfq则是基于服务量来分配,使得随机IO可以获得更加公平的调度机会,而且bfq对随机IO和软实时应用有特殊的优化,从设计理念上,bfq就牺牲了部分性能,以求在吞吐量和响应速度之间找到更好的平衡。


为了比较实际的运行情况,我们在开发环境中模拟了用户的case,分别启动了一个顺序写的Buffer IO和一个随机读的Direct IO,以此比较两种调度器的读写情况,可以发现bfq牺牲了顺序写的性能,以此来满足更多随机读的需求:

作为对照组,在没有竞争,单独进行顺序写和随机读的情况下,两者的差距并不大,mq-deadline的顺序性能要更高于bfq:

由此可以理解为,在kafka的特殊case中,一方面,bfq能够比mq-deadline提供给follower读取leader数据更多的IO,另一方面,bfq能够对更为随机的IO提供更好的排序和合并策略,使得每一次的IO更为线性。但我们依然要注意,bfq的纯顺序的情况中,性能是不如mq-deadline,并且在竞争的过程中也会牺牲大量的顺序性能,这需要用户根据自己的应用情况进行权衡和取舍。毕竟,没有绝对完美的算法,算法都是tradeoff的艺术。


最后感谢Tess团队CY的技术指导以及Rheos团队Wang Yu的积极配合,共同解决这个问题。这是我们首次尝试利用eBPF解决kubernetes集群磁盘IO相关的问题,希望是一个好的开始,给用户和业界提供一些新的思路。


参考文献

1.https://ebpf.io/what-is-ebpf

2.https://kernel.dk/systor13-final18.pdf

3.https://lwn.net/Articles/552904/

4.https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/monitoring_and_managing_system_status_and_performance/setting-the-disk-scheduler_monitoring-and-managing-system-status-and-performance

5.Jon C.R. Bennett and H. Zhang, "Hierarchical Packet Fair Queueing Algorithms", IEEE/ACM Transactions on Networking, 5(5):675-689, Oct 1997. 

6.https://www.kernel.org/doc/Documentation/block/deadline-iosched.txt

7.https://www.kernel.org/doc/html/latest/block/bfq-iosched.html

往期推荐

eBay大数据安全合规系列 - EB级集群升级挑战和实践

eBay大数据安全合规系列 - 系统篇

Elasticsearch集群容量的自适应管理


点击“阅读原文”, 一键投递

              eBay大量优质职位虚席以待

                我们的身边,还缺一个你


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存