k8s与监控--改造telegraf的buffer实现

改造telegraf的buffer实现

前言

最近在使用telegraf的场景中,要求数据在程序意外终止的时候不丢失。按照telegraf最初的原始实现,在running_output内部维护了两个buffer,分别是metrics和failMetrics。这两个buffer是基于go中channel实现的。由于没有持久化机制,在意外退出的时候,存在丢失数据的风险。所以这篇文章主要讲述之前telegraf保证数据安全的一些措施和我们对代码的一些优化。

telegraf关于数据安全的处理办法

关于两个buffer,定义在running_output.go的struct中。

// RunningOutput contains the output configuration
type RunningOutput struct {
    Name              string
    Output            telegraf.Output
    Config            *OutputConfig
    MetricBufferLimit int
    MetricBatchSize   int

    MetricsFiltered selfstat.Stat
    MetricsWritten  selfstat.Stat
    BufferSize      selfstat.Stat
    BufferLimit     selfstat.Stat
    WriteTime       selfstat.Stat

    metrics     *buffer.Buffer
    failMetrics *buffer.Buffer

    // Guards against concurrent calls to the Output as described in #3009
    sync.Mutex
}

这个两个buffer的大小提供了配置参数可以设置。

metrics:           buffer.NewBuffer(batchSize),
failMetrics:       buffer.NewBuffer(bufferLimit),

顾名思义。metrics存放要发送到指定output的metric,而failMetrics存放发送失败的metric。当然失败的metrics会在telegraf重发机制下再次发送。

    if ro.metrics.Len() == ro.MetricBatchSize {
        batch := ro.metrics.Batch(ro.MetricBatchSize)
        err := ro.write(batch)
        if err != nil {
            ro.failMetrics.Add(batch...)
        }
    }

在向metrics增加metrics的时候,做是否达到批量发送的数量,如果达到就调用发送方法。当然还有定时的解决方案,如果一直没有达到MetricBatchSize,也会在一定时间后发送数据。具体实现代码在agent.go中

    ticker := time.NewTicker(a.Config.Agent.FlushInterval.Duration)
    semaphore := make(chan struct{}, 1)
    for {
        select {
        case <-shutdown:
            log.Println("I! Hang on, flushing any cached metrics before shutdown")
            // wait for outMetricC to get flushed before flushing outputs
            wg.Wait()
            a.flush()
            return nil
        case <-ticker.C:
            go func() {
                select {
                case semaphore <- struct{}{}:
                    internal.RandomSleep(a.Config.Agent.FlushJitter.Duration, shutdown)
                    a.flush()
                    <-semaphore
                default:
                    // skipping this flush because one is already happening
                    log.Println("W! Skipping a scheduled flush because there is" +
                        " already a flush ongoing.")
                }
            }()

在程序接受到停止信号后,程序会首先flush剩下的数据到output中,然后退出进程。这样可以保证一定的数据安全。

基于redis实现buffer的持久化

在持久化机制的选型中,优先实现redis。本身redis性能高,而且具备完善的持久化。
具体的实现架构如下:

将原buffer中功能抽象出buffer.go接口。
具体代码:

package buffer

import (
    "github.com/influxdata/telegraf"
    "github.com/influxdata/telegraf/internal/buffer/memory"
    "github.com/influxdata/telegraf/internal/buffer/redis"
)

const (
    BufferTypeForMemory = "memory"
    BufferTypeForRedis  = "redis"
)

type Buffer interface {
    IsEmpty() bool
    Len() int
    Add(metrics ...telegraf.Metric)
    Batch(batchSize int) []telegraf.Metric
}

func NewBuffer(mod string, size int, key, addr string) Buffer {
    switch mod {
    case BufferTypeForRedis:
        return redis.NewBuffer(size, key, addr)
    default:
        return memory.NewBuffer(size)
    }
}

然后分别内存和redis实现了Buffer接口。
其中NewBuffer相当于一个工厂方法。
当然在后期可以实现基于file和db等buffer实现,来满足不同的场景和要求。

redis实现buffer的要点

由于要满足先进先出的要求,选择了redis的list数据结构。redis中的list是一个字符串list,所以telegraf中metric数据接口要符合序列化的要求。比如属性需要可导出,即public。所以这点需要改动telegraf对于metric struct的定义。另外可以选择json或是msgpack等序列化方式。我们这边是采用的json序列化的方式。

结语

改造以后,可以根据自己的需求通过配置文件来决定使用channel或是redis来实现buffer。各有优劣,内存实现的话,性能高,受到的依赖少。而redis这种分布式存储,决定了数据安全,但是性能会有一定的损耗,毕竟有大量的序列化和反序列化以及网络传输,当然依赖也增加了,取决于redis的可靠性,建议redis集群部署。

你可能感兴趣的:(kubernetes,日志,golang)