记录Thanos Compactor反复出现invalid size的排查

472 阅读4分钟

背景

集群部署了一套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挂载点的问题。