這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
改造telegraf的buffer實現
前言
最近在使用telegraf的情境中,要求資料在程式意外終止的時候不丟失。按照telegraf最初的原始實現,在running_output內部維護了兩個buffer,分別是metrics和failMetrics。這兩個buffer是基於go中channel實現的。由於沒有持久化機制,在意外退出的時候,存在遺失資料的風險。所以這篇文章主要講述之前telegraf保證資料安全的一些措施和我們對代碼的一些最佳化。
telegraf關於資料安全的處理辦法
關於兩個buffer,定義在running_output.go的struct中。
// RunningOutput contains the output configurationtype 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 bufferimport ( "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相當於一個Factory 方法。
當然在後期可以實現基於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叢集部署。