OpenTelemetry 實(shí)戰(zhàn):從零實(shí)現(xiàn)應(yīng)用指標(biāo)監(jiān)控
前言
在上一篇文章:OpenTelemetry 實(shí)戰(zhàn):從零實(shí)現(xiàn)分布式鏈路追蹤講解了鏈路相關(guān)的實(shí)戰(zhàn),本次我們繼續(xù)跟進(jìn)如何使用 OpenTelemetry 集成 metrics 監(jiān)控。
建議對指標(biāo)監(jiān)控不太熟的朋友可以先查看這篇前菜文章:從 Prometheus 到 OpenTelemetry:指標(biāo)監(jiān)控的演進(jìn)與實(shí)踐
名稱 | 作用 | 語言 | 版本 |
java-demo | 發(fā)送 gRPC 請求的客戶端 | Java | opentelemetry-agent: 2.4.0/SpringBoot: 2.7.14 |
k8s-combat | 提供 gRPC 服務(wù)的服務(wù)端 | Golang | go.opentelemetry.io/otel: 1.28/ Go: 1.22 |
Jaeger | trace 存儲的服務(wù)端以及 TraceUI 展示 | Golang | jaegertracing/all-in-one:1.56 |
opentelemetry-collector-contrib | OpenTelemetry 的 collector 服務(wù)端,用于收集 trace/metrics/logs 然后寫入到遠(yuǎn)端存儲 | Golang | otel/opentelemetry-collector-contrib:0.98.0 |
Prometheus | 作為 metrics 的存儲和展示組件,也可以用 VictoriaMetrics 等兼容 Prometheus 的存儲替代。 | Golang | quay.io/prometheus/prometheus:v2.49.1 |
快速開始
以上是加入 metrics 之后的流程圖,在原有的基礎(chǔ)上會新增一個(gè) Prometheus 組件,collector 會將 metrics 指標(biāo)數(shù)據(jù)通過遠(yuǎn)程的 remote write 的方式寫入到 Prometheus 中。
Prometheus 為了能兼容 OpenTelemetry 寫入過來的數(shù)據(jù),需要開啟相關(guān)特性才可以。
如果是 docker 啟動的話需要傳入相關(guān)參數(shù):
docker run -d -p 9292:9090 --name prometheus \
-v /prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
quay.io/prometheus/prometheus:v2.49.1 \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/prometheus \
--web.console.libraries=/etc/prometheus/console_libraries \
--web.console.templates=/etc/prometheus/consoles \
--enable-feature=exemplar-storage \
--enable-feature=otlp-write-receiver
--enable-feature=otlp-write-receiver 最主要的就是這個(gè)參數(shù),用于開啟接收 OTLP 格式的數(shù)據(jù)。
但使用這個(gè) Push 特性就會喪失掉 Prometheus 的許多 Pull 特性,比如服務(wù)發(fā)現(xiàn),定時(shí)抓取等,不過也還好,Push 和 Pull 可以同時(shí)使用,原本使用 Pull 抓取的組件依然不受影響。
修改 OpenTelemetry-Collector
接著我們需要修改下 Collector 的配置:
exporters:
debug:
otlp:
endpoint: "jaeger:4317"
tls:
insecure: true
otlphttp/prometheus:
endpoint: http://prometheus:9292/api/v1/otlp
tls:
insecure: true
processors:
batch:
service:
pipelines:
traces:
receivers:
- otlp
processors: [batch]
exporters:
- otlp
- debug
metrics:
exporters:
- otlphttp/prometheus
- debug
processors:
- batch
receivers:
- otlp
這里我們在 exporter 中新增了一個(gè) otlphttp/prometheus 的節(jié)點(diǎn),用于指定導(dǎo)出 prometheus 的 endpoint 地址。
同時(shí)我們還需要在 server.metrics.exporters 中配置相同的 key: otlphttp/prometheus。
需要注意的是這里我們一定得是配置在 metrics.exporters 這個(gè)節(jié)點(diǎn)下,如果配置在 traces.exporters 下時(shí),相當(dāng)于是告訴 collector 講 trace 的數(shù)據(jù)導(dǎo)出到 otlphttp/prometheus.endpoint 這個(gè) endpoint 里了。
所以重點(diǎn)是需要理解這里的配對關(guān)系。
運(yùn)行效果
這樣我們只需要將應(yīng)用啟動之后就可以在 Prometheus 中查詢到應(yīng)用上報(bào)的指標(biāo)了。
java -javaagent:opentelemetry-javaagent-2.4.0-SNAPSHOT.jar \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=none \
-Dotel.service.name=java-demo \
-Dotel.exporter.otlp.protocol=grpc \
-Dotel.propagators=tracecontext,baggage \
-Dotel.exporter.otlp.endpoint=http://127.0.0.1:5317 -jar target/demo-0.0.1-SNAPSHOT.jar
# Run go app
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:5317 OTEL_RESOURCE_ATTRIBUTES=service.name=k8s-combat
./k8s-combat
因?yàn)槲覀冊?collector 中開啟了 Debug 的 exporter,所以可以看到以下日志:
2024-07-22T06:34:08.060Z info MetricsExporter {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 18, "data points": 44}
此時(shí)是可以說明指標(biāo)上傳成功的。
然后我們打開 Prometheus 的地址:http://127.0.0.1:9292/graph便可以查詢到 Java 應(yīng)用和 Go 應(yīng)用上報(bào)的指標(biāo)。
圖片
OpenTelemetry 的 javaagent 會自動上報(bào) JVM 相關(guān)的指標(biāo)。
而在 Go 程序中我們還是需要顯式的配置一些埋點(diǎn):
func initMeterProvider() *sdkmetric.MeterProvider {
ctx := context.Background()
exporter, err := otlpmetricgrpc.New(ctx)
if err != nil {
log.Printf("new otlp metric grpc exporter failed: %v", err)
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter)),
sdkmetric.WithResource(initResource()),
) otel.SetMeterProvider(mp)
return mp
}
mp := initMeterProvider()
defer func() {
if err := mp.Shutdown(context.Background()); err != nil {
log.Printf("Error shutting down meter provider: %v", err)
}
}()
和 Tracer 類似,我們首先也得在 main 函數(shù)中調(diào)用 initMeterProvider() 函數(shù)來初始化 Meter,此時(shí)它會返回一個(gè) sdkmetric.MeterProvider 對象。
OpenTelemetry Go 的 SDK 中已經(jīng)提供了對 go runtime 的自動埋點(diǎn),我們只需要調(diào)用相關(guān)函數(shù)即可:
err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second))
if err != nil {
log.Fatal(err)
}
之后我們啟動應(yīng)用,在 Prometheus 中就可以看到 Go 應(yīng)用上報(bào)的相關(guān)指標(biāo)了。
圖片
圖片
runtime_uptime_milliseconds_total Go 的運(yùn)行時(shí)指標(biāo)。
Prometheus 中展示指標(biāo)的 UI 能力有限,通常我們都是配合 grafana 進(jìn)行展示的。
圖片
手動上報(bào)指標(biāo)
當(dāng)然除了 SDK 自動上報(bào)的指標(biāo)之外,我們也可以類似于 trace 那樣手動上報(bào)一些指標(biāo);
比如我就想記錄某個(gè)函數(shù)調(diào)用的次數(shù)。
var meter = otel.Meter("test.io/k8s/combat")
apiCounter, err = meter.Int64Counter(
"api.counter",
metric.WithDescription("Number of API calls."),
metric.WithUnit("{call}"),
)
if err != nil {
log.Err(err)
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
defer apiCounter.Add(ctx, 1)
return &pb.HelloReply{Message: fmt.Sprintf("hostname:%s, in:%s, md:%v", name, in.Name, md)}, nil
}
只需要創(chuàng)建一個(gè) Int64Counter 類型的指標(biāo),然后在需要埋點(diǎn)處調(diào)用它的函數(shù) apiCounter.Add(ctx, 1) 即可。
圖片
之后便可以在 Prometheus 中查到這個(gè)指標(biāo)了。
除此之外 OpenTelemetry 中的 metrics 定義和 Prometheus 也是類似的,還有以下幾種類型:
- Counter:單調(diào)遞增計(jì)數(shù)器,比如可以用來記錄訂單數(shù)、總的請求數(shù)。
- UpDownCounter:與 Counter 類似,只不過它可以遞減。
- Gauge:用于記錄隨時(shí)在變化的值,比如內(nèi)存使用量、CPU 使用量等。
- Histogram:通常用于記錄請求延遲、響應(yīng)時(shí)間等。
在 Java 中也提供有類似的 API 可以完成自定義指標(biāo):
messageInCounter = meter
.counterBuilder(MESSAGE_IN_COUNTER)
.setUnit("{message}")
.setDescription("The total number of messages received for this topic.")
.buildObserver();
對于 Gauge 類型的數(shù)據(jù)用法如下,使用 buildWithCallback 回調(diào)函數(shù)上報(bào)數(shù)據(jù),OpenTelemetry 會在框架層面每 30s 回調(diào)一次。
public static void registerObservers() {
Meter meter = MetricsRegistration.getMeter();
meter.gaugeBuilder("pulsar_producer_num_msg_send")
.setDescription("The number of messages published in the last interval")
.ofLongs()
.buildWithCallback(
r -> recordProducerMetrics(r, ProducerStats::getNumMsgsSent));
private static void recordProducerMetrics(ObservableLongMeasurement observableLongMeasurement, Function<ProducerStats, Long> getter) {
for (Producer producer : CollectionHelper.PRODUCER_COLLECTION.list()) {
ProducerStats stats = producer.getStats();
String topic = producer.getTopic();
if (topic.endsWith(RetryMessageUtil.RETRY_GROUP_TOPIC_SUFFIX)) {
continue;
} observableLongMeasurement.record(getter.apply(stats),
Attributes.of(PRODUCER_NAME, producer.getProducerName(), TOPIC, topic));
}}
更多具體用法可以參考官方文檔鏈接:https://opentelemetry.io/docs/languages/java/instrumentation/#metrics
如果我們不想將數(shù)據(jù)通過 collector 而是直接上報(bào)到 Prometheus 中,使用 OpenTelemetry 框架也是可以實(shí)現(xiàn)的。
我們只需要配置下環(huán)境變量:
export OTEL_METRICS_EXPORTER=prometheus
這樣我們就可以訪問 http://127.0.0.1:9464/metrics 獲取到當(dāng)前應(yīng)用暴露出來的指標(biāo),此時(shí)就可以在 Prometheus 里配置好采集 job 來獲取數(shù)據(jù)。
scrape_configs:
- job_name: "k8s-combat"
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ["k8s-combat:9464"]
這就是典型的 Pull 模型,而 OpenTelemetry 推薦使用的是 Push 模型,數(shù)據(jù)由 OpenTelemetry 進(jìn)行采集然后推送到 Prometheus。
這兩種模式各有好處:
Pull模型 | Push 模型 | |
優(yōu)點(diǎn) | 可以在一個(gè)集中的配置里管理所有的抓取端點(diǎn),也可以為每一個(gè)應(yīng)用單獨(dú)配置抓取頻次等數(shù)據(jù)。 | 在 OpenTelemetry 的 collector中可以集中對指標(biāo)做預(yù)處理之后再將過濾后的數(shù)據(jù)寫入 Prometheus,更加的靈活。 |
缺點(diǎn) | 1. 預(yù)處理指標(biāo)比較麻煩,所有的數(shù)據(jù)是到了 Prometheus 后再經(jīng)過relabel處理后再寫入存儲。 2. 需要配置服務(wù)發(fā)現(xiàn) | 1. 額外需要維護(hù)一個(gè)類似于 collector 這樣的指標(biāo)網(wǎng)關(guān)的組件 |
比如我們是用和 Prometheus 兼容的 VictoriaMetrics 采集了 istio 的相關(guān)指標(biāo),但里面的指標(biāo)太多了,我們需要刪除掉一部分。
就需要在采集任務(wù)里編寫規(guī)則:
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: isito-pod-scrape
spec:
podMetricsEndpoints:
- scheme: http
scrape_interval: "30s"
scrapeTimeout: "30s"
path: /stats/prometheus
metricRelabelConfigs:
- regex: ^envoy_.*|^url\_\_\_\_.*|istio_request_bytes_sum|istio_request_bytes_count|istio_response_bytes_sum|istio_request_bytes_sum|istio_request_duration_milliseconds_sum|istio_response_bytes_count|istio_request_duration_milliseconds_count|^ostrich_apigateway.*|istio_request_messages_total|istio_response_messages_total
action: drop_metrics
namespaceSelector:
any: true
換成在 collector 中處理后,這些邏輯都可以全部移動到 collector 中集中處理。
總結(jié)
metrics 的使用相對于 trace 更簡單一些,不需要理解復(fù)雜的 context、span 等概念,只需要搞清楚有哪幾種 metrics 類型,分別應(yīng)用在哪些不同的場景即可。
參考鏈接:
- https://prometheus.io/docs/prometheus/latest/feature_flags/#otlp-receiver
- https://opentelemetry.io/docs/languages/java/instrumentation/#metrics
- https://opentelemetry.io/docs/languages/go/instrumentation/#metrics