push 返回 204,查询却是空的
一句话总结
放进 Loki 的行先堆积在 ingester 的内存里,之后才作为数据块落到存储中。查询只会向 ingester 询问“最近几个小时”,所以用比这更早的时间戳放进去的行,会被接收,却看不到。
为什么需要它
有个团队正在做故障恢复。采集器停了两个小时,他们把这期间积压的文件恢复出来,推给了 Loki。push 全都返回了 204。可是在 Grafana 里打开那个时间段,什么也没有。
大家以为“push 在撒谎”。实际上,行已经好好地进了 ingester,只是查询没有去问ingester 那个区间而已。强制 flush,把数据块落到存储后,同样的查询就全部出来了。
工作原理
写入路径是这样的。
- push 到达后,ingester 把行追加到每个流打开着的数据块里。同样的内容也会写进 WAL,这样即使 ingester 挂了也能恢复。
- 数据块遇到三个条件之一就会关闭——达到目标大小(
chunk_target_size),一段时间没有新行(chunk_idle_period),或者打开得太久(max_chunk_age)。 - 关闭的数据块上传到对象存储,索引里记录该流和时间范围。
读取路径有两条。查询会问存储,如果是最近的区间,为了那些还没落到存储的行,也会问 ingester。决定“最近”的是 querier.query_ingesters_within,默认值是 3 小时。如果区间的终点比这更早,查询就会跳过 ingester。
所以用过去的时间戳放进去的行,会掉进这两条路径之间的缝隙。存储里还没有,ingester 里有,却没有人去问。等到时间过去,数据块自然关闭并上传,就开始能看到了——于是就有了“过了很久自己出现了”的说法。
这个设计不是失误。询问 ingester 的代价很高,如果连很旧的区间每次都问,所有查询都会变慢。只是在事后补推的恢复作业里,这个假设就不成立了。
运维上要记住三点。第一,如果为了恢复把过去的数据推了进去,就要等待 flush,或者强制执行。第二,过旧的数据可能一开始就会被拒绝——reject_old_samples 和 reject_old_samples_max_age 就是这个旋钮(本实验的设置特意把它关掉了)。第三,这个 Pod 里的 Loki 是所有组件都在一个进程里的单一二进制模式。在生产里,ingester、querier、compactor 分开运行时,同样的原理会原样适用,只是调用 flush 的位置和要盯的指标不同。
在现场相遇的样子
最常见的症状是“仪表板上最近 15 分钟好好的,昨天那段却是空的”。最近的区间由 ingester 回答,过去的区间由存储回答,如果上传到存储的路被堵住了,恰好就是这个样子。对象存储凭证过期的事故里,总是这样出现。
第二种是 ingester 重启之后的空白区间。WAL 重放结束之前,那个区间看起来是空的,重放结束就补上了。所以如果在滚动 ingester 的部署过程中收到“日志不见了”的反馈,先等几分钟才是对的。
第三种是数据块大小调优。目标大小设得太小,对象就会多得数不清,索引和查询都会变慢;设得太大,ingester 的内存就会变大,重启时能丢掉的东西也变多。
下一项实验要做什么
向 Pod 里的 Loki 分别放入最近的数据和五小时前的数据,亲眼看到第二份被接收了,查询里却是空的。在服务器的 /config 里找到决定那条界线的设置并确认,用数字记录强制 flush 前后,存储里的数据块文件数和查询结果有什么变化。最后亲手放入一条六小时前的行,让它变得可见。