背景
集群部署了一套thanos,在运行了一段时间后发现s3上空间占用比较高,于是考虑开启compactor服务来做合并以减少空间占用。但跑了几天后发现空间没有释放,排查后发现这个组件在完成3个次合并任务后就停止工作了,检查日志发现有报错,这里需要注意thanos的compact模块发生异常会停止工作,但服务不会退出
ts=2024-05-01T08:47:10.283195714Z caller=compact.go:527 level=error msg="critical error detected; halting" err="compaction: 2 errors: group 0@1262422153047559075: invalid result block /data/compact/0@1262422153047559075/01HZ9EEQBE8HSKWRPCAKMGTRS1: get all postings: decode postings: invalid size; group 0@2748437753707425374: invalid result block /data/compact/0@2748437753707425374/01HZ9EF01ER1WPZ8J2TAEWGN7D: get all postings: decode postings: invalid size"
排查过程
从thanos找了一下类似issue,thanos #6745这个提示是有数据块损坏,把提示有问题的block删除掉应该能恢复。 因为我中间重启过一下compactor,所以我怀疑从s3下载时被我停止服务导致的block数据不对,服务重启后用到了坏的数据块导致的。 于是我尝试先把compactor服务停止掉,再把compactor使用的PVC下的compact目录删除
# compactor数据目录下的结构
/data
/compact // block合并模块工作用到的目录
/downsample // 降采样模块工作使用到的目录
/meta-syncer // 从s3下载存放block里面的meta.json文件
经过此番操作,重启后的compactor会重新下载block数据再合并,通过观察日志发现之前合并失败的4个数据块这次合并及上传成功了。那么看来是确实是数据块下载烂了?
一波三折
可惜好景不长,合并成功一次后,后面一次的合并又出现了上面的报错,这次中途我并没有重启过服务。 于是我又重复了几次上面删除目录再重启服务的流程,发现同样的数据块合并有时成功,有时失败,难道是s3下载到的数据不稳定?
因为每次下载合并再到出报错这个过程太耗时了,于是这次我不再靠猜测和测试来验证了,直接从源码入手,看看到底是什么地方报的错误
首先根据报错invlid result block找到compact逻辑里面,我们查到报错是来自验证index数据这里
// pkg/compact/compact.go 负责block数据合并
func (cg *Group) compact(ctx context.Context, dir string, planner Planner, comp Compactor, blockDeletableChecker BlockDeletableChecker, compactionLifecycleCallback CompactionLifecycleCallback, errChan chan error) (shouldRerun bool, compID ulid.ULID, _ error) {
...这里省略了从s3下载并验证数据块的逻辑
level.Info(cg.logger).Log("msg", "compacted blocks", "new", compID,
"duration", time.Since(begin), "duration_ms", time.Since(begin).Milliseconds(), "overlapping_blocks", overlappingBlocks, "blocks", sourceBlockStr)
var stats block.HealthStats
// 报错来自下面的代码,这里的代码是在验证新生成的index是否正常
// Ensure the output block is valid.
err = tracing.DoInSpanWithErr(ctx, "compaction_verify_index", func(ctx context.Context) error {
stats, err = block.GatherIndexHealthStats(ctx, cg.logger, index, newMeta.MinTime, newMeta.MaxTime)
if err != nil {
return err
}
return stats.AnyErr()
})
if !cg.acceptMalformedIndex && err != nil {
return false, ulid.ULID{}, halt(errors.Wrapf(err, "invalid result block %s", bdir))
}
在检查index健康状态时会从index里面取Posting,这个Posting table用途还不懂,问GPT告诉我“posting list” 指的是索引中用于表示某个标签值含有哪些时间序列的列表。这在执行查询时非常重要,因为它帮助快速地定位到包含特定标签和值组合的时间序列数据。
// pkg/block/index.go
func GatherIndexHealthStats(ctx context.Context, logger log.Logger, fn string, minTime, maxTime int64) (stats HealthStats, err error) {
r, err := index.NewFileReader(fn)
if err != nil {
return stats, errors.Wrap(err, "open index file")
}
defer runutil.CloseWithErrCapture(&err, r, "gather index issue file reader")
key, value := index.AllPostingsKey()
// 这里是日志抛出err的地方
p, err := r.Postings(ctx, key, value)
if err != nil {
return stats, errors.Wrap(err, "get all postings")
}
...
这个Postings方法是来自prometheus的包, 根据日志报错我们知道是在处理posting table这里遇到了解码错误
// `go/pkg/mod/github.com/prometheus/prometheus@v0.48.1-0.20231212213830-d0c2d9c0b9cc/tsdb/index/index.go`
func (r *Reader) Postings(ctx context.Context, name string, values ...string) (Postings, error) {
if r.version == FormatV1 {
e, ok := r.postingsV1[name]
if !ok {
return EmptyPostings(), nil
}
res := make([]Postings, 0, len(values))
for _, v := range values {
postingsOff, ok := e[v]
if !ok {
continue
}
// Read from the postings table.
d := encoding.NewDecbufAt(r.b, int(postingsOff), castagnoliTable)
_, p, err := r.dec.Postings(d.Get())
if err != nil {
return nil, fmt.Errorf("decode postings: %w", err)
}
res = append(res, p)
}
return Merge(ctx, res...), nil
}
再继续看Posting里面的逻辑,可以找到最后出现invalid size出现在在Be32方法里面的长度校验逻辑这里,看起来确实是读到的数据异常才会进这个判断。
// go/pkg/mod/github.com/prometheus/prometheus@v0.48.1-0.20231212213830-d0c2d9c0b9cc/tsdb/encoding/encoding.go
func (d *Decbuf) Be32() uint32 {
if d.E != nil {
return 0
}
if len(d.B) < 4 {
d.E = ErrInvalidSize // 常量ErrInvalidSize = invalid size
return 0
}
x := binary.BigEndian.Uint32(d.B)
d.B = d.B[4:]
return x
}
既然这个验证逻辑来自prometheus,那我是不是应该看看prometheus这边是不是有类似问题,幸运的是搜索到了prometheus issue #10679 发现里面提及到了prometheus在NFS上使用会有一些问题。
巧了,我给compactor分配的PVC正好也是NFS。于是我把PVC挂载点直接换成了emptyDir类型。服务重新启动后,发现后续合并功能就没有出现上面的报错了,至此问题确定是NFS挂载点的问题。