Kubernetesのカスタムコントローラーで分散トレーシングを活用する方法を模索する
Kubernetesにおいて、複数のコントローラーが複数のリソースを順に処理するケースがあります。
例えばcert-managerでは、Certificate
リソースを作ると、CertificateRquest
, Order
, Challenge
と順番にリソースが作られて処理されます。
( +---------+ )
( | Ingress | ) Optional ACME Only!
( +---------+ )
| |
| +-------------+ +--------------------+ | +-------+ +-----------+
|-> | Certificate |----> | CertificateRequest | ----> | | Order | ----> | Challenge |
+-------------+ +--------------------+ | +-------+ +-----------+
|
こういうケースでは、分散トレーシングを活用すると処理が追いかけやすくなるのではないかと思います。
OpenTelemetryでは、分散トレーシングをおこなう際にどのような情報をコンポーネント間でやりとりすればいいのか、Conventionsが定められています。
しかし、Kubernetesのコントローラーに適したConventionはまだ存在しないようです。
というわけで、以下のようにリソースのannotations
にTraceID
とSpanID
を持たせてコントローラー間で受け渡す方法を考えてみます。
apiVersion: otel.zoetrope.github.io/v1
kind: Child
metadata:
annotations:
otel.zoetrope.github.io/span_id: 108dfd83c22b5944
otel.zoetrope.github.io/trace_id: d99ee82939ddc9196d4d59321a24ac66
name: child
namespace: default
まずは、親のコントローラーのReconcile処理です。
Parent
というカスタムリソースが作られると、それに応じてChild
というカスタムリソースを作成するという実装になっています。
このとき、Child
リソースのannotations
にTraceID
とSpanID
を埋め込みます。
func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
ctx, span := r.Tracer.Start(ctx, "parent-reconcile")
defer span.End()
traceID := span.SpanContext().TraceID()
spanID := span.SpanContext().SpanID()
logger := log.FromContext(ctx).WithValues("traceID", traceID, "spanID", spanID)
logger.Info("Reconcile")
var parent otelv1.Parent
err := r.Get(ctx, req.NamespacedName, &parent)
if err != nil {
if !errors.IsNotFound(err) {
span.RecordError(err)
}
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if !parent.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}
child := &otelv1.Child{
ObjectMeta: metav1.ObjectMeta{
Name: parent.Name + "-child",
Namespace: parent.Namespace,
},
}
op, err := ctrl.CreateOrUpdate(ctx, r.Client, child, func() error {
if child.Annotations == nil {
child.Annotations = make(map[string]string)
}
child.Annotations["otel.zoetrope.github.io/trace_id"] = traceID.String()
child.Annotations["otel.zoetrope.github.io/span_id"] = spanID.String()
return ctrl.SetControllerReference(&parent, child, r.Scheme)
})
if err != nil {
span.RecordError(err)
return ctrl.Result{}, err
}
if op != controllerutil.OperationResultNone {
logger.Info("crate or update child successfully")
}
return ctrl.Result{}, nil
}
次に子のコントローラーのReconcile処理です。
Child
リソースが作られると、そのリソースのannotations
からTraceID
とSpanID
を抜き出して、spanを作成しています。
なお、今回のコードでは親と子のコントローラーが同じプロセスで動いているのでtrace.ContextWithSpanContext
を利用していますが、異なるプロセスで動いている場合はtrace.ContextWithRemoteSpanContext
を使います。
func (r *ChildReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var child otelv1.Child
err := r.Get(ctx, req.NamespacedName, &child)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if !child.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}
spanCtx := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{},
SpanID: trace.SpanID{},
TraceState: trace.TraceState{},
TraceFlags: 01,
Remote: false,
})
if parentTraceID, ok := child.Annotations["otel.zoetrope.github.io/trace_id"]; ok {
traceID, err := trace.TraceIDFromHex(parentTraceID)
if err != nil {
return ctrl.Result{}, err
}
spanCtx = spanCtx.WithTraceID(traceID)
}
if parentSpanID, ok := child.Annotations["otel.zoetrope.github.io/span_id"]; ok {
spanID, err := trace.SpanIDFromHex(parentSpanID)
if err != nil {
return ctrl.Result{}, err
}
spanCtx = spanCtx.WithSpanID(spanID)
}
log.FromContext(ctx).Info("SpanContext is valid", "isValid", spanCtx.IsValid())
ctx, span := r.Tracer.Start(trace.ContextWithSpanContext(ctx, spanCtx), "child-reconcile")
defer span.End()
traceID := span.SpanContext().TraceID()
spanID := span.SpanContext().SpanID()
logger := log.FromContext(ctx).WithValues("traceID", traceID, "spanID", spanID)
logger.Info("Reconcile")
return ctrl.Result{}, nil
}
コントローラーを実行し、Grafana Tempoでトレーシングの様子を確認します。
以下のように、親のコントローラーのReconcileと子のコントローラーのReconcileが一連のトレースとして表示されました。
しかし上記の方法では、Child
リソースが別のコントローラーやユーザーによって変更された場合でも、一連のトレースとしてまとめられてしまいます。
もう少しうまくやる方法はないものでしょうか…🤔
ちなみにGrafana LokiとGrafana Tempoを連携させると、ログに含まれるTraceIDからトレースデータにジャンプすることができるようになります。とても便利!
(左側のLokiのUIからTempoというボタンをクリックすると、右側に選択したTraceIDのTempoの画面が開きます。)
今回書いたコードはこちら