本文介绍Go运行环境的链路追踪相关内容。

背景信息

阿里云链路追踪服务(Tracing Analysis)基于OpenTracing标准,兼容开源社区,为分布式应用的开发者提供了完整地分布式调用链查询和诊断、分布式拓扑动态发现、应用性能实时汇总等功能。

函数计算与链路追踪集成后,支持使用Jaeger SDKOpenTelemetry上传链路信息,使您能够跟踪函数的执行,帮助您快速分析和诊断Serverless架构下的性能瓶颈,提高Serverless场景的开发诊断效率。

功能简介

您可以在函数计算控制台配置链路追踪。具体操作,请参见配置链路追踪

为服务开启链路追踪后,函数计算会自动记录请求在系统侧的耗时,包含冷启动耗时、Initializer函数的耗时和函数的执行时间等。关于下图中系统Span的说明,请参见Span名称说明链路追踪

如您还需查看函数内业务侧的耗时,例如,在函数内访问RDS,NAS等服务的耗时,可以通过创建自定义Span来实现。

示例代码

函数计算的链路分析基于OpenTracing协议的Jaeger实现,Go运行时提供以下两种创建自定义Span的方式。

使用OpenTelemetry(推荐)

在Go语言的代码中,您可以通过OpenTelemetry SDK手动埋点,将数据上报到链路追踪服务端。完整的示例代码,请参见golang-tracing-openTelemetry

示例代码解析如下。
  • 添加依赖。
    go get github.com/aliyun/fc-runtime-go-sdk
    go get go get go.opentelemetry.io/otel
    go get go.opentelemetry.io/otel/sdk
    go get go.opentelemetry.io/otel/exporters/jaeger
  • 上报数据到链路追踪服务端。
    func HandleRequest(ctx context.Context, event MyEvent) (string, error) {
        // 获取函数计算上下文Tracing信息
        fctx, ok := fccontext.FromContext(ctx)
        if !ok {
            return "", fmt.Errorf("failed to get FcContext")
        }
        spanCtx, endpoint, err := getFcTracingInfo(fctx)
        if err != nil {
            return "", fmt.Errorf("failed to getFcTracingInfo, error: %v", err)
        }
        // 创建Tracer Provider
        tp, err := NewTracerProvider(endpoint)
        if err != nil {
            return "", fmt.Errorf("OpenTracingJaegerEndpoint: %s, error: %v", fctx.Tracing.JaegerEndpoint, err)
        }
        // 设置为全局
        otel.SetTracerProvider(tp)
        if err != nil {
            return "", fmt.Errorf("failed to getFcSpanCtx, error: %v", err)
        }
        // 创建自定义Span
        startMySpan(trace.ContextWithSpanContext(ctx, spanCtx))
        return fmt.Sprintf("hello world! 你好,%s!", event.Name), nil
    }
  • 创建一个tracerProvider,提供对Tracers的访问。
    func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
        // 创建Jaeger exporter
        exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
        if err != nil {
            return nil, err
        }
        tp := tracesdk.NewTracerProvider(
            // 注册exporter
            tracesdk.WithBatcher(exp),
            // 在Resource里记录应用信息
            tracesdk.WithResource(resource.NewWithAttributes(
                semconv.SchemaURL,
                semconv.ServiceNameKey.String("FCTracer"),
                attribute.String("environment", "production"),
                attribute.Int64("ID", 1),
            )),
        )
        return tp, nil
    }
  • 获取上下文的Tracing信息,将OpenTracingSpanContext转换为OpenTelemetrySpanContext。
    func getFcTracingInfo(fctx *fccontext.FcContext) (trace.SpanContext, string, error) {
        // 获取Tracing信息
        spanContext := trace.SpanContext{}
        endpoint := fctx.Tracing.JaegerEndpoint
        OpenTracingSpanContext := fctx.Tracing.OpenTracingSpanContext
        if len(endpoint) == 0 {
            return spanContext, endpoint, fmt.Errorf("invalid jaeger endpoint")
        }
        spanContextSlice := strings.Split(OpenTracingSpanContext, ":")
    
        // 填充TracingID高位
        tid, err := trace.TraceIDFromHex("0000000000000000" + spanContextSlice[0])
        if err != nil {
            return spanContext, endpoint, err
        }
        fid := trace.FlagsSampled
        spanContext = spanContext.WithTraceID(tid).WithTraceFlags(fid).WithRemote(true)
        return spanContext, endpoint, nil
    }
  • 创建tracer并通过转换的OpenTelemetrySpanContext创建子Span。每一个Span代表调用链中被命名并计时的连续性执行片段,您也可以基于该Span继续创建子Span。
    func startMySpan(ctx context.Context) {
        // 使用全局TracerProvider.
        tr := otel.Tracer("fc-Trace")
        ctx, parentSpan := tr.Start(ctx, "fc-operation")
        defer parentSpan.End()
        parentSpan.SetAttributes(attribute.Key("version").String("fc-v1"))
        time.Sleep(150 * time.Millisecond)
        child(ctx)
    }
    
    func child(ctx context.Context) {
        tr := otel.Tracer("fc-Trace")
        _, childSpan := tr.Start(ctx, "fc-operation-child")
        defer childSpan.End()
        time.Sleep(100 * time.Millisecond)
        childSpan.AddEvent("timeout")
    }

使用Jaeger SDK

您可以通过Jaeger SDK埋点,将数据上报到链路追踪服务端。完整的示例代码,请参见golang-tracing

示例代码解析如下。
  • 添加依赖。
    go get github.com/aliyun/fc-runtime-go-sdk
    go get github.com/opentracing/opentracing-go
    go get github.com/uber/jaeger-client-go
  • 上报数据到链路追踪服务端。
    func HandleRequest(ctx context.Context, event MyEvent) (string, error) {
        // 获取函数计算上下文Tracing信息
        fctx, _ := fccontext.FromContext(ctx)
        endpoint := fctx.Tracing.JaegerEndpoint
        OpenTracingSpanContext := fctx.Tracing.OpenTracingSpanContext
        if len(endpoint) == 0 {
            return "", fmt.Errorf("invalid jaeger endpoint")
        }
        // 创建Tracer
        tracer, closer := NewJaegerTracer("FCTracer", endpoint)
        defer closer.Close()
        // 恢复spanContext
        spanContext, err := jaeger.ContextFromString(OpenTracingSpanContext)
        if err != nil {
            return "", fmt.Errorf("OpenTracingSpanContext: %s, error: %v", fctx.Tracing.OpenTracingSpanContext, err)
        }
        // 创建自定义Span
        startMySpan(spanContext, tracer)
        return fmt.Sprintf("hello world! 你好,%s!", event.Name), nil
    }
  • 创建一个tracer对象,提供对Tracers的访问。
    func NewJaegerTracer(service, endpoint string) (opentracing.Tracer, io.Closer) {
        sender := transport.NewHTTPTransport(endpoint)
        tracer, closer := jaeger.NewTracer(service,
            jaeger.NewConstSampler(true),
            jaeger.NewRemoteReporter(sender))
        return tracer, closer
    }
  • 转换spanContext并创建自定义Span,您也可以基于该Span继续创建子Span。
    func startMySpan(context jaeger.SpanContext, tracer opentracing.Tracer) {
        parentSpan := tracer.StartSpan("MyFCSpan", opentracing.ChildOf(context))
        defer parentSpan.Finish()
        parentSpan.SetOperationName("fc-operation")
        parentSpan.SetTag("version", "fc-v1")
        time.Sleep(150 * time.Millisecond)
        // 开启子Span
        childSpan := tracer.StartSpan("fc-operation-child", opentracing.ChildOf(parentSpan.Context()))
        defer childSpan.Finish()
        time.Sleep(100 * time.Millisecond)
        childSpan.LogFields(
            log.String("type", "cache timeout"),
            log.Int("waited.millis", 100))
    }