GoでTracing (1)

GoでTracing

分散トレーシングとは

複数のサービスが連携して動作するようなシステムの場合、ログが一箇所にまとまっていなかったりリクエスト単位でサービスを横断して追跡できなかったりして解析が困難になることがあります。
分残トレーシングはサービスをまたがって解析できるようにする仕組みです。

トレーシングシステムとしては、ZipkinJaeger、GCPのCloud Traceなどがあります。

OpenTelemetryとは

OpenTelemetryは、ZipkinやJaegerといったプロダクト(ベンダー)に依存せずにトレーシングシステムを利用できるようにする標準的な規格です(大体あっているはず)。

これにより、ある製品をオンプレミスで実行するするときはZipkinを使い、クラウドで実行するときはCloud Traceを使い、ローカルで開発するときはJaegerを使う、といったようなことができるようになります。

Goでの実装例

SDKであるopentelemetry-goを使います。

まずはシンプルな例として、サービスをまたがらずに1つだけの場合です。内容は以下の通り。

  1. 関数aが100msかかる処理をしたあと関数bを呼ぶ
  2. 関数bが200msかかる処理をしたあと関数cを呼ぶ
  3. 関数cが300msかかる処理をする
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package main

import (
"context"
"fmt"
"io"
"log"
"os"
"time"

gcpexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
)

var tracer = otel.Tracer("github.com/pecolynx/go-otel/main")

func initTracerExporter(exporterType, jaegerEndPoint string) (sdktrace.SpanExporter, error) {
switch exporterType {
case "jaeger":
// Create the Jaeger exporter
return jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(jaegerEndPoint)))
case "gcp":
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
return gcpexporter.New(gcpexporter.WithProjectID(projectID))
case "stdout":
return stdouttrace.New(
stdouttrace.WithPrettyPrint(),
stdouttrace.WithWriter(os.Stderr),
)
case "none":
return stdouttrace.New(
stdouttrace.WithPrettyPrint(),
stdouttrace.WithWriter(io.Discard),
)
default:
return nil, fmt.Errorf("unsupported exporter: %s", exporterType)
}
}

func initTracerProvider(exporter sdktrace.SpanExporter, serviceName string) (*sdktrace.TracerProvider, error) {
tp := sdktrace.NewTracerProvider(
// Always be sure to batch in production.
sdktrace.WithBatcher(exporter),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
// Record information about this application in a Resource.
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)

return tp, nil
}

func a(ctx context.Context) {
ctx, span := tracer.Start(ctx, "a")
defer span.End()
time.Sleep(100 * time.Millisecond)
b(ctx)
}

func b(ctx context.Context) {
ctx, span := tracer.Start(ctx, "b")
defer span.End()
time.Sleep(200 * time.Millisecond)
c(ctx)
}

func c(ctx context.Context) {
_, span := tracer.Start(ctx, "c")
defer span.End()
time.Sleep(300 * time.Millisecond)
}

func main() {
// exporterType := "none"
// exporterType := "stdout"
exporterType := "jaeger"
jaegerEndpoint := "http://localhost:14268/api/traces"
ctx := context.Background()
exp, err := initTracerExporter(exporterType, jaegerEndpoint)
if err != nil {
log.Fatal(err)
}
tp, err := initTracerProvider(exp, "go-otel")
otel.SetTracerProvider(tp)
if err != nil {
log.Fatal(err)
}
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

a(ctx)

defer tp.ForceFlush(ctx) // flushes any pending spans
}

トレーシングシステムとしてはJaegerを選択しました。
Jaegerをローカルで実行する方法はこちらです。

Jaegerを立ち上げたあと、上記のプログラムを実行します。
その後、ブラウザでhttp://localhost:16686にアクセスし、Find Tracesをクリックしてリクエストを選択します。
以下のような結果を確認できます。

実行結果

関数aの実行に600ms、関数bの実行に500ms、関数cの実行に300msかかっていることがわかります。