GoでOpenTelemetryのMetricの計装をしたらメモリが破滅した

おわかりだろうか?

追加した実装は下記のようなものだった。

	metricExporter, err := otlpmetrichttp.New(
		ctx,
		otlpmetrichttp.WithEndpoint(otelAgentAddr),
		otlpmetrichttp.WithInsecure(),
	)
	if err != nil {
		return nil, err
	}

	meterProvider := metric.NewMeterProvider(
		metric.WithResource(resource),
		metric.WithReader(
			metric.NewPeriodicReader(
				metricExporter,
				metric.WithInterval(time.Minute),
			),
		),
	)

あとはリクエストのカウントしたりとかそういうシンプルなものです。

counter.Add(
  ctx,
  1,
  metric.WithAttributes(semconv.HTTPResponseStatusCode(int(rww.status))),
  metric.WithAttributes(semconv.ServerAddress(r.URL.Host)),
)

collecotorの設定も下記のような感じ。

receivers:
  otlp:
    protocols:
      http:
      grpc:
processors:
  batch:
  memory_limiter:
exporters:
  prometheusremotewrite:
    endpoint: xxxxx
service:
  pipelines:
    metrics:
      receivers: [otlp, prometheus]
      exporters: [prometheusremotewrite]
      processors: [memory_limiter, batch]

collectorでoltpとして収集して、prometheusに送りつけることを想定していました。ところが、過剰にメモリが利用されるという事象が発生しました。

原因がわからなかったため、下記のようにしてdumpデータを取りました。

exporters:
  file/no_rotation:
    path: /tmp/dump_otel
service:
  pipelines:
    metrics:
      receivers: [otlp, prometheus]
      exporters: [file/no_rotation]
      processors: [memory_limiter, batch]

このようにすると実際のメトリックをfileに出力することが出来ます。そして、中身を確認すると下記のような身に覚えのないメトリックが存在しました。

          "metrics": [
            {
              "name": "http.client.request.size",
              "description": "Measures the size of HTTP request messages.",
              "unit": "By",
              "sum": {

悩んでいると、今回追加した実装の前に、http.Handlerとしてotelの計装を追加していたことを思い出しました。

		handler = otelhttp.NewHandler(handler, "example",
			otelhttp.WithTracerProvider(otel.GetTracerProvider()),
		)

この実装をしておくと、http serverの自動計装を行ってくれるものです。そして、ソースを確認すると下記の実装が見つかりました。

結論、http.Handlerをそのまま使うと、グローバルのMeterProviderを利用して、全リクエストのメトリクスを取得してしまいます。これは我々のような事業者のトラフィックだと信じられない量のメトリクスを取得してしまいます。

対応としてはhandlerのメトリクス取得を間引くか、不要であれは下記のようにnoopに渡すといいです。

handler = otelhttp.NewHandler(handler, "example",
			otelhttp.WithTracerProvider(otel.GetTracerProvider()),
			otelhttp.WithMeterProvider(noop.NewMeterProvider()),
		)

もしくは僕は試していませんがViewを使ってカーディナリティを下げるという手段も良さそうです。

ページを見ていただくとわかりますが、メトリクスのアトリビュートから特定のキーを持つものを削除することができるのでそれによってカーディナリティが下がり、結果的に出力されるメトリクスの量を減らすことが出来ます。

自動計装非常に便利なのですが、うっかりしているとこういうことを踏んでしまうので、気をつけないといけないなと思いました。