从源码看Prometheus的远程写调优

本周在配置Prometheus的远端存储的时,发现配置完运行一段时间后,日志中有警告信息: "Skipping resharding, last successful send was beyond threshold";排查后发现,原来Prometheus对remote write的配置在最佳实践中早有提及相关优化建议。

日志信息

这里测试把InfluxDB作为Prometheus的远端存储,不做配置优化的情况下,我们先来看一下详细的报错信息:

ts=2020-05-14T03:07:15.114Z caller=dedupe.go:112 component=remote level=warn remote_name=11a319 url="http://192.168.1.1:8086/api/v1/prom/write?db=prometheus" msg="Skipping resharding, last successful send was beyond threshold" lastSendTimestamp=1589425620 minSendTimestamp=1589425625
复制代码

日志信息的大意为“上次成功发送超出阀值”;说实话,这里的日志提示的还是比较晦涩;不禁让人反问:“超出什么阀值”?提取日志中的关键字,在GitHub Prometheus的源码中搜索,我们一步步来看下具体的代码实现:

定义队列管理器

首先定义了一个名为"QueueMananger"的结构体,暂且称他为"队列管理器"。Shou you the code:

type QueueManager struct {
    // https://golang.org/pkg/sync/atomic/#pkg-note-BUG
    lastSendTimestamp int64

    logger         log.Logger
    flushDeadline  time.Duration
    cfg            config.QueueConfig
    externalLabels labels.Labels
    relabelConfigs []*relabel.Config
    watcher        *wal.Watcher

    clientMtx   sync.RWMutex
    storeClient StorageClient

    seriesMtx            sync.Mutex
    seriesLabels         map[uint64]labels.Labels
    seriesSegmentIndexes map[uint64]int
    droppedSeries        map[uint64]struct{}

    shards      *shards
    numShards   int
    reshardChan chan int
    quit        chan struct{}
    wg          sync.WaitGroup

    samplesIn, samplesDropped, samplesOut, samplesOutDuration *ewmaRate

    metrics *queueManagerMetrics
}
复制代码

通过队列管理器(QueueManager)结构体的定义,我们注意如下几个字段:

  • numShards: 分片数量,int类型;
  • reshardChan: reshard通道,channel类型;
  • cfg: 对应Prometheus的配置参数,config.QueueConfig类型;

队列管理器初始化

NewQueueManager函数是队列管理器的初始化方法;Show you the code:

// NewQueueManager builds a new QueueManager.
func NewQueueManager(
    metrics *queueManagerMetrics,
    watcherMetrics *wal.WatcherMetrics,
    readerMetrics *wal.LiveReaderMetrics,
    logger log.Logger,
    walDir string,
    samplesIn *ewmaRate,
    cfg config.QueueConfig,
    externalLabels labels.Labels,
    relabelConfigs []*relabel.Config,
    client StorageClient,
    flushDeadline time.Duration,
) *QueueManager {
    if logger == nil {
        logger = log.NewNopLogger()
    }

    logger = log.With(logger, remoteName, client.Name(), endpoint, client.Endpoint())
    t := &QueueManager{
        logger:         logger,
        flushDeadline:  flushDeadline,
        cfg:            cfg,
        externalLabels: externalLabels,
        relabelConfigs: relabelConfigs,
        storeClient:    client,
        seriesLabels:         make(map[uint64]labels.Labels),
        seriesSegmentIndexes: make(map[uint64]int),
        droppedSeries:        make(map[uint64]struct{}),
        numShards:   cfg.MinShards,
        reshardChan: make(chan int),
        quit:        make(chan struct{}),
        samplesIn:          samplesIn,
        samplesDropped:     newEWMARate(ewmaWeight, shardUpdateDuration),
        samplesOut:         newEWMARate(ewmaWeight, shardUpdateDuration),
        samplesOutDuration: newEWMARate(ewmaWeight, shardUpdateDuration),

        metrics: metrics,
    }

    t.watcher = wal.NewWatcher(watcherMetrics, readerMetrics, logger, client.Name(), t, walDir)
    t.shards = t.newShards()
    return t
}

复制代码

通过初始化方法,我们可以知道如下几点:

  • numShards:分片数量,赋值为cfg.MinShards,即Prometheus remote_write的配置参数min_shards的值;相当于远程写启动时采用min_shards配置的数量,作为使用分片的默认值;
  • reshardChan:这里声明了一个int类型的channel,且无缓冲区;上面提到的队列和shard,其实是依托于golang channel来实现的;我们知道channel从根本上来说,只是一个数据结构,可以被写入数据,也可以被读取数据;所谓发送数据到channel,或者从channel读取数据,说白了就是对一个数据结构的操作,仅此而已;

触发reshard条件

文章开头的日志信息,我们看到提示是"skipping resharding",即跳过了reshard动作;我们不禁要发出三连问:reshard是什么(what)?为什么需要reshard(why)?怎么样触发reshard(how)?

下面的代码解释了:什么情况下resharding动作应该发生;return true时,代表应该发生reshard动作;

// shouldReshard returns if resharding should occur
func (t *QueueManager) shouldReshard(desiredShards int) bool {
    if desiredShards == t.numShards {
        return false
    }
    // We shouldn't reshard if Prometheus hasn't been able to send to the
    // remote endpoint successfully within some period of time.
    minSendTimestamp := time.Now().Add(-2 * time.Duration(t.cfg.BatchSendDeadline)).Unix()
    lsts := atomic.LoadInt64(&t.lastSendTimestamp)
    if lsts < minSendTimestamp {
        level.Warn(t.logger).Log("msg", "Skipping resharding, last successful send was beyond threshold", "lastSendTimestamp", lsts, "minSendTimestamp", minSendTimestamp)
        return false
    }
    return true
}
复制代码
  1. 当需要的分片数和numShards相等时,不触发reshard动作
  2. 最小发送数据时间戳 = 当前时间戳 – 2 * BatchSendDeadline
  3. lsts即最近一次发送数据的时间戳
  4. 当lsts小于最小发送时间戳时,记录日志,不触发reshard动作;
  5. 不满足上述1和4条件时,触发reshard动作

从这里我们终于找到文章开头处日志信息的出处,原来是因为“最近一次发送数据的时间戳”小于“最小发送数据时间戳”,也即跟BatchSendDeadline的配置有关;

Prometheus的远程写

在理解reshard之前,我们先要了解shard的概念。这就说到了Prometheus的Remote Write。

每个远程写目的地都启动一个队列,该队列从write-ahead log (WAL)中读取数据,将样本写到一个由shard(即分片)拥有的内存队列中,然后分片将请求发送到配置的端点。数据流程如下:

      |-->  queue (shard_1)   --> remote endpoint
WAL --|-->  queue (shard_...) --> remote endpoint
      |-->  queue (shard_n)   --> remote endpoint
复制代码

当一个分片备份并填满它的队列时,Prometheus将阻止从WAL中读取任何分片。如果失败了,则进行重试,其间不会丢失数据,除非远程端点保持关闭状态超过2小时。2小时后,WAL将被压缩,未发送的数据将丢失。
在远程写过程中,Prometheus将根据输入采样速率、未发送的采样数量和发送每个采样数据所需的时间,不断计算出最优的分片数量(即上面提到的numShards)。

远程写内存使用

使用远程写操作会增加Prometheus的内存占用。大多数用户报告内存使用量增加了25%,但是这个数字取决于数据的结构。对于WAL中的每个时间序列,远程写缓存一个key为时间序列ID,value为标签值的map,导致大量的时间序列变动,从而显著地增加内存使用量。

内存公式

影响内存使用的因素有:

  • series cache
  • shard
  • shard queue

公式:

Shard memory = number of shards * (capacity + max_samples_per_send)

避免OOM的调整方法

调优时,请考虑减少max_shards同时增加capacity和max_samples_per_send以避免内存不足。原则如下:

  • 减少max_shards
  • 增加capacity
  • 增加max_samples_per_send

远程写调优

前面啰嗦了半天,直接上结论

capacity

3-10倍max_samples_per_send

max_shards

建议默认(1000);不建议太多,会导致OOM;除非远程存储写入非常缓慢,否则不建议将max_shards的值调整为比默认值大。相反,调整为小于默认值会减少内存占用;

min_shards

默认(1),可适当调大;如果远端写入落后,Prometheus会自动扩展分片的数量;增加最小分片将允许 Prometheus在计算所需的分片数时避免在开始时落后。

max_samples_per_send

默认值(100)足够小,适用于大多数系统,可适当调大;根据所使用的后端,适当调整每次发送的最大样本数。因为许多系统,通过每批发送更多样本而不会显著增加延迟。而有些后端,如果每个请求中发送大量样本(simples),就会出现问题。

batch_send_deadline

对时间不敏感时可适当增大;设置单个分片发送之间的最大时间;即使排队的分片尚未到达max_samples_per_send,也会发送请求;对于不区分延迟的低容量系统,可以增加此值,以提高请求效率;

min_backoff

重试失败请求之前的最短等待时间或控制重试失败请求之前等待的最短时间。

max_backoff

控制重试失败请求之前等待的最大时间量。

结束语

实际应用时,需要可根据优化建议,适当调整测试,达到系统最优水平。

参考文档


发表评论

电子邮件地址不会被公开。 必填项已用*标注