登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go OpenTelemetry 怎么从 Trace 看清服务依赖关系

来源:17golang原创

时间:2026-10-05 16:27:17 335浏览 收藏

Go 服务接入 OpenTelemetry 后,最容易遇到的不是“没有任何 span”,而是 Trace 里只有零散节点:入口请求能看到,下游服务却像凭空消失。要让观测工具整理出服务依赖关系,关键是同时满足四件事:每个进程有稳定的 service.name,入站和出站请求都生成 span,跨服务请求传播上下文,进程退出前把已结束的 span 导出。

官方地址:https://opentelemetry.io/docs/languages/go/

本文把问题限定在 Go 的 HTTP 服务和 Trace 链路,不讨论某个线上监控产品的账号、收费或部署细节。你可以把 OTLP Collector 或其他兼容后端替换到导出端。

先划清服务依赖图需要的三类数据

服务图的“节点”通常来自资源属性,最重要的是服务名;“边”来自一次请求从客户端到服务端的关联;“边上的证据”则来自 span 的时间、状态和属性。只给一个服务创建手动 span,无法证明它调用了谁;只在客户端埋点而不传播上下文,下游也会变成另一条孤立 Trace。

因此先按下面的边界检查接入方案:

  • 节点:每个 Go 进程明确设置唯一的 service.name,不要让不同服务共享默认值。
  • 边:入站 HTTP 和出站 HTTP 都使用 OpenTelemetry instrumentation,分别记录服务接收和发起的请求。
  • 关联:把当前请求的 context 交给下游客户端,让 Trace 上下文随请求头传播。
两个 Go 服务通过 Trace 上下文连接并向 OTLP 导出端发送 span 的结构说明图
图1:Trace 上下文关系说明图,展示 Go 服务之间如何形成可读的调用边。

为 Go 进程初始化资源和 TracerProvider

Go 官方文档把 API、SDK、资源和 exporter 分开:业务代码通过 API 创建 span,SDK 负责处理和导出,Resource 描述服务身份。下面是一个精简的初始化骨架;newExporter 代表你选择的 OTLP 或本地调试 exporter,实际项目中应返回对应的 sdktrace.SpanExporter。

package telemetry

import (
    "context"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
)

// Setup 创建带有稳定服务名的 TracerProvider,并返回关闭函数。
func Setup(ctx context.Context, serviceName string, exporter sdktrace.SpanExporter) (*sdktrace.TracerProvider, func(context.Context) error, error) {
    // Resource 是服务图节点的身份;不同进程必须使用不同的 service.name。
    res, err := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(semconv.SchemaURL,
            semconv.ServiceName(serviceName),
        ),
    )
    if err != nil {
        return nil, nil, err
    }

    // BatchSpanProcessor 避免每个 span 都同步阻塞业务请求。
    provider := sdktrace.NewTracerProvider(
        sdktrace.WithResource(res),
        sdktrace.WithBatcher(exporter),
    )
    otel.SetTracerProvider(provider)

    // 退出时 Shutdown 会刷新队列并释放 exporter 资源。
    shutdown := func(shutdownCtx context.Context) error {
        timeoutCtx, cancel := context.WithTimeout(shutdownCtx, 5*time.Second)
        defer cancel()
        return provider.Shutdown(timeoutCtx)
    }
    return provider, shutdown, nil
}

这里的 service.name 不只是展示字段:它决定后端是否能把来自不同进程的 span 分成清晰的服务节点。BatchSpanProcessor 会异步批量处理已结束的 span,而 Shutdown 用于退出前刷新队列;如果程序被硬杀或没有执行关闭逻辑,短时间内产生的尾部 span 可能还没送出。

怎么把入站和出站请求连成一条 Trace

接入 HTTP 时,不要只在 handler 内调用 tracer.Start 就结束。服务端需要创建入站 span,客户端需要从当前请求的 context 创建出站 span,并让传播器写入 HTTP headers。官方 Go instrumentation 提供了 otelhttp,可减少手工处理这些细节。

import (
    "context"
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// BuildHTTPClient 同时为出站请求创建 span,并把 context 传播给下游。
func BuildHTTPClient() *http.Client {
    // Transport 会读取请求 context,并注入 W3C Trace Context 请求头。
    transport := otelhttp.NewTransport(http.DefaultTransport)
    return &http.Client{Transport: transport}
}

// BuildHTTPHandler 为入站请求创建服务端 span,handler 内仍可继续使用 r.Context()。
func BuildHTTPHandler(next http.Handler) http.Handler {
    return otelhttp.NewHandler(next, "orders-server")
}

// CallInventory 保留当前请求 context,避免下游请求脱离父 Trace。
func CallInventory(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }
    return client.Do(req)
}

真正的连接点是 http.NewRequestWithContext:如果调用下游时改用 context.Background(),客户端 span 可能仍然存在,但它不再携带当前请求的父上下文,依赖图就会出现孤立节点或断边。URL、HTTP 方法和状态等属性应保持可聚合,避免把每个用户 ID 直接拼进 span 名称。

服务依赖图缺边时,按 Trace 逐层排查

看到入口服务却看不到下游关系时,按“身份—传播—采样—导出”的顺序排查,通常比盯着最终图形更快:

  1. 身份:确认两个进程的 service.name 不为空且彼此不同;否则节点会被合并或显示成默认服务。
  2. 传播:在客户端调用前后确认使用的是同一个请求 context,并确认服务端 instrumentation 读取了传播头。
  3. 采样:如果只保留部分 Trace,刚好未被采样的请求不会出现在关系图中,不能据此判断代码没有调用下游。
  4. 导出:确认 exporter 地址、协议和关闭时机;开发环境可先用 console exporter 看是否真的生成了 span,再切换 OTLP。
按 service.name、传播头、采样策略和导出端排查服务依赖图缺边的静态诊断关系图
图2:依赖图缺边诊断说明图,按服务名、传播、采样和导出四个边界定位问题。

排查时还要区分“图没有边”和“代码没有边”:前者可能是采样、导出或传播问题,后者才是业务确实没有发起请求。先在 Trace 数据中确认 span 是否存在,再判断 span 之间是否共享 Trace 上下文,最后才调整图形展示或后端查询条件。

小结与常见问题

Go OpenTelemetry 想呈现服务依赖关系,最小闭环是稳定资源身份、入出站 HTTP instrumentation、上下文传播和可靠导出。先把这四层接通,再增加数据库、消息队列或业务 span,图上的关系才会逐渐接近真实运行路径。

只使用手动 span 可以吗?

可以,但你需要自己维护入站、出站和传播逻辑,容易漏掉边界。对 HTTP 等通用组件,优先使用官方 registry 中的 instrumentation,再用手动 span 补充业务语义。

为什么本地能看到 span,服务图仍然不完整?

优先检查两个服务是否使用不同的 service.name、下游请求是否沿用了原 context,以及 BatchSpanProcessor 是否在进程退出前完成 Shutdown。单点 span 存在并不等于父子关系已经建立。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>