// loadWAL 是 TSDB 启动时恢复数据的核心函数:通过回放 WAL 日志,恢复内存中时间序列、样本、直方图等数据
// 关键作用:保证 TSDB 重启后,未持久化到磁盘的内存数据不丢失(WAL 是 TSDB 数据一致性的核心保障)
// 参数详解:
// - r: wlog.Reader 类型,WAL 日志读取器(负责从磁盘读取 WAL 段文件内容)
// - syms: labels.SymbolTable 类型,标签符号表(将重复标签字符串编码为整数,减少 WAL 存储与解码开销)
// - multiRef: map[chunks.HeadSeriesRef]chunks.HeadSeriesRef 类型,序列 ID 映射表(处理「重复序列」场景:同一标签组的序列被多次创建,需映射到同一内存序列)
// - mmappedChunks: map[chunks.HeadSeriesRef][]*mmappedChunk 类型,内存映射块集合(已持久化到磁盘的 Chunk,通过内存映射加载,避免全量读入内存)
// - oooMmappedChunks: map[chunks.HeadSeriesRef][]*mmappedChunk 类型,乱序内存映射块集合(专门存储乱序样本的持久化 Chunk)
// - lastSegment: int 类型,最后一个 WAL 段的编号(用于设置序列的 WAL 过期时间,控制 WAL 段清理逻辑)
// 返回值:error 类型,回放过程中的错误(如 WAL 损坏、序列创建失败)
func (h *Head) loadWAL(r *wlog.Reader, syms *labels.SymbolTable, multiRef map[chunks.HeadSeriesRef]chunks.HeadSeriesRef, mmappedChunks, oooMmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk, lastSegment int) (err error) {
// -------------------------- 1. 初始化异常跟踪变量 --------------------------
// unknownSeriesRefs:线程安全集合,跟踪「被其他记录引用但尚未创建的序列 ID」(如样本引用了未加载的序列)
unknownSeriesRefs := &seriesRefSet{refs: make(map[chunks.HeadSeriesRef]struct{}), mtx: sync.Mutex{}}
// 以下原子变量(atomic.Uint64)用于统计不同类型记录的「未知序列引用数」(原子操作保证多协程计数准确)
var unknownSampleRefs atomic.Uint64 // 样本记录引用的未知序列数
var unknownExemplarRefs atomic.Uint64 // 示例(Exemplar)记录引用的未知序列数
var unknownHistogramRefs atomic.Uint64 // 直方图记录引用的未知序列数
var unknownMetadataRefs atomic.Uint64 // 元数据(如指标类型、帮助信息)记录引用的未知序列数
var unknownTombstoneRefs atomic.Uint64 // 墓碑(删除标记)记录引用的未知序列数
var mmapOverlappingChunks atomic.Uint64 // 重复序列的内存映射块重叠数(用于数据一致性检查:如两个重复序列的 Chunk 时间范围重叠)
// -------------------------- 2. 初始化并发处理组件 --------------------------
var (
wg sync.WaitGroup // 协程等待组:确保所有 WAL 处理协程完成后,再执行后续逻辑
concurrency = h.opts.WALReplayConcurrency // 回放并发度(配置项,默认与 CPU 核心数一致,充分利用多核资源)
processors = make([]walSubsetProcessor, concurrency) // 并发处理器数组:按序列 ID 分片处理,避免单协程瓶颈
exemplarsInput chan record.RefExemplar // 示例数据输入通道(单独处理示例,避免阻塞样本/直方图处理流程)
// 分片缓冲区:将样本/直方图按序列 ID 哈希分片,分发到不同处理器(负载均衡)
shards = make([][]record.RefSample, concurrency) // 样本分片缓冲区
histogramShards = make([][]histogramRecord, concurrency) // 直方图分片缓冲区
decoded = make(chan interface{}, 10) // 解码后数据通道(缓冲大小 10,减少解码协程阻塞)
decodeErr, seriesCreationErr error // 全局错误变量:解码错误、序列创建错误(需跨协程传递)
)
// -------------------------- 3. 延迟清理:处理异常退出场景 --------------------------
defer func() {
// 若发生 WAL 损坏错误(*wlog.CorruptionErr)或序列创建错误,需主动清理资源,避免协程泄漏
_, isCorruption := err.(*wlog.CorruptionErr)
if isCorruption || seriesCreationErr != nil {
// 关闭所有处理器并清空通道(确保处理器协程正常退出)
for i := 0; i < concurrency; i++ {
processors[i].closeAndDrain()
}
close(exemplarsInput) // 关闭示例输入通道
wg.Wait() // 等待所有协程完成,避免资源泄漏
}
}()
// -------------------------- 4. 启动样本并发处理器协程 --------------------------
wg.Add(concurrency) // 等待组计数器 += 并发度(每个处理器对应一个协程)
for i := 0; i < concurrency; i++ {
processors[i].setup() // 初始化处理器:创建输入/输出通道,分配缓冲区
// 为每个处理器启动独立协程:处理分配给它的序列分片
go func(wp *walSubsetProcessor) {
// 核心逻辑:处理分片内的样本/直方图,返回异常统计(未知序列、重叠块等)
missingSeries, unknownSamples, unknownHistograms, overlapping := wp.processWALSamples(h, mmappedChunks, oooMmappedChunks)
// 汇总异常统计:将本分片的未知序列合并到全局集合
unknownSeriesRefs.merge(missingSeries)
unknownSampleRefs.Add(unknownSamples) // 累加未知样本引用数
mmapOverlappingChunks.Add(overlapping) // 累加 Chunk 重叠数
unknownHistogramRefs.Add(unknownHistograms) // 累加未知直方图引用数
wg.Done() // 协程完成,等待组计数器 -= 1
}(&processors[i])
}
// -------------------------- 5. 启动示例(Exemplar)处理协程 --------------------------
wg.Add(1) // 等待组计数器 += 1(示例处理为独立协程)
exemplarsInput = make(chan record.RefExemplar, 300) // 带缓冲通道(大小 300),减少生产者阻塞
go func(input <-chan record.RefExemplar) {
missingSeries := make(map[chunks.HeadSeriesRef]struct{}) // 本协程内的未知序列集合
var err error
defer wg.Done() // 协程退出时,等待组计数器 -= 1
// 循环读取示例数据,直到通道关闭
for e := range input {
// 检查示例关联的序列是否存在(通过序列 ID 从内存中查询)
ms := h.series.getByID(e.Ref)
if ms == nil {
unknownExemplarRefs.Inc() // 累加未知示例引用数
missingSeries[e.Ref] = struct{}{} // 暂存未知序列 ID
continue
}
// 向序列添加示例:仅忽略「乱序示例」错误(WAL 回放应按时间顺序,乱序为异常但不阻断流程)
err = h.exemplars.AddExemplar(
ms.labels(), // 序列标签(用于示例关联)
exemplar.Exemplar{Ts: e.T, Value: e.V, Labels: e.Labels}, // 示例数据(时间戳、值、标签)
)
// 若错误不是「乱序示例」,则打印告警日志(乱序示例在 WAL 回放中罕见,需关注)
if err != nil && !errors.Is(err, storage.ErrOutOfOrderExemplar) {
h.logger.Warn("Unexpected error when replaying WAL on exemplar record", "err", err)
}
}
unknownSeriesRefs.merge(missingSeries) // 合并本协程的未知序列到全局
}(exemplarsInput)
// 启动一个匿名协goroutine负责解码WAL日志记录并发送到decoded通道
go func() {
// 延迟关闭decoded通道,通知主流程解码已完成
defer close(decoded)
var err error
// 创建基于符号表的解码器(用于高效解码标签)
dec := record.NewDecoder(syms)
// 循环读取WAL中的每一条记录
for r.Next() {
// 根据记录类型选择对应的解码逻辑
switch dec.Type(r.Record()) {
case record.Series: // 处理序列记录
// 从对象池获取复用的切片(减少内存分配)
series := h.wlReplaySeriesPool.Get()[:0]
// 解码序列数据
series, err = dec.Series(r.Record(), series)
if err != nil {
// 包装为WAL损坏错误,包含具体位置信息
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode series: %w", err),
Segment: r.Segment(), // 损坏发生的段号
Offset: r.Offset(), // 段内偏移量
}
return // 解码失败,退出协程
}
// 将解码后的序列发送到处理通道
decoded <- series
case record.Samples: // 处理样本记录
samples := h.wlReplaySamplesPool.Get()[:0]
samples, err = dec.Samples(r.Record(), samples)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode samples: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- samples
case record.Tombstones: // 处理墓碑记录(删除标记)
tstones := h.wlReplaytStonesPool.Get()[:0]
tstones, err = dec.Tombstones(r.Record(), tstones)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode tombstones: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- tstones
case record.Exemplars: // 处理示例记录
exemplars := h.wlReplayExemplarsPool.Get()[:0]
exemplars, err = dec.Exemplars(r.Record(), exemplars)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode exemplars: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- exemplars
case record.HistogramSamples, record.CustomBucketsHistogramSamples: // 处理普通直方图记录
hists := h.wlReplayHistogramsPool.Get()[:0]
hists, err = dec.HistogramSamples(r.Record(), hists)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode histograms: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- hists
case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples: // 处理浮点直方图记录
hists := h.wlReplayFloatHistogramsPool.Get()[:0]
hists, err = dec.FloatHistogramSamples(r.Record(), hists)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode float histograms: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- hists
case record.Metadata: // 处理元数据记录(指标类型、帮助信息等)
meta := h.wlReplayMetadataPool.Get()[:0]
meta, err := dec.Metadata(r.Record(), meta)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode metadata: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- meta
default:
// 忽略未知类型的记录(兼容未来扩展或旧版本日志)
}
}
}()
// 记录按从旧到新的顺序回放
// 存储本流程中发现的未知序列ID
missingSeries := make(map[chunks.HeadSeriesRef]struct{})
// Outer标签用于跳出多层循环
Outer:
// 循环处理解码后的记录
for d := range decoded {
// 根据记录类型进行分发处理
switch v := d.(type) {
case []record.RefSeries: // 处理序列记录
for _, walSeries := range v {
// 根据WAL中的序列ID和标签获取或创建内存序列
// 最后一个参数false表示:若序列已存在则不更新标签
mSeries, created, err := h.getOrCreateWithID(walSeries.Ref, walSeries.Labels.Hash(), walSeries.Labels, false)
if err != nil {
seriesCreationErr = err // 记录序列创建错误
break Outer // 跳出外层循环,终止处理
}
// 更新全局最大序列ID,确保后续创建的ID不重复
if chunks.HeadSeriesRef(h.lastSeriesID.Load()) < walSeries.Ref {
h.lastSeriesID.Store(uint64(walSeries.Ref))
}
// 处理重复序列(同一标签组的序列被多次创建)
if !created {
multiRef[walSeries.Ref] = mSeries.ref // 建立ID映射
// 为重复序列设置WAL过期时间,确保在后续检查点中保留
h.setWALExpiry(walSeries.Ref, lastSegment)
}
// 按序列ID哈希分片,分发到对应的处理器
idx := uint64(mSeries.ref) % uint64(concurrency)
processors[idx].input <- walSubsetProcessorInputItem{
walSeriesRef: walSeries.Ref,
existingSeries: mSeries,
}
}
// 将处理完的序列切片归还对象池
h.wlReplaySeriesPool.Put(v)
case []record.RefSample: // 处理样本记录
samples := v
// 获取全局最小有效时间(过滤过期样本)
minValidTime := h.minValidTime.Load()
// 将样本分批处理(每批最多5000个)
// 防止大批次样本导致内存占用过高
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化分片缓冲区(复用处理器的缓冲)
for i := 0; i < concurrency; i++ {
if shards[i] == nil {
shards[i] = processors[i].reuseBuf()
}
}
// 遍历样本,按序列ID分片
for _, sam := range samples[:m] {
// 过滤过期样本
if sam.T < minValidTime {
continue
}
// 替换重复序列的ID(使用映射表)
if r, ok := multiRef[sam.Ref]; ok {
sam.Ref = r
}
// 按序列ID哈希计算分片索引
mod := uint64(sam.Ref) % uint64(concurrency)
shards[mod] = append(shards[mod], sam)
}
// 将分片后的样本发送到对应处理器
for i := 0; i < concurrency; i++ {
if len(shards[i]) > 0 {
processors[i].input <- walSubsetProcessorInputItem{samples: shards[i]}
shards[i] = nil // 清空缓冲区,准备下次使用
}
}
// 处理剩余样本
samples = samples[m:]
}
// 将样本切片归还对象池
h.wlReplaySamplesPool.Put(v)
case []tombstones.Stone: // 处理墓碑记录
for _, s := range v {
for _, itv := range s.Intervals {
// 过滤过期的删除区间
if itv.Maxt < h.minValidTime.Load() {
continue
}
// 替换重复序列的ID
if r, ok := multiRef[chunks.HeadSeriesRef(s.Ref)]; ok {
s.Ref = storage.SeriesRef(r)
}
// 检查序列是否存在
if m := h.series.getByID(chunks.HeadSeriesRef(s.Ref)); m == nil {
unknownTombstoneRefs.Inc() // 统计未知序列引用
missingSeries[chunks.HeadSeriesRef(s.Ref)] = struct{}{}
continue
}
// 应用删除区间
h.tombstones.AddInterval(s.Ref, itv)
}
}
// 将墓碑切片归还对象池
h.wlReplaytStonesPool.Put(v)
case []record.RefExemplar: // 处理示例记录
for _, e := range v {
// 过滤过期示例
if e.T < h.minValidTime.Load() {
continue
}
// 替换重复序列的ID
if r, ok := multiRef[e.Ref]; ok {
e.Ref = r
}
// 发送到示例处理器通道
exemplarsInput <- e
}
// 将示例切片归还对象池
h.wlReplayExemplarsPool.Put(v)
case []record.RefHistogramSample: // 处理普通直方图记录
samples := v
minValidTime := h.minValidTime.Load()
// 分批处理直方图样本(每批最多5000个)
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化直方图分片缓冲区
for i := 0; i < concurrency; i++ {
if histogramShards[i] == nil {
histogramShards[i] = processors[i].reuseHistogramBuf()
}
}
// 遍历直方图样本,按序列ID分片
for _, sam := range samples[:m] {
// 过滤过期样本
if sam.T < minValidTime {
continue
}
// 替换重复序列的ID
if r, ok := multiRef[sam.Ref]; ok {
sam.Ref = r
}
// 按序列ID哈希计算分片索引
mod := uint64(sam.Ref) % uint64(concurrency)
// 转换为统一的histogramRecord格式
histogramShards[mod] = append(histogramShards[mod], histogramRecord{
ref: sam.Ref,
t: sam.T,
h: sam.H,
})
}
// 将分片后的直方图样本发送到对应处理器
for i := 0; i < concurrency; i++ {
if len(histogramShards[i]) > 0 {
processors[i].input <- walSubsetProcessorInputItem{histogramSamples: histogramShards[i]}
histogramShards[i] = nil // 清空缓冲区
}
}
// 处理剩余样本
samples = samples[m:]
}
// 将直方图切片归还对象池
h.wlReplayHistogramsPool.Put(v)
case []record.RefFloatHistogramSample: // 处理浮点直方图记录
samples := v
minValidTime := h.minValidTime.Load()
// 分批处理浮点直方图样本(每批最多5000个)
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化直方图分片缓冲区
for i := 0; i < concurrency; i++ {
if histogramShards[i] == nil {
histogramShards[i] = processors[i].reuseHistogramBuf()
}
}
// 遍历浮点直方图样本,按序列ID分片
for _, sam := range samples[:m] {
// 过滤过期样本
if sam.T < minValidTime {
continue
}
// 替换重复序列的ID
if r, ok := multiRef[sam.Ref]; ok {
sam.Ref = r
}
// 按序列ID哈希计算分片索引
mod := uint64(sam.Ref) % uint64(concurrency)
// 转换为统一的histogramRecord格式
histogramShards[mod] = append(histogramShards[mod], histogramRecord{
ref: sam.Ref,
t: sam.T,
fh: sam.FH,
})
}
// 将分片后的浮点直方图样本发送到对应处理器
for i := 0; i < concurrency; i++ {
if len(histogramShards[i]) > 0 {
processors[i].input <- walSubsetProcessorInputItem{histogramSamples: histogramShards[i]}
histogramShards[i] = nil // 清空缓冲区
}
}
// 处理剩余样本
samples = samples[m:]
}
// 将浮点直方图切片归还对象池
h.wlReplayFloatHistogramsPool.Put(v)
case []record.RefMetadata: // 处理元数据记录
for _, m := range v {
// 替换重复序列的ID
if r, ok := multiRef[m.Ref]; ok {
m.Ref = r
}
// 查找序列
s := h.series.getByID(m.Ref)
if s == nil {
unknownMetadataRefs.Inc() // 统计未知序列引用
missingSeries[m.Ref] = struct{}{}
continue
}
// 更新序列元数据(类型、单位、帮助信息)
s.meta = &metadata.Metadata{
Type: record.ToMetricType(m.Type),
Unit: m.Unit,
Help: m.Help,
}
}
// 将元数据切片归还对象池
h.wlReplayMetadataPool.Put(v)
default:
// 遇到未知类型的记录,触发panic(通常表示代码与数据版本不兼容)
panic(fmt.Errorf("unexpected decoded type: %T", d))
}
}
// 将本流程中发现的未知序列合并到全局集合
unknownSeriesRefs.merge(missingSeries)
// 检查解码过程是否出错
if decodeErr != nil {
return decodeErr
}
// 检查序列创建过程是否出错
if seriesCreationErr != nil {
// 清空decoded通道,避免阻塞解码协程
for range decoded {
}
return seriesCreationErr
}
// 通知所有处理器终止,并等待它们完成
for i := 0; i < concurrency; i++ {
processors[i].closeAndDrain()
}
// 关闭示例输入通道
close(exemplarsInput)
// 等待所有处理协程完成
wg.Wait()
// 检查WAL读取是否有错误
if err := r.Err(); err != nil {
return fmt.Errorf("read records: %w", err)
}
// 若存在未知序列引用,打印告警日志并更新监控指标
if unknownSampleRefs.Load()+unknownExemplarRefs.Load()+unknownHistogramRefs.Load()+unknownMetadataRefs.Load()+unknownTombstoneRefs.Load() > 0 {
h.logger.Warn(
"Unknown series references",
"series", unknownSeriesRefs.count(),
"samples", unknownSampleRefs.Load(),
"exemplars", unknownExemplarRefs.Load(),
"histograms", unknownHistogramRefs.Load(),
"metadata", unknownMetadataRefs.Load(),
"tombstones", unknownTombstoneRefs.Load(),
)
// 更新监控指标(仅在值大于0时)
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownSeriesRefs.count()), "series")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownSampleRefs.Load()), "samples")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownExemplarRefs.Load()), "exemplars")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownHistogramRefs.Load()), "histograms")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownMetadataRefs.Load()), "metadata")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownTombstoneRefs.Load()), "tombstones")
}
// 若存在内存映射块重叠,打印信息日志
if count := mmapOverlappingChunks.Load(); count > 0 {
h.logger.Info("Overlapping m-map chunks on duplicate series records", "count", count)
}
// 所有处理完成,返回nil表示成功
return nil
// Copyright 2021 The Prometheus Authors
// 版权所有 2021 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// 根据Apache许可证2.0版(以下简称"许可证")授权;
// you may not use this file except in compliance with the License.
// 除非遵守许可证,否则您不得使用此文件。
// You may obtain a copy of the License at
// 您可以在以下位置获取许可证副本:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 除非适用法律要求或书面同意,否则根据许可证分发的软件
// 按"原样"分发,不附带任何明示或暗示的担保或条件。
// 请参阅许可证以了解管理权限和限制的具体语言。
package tsdb
// 声明所属包为tsdb(时序数据库)
import (
"errors"
"fmt"
"maps"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus" // Prometheus的监控指标库
"go.uber.org/atomic" // 原子操作库
"github.com/prometheus/prometheus/model/exemplar" // 样本相关模型
"github.com/prometheus/prometheus/model/histogram" // 直方图模型
"github.com/prometheus/prometheus/model/labels" // 标签模型
"github.com/prometheus/prometheus/model/metadata" // 元数据模型
"github.com/prometheus/prometheus/storage" // 存储接口
"github.com/prometheus/prometheus/tsdb/chunkenc" // 块编码
"github.com/prometheus/prometheus/tsdb/chunks" // 块相关
"github.com/prometheus/prometheus/tsdb/encoding" // 编码工具
tsdb_errors "github.com/prometheus/prometheus/tsdb/errors" // 自定义错误
"github.com/prometheus/prometheus/tsdb/fileutil" // 文件工具
"github.com/prometheus/prometheus/tsdb/record" // 记录相关
"github.com/prometheus/prometheus/tsdb/tombstones" // 墓碑(数据删除标记)
"github.com/prometheus/prometheus/tsdb/wlog" // 写日志(WAL相关)
)
// histogramRecord 结合了RefHistogramSample和RefFloatHistogramSample,以简化WAL重放。
// 关键点:用于统一处理两种直方图样本,减少重放逻辑的复杂性
type histogramRecord struct {
ref chunks.HeadSeriesRef // 系列引用
t int64 // 时间戳
h *histogram.Histogram // 直方图
fh *histogram.FloatHistogram // 浮点直方图
}
// seriesRefSet 用于跟踪系列引用的集合,带并发安全锁
type seriesRefSet struct {
refs map[chunks.HeadSeriesRef]struct{} // 存储系列引用的集合
mtx sync.Mutex // 互斥锁,保证并发安全
}
// merge 将另一个系列引用集合合并到当前集合中
func (s *seriesRefSet) merge(other map[chunks.HeadSeriesRef]struct{}) {
s.mtx.Lock()
defer s.mtx.Unlock()
maps.Copy(s.refs, other) // 使用maps.Copy高效合并
}
// count 返回集合中系列引用的数量
func (s *seriesRefSet) count() int {
s.mtx.Lock()
defer s.mtx.Unlock()
return len(s.refs)
}
// counterAddNonZero 当值大于0时,才向Prometheus计数器添加值
// 关键点:避免无效的0值更新,减少指标噪音
func counterAddNonZero(v *prometheus.CounterVec, value float64, lvs ...string) {
if value > 0 {
v.WithLabelValues(lvs...).Add(value)
}
}
3.3、第三部分:WAL 加载函数(loadWAL)—— 初始化部分
https://github.com/prometheus/prometheus/blob/v3.4.2/tsdb/head_wal.go#L78
// loadWAL 从WAL读取器加载数据,恢复Head的状态
// 参数说明:
// - r: WAL读取器
// - syms: 标签符号表,用于编码/解码标签
// - multiRef: 多引用映射,处理重复系列
// - mmappedChunks: 内存映射块
// - oooMmappedChunks: 乱序内存映射块
// - lastSegment: 最后一个段索引
func (h *Head) loadWAL(r *wlog.Reader, syms *labels.SymbolTable, multiRef map[chunks.HeadSeriesRef]chunks.HeadSeriesRef, mmappedChunks, oooMmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk, lastSegment int) (err error) {
// 跟踪被其他记录引用但缺失的系列记录数量
unknownSeriesRefs := &seriesRefSet{refs: make(map[chunks.HeadSeriesRef]struct{}), mtx: sync.Mutex{}}
// 跟踪引用未知系列的不同记录类型的数量,用于错误报告
var unknownSampleRefs atomic.Uint64
var unknownExemplarRefs atomic.Uint64
var unknownHistogramRefs atomic.Uint64
var unknownMetadataRefs atomic.Uint64
var unknownTombstoneRefs atomic.Uint64
// 跟踪具有重叠内存映射块的系列记录数量
var mmapOverlappingChunks atomic.Uint64
// 启动工作线程,每个线程处理系列ID空间分区中的样本
var (
wg sync.WaitGroup // 等待组,用于同步工作线程
concurrency = h.opts.WALReplayConcurrency // 并发数,由配置决定
processors = make([]walSubsetProcessor, concurrency) // 处理器数组
exemplarsInput chan record.RefExemplar // exemplar输入通道
shards = make([][]record.RefSample, concurrency) // 样本分片
histogramShards = make([][]histogramRecord, concurrency) // 直方图分片
decoded = make(chan interface{}, 10) // 解码后的数据通道
decodeErr, seriesCreationErr error // 解码错误和系列创建错误
)
// 延迟函数:在函数退出时处理资源清理
defer func() {
// 若发生损坏错误或系列创建错误,确保终止所有工作线程后再退出
_, ok := err.(*wlog.CorruptionErr)
if ok || seriesCreationErr != nil {
for i := 0; i < concurrency; i++ {
processors[i].closeAndDrain()
}
close(exemplarsInput)
wg.Wait()
}
}()
// 启动并发的样本处理器工作线程
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
processors[i].setup() // 初始化处理器
go func(wp *walSubsetProcessor) {
// 处理WAL样本,返回缺失的系列、未知样本数等
missingSeries, unknownSamples, unknownHistograms, overlapping := wp.processWALSamples(h, mmappedChunks, oooMmappedChunks)
unknownSeriesRefs.merge(missingSeries)
unknownSampleRefs.Add(unknownSamples)
mmapOverlappingChunks.Add(overlapping)
unknownHistogramRefs.Add(unknownHistograms)
wg.Done()
}(&processors[i])
}
// 启动exemplar处理工作线程
wg.Add(1)
exemplarsInput = make(chan record.RefExemplar, 300)
go func(input <-chan record.RefExemplar) {
missingSeries := make(map[chunks.HeadSeriesRef]struct{})
var err error
defer wg.Done()
for e := range input {
// 根据引用获取系列
ms := h.series.getByID(e.Ref)
if ms == nil {
unknownExemplarRefs.Inc()
missingSeries[e.Ref] = struct{}{}
continue
}
// 重放WAL时不应出现乱序exemplar,若非此类错误则记录警告
err = h.exemplars.AddExemplar(ms.labels(), exemplar.Exemplar{Ts: e.T, Value: e.V, Labels: e.Labels})
if err != nil && errors.Is(err, storage.ErrOutOfOrderExemplar) {
h.logger.Warn("重放WAL的exemplar记录时出现意外错误", "err", err)
}
}
unknownSeriesRefs.merge(missingSeries)
}(exemplarsInput)
3.4、第四部分:WAL 加载函数(loadWAL)—— 解码协程与多类型数据(series / samples / tombstones 等)处理分发
https://github.com/prometheus/prometheus/blob/v3.4.2/tsdb/head_wal.go#L155
// 启动解码 goroutine,将 WAL 记录解码并发送到 decoded 通道
go func() {
defer close(decoded) // 退出时关闭 decoded 通道
var err error
dec := record.NewDecoder(syms) // 创建记录解码器
for r.Next() { // 读取下一条 WAL 记录
switch dec.Type(r.Record()) { // 根据记录类型处理
case record.Series: // 系列记录
series := h.wlReplaySeriesPool.Get()[:0] // 从对象池获取系列切片
series, err = dec.Series(r.Record(), series) // 解码系列记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode series: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- series // 将解码后的系列发送到通道
case record.Samples: // 样本记录
samples := h.wlReplaySamplesPool.Get()[:0] // 从对象池获取样本切片
samples, err = dec.Samples(r.Record(), samples) // 解码样本记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode samples: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- samples // 将解码后的样本发送到通道
case record.Tombstones: // 墓碑记录(删除标记)
tstones := h.wlReplaytStonesPool.Get()[:0] // 从对象池获取墓碑切片
tstones, err = dec.Tombstones(r.Record(), tstones) // 解码墓碑记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode tombstones: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- tstones // 将解码后的墓碑发送到通道
case record.Exemplars: // Exemplar 记录
exemplars := h.wlReplayExemplarsPool.Get()[:0] // 从对象池获取 exemplar 切片
exemplars, err = dec.Exemplars(r.Record(), exemplars) // 解码 exemplar 记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode exemplars: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- exemplars // 将解码后的 exemplar 发送到通道
case record.HistogramSamples, record.CustomBucketsHistogramSamples: // 直方图记录
hists := h.wlReplayHistogramsPool.Get()[:0] // 从对象池获取直方图切片
hists, err = dec.HistogramSamples(r.Record(), hists) // 解码直方图记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode histograms: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- hists // 将解码后的直方图发送到通道
case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples: // 浮点直方图记录
hists := h.wlReplayFloatHistogramsPool.Get()[:0] // 从对象池获取浮点直方图切片
hists, err = dec.FloatHistogramSamples(r.Record(), hists) // 解码浮点直方图记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode float histograms: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- hists // 将解码后的浮点直方图发送到通道
case record.Metadata: // 元数据记录
meta := h.wlReplayMetadataPool.Get()[:0] // 从对象池获取元数据切片
meta, err := dec.Metadata(r.Record(), meta) // 解码元数据记录
if err != nil {
decodeErr = &wlog.CorruptionErr{ // 记录损坏错误
Err: fmt.Errorf("decode metadata: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decoded <- meta // 将解码后的元数据发送到通道
default: // 未知类型,忽略
// Noop.
}
}
}()
// 记录总是从最旧到最新重放
missingSeries := make(map[chunks.HeadSeriesRef]struct{})
Outer:
for d := range decoded { // 从 decoded 通道读取解码后的数据
switch v := d.(type) { // 根据数据类型处理
case []record.RefSeries: // 系列数据
for _, walSeries := range v { // 遍历每个系列
// 获取或创建具有指定 ID 的系列
mSeries, created, err := h.getOrCreateWithID(walSeries.Ref, walSeries.Labels.Hash(), walSeries.Labels, false)
if err != nil {
seriesCreationErr = err // 记录系列创建错误
break Outer
}
// 更新最后一个系列 ID
if chunks.HeadSeriesRef(h.lastSeriesID.Load()) < walSeries.Ref {
h.lastSeriesID.Store(uint64(walSeries.Ref))
}
if !created { // 如果系列已存在,记录映射关系
multiRef[walSeries.Ref] = mSeries.ref
// 为重复系列设置 WAL 过期时间,使其在后续检查点中保留
h.setWALExpiry(walSeries.Ref, lastSegment)
}
// 将系列分配给对应的处理器
idx := uint64(mSeries.ref) % uint64(concurrency)
processors[idx].input <- walSubsetProcessorInputItem{walSeriesRef: walSeries.Ref, existingSeries: mSeries}
}
h.wlReplaySeriesPool.Put(v) // 将系列切片放回对象池
case []record.RefSample: // 样本数据
samples := v
minValidTime := h.minValidTime.Load() // 获取最小有效时间戳
// 将样本分成 5000 个或更少的块,避免大内存占用
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化每个分片的缓冲区
for i := 0; i < concurrency; i++ {
if shards[i] == nil {
shards[i] = processors[i].reuseBuf()
}
}
// 将样本分配到对应的分片
for _, sam := range samples[:m] {
if sam.T < minValidTime {
continue // 忽略早于最小有效时间的样本
}
if r, ok := multiRef[sam.Ref]; ok { // 处理重复引用
sam.Ref = r
}
mod := uint64(sam.Ref) % uint64(concurrency) // 计算分片索引
shards[mod] = append(shards[mod], sam) // 添加到分片
}
// 将每个分片的样本发送到对应的处理器
for i := 0; i < concurrency; i++ {
if len(shards[i]) > 0 {
processors[i].input <- walSubsetProcessorInputItem{samples: shards[i]}
shards[i] = nil // 清空分片
}
}
samples = samples[m:] // 处理剩余样本
}
h.wlReplaySamplesPool.Put(v) // 将样本切片放回对象池
case []tombstones.Stone: // 墓碑数据
for _, s := range v { // 遍历每个墓碑
for _, itv := range s.Intervals { // 遍历每个时间间隔
if itv.Maxt < h.minValidTime.Load() {
continue // 忽略早于最小有效时间的墓碑
}
// 处理重复引用
if r, ok := multiRef[chunks.HeadSeriesRef(s.Ref)]; ok {
s.Ref = storage.SeriesRef(r)
}
// 检查系列是否存在
if m := h.series.getByID(chunks.HeadSeriesRef(s.Ref)); m == nil {
unknownTombstoneRefs.Inc() // 累加未知墓碑引用计数
missingSeries[chunks.HeadSeriesRef(s.Ref)] = struct{}{} // 记录缺失的系列
continue
}
h.tombstones.AddInterval(s.Ref, itv) // 添加时间间隔到墓碑
}
}
h.wlReplaytStonesPool.Put(v) // 将墓碑切片放回对象池
case []record.RefExemplar: // Exemplar 数据
for _, e := range v { // 遍历每个 exemplar
if e.T < h.minValidTime.Load() {
continue // 忽略早于最小有效时间的 exemplar
}
// 处理重复引用
if r, ok := multiRef[e.Ref]; ok {
e.Ref = r
}
exemplarsInput <- e // 发送到 exemplar 处理通道
}
h.wlReplayExemplarsPool.Put(v) // 将 exemplar 切片放回对象池
case []record.RefHistogramSample: // 直方图数据
samples := v
minValidTime := h.minValidTime.Load() // 获取最小有效时间戳
// 将直方图样本分成 5000 个或更少的块
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化每个分片的缓冲区
for i := 0; i < concurrency; i++ {
if histogramShards[i] == nil {
histogramShards[i] = processors[i].reuseHistogramBuf()
}
}
// 将直方图样本分配到对应的分片
for _, sam := range samples[:m] {
if sam.T < minValidTime {
continue // 忽略早于最小有效时间的样本
}
if r, ok := multiRef[sam.Ref]; ok { // 处理重复引用
sam.Ref = r
}
mod := uint64(sam.Ref) % uint64(concurrency) // 计算分片索引
histogramShards[mod] = append(histogramShards[mod], histogramRecord{ref: sam.Ref, t: sam.T, h: sam.H}) // 添加到分片
}
// 将每个分片的直方图样本发送到对应的处理器
for i := 0; i < concurrency; i++ {
if len(histogramShards[i]) > 0 {
processors[i].input <- walSubsetProcessorInputItem{histogramSamples: histogramShards[i]}
histogramShards[i] = nil // 清空分片
}
}
samples = samples[m:] // 处理剩余样本
}
h.wlReplayHistogramsPool.Put(v) // 将直方图切片放回对象池
case []record.RefFloatHistogramSample: // 浮点直方图数据
samples := v
minValidTime := h.minValidTime.Load() // 获取最小有效时间戳
// 将浮点直方图样本分成 5000 个或更少的块
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化每个分片的缓冲区
for i := 0; i < concurrency; i++ {
if histogramShards[i] == nil {
histogramShards[i] = processors[i].reuseHistogramBuf()
}
}
// 将浮点直方图样本分配到对应的分片
for _, sam := range samples[:m] {
if sam.T < minValidTime {
continue // 忽略早于最小有效时间的样本
}
if r, ok := multiRef[sam.Ref]; ok { // 处理重复引用
sam.Ref = r
}
mod := uint64(sam.Ref) % uint64(concurrency) // 计算分片索引
histogramShards[mod] = append(histogramShards[mod], histogramRecord{ref: sam.Ref, t: sam.T, fh: sam.FH}) // 添加到分片
}
// 将每个分片的浮点直方图样本发送到对应的处理器
for i := 0; i < concurrency; i++ {
if len(histogramShards[i]) > 0 {
processors[i].input <- walSubsetProcessorInputItem{histogramSamples: histogramShards[i]}
histogramShards[i] = nil // 清空分片
}
}
samples = samples[m:] // 处理剩余样本
}
h.wlReplayFloatHistogramsPool.Put(v) // 将浮点直方图切片放回对象池
case []record.RefMetadata: // 元数据
for _, m := range v { // 遍历每个元数据
if r, ok := multiRef[m.Ref]; ok { // 处理重复引用
m.Ref = r
}
s := h.series.getByID(m.Ref) // 获取对应的系列
if s == nil { // 系列不存在
unknownMetadataRefs.Inc() // 累加未知元数据引用计数
missingSeries[m.Ref] = struct{}{} // 记录缺失的系列
continue
}
// 更新系列的元数据
s.meta = &metadata.Metadata{
Type: record.ToMetricType(m.Type),
Unit: m.Unit,
Help: m.Help,
}
}
h.wlReplayMetadataPool.Put(v) // 将元数据切片放回对象池
default: // 未知类型, panic
panic(fmt.Errorf("unexpected decoded type: %T", d))
}
}
unknownSeriesRefs.merge(missingSeries) // 合并缺失的系列
// 检查解码错误
if decodeErr != nil {
return decodeErr
}
// 检查系列创建错误
if seriesCreationErr != nil {
// 清空通道以解除 goroutine 的阻塞
for range decoded {
}
return seriesCreationErr
}
// 向每个工作线程发送终止信号,并等待它们关闭输出通道
for i := 0; i < concurrency; i++ {
processors[i].closeAndDrain()
}
close(exemplarsInput) // 关闭 exemplar 输入通道
wg.Wait() // 等待所有工作线程完成
// 检查读取错误
if err := r.Err(); err != nil {
return fmt.Errorf("read records: %w", err)
}
// 报告未知引用的统计信息
if unknownSampleRefs.Load()+unknownExemplarRefs.Load()+unknownHistogramRefs.Load()+unknownMetadataRefs.Load()+unknownTombstoneRefs.Load() > 0 {
h.logger.Warn(
"Unknown series references",
"series", unknownSeriesRefs.count(),
"samples", unknownSampleRefs.Load(),
"exemplars", unknownExemplarRefs.Load(),
"histograms", unknownHistogramRefs.Load(),
"metadata", unknownMetadataRefs.Load(),
"tombstones", unknownTombstoneRefs.Load(),
)
// 更新 Prometheus 指标
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownSeriesRefs.count()), "series")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownSampleRefs.Load()), "samples")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownExemplarRefs.Load()), "exemplars")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownHistogramRefs.Load()), "histograms")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownMetadataRefs.Load()), "metadata")
counterAddNonZero(h.metrics.walReplayUnknownRefsTotal, float64(unknownTombstoneRefs.Load()), "tombstones")
}
// 报告重叠的内存映射块
if count := mmapOverlappingChunks.Load(); count > 0 {
h.logger.Info("Overlapping m-map chunks on duplicate series records", "count", count)
}
return nil
}
https://github.com/prometheus/prometheus/blob/v3.4.2/tsdb/head_wal.go#L483
// resetSeriesWithMMappedChunks 仅在 WAL 重放期间使用,用于重置系列的内存映射块
func (h *Head) resetSeriesWithMMappedChunks(mSeries *memSeries, mmc, oooMmc []*mmappedChunk, walSeriesRef chunks.HeadSeriesRef) (overlapped bool) {
// 检查当前系列引用与 WAL 中的系列引用是否一致,不一致可能存在重复系列
if mSeries.ref != walSeriesRef {
// 当新旧内存映射块都存在时,检查时间范围是否重叠
if len(mSeries.mmappedChunks) > 0 && len(mmc) > 0 {
if overlapsClosedInterval(
// 旧块的最小时间(取第一个块的 minTime)
mSeries.mmappedChunks[0].minTime,
// 旧块的最大时间(取最后一个块的 maxTime)
mSeries.mmappedChunks[len(mSeries.mmappedChunks)-1].maxTime,
// 新块的最小时间(取第一个块的 minTime)
mmc[0].minTime,
// 新块的最大时间(取最后一个块的 maxTime)
mmc[len(mmc)-1].maxTime,
) {
// 记录重叠日志,包含系列标签、新旧引用及时间范围
h.logger.Debug(
"M-mapped chunks overlap on a duplicate series record",
"series", mSeries.labels().String(),
"oldref", mSeries.ref,
"oldmint", mSeries.mmappedChunks[0].minTime,
"oldmaxt", mSeries.mmappedChunks[len(mSeries.mmappedChunks)-1].maxTime,
"newref", walSeriesRef,
"newmint", mmc[0].minTime,
"newmaxt", mmc[len(mmc)-1].maxTime,
)
overlapped = true // 标记为重叠状态
}
}
}
// 更新块指标:新增块数量(主块 + 乱序块)
h.metrics.chunksCreated.Add(float64(len(mmc) + len(oooMmc)))
// 更新块指标:移除的旧主块数量
h.metrics.chunksRemoved.Add(float64(len(mSeries.mmappedChunks)))
// 更新块指标:总块数变化(新增 - 移除的旧主块)
h.metrics.chunks.Add(float64(len(mmc) + len(oooMmc) - len(mSeries.mmappedChunks)))
// 若存在旧乱序块,更新指标:移除旧乱序块数量
if mSeries.ooo != nil {
h.metrics.chunksRemoved.Add(float64(len(mSeries.ooo.oooMmappedChunks)))
h.metrics.chunks.Sub(float64(len(mSeries.ooo.oooMmappedChunks)))
}
// 用新的主内存映射块替换旧块
mSeries.mmappedChunks = mmc
// 处理乱序块:若新乱序块为空,清空系列的乱序字段;否则初始化或更新乱序块
if len(oooMmc) == 0 {
mSeries.ooo = nil
} else {
if mSeries.ooo == nil {
mSeries.ooo = &memSeriesOOOFields{} // 初始化乱序字段结构体
}
// 用新乱序块覆盖旧乱序块
*mSeries.ooo = memSeriesOOOFields{oooMmappedChunks: oooMmc}
}
// 缓存主块的最大时间,用于后续样本追加时快速判断是否有效(跳过无效追加)
if len(mmc) == 0 {
mSeries.mmMaxTime = math.MinInt64 // 无主块时设为最小时间
} else {
mSeries.mmMaxTime = mmc[len(mmc)-1].maxTime // 取最后一个主块的最大时间
// 更新全局主块的时间范围(最小和最大时间)
h.updateMinMaxTime(mmc[0].minTime, mSeries.mmMaxTime)
}
// 若存在新乱序块,计算其最小和最大时间并更新全局乱序块时间范围
if len(oooMmc) != 0 {
// 初始化乱序块的最小时间为最大整数,最大时间为最小整数
mint, maxt := int64(math.MaxInt64), int64(math.MinInt64)
// 遍历所有乱序块,找到实际的最小和最大时间(乱序块时间无序)
for _, ch := range oooMmc {
if ch.minTime < mint {
mint = ch.minTime
}
if ch.maxTime > maxt {
maxt = ch.maxTime
}
}
// 更新全局乱序块的时间范围
h.updateMinOOOMaxOOOTime(mint, maxt)
}
// WAL 重放的样本已压缩为内存映射块,因此重置头部块(未压缩的临时块)相关字段
mSeries.nextAt = 0 // 重置下一个样本的时间戳
mSeries.headChunks = nil // 清空头部块链表
mSeries.app = nil // 清空块追加器
return // 返回是否存在块重叠的状态
}
3.6、第六部分:WAL 加载函数 ——WAL 子集处理器(walSubsetProcessor)结构与方法实现
https://github.com/prometheus/prometheus/blob/v3.4.2/tsdb/head_wal.go#L554
该组件是 WAL 重放并行化的关键,通过分治策略将大量 WAL 记录分配到多个处理器并发处理,并通过缓冲区复用优化性能,确保重放过程高效稳定。
// walSubsetProcessor 用于在 WAL 重放时并行处理部分系列数据的处理器,负责分发和处理样本、直方图等记录
type walSubsetProcessor struct {
input chan walSubsetProcessorInputItem // 输入通道,接收待处理的 WAL 记录项
output chan []record.RefSample // 输出通道,返回处理后的样本数据(用于缓冲区复用)
histogramsOutput chan []histogramRecord // 输出通道,返回处理后的直方图数据(用于缓冲区复用)
}
// walSubsetProcessorInputItem 表示 WAL 子集处理器的输入项,包含多种待处理的数据类型
type walSubsetProcessorInputItem struct {
samples []record.RefSample // 待处理的普通样本数据
histogramSamples []histogramRecord // 待处理的直方图样本数据
existingSeries *memSeries // 已存在的内存系列(用于更新或重置)
walSeriesRef chunks.HeadSeriesRef // WAL 中记录的系列引用(用于关联系列)
}
// setup 初始化 WAL 子集处理器的通道,设置缓冲区大小为 300,用于并发处理时的流量控制
func (wp *walSubsetProcessor) setup() {
wp.input = make(chan walSubsetProcessorInputItem, 300)
wp.output = make(chan []record.RefSample, 300)
wp.histogramsOutput = make(chan []histogramRecord, 300)
}
// closeAndDrain 关闭输入通道并清空输出通道,确保资源释放,避免 goroutine 泄漏
func (wp *walSubsetProcessor) closeAndDrain() {
close(wp.input) // 关闭输入通道,阻止新数据进入
for range wp.output { // 清空输出通道中的残留数据
}
for range wp.histogramsOutput { // 清空直方图输出通道中的残留数据
}
}
// reuseBuf 尝试从输出通道获取一个已有的样本缓冲区并重置长度,用于复用内存减少分配
// 若输出通道无数据,则返回 nil(将在后续处理中创建新缓冲区)
func (wp *walSubsetProcessor) reuseBuf() []record.RefSample {
select {
case buf := <-wp.output: // 从输出通道获取之前的缓冲区
return buf[:0] // 重置缓冲区长度为 0(保留底层数组复用)
default: // 若通道为空,返回 nil
}
return nil
}
// reuseHistogramBuf 尝试从直方图输出通道获取一个已有的直方图缓冲区并重置长度,用于复用内存
// 若通道无数据,则返回 nil(后续处理中创建新缓冲区)
func (wp *walSubsetProcessor) reuseHistogramBuf() []histogramRecord {
select {
case buf := <-wp.histogramsOutput: // 从直方图输出通道获取之前的缓冲区
return buf[:0] // 重置缓冲区长度为 0(保留底层数组复用)
default: // 若通道为空,返回 nil
}
return nil
}
关键代码解读
3.6.1、walSubsetProcessor 结构体设计
该结构体是 WAL 重放时并行处理的核心组件,通过三个通道实现数据的输入、处理结果输出和缓冲区复用:
-
-
-
- input 通道接收待处理的各类数据(样本、直方图、系列信息等),缓冲区大小 300 可平衡并发处理中的数据积压与内存占用。
- output 和 histogramsOutput 通道分别用于回收处理后的样本和直方图缓冲区,支持内存复用,减少频繁分配 / 释放带来的性能开销。
3.6.2、walSubsetProcessorInputItem 数据类型
作为处理器的输入载体,它整合了多种 WAL 记录类型:
-
-
-
- 普通样本(samples)和直方图样本(histogramSamples)是主要处理对象;
- existingSeries 和 walSeriesRef 用于关联已存在的内存系列,支持系列数据的更新或重置(如之前分析的 resetSeriesWithMMappedChunks 函数)。
- 这种设计使处理器能统一处理不同类型的 WAL 记录,简化并行分发逻辑。
3.6.3、缓冲区复用机制
reuseBuf 和 reuseHistogramBuf 方法通过从输出通道回收旧缓冲区并重置长度,实现内存复用:
-
-
-
- 在高并发场景下,频繁创建和销毁切片会导致 GC 压力,复用缓冲区可显著提升性能。
- 利用 select 的 default 分支,确保在通道为空时不阻塞,直接返回 nil 以创建新缓冲区,兼顾效率与灵活性。
3.6.4、资源释放逻辑
closeAndDrain 方法在处理器关闭时清理资源:
-
-
-
- 关闭 input 通道防止新数据写入;
- 遍历清空输出通道,确保所有残留数据被消费,避免持有未释放的内存,防止 goroutine 泄漏(若通道未清空,发送方可能因缓冲区满而阻塞)。
3.7、第七部分:WAL 子集处理器的样本处理方法(processWALSamples)实现
// processWALSamples 处理输入通道中的样本数据,将其添加到 Head 中,并将缓冲区发送到输出通道以供复用
// 早于 minValidTime 时间戳的样本会被丢弃
func (wp *walSubsetProcessor) processWALSamples(h *Head, mmappedChunks, oooMmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk) (map[chunks.HeadSeriesRef]struct{}, uint64, uint64, uint64) {
// 延迟关闭输出通道,确保所有处理完成后释放资源
defer close(wp.output)
defer close(wp.histogramsOutput)
// 记录缺失的系列引用、未知样本引用数、未知直方图引用数和重叠的内存映射块数
missingSeries := make(map[chunks.HeadSeriesRef]struct{})
var unknownSampleRefs, unknownHistogramRefs, mmapOverlappingChunks uint64
// 加载当前有效的最小时间戳(早于该时间的样本会被丢弃)
minValidTime := h.minValidTime.Load()
// 初始化样本时间范围的最小和最大值(用于后续更新 Head 的全局时间范围)
mint, maxt := int64(math.MaxInt64), int64(math.MinInt64)
// 构建块追加选项,包含块映射器、块时间范围和每个块的样本数配置
appendChunkOpts := chunkOpts{
chunkDiskMapper: h.chunkDiskMapper,
chunkRange: h.chunkRange.Load(),
samplesPerChunk: h.opts.SamplesPerChunk,
}
// 循环读取输入通道中的数据项,处理每一批样本或直方图
for in := range wp.input {
// 如果输入项包含已存在的系列(existingSeries),则用内存映射块重置该系列
if in.existingSeries != nil {
// 获取该系列对应的内存映射块和乱序内存映射块
mmc := mmappedChunks[in.walSeriesRef]
oooMmc := oooMmappedChunks[in.walSeriesRef]
// 重置系列并检查是否有重叠的内存映射块,若有则递增计数
if h.resetSeriesWithMMappedChunks(in.existingSeries, mmc, oooMmc, in.walSeriesRef) {
mmapOverlappingChunks++
}
// 处理完系列重置后,继续下一个输入项
continue
}
// 处理普通样本数据
for _, s := range in.samples {
// 根据样本的系列引用获取对应的内存系列
ms := h.series.getByID(s.Ref)
if ms == nil {
// 若系列不存在,记录未知引用并跳过该样本
unknownSampleRefs++
missingSeries[s.Ref] = struct{}{}
continue
}
// 若样本时间早于或等于系列的内存映射最大时间,说明已存在于映射块中,跳过
if s.T <= ms.mmMaxTime {
continue
}
// 将样本追加到系列中,若创建了新块则更新指标并尝试内存映射该块
if _, chunkCreated := ms.append(s.T, s.V, 0, appendChunkOpts); chunkCreated {
h.metrics.chunksCreated.Inc()
h.metrics.chunks.Inc()
_ = ms.mmapChunks(h.chunkDiskMapper)
}
// 更新当前处理的样本时间范围
if s.T > maxt {
maxt = s.T
}
if s.T < mint {
mint = s.T
}
}
// 将处理完的样本缓冲区发送到输出通道供复用(非阻塞,通道满则丢弃)
select {
case wp.output <- in.samples:
default:
}
// 处理直方图样本数据
for _, s := range in.histogramSamples {
// 早于最小有效时间的直方图样本直接丢弃
if s.t < minValidTime {
continue
}
// 根据直方图的系列引用获取对应的内存系列
ms := h.series.getByID(s.ref)
if ms == nil {
// 若系列不存在,记录未知引用并跳过该直方图
unknownHistogramRefs++
missingSeries[s.ref] = struct{}{}
continue
}
// 若直方图时间早于或等于系列的内存映射最大时间,说明已存在于映射块中,跳过
if s.t <= ms.mmMaxTime {
continue
}
// 标记是否创建了新块
var chunkCreated bool
// 根据直方图类型(普通/浮点)追加到系列中
if s.h != nil {
_, chunkCreated = ms.appendHistogram(s.t, s.h, 0, appendChunkOpts)
} else {
_, chunkCreated = ms.appendFloatHistogram(s.t, s.fh, 0, appendChunkOpts)
}
// 若创建了新块,更新块相关指标
if chunkCreated {
h.metrics.chunksCreated.Inc()
h.metrics.chunks.Inc()
}
// 更新当前处理的直方图时间范围
if s.t > maxt {
maxt = s.t
}
if s.t < mint {
mint = s.t
}
}
// 将处理完的直方图缓冲区发送到输出通道供复用(非阻塞,通道满则丢弃)
select {
case wp.histogramsOutput <- in.histogramSamples:
default:
}
}
// 根据处理的样本和直方图时间范围,更新 Head 的全局最小和最大时间
h.updateMinMaxTime(mint, maxt)
// 返回处理过程中收集的缺失系列、未知引用数和重叠块数
return missingSeries, unknownSampleRefs, unknownHistogramRefs, mmapOverlappingChunks
}
3.8、第八部分:WBL(Write-Ahead Log 扩展日志)加载处理方法(loadWBL)实现
// loadWBL 从 WBL 读取器加载数据,处理样本、直方图样本和内存映射标记,并将其应用到 Head 中
// 参数包括 WBL 读取器、符号表、系列引用映射以及最后一个内存映射块引用
func (h *Head) loadWBL(r *wlog.Reader, syms *labels.SymbolTable, multiRef map[chunks.HeadSeriesRef]chunks.HeadSeriesRef, lastMmapRef chunks.ChunkDiskMapperRef) (err error) {
// 跟踪其他记录引用但缺失的系列引用
unknownSeriesRefs := &seriesRefSet{refs: make(map[chunks.HeadSeriesRef]struct{}), mtx: sync.Mutex{}}
// 跟踪引用未知系列的样本、直方图样本和内存映射标记的数量(用于错误报告)
var unknownSampleRefs, unknownHistogramRefs, mmapMarkerUnknownRefs atomic.Uint64
// 解析最后一个内存映射块引用的序列号和偏移量
lastSeq, lastOff := lastMmapRef.Unpack()
// 启动工作线程,每个线程处理系列 ID 空间分区中的样本
var (
wg sync.WaitGroup
concurrency = h.opts.WALReplayConcurrency // 并发度,由配置指定
processors = make([]wblSubsetProcessor, concurrency) // 每个工作线程的处理器实例
shards = make([][]record.RefSample, concurrency) // 样本分片,按系列 ID 哈希分配给不同处理器
histogramShards = make([][]histogramRecord, concurrency) // 直方图样本分片,同上
decodedCh = make(chan interface{}, 10) // 用于传递解码后的记录
decodeErr error // 解码过程中发生的错误
)
// 延迟处理:若发生 corruption 错误,确保所有工作线程终止后再退出,并包装错误标识
defer func() {
_, ok := err.(*wlog.CorruptionErr)
if ok {
err = &errLoadWbl{err: err} // 包装为 WBL 加载错误
// 关闭所有处理器并释放资源
for i := 0; i < concurrency; i++ {
processors[i].closeAndDrain()
}
wg.Wait() // 等待所有工作线程结束
}
}()
// 启动并发工作线程
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
processors[i].setup() // 初始化处理器
// 启动工作线程处理 WBL 样本
go func(wp *wblSubsetProcessor) {
// 处理样本并获取缺失的系列、未知样本和直方图数量
missingSeries, unknownSamples, unknownHistograms := wp.processWBLSamples(h)
unknownSeriesRefs.merge(missingSeries) // 合并缺失的系列引用
unknownSampleRefs.Add(unknownSamples) // 累加未知样本计数
unknownHistogramRefs.Add(unknownHistograms) // 累加未知直方图计数
wg.Done() // 标记工作线程完成
}(&processors[i])
}
// 启动 goroutine 解码 WBL 记录并发送到 decodedCh 通道
go func() {
defer close(decodedCh) // 处理完成后关闭通道
dec := record.NewDecoder(syms) // 创建记录解码器
for r.Next() { // 循环读取 WBL 记录
var err error
rec := r.Record() // 获取当前记录
switch dec.Type(rec) { // 根据记录类型解码
case record.Samples:
// 解码样本记录
samples := h.wlReplaySamplesPool.Get()[:0] // 从对象池获取缓冲区
samples, err = dec.Samples(rec, samples)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode samples: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decodedCh <- samples // 发送解码后的样本到通道
case record.MmapMarkers:
// 解码内存映射标记记录
markers := h.wlReplayMmapMarkersPool.Get()[:0] // 从对象池获取缓冲区
markers, err = dec.MmapMarkers(rec, markers)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode mmap markers: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decodedCh <- markers // 发送解码后的标记到通道
case record.HistogramSamples, record.CustomBucketsHistogramSamples:
// 解码直方图样本记录
hists := h.wlReplayHistogramsPool.Get()[:0] // 从对象池获取缓冲区
hists, err = dec.HistogramSamples(rec, hists)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode histograms: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decodedCh <- hists // 发送解码后的直方图到通道
case record.FloatHistogramSamples, record.CustomBucketsFloatHistogramSamples:
// 解码浮点直方图样本记录
hists := h.wlReplayFloatHistogramsPool.Get()[:0] // 从对象池获取缓冲区
hists, err = dec.FloatHistogramSamples(rec, hists)
if err != nil {
decodeErr = &wlog.CorruptionErr{
Err: fmt.Errorf("decode float histograms: %w", err),
Segment: r.Segment(),
Offset: r.Offset(),
}
return
}
decodedCh <- hists // 发送解码后的浮点直方图到通道
default:
// 忽略其他类型的记录
}
}
}()
// 记录总是从最旧到最新重放
missingSeries := make(map[chunks.HeadSeriesRef]struct{})
// 处理解码后的记录
for d := range decodedCh {
switch v := d.(type) {
case []record.RefSample:
// 处理普通样本
samples := v
// 将样本分块(每块不超过 5000 个),避免大缓冲区占用过多内存
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化每个处理器的样本分片缓冲区
for i := 0; i < concurrency; i++ {
if shards[i] == nil {
shards[i] = processors[i].reuseBuf() // 复用处理器的缓冲区
}
}
// 按系列 ID 哈希分配样本到不同分片
for _, sam := range samples[:m] {
// 替换为映射后的系列引用(处理重复系列)
if r, ok := multiRef[sam.Ref]; ok {
sam.Ref = r
}
mod := uint64(sam.Ref) % uint64(concurrency) // 计算哈希值分配到对应处理器
shards[mod] = append(shards[mod], sam)
}
// 将分片发送到对应处理器处理
for i := 0; i < concurrency; i++ {
if len(shards[i]) > 0 {
processors[i].input <- wblSubsetProcessorInputItem{samples: shards[i]}
shards[i] = nil // 清空分片,准备下一轮
}
}
samples = samples[m:] // 处理剩余样本
}
h.wlReplaySamplesPool.Put(v) // 将缓冲区放回对象池
case []record.RefMmapMarker:
// 处理内存映射标记
markers := v
for _, rm := range markers {
seq, off := rm.MmapRef.Unpack() // 解析标记的序列号和偏移量
// 忽略在最后一个已知内存映射块之后的标记(尚未加载)
if seq > lastSeq || (seq == lastSeq && off > lastOff) {
continue
}
// 替换为映射后的系列引用(处理重复系列)
if r, ok := multiRef[rm.Ref]; ok {
rm.Ref = r
}
// 查找对应的内存系列
ms := h.series.getByID(rm.Ref)
if ms == nil {
// 系列不存在,记录未知引用
mmapMarkerUnknownRefs.Inc()
missingSeries[rm.Ref] = struct{}{}
continue
}
// 按系列 ID 哈希分配到对应处理器
idx := uint64(ms.ref) % uint64(concurrency)
processors[idx].input <- wblSubsetProcessorInputItem{mmappedSeries: ms}
}
case []record.RefHistogramSample:
// 处理直方图样本
samples := v
// 分块处理(每块不超过 5000 个)
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化每个处理器的直方图分片缓冲区
for i := 0; i < concurrency; i++ {
if histogramShards[i] == nil {
histogramShards[i] = processors[i].reuseHistogramBuf() // 复用缓冲区
}
}
// 按系列 ID 哈希分配直方图样本到不同分片
for _, sam := range samples[:m] {
// 替换为映射后的系列引用
if r, ok := multiRef[sam.Ref]; ok {
sam.Ref = r
}
mod := uint64(sam.Ref) % uint64(concurrency)
histogramShards[mod] = append(histogramShards[mod], histogramRecord{ref: sam.Ref, t: sam.T, h: sam.H})
}
// 将分片发送到对应处理器处理
for i := 0; i < concurrency; i++ {
if len(histogramShards[i]) > 0 {
processors[i].input <- wblSubsetProcessorInputItem{histogramSamples: histogramShards[i]}
histogramShards[i] = nil // 清空分片
}
}
samples = samples[m:] // 处理剩余样本
}
h.wlReplayHistogramsPool.Put(v) // 缓冲区放回对象池
case []record.RefFloatHistogramSample:
// 处理浮点直方图样本
samples := v
// 分块处理(每块不超过 5000 个)
for len(samples) > 0 {
m := 5000
if len(samples) < m {
m = len(samples)
}
// 初始化每个处理器的直方图分片缓冲区
for i := 0; i < concurrency; i++ {
if histogramShards[i] == nil {
histogramShards[i] = processors[i].reuseHistogramBuf() // 复用缓冲区
}
}
// 按系列 ID 哈希分配浮点直方图样本到不同分片
for _, sam := range samples[:m] {
// 替换为映射后的系列引用
if r, ok := multiRef[sam.Ref]; ok {
sam.Ref = r
}
mod := uint64(sam.Ref) % uint64(concurrency)
histogramShards[mod] = append(histogramShards[mod], histogramRecord{ref: sam.Ref, t: sam.T, fh: sam.FH})
}
// 将分片发送到对应处理器处理
for i := 0; i < concurrency; i++ {
if len(histogramShards[i]) > 0 {
processors[i].input <- wblSubsetProcessorInputItem{histogramSamples: histogramShards[i]}
histogramShards[i] = nil // 清空分片
}
}
samples = samples[m:] // 处理剩余样本
}
h.wlReplayFloatHistogramsPool.Put(v) // 缓冲区放回对象池
default:
// 遇到未预期的解码类型,触发 panic
panic(fmt.Errorf("unexpected decodedCh type: %T", d))
}
}
// 合并处理过程中发现的缺失系列引用
unknownSeriesRefs.merge(missingSeries)
// 检查解码过程中是否发生错误
if decodeErr != nil {
return decodeErr
}
// 通知所有工作线程终止并等待它们完成
for i := 0; i < concurrency; i++ {
processors[i].closeAndDrain()
}
wg.Wait()
// 检查 WBL 读取器是否发生错误
if err := r.Err(); err != nil {
return fmt.Errorf("read records: %w", err)
}
// 若存在未知引用,记录警告日志并更新指标
if unknownSampleRefs.Load()+unknownHistogramRefs.Load()+mmapMarkerUnknownRefs.Load() > 0 {
h.logger.Warn(
"Unknown series references for ooo WAL replay",
"series", unknownSeriesRefs.count(),
"samples", unknownSampleRefs.Load(),
"histograms", unknownHistogramRefs.Load(),
"mmap_markers", mmapMarkerUnknownRefs.Load(),
)
// 仅在计数非零时更新指标
counterAddNonZero(h.metrics.wblReplayUnknownRefsTotal, float64(unknownSeriesRefs.count()), "series")
counterAddNonZero(h.metrics.wblReplayUnknownRefsTotal, float64(unknownSampleRefs.Load()), "samples")
counterAddNonZero(h.metrics.wblReplayUnknownRefsTotal, float64(unknownHistogramRefs.Load()), "histograms")
counterAddNonZero(h.metrics.wblReplayUnknownRefsTotal, float64(mmapMarkerUnknownRefs.Load()), "mmap_markers")
}
return nil
}
3.9、第九部分:WBL 相关错误类型定义
// 定义WBL加载错误类型
type errLoadWbl struct {
err error
}
// 实现Error接口,返回错误信息
func (e errLoadWbl) Error() string {
return e.err.Error()
}
// 返回底层错误原因
func (e errLoadWbl) Cause() error {
return e.err
}
// 实现Unwrap接口,用于错误解包
func (e errLoadWbl) Unwrap() error {
return e.err
}
3.10、第十部分:WBL 子集处理器
3.10.1、wbl 子集处理器定义
// WBL子集处理器,用于并行处理WBL中的样本数据
type wblSubsetProcessor struct {
input chan wblSubsetProcessorInputItem // 输入通道,接收待处理的数据项
output chan []record.RefSample // 输出通道,用于样本缓冲区重用
histogramsOutput chan []histogramRecord // 输出通道,用于直方图缓冲区重用
}
// WBL子集处理器的输入数据项
type wblSubsetProcessorInputItem struct {
mmappedSeries *memSeries // 内存映射的序列
samples []record.RefSample // 样本数据
histogramSamples []histogramRecord // 直方图样本数据
}
3.10.2、WBL 子集处理器处理器方法
// 初始化处理器的通道
func (wp *wblSubsetProcessor) setup() {
wp.output = make(chan []record.RefSample, 300)
wp.histogramsOutput = make(chan []histogramRecord, 300)
wp.input = make(chan wblSubsetProcessorInputItem, 300)
}
// 关闭处理器并清空通道
func (wp *wblSubsetProcessor) closeAndDrain() {
close(wp.input)
// 清空输出通道
for range wp.output {
}
for range wp.histogramsOutput {
}
}
// 重用样本缓冲区,如果输出通道有缓冲区则返回,否则返回nil
func (wp *wblSubsetProcessor) reuseBuf() []record.RefSample {
select {
case buf := <-wp.output:
return buf[:0]
default:
}
return nil
}
// 重用直方图缓冲区,如果输出通道有缓冲区则返回,否则返回nil
func (wp *wblSubsetProcessor) reuseHistogramBuf() []histogramRecord {
select {
case buf := <-wp.histogramsOutput:
return buf[:0]
default:
}
return nil
}
3.11、第十一部分:WBL 样本处理逻辑
// 处理WBL样本,将样本添加到头部并将缓冲区传递到输出通道以供重用
func (wp *wblSubsetProcessor) processWBLSamples(h *Head) (map[chunks.HeadSeriesRef]struct{}, uint64, uint64) {
defer close(wp.output)
defer close(wp.histogramsOutput)
missingSeries := make(map[chunks.HeadSeriesRef]struct{}) // 记录缺失的序列引用
var unknownSampleRefs, unknownHistogramRefs uint64 // 记录未知的样本和直方图引用数量
oooCapMax := h.opts.OutOfOrderCapMax.Load() // 加载乱序样本容量上限配置
// 不检查乱序样本的最小有效时间
mint, maxt := int64(math.MaxInt64), int64(math.MinInt64)
// 处理输入通道中的所有数据项
for in := range wp.input {
// 如果存在内存映射序列且有乱序数据,清除头部块
if in.mmappedSeries != nil && in.mmappedSeries.ooo != nil {
// 到目前为止所有样本都已内存映射,因此清除headChunk
// 注意:如果由于块大小参数更改导致一些样本进入内存映射块,此处不处理
in.mmappedSeries.ooo.oooHeadChunk = nil
continue
}
// 处理普通样本
for _, s := range in.samples {
ms := h.series.getByID(s.Ref)
if ms == nil {
unknownSampleRefs++
missingSeries[s.Ref] = struct{}{}
continue
}
// 将样本插入序列
ok, chunkCreated, _ := ms.insert(s.T, s.V, nil, nil, h.chunkDiskMapper, oooCapMax, h.logger)
if chunkCreated {
h.metrics.chunksCreated.Inc()
h.metrics.chunks.Inc()
}
if ok {
// 更新时间范围
if s.T < mint {
mint = s.T
}
if s.T > maxt {
maxt = s.T
}
}
}
// 将处理完的样本缓冲区发送到输出通道供重用
select {
case wp.output <- in.samples:
default:
}
// 处理直方图样本
for _, s := range in.histogramSamples {
ms := h.series.getByID(s.ref)
if ms == nil {
unknownHistogramRefs++
missingSeries[s.ref] = struct{}{}
continue
}
var chunkCreated bool
var ok bool
// 根据直方图类型插入数据
if s.h != nil {
ok, chunkCreated, _ = ms.insert(s.t, 0, s.h, nil, h.chunkDiskMapper, oooCapMax, h.logger)
} else {
ok, chunkCreated, _ = ms.insert(s.t, 0, nil, s.fh, h.chunkDiskMapper, oooCapMax, h.logger)
}
if chunkCreated {
h.metrics.chunksCreated.Inc()
h.metrics.chunks.Inc()
}
if ok {
// 更新时间范围
if s.t > maxt {
maxt = s.t
}
if s.t < mint {
mint = s.t
}
}
}
// 将处理完的直方图缓冲区发送到输出通道供重用
select {
case wp.histogramsOutput <- in.histogramSamples:
default:
}
}
// 更新乱序样本的最小和最大时间
h.updateMinOOOMaxOOOTime(mint, maxt)
return missingSeries, unknownSampleRefs, unknownHistogramRefs
}
3.13、第十三部分:块快照记录类型定义
// 块快照记录类型常量
const (
chunkSnapshotRecordTypeSeries uint8 = 1 // 序列记录类型
chunkSnapshotRecordTypeTombstones uint8 = 2 // 墓碑记录类型
chunkSnapshotRecordTypeExemplars uint8 = 3 // 示例记录类型
)
// 块快照记录结构,包含序列的相关信息
type chunkSnapshotRecord struct {
ref chunks.HeadSeriesRef // 序列引用
lset labels.Labels // 标签集
mc *memChunk // 内存块
lastValue float64 // 最后一个值
lastHistogramValue *histogram.Histogram // 最后一个直方图值
lastFloatHistogramValue *histogram.FloatHistogram // 最后一个浮点直方图值
}
3.14、第十四部分:内存序列编码为快照记录
// 将内存序列编码为快照记录
func (s *memSeries) encodeToSnapshotRecord(b []byte) []byte {
buf := encoding.Encbuf{B: b}
buf.PutByte(chunkSnapshotRecordTypeSeries) // 写入记录类型
buf.PutBE64(uint64(s.ref)) // 写入序列引用
record.EncodeLabels(&buf, s.labels()) // 写入标签
buf.PutBE64int64(0) // 向后兼容,原为chunkRange,现已不用
s.Lock()
if s.headChunks == nil {
buf.PutUvarint(0) // 没有头部块
} else {
enc := s.headChunks.chunk.Encoding()
buf.PutUvarint(1) // 有头部块
buf.PutBE64int64(s.headChunks.minTime) // 写入最小时间
buf.PutBE64int64(s.headChunks.maxTime) // 写入最大时间
buf.PutByte(byte(enc)) // 写入编码类型
buf.PutUvarintBytes(s.headChunks.chunk.Bytes()) // 写入块数据
// 根据编码类型处理最后一个值
switch enc {
case chunkenc.EncXOR:
// 向后兼容旧的sampleBuf,包含最后4个样本
for i := 0; i < 3; i++ {
buf.PutBE64int64(0)
buf.PutBEFloat64(0)
}
buf.PutBE64int64(0)
buf.PutBEFloat64(s.lastValue)
case chunkenc.EncHistogram:
record.EncodeHistogram(&buf, s.lastHistogramValue)
default: // chunkenc.FloatHistogram
record.EncodeFloatHistogram(&buf, s.lastFloatHistogramValue)
}
}
s.Unlock()
return buf.Get()
}
3.15、第十五部分:从块快照解码序列
// 从块快照解码序列
func decodeSeriesFromChunkSnapshot(d *record.Decoder, b []byte) (csr chunkSnapshotRecord, err error) {
dec := encoding.Decbuf{B: b}
// 验证记录类型
if flag := dec.Byte(); flag != chunkSnapshotRecordTypeSeries {
return csr, fmt.Errorf("invalid record type %x", flag)
}
csr.ref = chunks.HeadSeriesRef(dec.Be64()) // 解码序列引用
csr.lset = d.DecodeLabels(&dec) // 解码标签集
_ = dec.Be64int64() // 原为chunkRange,现已不用
if dec.Uvarint() == 0 {
return // 没有头部块
}
// 解码内存块信息
csr.mc = &memChunk{}
csr.mc.minTime = dec.Be64int64()
csr.mc.maxTime = dec.Be64int64()
enc := chunkenc.Encoding(dec.Byte())
// 复制块数据(底层字节将在以后重用)
chunkBytes := dec.UvarintBytes()
chunkBytesCopy := make([]byte, len(chunkBytes))
copy(chunkBytesCopy, chunkBytes)
// 从数据创建块
chk, err := chunkenc.FromData(enc, chunkBytesCopy)
if err != nil {
return csr, fmt.Errorf("chunk from data: %w", err)
}
csr.mc.chunk = chk
// 根据编码类型解码最后一个值
switch enc {
case chunkenc.EncXOR:
// 向后兼容旧的sampleBuf
for i := 0; i < 3; i++ {
_ = dec.Be64int64()
_ = dec.Be64Float64()
}
_ = dec.Be64int64()
csr.lastValue = dec.Be64Float64()
case chunkenc.EncHistogram:
csr.lastHistogramValue = &histogram.Histogram{}
record.DecodeHistogram(&dec, csr.lastHistogramValue)
default: // chunkenc.FloatHistogram
csr.lastFloatHistogramValue = &histogram.FloatHistogram{}
record.DecodeFloatHistogram(&dec, csr.lastFloatHistogramValue)
}
// 检查解码错误
err = dec.Err()
if err != nil && len(dec.B) > 0 {
err = fmt.Errorf("unexpected %d bytes left in entry", len(dec.B))
}
return
}
3.16、墓碑数据快照编解码
// 将墓碑数据编码为快照记录
func encodeTombstonesToSnapshotRecord(tr tombstones.Reader) ([]byte, error) {
buf := encoding.Encbuf{}
buf.PutByte(chunkSnapshotRecordTypeTombstones) // 写入记录类型
b, err := tombstones.Encode(tr) // 编码墓碑数据
if err != nil {
return nil, fmt.Errorf("encode tombstones: %w", err)
}
buf.PutUvarintBytes(b) // 写入编码后的墓碑数据
return buf.Get(), nil
}
// 从快照记录解码墓碑数据
func decodeTombstonesSnapshotRecord(b []byte) (tombstones.Reader, error) {
dec := encoding.Decbuf{B: b}
// 验证记录类型
if flag := dec.Byte(); flag != chunkSnapshotRecordTypeTombstones {
return nil, fmt.Errorf("invalid record type %x", flag)
}
// 解码墓碑数据
tr, err := tombstones.Decode(dec.UvarintBytes())
if err != nil {
return tr, fmt.Errorf("decode tombstones: %w", err)
}
return tr, nil
}
3.17、块快照创建功能
// ChunkSnapshot创建头部中所有序列和墓碑的快照
// 如果快照创建成功,会删除旧的块快照
// 块快照存储在名为chunk_snapshot.N.M的目录中,使用WAL包写入
// N是快照期间存在的最后一个WAL段,M是段N中已写入数据的偏移量
// 快照首先包含所有序列(每个在单独的记录中且未排序),然后是墓碑(单个记录),最后是示例(>=1个记录)
// 示例按照写入循环缓冲区的顺序排列
func (h *Head) ChunkSnapshot() (*ChunkSnapshotStats, error) {
if h.wal == nil {
// 如果没有存储WAL,创建快照没有意义
h.logger.Warn("skipping chunk snapshotting as WAL is disabled")
return &ChunkSnapshotStats{}, nil
}
h.chunkSnapshotMtx.Lock()
defer h.chunkSnapshotMtx.Unlock()
stats := &ChunkSnapshotStats{}
// 获取最后一个WAL段和偏移量
wlast, woffset, err := h.wal.LastSegmentAndOffset()
if err != nil && !errors.Is(err, record.ErrNotFound) {
return stats, fmt.Errorf("get last wal segment and offset: %w", err)
}
// 查找最后一个块快照
_, cslast, csoffset, err := LastChunkSnapshot(h.opts.ChunkDirRoot)
if err != nil && !errors.Is(err, record.ErrNotFound) {
return stats, fmt.Errorf("find last chunk snapshot: %w", err)
}
// 如果WAL没有新数据,无需创建快照
if wlast == cslast && woffset == csoffset {
return stats, nil
}
snapshotName := chunkSnapshotDir(wlast, woffset)
cpdir := filepath.Join(h.opts.ChunkDirRoot, snapshotName)
cpdirtmp := cpdir + ".tmp" // 临时目录,用于原子操作
stats.Dir = cpdir
// 创建临时目录
if err := os.MkdirAll(cpdirtmp, 0o777); err != nil {
return stats, fmt.Errorf("create chunk snapshot dir: %w", err)
}
// 打开块快照
cp, err := wlog.New(nil, nil, cpdirtmp, h.wal.CompressionType())
if err != nil {
return stats, fmt.Errorf("open chunk snapshot: %w", err)
}
// 确保错误返回时不会留下临时文件
defer func() {
cp.Close()
os.RemoveAll(cpdirtmp)
}()
var (
buf []byte // 缓冲区
recs [][]byte // 记录集合
)
// 将所有序列添加到快照
stripeSize := h.series.size
for i := 0; i < stripeSize; i++ {
h.series.locks[i].RLock()
for _, s := range h.series.series[i] {
start := len(buf)
buf = s.encodeToSnapshotRecord(buf)
if len(buf[start:]) == 0 {
continue // 内容已被丢弃
}
recs = append(recs, buf[start:])
// 每10MB刷新一次记录
if len(buf) > 10*1024*1024 {
if err := cp.Log(recs...); err != nil {
h.series.locks[i].RUnlock()
return stats, fmt.Errorf("flush records: %w", err)
}
buf, recs = buf[:0], recs[:0]
}
}
stats.TotalSeries += len(h.series.series[i])
h.series.locks[i].RUnlock()
}
// 将墓碑添加到快照
tombstonesReader, err := h.Tombstones()
if err != nil {
return stats, fmt.Errorf("get tombstones: %w", err)
}
rec, err := encodeTombstonesToSnapshotRecord(tombstonesReader)
if err != nil {
return stats, fmt.Errorf("encode tombstones: %w", err)
}
recs = append(recs, rec)
// 刷新剩余的序列记录和墓碑
if err := cp.Log(recs...); err != nil {
return stats, fmt.Errorf("flush records: %w", err)
}
buf = buf[:0]
// 将示例添加到快照
// 批量记录,每个记录最多包含10000个示例
// 假设每个示例100字节(高估),约1MB
maxExemplarsPerRecord := 10000
batch := make([]record.RefExemplar, 0, maxExemplarsPerRecord)
enc := record.Encoder{}
// 刷新示例批次的函数
flushExemplars := func() error {
if len(batch) == 0 {
return nil
}
buf = buf[:0]
encbuf := encoding.Encbuf{B: buf}
encbuf.PutByte(chunkSnapshotRecordTypeExemplars)
enc.EncodeExemplarsIntoBuffer(batch, &encbuf)
if err := cp.Log(encbuf.Get()); err != nil {
return fmt.Errorf("log exemplars: %w", err)
}
buf, batch = buf[:0], batch[:0]
return nil
}
// 迭代所有示例并添加到快照
err = h.exemplars.IterateExemplars(func(seriesLabels labels.Labels, e exemplar.Exemplar) error {
if len(batch) >= maxExemplarsPerRecord {
if err := flushExemplars(); err != nil {
return fmt.Errorf("flush exemplars: %w", err)
}
}
// 查找示例对应的序列
ms := h.series.getByHash(seriesLabels.Hash(), seriesLabels)
if ms == nil {
// 示例可能引用了一些旧序列,丢弃这些示例
return nil
}
batch = append(batch, record.RefExemplar{
Ref: ms.ref,
T: e.Ts,
V: e.Value,
Labels: e.Labels,
})
return nil
})
if err != nil {
return stats, fmt.Errorf("iterate exemplars: %w", err)
}
// 刷新剩余的示例
if err := flushExemplars(); err != nil {
return stats, fmt.Errorf("flush exemplars at the end: %w", err)
}
// 关闭块快照
if err := cp.Close(); err != nil {
return stats, fmt.Errorf("close chunk snapshot: %w", err)
}
// 原子重命名临时目录
if err := fileutil.Replace(cpdirtmp, cpdir); err != nil {
return stats, fmt.Errorf("rename chunk snapshot directory: %w", err)
}
// 删除旧的块快照
if err := DeleteChunkSnapshots(h.opts.ChunkDirRoot, wlast, woffset); err != nil {
// 剩余的旧块快照不会导致后续问题,只会占用磁盘空间
// 由于存在更高版本的块快照,它们会被忽略
h.logger.Error("delete old chunk snapshots", "err", err)
}
return stats, nil
}
3.18、块快照目录工具函数
// 生成块快照目录名
func chunkSnapshotDir(wlast, woffset int) string {
return fmt.Sprintf(chunkSnapshotPrefix+"%06d.%010d", wlast, woffset)
}
// 执行块快照创建
func (h *Head) performChunkSnapshot() error {
h.logger.Info("creating chunk snapshot")
startTime := time.Now()
stats, err := h.ChunkSnapshot()
elapsed := time.Since(startTime)
if err == nil {
h.logger.Info("chunk snapshot complete", "duration", elapsed.String(), "num_series", stats.TotalSeries, "dir", stats.Dir)
}
if err != nil {
return fmt.Errorf("chunk snapshot: %w", err)
}
return nil
}
// ChunkSnapshotStats返回创建的块快照的统计信息
type ChunkSnapshotStats struct {
TotalSeries int // 总序列数
Dir string // 快照目录
}
3.19、块快照查找与删除
// LastChunkSnapshot返回最近的块快照的目录名和索引
// 如果目录中没有块快照,返回ErrNotFound
func LastChunkSnapshot(dir string) (string, int, int, error) {
files, err := os.ReadDir(dir)
if err != nil {
return "", 0, 0, err
}
maxIdx, maxOffset := -1, -1
maxFileName := ""
for i := 0; i < len(files); i++ {
fi := files[i]
if !strings.HasPrefix(fi.Name(), chunkSnapshotPrefix) {
continue
}
if !fi.IsDir() {
return "", 0, 0, fmt.Errorf("chunk snapshot %s is not a directory", fi.Name())
}
// 解析快照目录名
splits := strings.Split(fi.Name()[len(chunkSnapshotPrefix):], ".")
if len(splits) != 2 {
// 块快照格式不正确,忽略
continue
}
idx, err := strconv.Atoi(splits[0])
if err != nil {
continue
}
offset, err := strconv.Atoi(splits[1])
if err != nil {
continue
}
// 找到最新的快照
if idx > maxIdx || (idx == maxIdx && offset > maxOffset) {
maxIdx, maxOffset = idx, offset
maxFileName = filepath.Join(dir, fi.Name())
}
}
if maxFileName == "" {
return "", 0, 0, record.ErrNotFound
}
return maxFileName, maxIdx, maxOffset, nil
}
// DeleteChunkSnapshots删除目录中低于给定索引的所有块快照
func DeleteChunkSnapshots(dir string, maxIndex, maxOffset int) error {
files, err := os.ReadDir(dir)
if err != nil {
return err
}
errs := tsdb_errors.NewMulti()
for _, fi := range files {
if !strings.HasPrefix(fi.Name(), chunkSnapshotPrefix) {
continue
}
// 解析快照目录名
splits := strings.Split(fi.Name()[len(chunkSnapshotPrefix):], ".")
if len(splits) != 2 {
continue
}
idx, err := strconv.Atoi(splits[0])
if err != nil {
continue
}
offset, err := strconv.Atoi(splits[1])
if err != nil {
continue
}
// 删除旧快照
if idx < maxIndex || (idx == maxIndex && offset < maxOffset) {
if err := os.RemoveAll(filepath.Join(dir, fi.Name())); err != nil {
errs.Add(err)
}
}
}
return errs.Err()
}
3.20、块快照加载功能
// loadChunkSnapshot重放块快照并从快照恢复Head状态
// 如果返回任何错误,调用者有责任清除Head的内容
func (h *Head) loadChunkSnapshot() (int, int, map[chunks.HeadSeriesRef]*memSeries, error) {
// 查找最近的块快照
dir, snapIdx, snapOffset, err := LastChunkSnapshot(h.opts.ChunkDirRoot)
if err != nil {
if errors.Is(err, record.ErrNotFound) {
return snapIdx, snapOffset, nil, nil
}
return snapIdx, snapOffset, nil, fmt.Errorf("find last chunk snapshot: %w", err)
}
start := time.Now()
// 打开块快照的段读取器
sr, err := wlog.NewSegmentsReader(dir)
if err != nil {
return snapIdx, snapOffset, nil, fmt.Errorf("open chunk snapshot: %w", err)
}
defer func() {
if err := sr.Close(); err != nil {
h.logger.Warn("error while closing the wal segments reader", "err", err)
}
}()
var (
numSeries = 0 // 序列数量
unknownRefs = int64(0) // 未知引用数量
concurrency = h.opts.WALReplayConcurrency // 并发数
wg sync.WaitGroup // 等待组
recordChan = make(chan chunkSnapshotRecord, 5*concurrency) // 记录通道
shardedRefSeries = make([]map[chunks.HeadSeriesRef]*memSeries, concurrency) // 分片的序列引用映射
errChan = make(chan error, concurrency) // 错误通道
refSeries map[chunks.HeadSeriesRef]*memSeries // 序列引用映射
exemplarBuf []record.RefExemplar // 示例缓冲区
syms = labels.NewSymbolTable() // 整个快照的新符号表
dec = record.NewDecoder(syms) // 解码器
)
// 启动并发处理器
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(idx int, rc <-chan chunkSnapshotRecord) {
defer wg.Done()
defer func() {
// 如果有错误,清空通道以解除主线程阻塞
for range rc {
}
}()
shardedRefSeries[idx] = make(map[chunks.HeadSeriesRef]*memSeries)
localRefSeries := shardedRefSeries[idx]
// 处理记录通道中的所有记录
for csr := range rc {
series, _, err := h.getOrCreateWithID(csr.ref, csr.lset.Hash(), csr.lset, false)
if err != nil {
errChan <- err
return
}
localRefSeries[csr.ref] = series
// 更新最后一个序列ID
for {
seriesID := uint64(series.ref)
lastSeriesID := h.lastSeriesID.Load()
if lastSeriesID >= seriesID || h.lastSeriesID.CompareAndSwap(lastSeriesID, seriesID) {
break
}
}
if csr.mc == nil {
continue
}
// 恢复序列的块信息
series.nextAt = csr.mc.maxTime // 这将在追加时创建新块
series.headChunks = csr.mc
series.lastValue = csr.lastValue
series.lastHistogramValue = csr.lastHistogramValue
series.lastFloatHistogramValue = csr.lastFloatHistogramValue
// 恢复块的追加器
app, err := series.headChunks.chunk.Appender()
if err != nil {
errChan <- err
return
}
series.app = app
// 更新时间范围
h.updateMinMaxTime(csr.mc.minTime, csr.mc.maxTime)
}
}(i, recordChan)
}
// 读取快照记录并处理
r := wlog.NewReader(sr)
var loopErr error
Outer:
for r.Next() {
select {
case err := <-errChan:
errChan <- err
break Outer
default:
}
rec := r.Record()
switch rec[0] {
case chunkSnapshotRecordTypeSeries:
numSeries++
// 解码序列记录
csr, err := decodeSeriesFromChunkSnapshot(&dec, rec)
if err != nil {
loopErr = fmt.Errorf("decode series record: %w", err)
break Outer
}
recordChan <- csr
case chunkSnapshotRecordTypeTombstones:
// 解码墓碑记录
tr, err := decodeTombstonesSnapshotRecord(rec)
if err != nil {
loopErr = fmt.Errorf("decode tombstones: %w", err)
break Outer
}
// 应用墓碑数据
if err = tr.Iter(func(ref storage.SeriesRef, ivs tombstones.Intervals) error {
h.tombstones.AddInterval(ref, ivs...)
return nil
}); err != nil {
loopErr = fmt.Errorf("iterate tombstones: %w", err)
break Outer
}
case chunkSnapshotRecordTypeExemplars:
// 示例在快照的末尾,此时所有序列都已加载
if len(refSeries) == 0 {
close(recordChan)
wg.Wait()
// 合并分片的序列引用映射
refSeries = make(map[chunks.HeadSeriesRef]*memSeries, numSeries)
for _, shard := range shardedRefSeries {
for k, v := range shard {
refSeries[k] = v
}
}
}
// 如果禁用示例存储,跳过
if !h.opts.EnableExemplarStorage || h.opts.MaxExemplars.Load() <= 0 {
continue Outer
}
decbuf := encoding.Decbuf{B: rec[1:]}
// 解码示例数据
exemplarBuf = exemplarBuf[:0]
exemplarBuf, err = dec.ExemplarsFromBuffer(&decbuf, exemplarBuf)
if err != nil {
loopErr = fmt.Errorf("exemplars from buffer: %w", err)
break Outer
}
// 添加示例到头部
for _, e := range exemplarBuf {
ms, ok := refSeries[e.Ref]
if !ok {
unknownRefs++
continue
}
if err := h.exemplars.AddExemplar(ms.labels(), exemplar.Exemplar{
Labels: e.Labels,
Value: e.V,
Ts: e.T,
}); err != nil {
loopErr = fmt.Errorf("add exemplar: %w", err)
break Outer
}
}
default:
// 不支持的快照记录类型
loopErr = fmt.Errorf("unsupported snapshot record type 0b%b", rec[0])
break Outer
}
}
// 如果没有示例记录,在此处构建映射
if len(refSeries) == 0 {
close(recordChan)
wg.Wait()
}
// 收集错误
close(errChan)
merr := tsdb_errors.NewMulti()
if loopErr != nil {
merr.Add(fmt.Errorf("decode loop: %w", loopErr))
}
for err := range errChan {
merr.Add(fmt.Errorf("record processing: %w", err))
}
if err := merr.Err(); err != nil {
return -1, -1, nil, err
}
// 检查读取错误
if err := r.Err(); err != nil {
return -1, -1, nil, fmt.Errorf("read records: %w", err)
}
// 构建序列引用映射(如果尚未构建)
if len(refSeries) == 0 {
refSeries = make(map[chunks.HeadSeriesRef]*memSeries, numSeries)
for _, shard := range shardedRefSeries {
for k, v := range shard {
refSeries[k] = v
}
}
}
// 记录快照加载信息
elapsed := time.Since(start)
h.logger.Info("chunk snapshot loaded", "dir", dir, "num_series", numSeries, "duration", elapsed.String())
if unknownRefs > 0 {
h.logger.Warn("unknown series references during chunk snapshot replay", "count", unknownRefs)
}
return snapIdx, snapOffset, refSeries, nil
}