前言
在k8s資源審計和計費這塊,容器和虛機有很大區別。相對虛機來講,容器不容易實現。
資源指標收集可以採用heapster,也可以用prometheus。之前文章有介紹過,prometheus的儲存的瓶頸和查詢較大資料量,容易oom這兩個問題。所以選擇了heapster。此外,heapster不僅內部實現了很多aggregator和calculator,做了很多彙總層的工作。而採用prometheus,你需要在查詢的時候做彙總。
heapster支援諸多metrics輸出,稱為sink。目前支援的sink如:
而我比較傾向於clickhouse資料庫,關於clickhouse,其實前面的文章介紹過很多了。
所以本文主要講如何為heapster增加clickhouse sink。
程式碼分析和實現
看代碼,增加一種sink還是很簡單的。典型的工廠設計模式,實現 Name,Stop,ExportData 介面方法即可。最後再提供一個初始化函數,供factory調用即可。
初始化方法 NewClickhouseSink
具體代碼:
config, err := clickhouse_common.BuildConfig(uri) if err != nil { return nil, err } client, err := sql.Open("clickhouse", config.DSN) if err != nil { glog.Errorf("connecting to clickhouse: %v", err) return nil, err } sink := &clickhouseSink{ c: *config, client: client, conChan: make(chan struct{}, config.Concurrency), } glog.Infof("created clickhouse sink with options: host:%s user:%s db:%s", config.Host, config.UserName, config.Database) return sink, nil
基本上就是擷取設定檔,初始化clickhouse 的client。
在factory.go 中 build方法中,加入剛剛實現的初始化函數
func (this *SinkFactory) Build(uri flags.Uri) (core.DataSink, error) { switch uri.Key { case "elasticsearch": return elasticsearch.NewElasticsearchSink(&uri.Val) case "gcm": return gcm.CreateGCMSink(&uri.Val) case "stackdriver": return stackdriver.CreateStackdriverSink(&uri.Val) case "statsd": return statsd.NewStatsdSink(&uri.Val) case "graphite": return graphite.NewGraphiteSink(&uri.Val) case "hawkular": return hawkular.NewHawkularSink(&uri.Val) case "influxdb": return influxdb.CreateInfluxdbSink(&uri.Val) case "kafka": return kafka.NewKafkaSink(&uri.Val) case "librato": return librato.CreateLibratoSink(&uri.Val) case "log": return logsink.NewLogSink(), nil case "metric": return metricsink.NewMetricSink(140*time.Second, 15*time.Minute, []string{ core.MetricCpuUsageRate.MetricDescriptor.Name, core.MetricMemoryUsage.MetricDescriptor.Name}), nil case "opentsdb": return opentsdb.CreateOpenTSDBSink(&uri.Val) case "wavefront": return wavefront.NewWavefrontSink(&uri.Val) case "riemann": return riemann.CreateRiemannSink(&uri.Val) case "honeycomb": return honeycomb.NewHoneycombSink(&uri.Val) case "clickhouse": return clickhouse.NewClickhouseSink(&uri.Val) default: return nil, fmt.Errorf("Sink not recognized: %s", uri.Key) }}
Name 和 Stop
func (sink *clickhouseSink) Name() string { return "clickhouse"}func (tsdbSink *clickhouseSink) Stop() { // Do nothing}
stop 函數在heapster關閉的時候調用,執行一些非託管資源的關閉。
ExportData
這是核心的地方。
func (sink *clickhouseSink) ExportData(dataBatch *core.DataBatch) { sink.Lock() defer sink.Unlock() if err := sink.client.Ping(); err != nil { glog.Warningf("Failed to ping clickhouse: %v", err) return } dataPoints := make([]point, 0, 0) for _, metricSet := range dataBatch.MetricSets { for metricName, metricValue := range metricSet.MetricValues { var value float64 if core.ValueInt64 == metricValue.ValueType { value = float64(metricValue.IntValue) } else if core.ValueFloat == metricValue.ValueType { value = float64(metricValue.FloatValue) } else { continue } pt := point{ name: metricName, cluster: sink.c.ClusterName, val: value, ts: dataBatch.Timestamp, } for key, value := range metricSet.Labels { if _, exists := clickhouseBlacklistLabels[key]; !exists { if value != "" { if key == "labels" { lbs := strings.Split(value, ",") for _, lb := range lbs { ts := strings.Split(lb, ":") if len(ts) == 2 && ts[0] != "" && ts[1] != "" { pt.tags = append(pt.tags, fmt.Sprintf("%s=%s", ts[0], ts[1])) } } } else { pt.tags = append(pt.tags, fmt.Sprintf("%s=%s", key, value)) } } } } dataPoints = append(dataPoints, pt) if len(dataPoints) >= sink.c.BatchSize { sink.concurrentSendData(dataPoints) dataPoints = make([]point, 0, 0) } } } if len(dataPoints) >= 0 { sink.concurrentSendData(dataPoints) } sink.wg.Wait()}
主要有以下幾個地方需要注意
- 資料的格式轉換。需要將heapster 中DataBatch 轉化為你目的儲存的格式。其實這塊做過pipeline 多output的人,很容易理解。
- 批量寫入。一般在大資料量的時候,批量寫入是一種有效手段。
- 根據設定參數,並發寫入目的儲存。用到了golang的協程。下面這段代碼實現了一個協程的發送資料。
func (sink *clickhouseSink) concurrentSendData(dataPoints []point) { sink.wg.Add(1) // use the channel to block until there's less than the maximum number of concurrent requests running sink.conChan <- struct{}{} go func(dataPoints []point) { sink.sendData(dataPoints) }(dataPoints)}
擷取配置參數
這塊在clickhouse.go中,主要做了擷取配置參數和參數初始化一些預設值,以及對配置參數校正的工作。
dockerfile的更改
原來的基礎鏡像是基於scratch
FROM scratchCOPY heapster eventer /COPY ca-certificates.crt /etc/ssl/certs/# nobody:nobodyUSER 65534:65534ENTRYPOINT ["/heapster"]
由於需要改timezone的問題,改成了基於alpine。
FROM alpineRUN apk add -U tzdataRUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtimeCOPY heapster eventer /COPY ca-certificates.crt /etc/ssl/certs/RUN chmod +x /heapsterENTRYPOINT ["/heapster"]
實際上,基於scratch增加timezone並且更改,也可以做到,只不過需要裝一些包指令,結果就是鏡像變大。與其如此,不如基於我比較熟悉的alpine實現。
總結
fork的項目地址。實際作業記錄:
由於ck的出色的寫入效能,運行非常穩定。