Closed9

Kubernetesのカスタムコントローラーで分散トレーシングを活用する方法を模索する

zoetrozoetro

Kubernetesにおいて、複数のコントローラーが複数のリソースを順に処理するケースがあります。
例えばcert-managerでは、Certificateリソースを作ると、CertificateRquest, Order, Challengeと順番にリソースが作られて処理されます。

(  +---------+  )
  (  | Ingress |  ) Optional                                              ACME Only!
  (  +---------+  )
         |                                                     |
         |   +-------------+      +--------------------+       |  +-------+       +-----------+
         |-> | Certificate |----> | CertificateRequest | ----> |  | Order | ----> | Challenge |
             +-------------+      +--------------------+       |  +-------+       +-----------+
                                                               |

https://cert-manager.io/docs/troubleshooting/

こういうケースでは、分散トレーシングを活用すると処理が追いかけやすくなるのではないかと思います。

zoetrozoetro

というわけで、以下のようにリソースのannotationsTraceIDSpanIDを持たせてコントローラー間で受け渡す方法を考えてみます。

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
zoetrozoetro

まずは、親のコントローラーのReconcile処理です。
Parentというカスタムリソースが作られると、それに応じてChildというカスタムリソースを作成するという実装になっています。
このとき、ChildリソースのannotationsTraceIDSpanIDを埋め込みます。

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
}
zoetrozoetro

次に子のコントローラーのReconcile処理です。
Childリソースが作られると、そのリソースのannotationsからTraceIDSpanIDを抜き出して、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
}
zoetrozoetro

コントローラーを実行し、Grafana Tempoでトレーシングの様子を確認します。
以下のように、親のコントローラーのReconcileと子のコントローラーのReconcileが一連のトレースとして表示されました。

zoetrozoetro

しかし上記の方法では、Childリソースが別のコントローラーやユーザーによって変更された場合でも、一連のトレースとしてまとめられてしまいます。

もう少しうまくやる方法はないものでしょうか…🤔

zoetrozoetro

ちなみにGrafana LokiとGrafana Tempoを連携させると、ログに含まれるTraceIDからトレースデータにジャンプすることができるようになります。とても便利!
(左側のLokiのUIからTempoというボタンをクリックすると、右側に選択したTraceIDのTempoの画面が開きます。)

このスクラップは2023/05/09にクローズされました