iT邦幫忙

2025 iThome 鐵人賽

DAY 23
1
DevOps

被稱作Server Restart Engineer的我,也想了解如何實踐可觀測性工程系列 第 23

Day 23 - 從 ClickHouse Exporter 看資料處理流程:OTLP 到 ClickHouse 的轉換

  • 分享至 

  • xImage
  •  

昨天我們透過 ClickHouse Exporter 了解了 Factory Pattern 如何統一管理不同 signal 的建立,以及 start() 和 shutdown() 如何處理 exporter 的生命週期。

今天,就讓我們來看當一筆 OTLP 格式的資料進入 collector 後,exporter 如何將它轉換成 ClickHouse 可以寫入的格式?回顧昨天提到的 createMetricExporter 函式,裡面剛好就有呼叫到核心的資料轉換函式:

return exporterhelper.NewMetrics(
    ctx, set, cfg,
    exp.pushMetricsData, 
    // ...
)

這個 pushMetricsData 就是資料轉換的核心。今天我們將深入這個函式來了解:

  1. OTLP 的資料結構:metrics 在 OTLP 中如何表示?
  2. Schema Mapping:如何將 OTLP 的巢狀結構攤平成 ClickHouse 的 table schema?
  3. Batch Insert 策略:如何高效地批次寫入資料?
  4. 對 Lakehouse 的啟發:這些設計如何應用到 Parquet/Iceberg?

由於各個 signal 所包含的資訊不盡相同,因此在今天的原始碼介紹中,我們會以 metrics 作為範例。

pushMetricsData 來看轉換流程

當 collector 收到一批 metrics 資料時,collector 便會呼叫 pushMetricsData 這個函式,這個函式傳入的參數有兩個,分別是用來控制超時和取消操作的 ctx 以及型別為 pmetric.Metrics 的參數 md,是 OTLP 格式 metrcis 資料。

func (e *metricsExporter) pushMetricsData(ctx context.Context, md pmetric.Metrics) error

OTLP Metrics 的資料結構

在深入轉換邏輯前,我們需要先理解 OTLP metrics 的層次結構。pmetric.Metrics 是一個多層巢狀的資料結構:

MetricsData
└── ResourceMetrics (repeated)
    ├── Resource (資源屬性,如 service.name)
    └── ScopeMetrics (repeated)
        ├── Scope (instrumentation scope)
        └── Metric (repeated)
            ├── Name (metric 名稱)
            ├── Description
            ├── Unit
            └── Data (根據 metric type 不同)
                ├── Gauge
                ├── Sum
                ├── Histogram
                ├── ExponentialHistogram
                └── Summary

這樣的資料結構所代表的意義為:

  • ResourceMetrics:一個 Resource 代表一個資料來源(例如一個 service instance)。同一個 service 的所有 metrics 共享相同的 Resource attributes
  • ScopeMetrics:一個 Scope 代表一個 instrumentation library(例如 HTTP client library)。這樣可以追蹤 metrics 的來源
  • Metric:實際的指標資料,一個 Metric 可以包含多個 data points

展開 OTLP 格式的巢狀結構

如同前面提過,columnar database的 table 是扁平(flat)的,而 ClickHouse 也不例外。因此,pushMetricsData 的主要工作就是把 OTLP 這個巢狀架構的資料給展開,符合 ClickHouse table 的架構:

func (e *metricsExporter) pushMetricsData(ctx context.Context, md pmetric.Metrics) error {
	metricsMap := metrics.NewMetricsModel(e.tablesConfig, e.cfg.database())
	for i := 0; i < md.ResourceMetrics().Len(); i++ {
		metrics := md.ResourceMetrics().At(i)
		resAttr := metrics.Resource().Attributes()
		for j := 0; j < metrics.ScopeMetrics().Len(); j++ {
			rs := metrics.ScopeMetrics().At(j).Metrics()
			scopeInstr := metrics.ScopeMetrics().At(j).Scope()
			scopeURL := metrics.ScopeMetrics().At(j).SchemaUrl()
			for k := 0; k < rs.Len(); k++ {
				r := rs.At(k)
				var errs error
				//exhaustive:enforce
				switch r.Type() {
				case pmetric.MetricTypeGauge:
					errs = errors.Join(errs, metricsMap[pmetric.MetricTypeGauge].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Gauge(), r.Name(), r.Description(), r.Unit()))
				case pmetric.MetricTypeSum:
					errs = errors.Join(errs, metricsMap[pmetric.MetricTypeSum].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Sum(), r.Name(), r.Description(), r.Unit()))
				case pmetric.MetricTypeHistogram:
					errs = errors.Join(errs, metricsMap[pmetric.MetricTypeHistogram].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Histogram(), r.Name(), r.Description(), r.Unit()))
				case pmetric.MetricTypeExponentialHistogram:
					errs = errors.Join(errs, metricsMap[pmetric.MetricTypeExponentialHistogram].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.ExponentialHistogram(), r.Name(), r.Description(), r.Unit()))
				case pmetric.MetricTypeSummary:
					errs = errors.Join(errs, metricsMap[pmetric.MetricTypeSummary].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Summary(), r.Name(), r.Description(), r.Unit()))
				case pmetric.MetricTypeEmpty:
					return errors.New("metrics type is unset")
				default:
					return errors.New("unsupported metrics type")
				}
				if errs != nil {
					return errs
				}
			}
		}
	}

	return metrics.InsertMetrics(ctx, e.db, metricsMap)
}

可以看到這個函式用了三層迴圈來處理資料,而每一層迴圈都可以對應到 Metrics Data 的架構:

  1. 第一層遍歷所有 Resources(不同的 service instances)
  2. 第二層遍歷每個 Resource 的 Scopes(不同的 instrumentation libraries)
  3. 第三層遍歷每個 Scope 的 Metrics(實際的指標資料)

Metric Type 分流:為什麼需要不同的 table?

在第三層迴圈裡面有一個 switch case 判斷式,這邊是將不同類型的 metrics 送進不同的函式中,最終資料會寫入不同的 table。這是因為不同 metric type 的資料結構差異很大:

  • Gauge:單一數值(例如 cpu_temperature = 72.5)
  • Sum:累積數值(例如 http_requests_total = 1523)
  • Histogram:分布資料(例如 http_request_duration 的 bucket 分布)
  • ExponentialHistogram:使用指數級 bucket 的 histogram
  • Summary:百分位數資料(例如 p50, p95, p99)

如果資料類型的差異非常大,雖然一樣可以存在同個 table 當中,但可能會導致資料表有過多的 null 值、schema 難以維護以及查詢效能變差等議題。因此對於 Metrics 這個 signal,ClickHouse 採取一種資料類型寫入一張 table 的策略。

而 traces 和 logs 就不像 Metrics 有各種不同的資料型別,因此都只會各自寫入一張 table。

結語

今天我們從 pushMetricsData 函式出發,了解了 ClickHouse Exporter 如何處理 OTLP metrics 資料:

  1. 理解 OTLP 的層次結構:ResourceMetrics → ScopeMetrics → Metric 的三層巢狀設計
  2. 展開巢狀結構:透過三層迴圈遍歷所有資料
  3. Metric Type 分流:不同類型的 metrics 寫入不同的 table,避免 schema 複雜度

不過,我們還能再更深入的探討,OTLP 的巢狀結構如何轉換成 ClickHouse 的扁平 table?所以,明天我們將以 Gauge 為例,深入分析 table schema 的設計、資料欄位的對應關係,以及這些設計對我們自己在設計 Lakehouse 架構的啟發。

參考資料

opentelemetry-collector/exporter/exporterhelper


上一篇
Day 22 - 從 ClickHouse Exporter 看資料匯出架構:Factory Pattern 與 exporter 生命週期管理
系列文
被稱作Server Restart Engineer的我,也想了解如何實踐可觀測性工程23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言