OpenTelemetry 實戰:從零實現應用指標監控

crossoverJie發表於2024-08-28

前言

在上一篇文章:OpenTelemetry 實戰:從零實現分散式鏈路追蹤講解了鏈路相關的實戰,本次我們繼續跟進如何使用 OpenTelemetry 整合 metrics 監控。

建議對指標監控不太熟的朋友可以先檢視這篇前菜文章:從 Prometheus 到 OpenTelemetry:指標監控的演進與實踐

名稱 作用 語言 版本
java-demo 傳送 gRPC 請求的客戶端 Java opentelemetry-agent: 2.4.0/SpringBoot: 2.7.14
k8s-combat 提供 gRPC 服務的服務端 Golang go.opentelemetry.io/otel: 1.28/ Go: 1.22
Jaeger trace 儲存的服務端以及 TraceUI 展示 Golang jaegertracing/all-in-one:1.56
opentelemetry-collector-contrib OpenTelemetry 的 collector 服務端,用於收集 trace/metrics/logs 然後寫入到遠端儲存 Golang otel/opentelemetry-collector-contrib:0.98.0
Prometheus 作為 metrics 的儲存和展示元件,也可以用 VictoriaMetrics 等相容 Prometheus 的儲存替代。 Golang quay.io/prometheus/prometheus:v2.49.1
image.png

快速開始

以上是加入 metrics 之後的流程圖,在原有的基礎上會新增一個 Prometheus 元件,collector 會將 metrics 指標資料透過遠端的 remote write 的方式寫入到 Prometheus 中。

Prometheus 為了能相容 OpenTelemetry 寫入過來的資料,需要開啟相關特性才可以。

如果是 docker 啟動的話需要傳入相關引數:

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 最主要的就是這個引數,用於開啟接收 OTLP 格式的資料。

但使用這個 Push 特性就會喪失掉 Prometheus 的許多 Pull 特性,比如服務發現,定時抓取等,不過也還好,Push 和 Pull 可以同時使用,原本使用 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 中新增了一個 otlphttp/prometheus 的節點,用於指定匯出 prometheusendpoint 地址。

同時我們還需要在 server.metrics.exporters 中配置相同的 key: otlphttp/prometheus

需要注意的是這裡我們一定得是配置在 metrics.exporters 這個節點下,如果配置在 traces.exporters 下時,相當於是告訴 collector 講 trace 的資料匯出到 otlphttp/prometheus.endpoint 這個 endpoint 裡了。

所以重點是需要理解這裡的配對關係。

執行效果

這樣我們只需要將應用啟動之後就可以在 Prometheus 中查詢到應用上報的指標了。

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

因為我們在 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}

此時是可以說明指標上傳成功的。

然後我們開啟 Prometheus 的地址:http://127.0.0.1:9292/graph
便可以查詢到 Java 應用和 Go 應用上報的指標。

OpenTelemetry 的 javaagent 會自動上報 JVM 相關的指標。


而在 Go 程式中我們還是需要顯式的配置一些埋點:

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 函式中呼叫 initMeterProvider() 函式來初始化 Meter,此時它會返回一個 sdkmetric.MeterProvider 物件。

OpenTelemetry Go 的 SDK 中已經提供了對 go runtime 的自動埋點,我們只需要呼叫相關函式即可:

err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second))
if err != nil {
    log.Fatal(err)
}

之後我們啟動應用,在 Prometheus 中就可以看到 Go 應用上報的相關指標了。
image.png

runtime_uptime_milliseconds_total Go 的執行時指標

Prometheus 中展示指標的 UI 能力有限,通常我們都是配合 grafana 進行展示的。
image.png

手動上報指標

當然除了 SDK 自動上報的指標之外,我們也可以類似於 trace 那樣手動上報一些指標;

比如我就想記錄某個函式呼叫的次數。

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  
}

只需要建立一個 Int64Counter 型別的指標,然後在需要埋點處呼叫它的函式 apiCounter.Add(ctx, 1) 即可。

image.png
之後便可以在 Prometheus 中查到這個指標了。

除此之外 OpenTelemetry 中的 metrics 定義和 Prometheus 也是類似的,還有以下幾種型別:

  • Counter:單調遞增計數器,比如可以用來記錄訂單數、總的請求數。
  • UpDownCounter:與 Counter 類似,只不過它可以遞減。
  • Gauge:用於記錄隨時在變化的值,比如記憶體使用量、CPU 使用量等。
  • Histogram:通常用於記錄請求延遲、響應時間等。

在 Java 中也提供有類似的 API 可以完成自定義指標:

messageInCounter = meter    
        .counterBuilder(MESSAGE_IN_COUNTER)    
        .setUnit("{message}")    
        .setDescription("The total number of messages received for this topic.")    
        .buildObserver();

對於 Gauge 型別的資料用法如下,使用 buildWithCallback 回撥函式上報資料,OpenTelemetry 會在框架層面每 30s 回撥一次。

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

如果我們不想將資料透過 collector 而是直接上報到 Prometheus 中,使用 OpenTelemetry 框架也是可以實現的。

我們只需要配置下環境變數:

export OTEL_METRICS_EXPORTER=prometheus

這樣我們就可以訪問 http://127.0.0.1:9464/metrics 獲取到當前應用暴露出來的指標,此時就可以在 Prometheus 裡配置好採集 job 來獲取資料。

scrape_configs:
  - job_name: "k8s-combat"
    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.
    static_configs:
      - targets: ["k8s-combat:9464"]   

這就是典型的 Pull 模型,而 OpenTelemetry 推薦使用的是 Push 模型,資料由 OpenTelemetry 進行採集然後推送到 Prometheus。

這兩種模式各有好處:

Pull模型 Push 模型
優點 可以在一個集中的配置裡管理所有的抓取端點,也可以為每一個應用單獨配置抓取頻次等資料。 在 OpenTelemetry 的 collector中可以集中對指標做預處理之後再將過濾後的資料寫入 Prometheus,更加的靈活。
缺點 1. 預處理指標比較麻煩,所有的資料是到了 Prometheus 後再經過relabel處理後再寫入儲存。
2. 需要配置服務發現
1. 額外需要維護一個類似於 collector 這樣的指標閘道器的元件

比如我們是用和 Prometheus 相容的 VictoriaMetrics 採集了 istio 的相關指標,但裡面的指標太多了,我們需要刪除掉一部分。

就需要在採集任務裡編寫規則:

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 中集中處理。

總結

metrics 的使用相對於 trace 更簡單一些,不需要理解複雜的 context、span 等概念,只需要搞清楚有哪幾種 metrics 型別,分別應用在哪些不同的場景即可。

參考連結:

  • 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

相關文章