Mimir 源码分析(一):海量 series chunk 同时落盘带来的挑战
背景
熟悉 Prometheus 的 TSDB 头块的同学应该都比较清楚,一个时序的写入 chunk 落盘,一般有两种情况:
chunk 写满 120 个采样点,这个 chunk 在写满之前,会一直在内存中,直到写满 120 个样本后,才开始进行 mmap 映射和写磁盘。
由于同一个 chunk 不会跨多个 block, 所以即使 chunk 没有写满 120 个点,在写入数据点跨越 block 时间区间边界的时候(默认 2h 倍数),它也会被强制落盘。
问题产生
10 亿指标的冲击
我们以 20s 为间隔,对每一个 series 进行抓取,这样需要 40 分钟就达到 120 个样本,就会触发 chunk 落盘。当 series 少的时候,没有问题,看不出 I/O 对系统的冲击;但有大量的 series 的时候,每隔 40 分钟,就会有大批 chunks 同时写满,等待落盘,在 10 亿活跃指标的压力下,每 40min 单个写入节点(ingester) 产生 7G 的数据需要落盘,I/O 问题显而易见。
下图显示了每隔 40min,写入延时 P99 的数据,从 10ms,冲到 20s-60s。
上述 I/O 尖刺,是因为在 Prometheus TSDB 中,chunk 的落盘是同步的,这样会导致数据提交处于阻塞,直到所有 chunks 都完成落盘。
具体代码如下: prometheus\tsdb\chunks\head_chunks.go#L421
通过阅读源码,我们可以更深刻的理解 TSDB 的 chunk 落盘机制,当大量写满的 series 涌入后,快速的占用公用的 8M 写入缓存(cdm.writeBufferSize),最后再调用 cdm.flushBuffer()
刷新到磁盘,这个过程不断重复导致源源不断的落盘指令被执行着,耗费磁盘 I/O。
如果是你,怎么解决
针对上面的问题,我们一般会想到如下解决办法:
1、既然大量指标 40min 同时写满,那么把这个时间进行离散,是不是就能缓解瞬间压力?
时间离散应该能部分缓解写入压力,但只是减少了 chunk 同一时间落盘这个概率,如果必须有同一时间落盘的事件触发(样本跨越 block 边界),I/O 尖刺会再次发生。
2、既然公用 8M 的 写入 buffer 不够用,是不是提高 buffer 就能缓解呢?
提高 buffer 应该也能缓解压力,但是没有改变阻塞的本质。当 buffer 足够大,内存资源足够充分,应该也能缓解磁盘 I/O 尖刺的问题。
3、既然同步写入阻塞,是不是改成异步会更合适呢?
改成异步,听起来更合适。我们首先要明确一点,chunk 落盘的数据,已经是历史数据,这 120 个样本数据何时写到磁盘,只要不影响内存中的最新数据接收,就可以。但是需要解决资源同步的问题,比如当前待落盘的 chunk,需要及时返回当前 chunk 的 mmappedChunks 信息,给其他接口使用。
解决方案
下面我们来看看 Mimir 针对这个问题的解决办法。
引入 jitter
Mimir 做的第一个尝试,是把 120 个样本的硬编码配置,改成可以动态改变的配置 jitter,即每个 sereis 的 chunk,判断是否写满,不再严格按照 120 执行,加上一个动态变化的数字,您可以认为,它把海量的 series 的 chunk 落盘做到了时间离散化。
具体代码参考 prometheus\tsdb\head_append.go 的 addJitterToChunkEndTime 函数:
提交记录在这里,grafana/mimir#495 中引入 jitter 的方法,在当前版本还在使用,它可以部分缓解磁盘 I/O 高的问题。
下图显示了引入 jitter 后,磁盘 I/O 的尖刺数从 3 次变成 1 次。
可以看到,从 8 点到 10 点,不包含 8 点,有 3 个尖刺。10 点半以后,引入新版本,看到从 12 点到 14 点,只有 14 点 1 个尖刺。其中 12 点和 14 点的尖刺,是到达 2h 整点,还未写满 120 个 sample 的 chunk 落盘导致的。
之所以说,部分缓解磁盘 I/O 高的问题,因为 chunk 不能跨越 block 边界,导致时间窗口(默认 2h)到达边界后,所有 chunk 必须被截断。所以当 append 数据的时候,就需要根据这个集合, [2h 整点,s 写满 120 个 samples 的预期时间] 进行取小操作,限制 chunk 必须 2h 对齐的代码如下:
chunk 异步落盘
为了更好的解决磁盘 I/O 的问题,Mimir 修改了 TSDB 的落盘逻辑-把 chunk 落盘放在一个队列里完成,即实现了异步落盘,同时,查询不受影响。
异步落盘代码如下:
针对这部分代码,Mimir 已向 Prometheus 提了 PR,并合并了,具体参考 Prometheus代码。但这个功能默认关闭,如果你恰好有这个问题,不妨尝试修改配置,试用一下,对应修改配置如下:
(请注意,上面的 jitter 方案,当前还没有提交给 Prometheus 提交)。
为了验证 TSDB 的 chunks 落盘时延,我们对比的部署了两种场景:在 A 区的抓取模块开启异步落盘,在 B 和 C 区使用同步落盘代码(jitter 机制仍然存在)。下图显示了异步落盘队列,将 P99 的延时从 45s 降低到 3s。
【从 20 点到 22 点,绿色图线代表异步落盘机制引入后的尖刺情况,黄色图线代表只有 jitter 机制下的尖刺情况。】
总结
通过看 Mimir 对磁盘 I/O 的尖刺问题处理,我们可以有几点启发:
分布式系统,处理批量事件的时候,离散化处理,是一个简单有效的思路
磁盘并发写高的时候,如何进行异步化改造,是一个永恒的话题。
现在打开你的 Grafana 监控面板,查看 Prometheus 的磁盘 I/O 情况,如果也出现尖刺,可以考虑使用上面的方案,进行解决。
版权声明: 本文为 InfoQ 作者【Grafana 爱好者】的原创文章。
原文链接:【http://xie.infoq.cn/article/14ad0bd19a8256fabf3a4807d】。文章转载请联系作者。
评论