README
¶
示例概览:example_service 多服务链路追踪演示
example_service 目录下包含一组互相调用的示例服务,用于演示:
- 多个 HTTP 服务之间的调用链路(网关 -> 订单 -> 支付 -> 用户 等)
- 如何在每个服务里初始化 Jaeger 链路追踪
- 如何在服务之间通过
rest.Client进行调用,并接入自定义的 OpenTelemetry Hook(NewReqxOtelHook) - 同时演示 gin 和 go-restful 两种 HTTP 框架的使用方式
服务列表(端口均不冲突):
gateway:网关服务,对外统一入口,端口18080user:用户服务,提供用户基础信息查询,端口18081order:订单服务,提供订单列表查询,端口18082payment:支付服务,提供订单支付状态查询,端口18083
调用关系与链路结构
整体调用链大致如下(从外部调用网关开始):
sequenceDiagram
participant Client as 外部调用方
participant Gateway as gateway-service (gin)
participant User as user-service (gin)
participant Order as order-service (go-restful)
participant Payment as payment-service (gin)
Client->>Gateway: HTTP /api/checkout/:userID
Gateway->>User: HTTP GET /api/v1/user/:id
Gateway->>Order: HTTP GET /api/v1/orders?userID=
Order->>Payment: HTTP GET /api/v1/payment/:orderID?userID=
Payment->>User: HTTP GET /api/v1/user/:id
在 Jaeger UI 中,你应该能够看到一条完整的 Trace,从外部请求网关开始,一路串到多个下游服务,中间的所有出站 HTTP 调用都会作为子 Span 展示出来。
各服务职责与实现要点
1. 网关服务:gateway
- 文件位置:
[main.go](/rest/example/example_service/gateway/main.go) - 监听地址:
":18080" - 主要职责:
- 对外暴露
GET /api/checkout/:userID接口 - 聚合
user-service和order-service的数据,返回一个包含用户信息和订单列表的响应
- 对外暴露
- 链路追踪初始化:
- 使用
jaeger.InitTracer(ctx, "gateway-service")初始化当前进程的TracerProvider - 使用
otelgin.Middleware("gateway-service-gin")为 gin 路由接入 Server 端链路追踪中间件
- 使用
- 对
rest.Client的使用:- 通过
otel.Tracer("gateway-service-goto-next")获取一个trace.Tracer - 创建基础客户端:
tracer := otel.Tracer("gateway-service-goto-next") baseClient := rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tracer)) - 每次处理请求时通过
baseClient.Clone()克隆一份客户端,避免并发场景下修改配置互相影响。 - 调用下游服务示例:
resp := client.SetBaseURL(userServiceBase). Get("/api/v1/user/" + userID). Do(ctx) NewReqxOtelHook会在每一个出站 HTTP 请求前后:- 自动创建 Client Span
- 注入 Trace 上下文到请求头
- 记录 HTTP 方法、URL 等语义属性
- 从 Span 中提取 TraceID 打日志,可选地写入自定义请求头(供老系统使用)
- 通过
2. 用户服务:user
- 文件位置:
[main.go](/rest/example/example_service/user/main.go) - 监听地址:
":18081" - 主要职责:
- 对外暴露
GET /api/v1/user/:id接口 - 返回一个简单的用户信息结构体(示例中使用静态拼接)
- 对外暴露
- 链路追踪使用:
- 同样使用
jaeger.InitTracer(ctx, "user-service")初始化当前服务的 Tracer - 使用
otelgin.Middleware("user-service-gin")对 gin 的入口进行自动埋点
- 同样使用
- 对
rest.Client的使用:- 用户服务本身 不再向下游发 HTTP 请求,因此这里没有用到
rest.Client - 它主要作为 “被调用方” 出现在调用链中。
- 用户服务本身 不再向下游发 HTTP 请求,因此这里没有用到
3. 订单服务:order
- 文件位置:
[main.go](/rest/example/example_service/order/main.go) - 监听地址:
":18082" - 使用框架:
go-restful - 主要职责:
- 对外暴露
GET /api/v1/orders?userID=xxx接口 - 为指定用户生成一组模拟订单,并在内部继续调用支付服务与用户服务丰富信息
- 对外暴露
- 链路追踪初始化:
- 使用
jaeger.InitTracer(ctx, "order-service")初始化当前服务的 Tracer - 使用
otel.Tracer("order-service-go-restful")在getOrders处理函数内手动创建 Server Span:ctx := req.Request.Context() propagator := otel.GetTextMapPropagator() ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Request.Header)) tracer := otel.Tracer("order-service-go-restful") ctx, span := tracer.Start(ctx, "GET /api/v1/orders")
- 使用
- 对
rest.Client的使用:- 在
main中创建带 Hook 的全局客户端:tracer := otel.Tracer("order-service-goto-next") httpClient = rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tracer)) - 在
getOrders中,为每个请求克隆一份客户端:client := httpClient.Clone() - 调用
payment-service与user-service:// 调支付服务 resp1 := client.SetBaseURL(paymentServiceBase). Get("/api/v1/payment/"+orders[i].ID). Param("userID", userID). Do(ctx) // 调用户服务 resp2 := client.SetBaseURL(userServiceBase). Get("/api/v1/user/" + userID). Do(ctx) - 通过
NewReqxOtelHook,这些出站 HTTP 调用会自动出现在 Trace 中,形成:gateway -> order-service -> payment-service -> user-service的完整链路。
- 在
4. 支付服务:payment
- 文件位置:
[main.go](/rest/example/example_service/payment/main.go) - 监听地址:
":18083" - 使用框架:
gin - 主要职责:
- 对外暴露
GET /api/v1/payment/:orderID?userID=xxx接口 - 模拟判断某订单的支付状态
- 内部再调用一次
user-service,丰富返回结果中的用户信息
- 对外暴露
- 链路追踪初始化:
- 使用
jaeger.InitTracer(ctx, "payment-service") - 使用
otelgin.Middleware("payment-service-gin")接入 gin 中间件
- 使用
- 对
rest.Client的使用:- 在
main中初始化全局httpClient:tracer := otel.Tracer("payment-service-goto-next") httpClient = rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tracer)) - 在处理函数中
Clone()出一份请求级客户端,然后调用user-service:client := httpClient.Clone() resp := client.SetBaseURL(userServiceBase). Get("/api/v1/user/" + userID). Do(ctx)
- 在
关于 rest.Client 的使用要点
-
初始化:
- 推荐在服务启动时创建一个“基础客户端”,配置好超时、重试策略、链路追踪 Hook 等。
- 示例中使用:
baseClient := rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tracer))
-
每请求克隆:
- 由于一个
rest.Client在使用时,会不断通过链式调用修改内部状态(SetBaseURL、Header、Param等),所以 不要在多个请求之间直接复用同一个实例。 - 正确方式是:
client := baseClient.Clone() - 这样可以在并发场景下保证不同请求之间互不干扰。
- 由于一个
-
链路追踪 Hook:
trace.NewReqxOtelHook(tracer)会在每个请求时:- 创建
SpanKindClient类型的 Span - 打上标准 HTTP 语义属性(如方法、URL、状态码等)
- 将 Trace 上下文注入 HTTP 头部(W3C Trace Context 格式)
- 从 Span 提取 TraceID 打日志,并可写入自定义请求头(如
X-Trace-ID)供老系统使用
- 创建
-
与 gin / go-restful 中间件配合:
- gin 侧用
otelgin.Middleware(...)自动为入口请求创建 Server Span - go-restful 侧示例中展示了如何手动从请求头中
Extract出上下文,并创建 Span rest.Client会基于这些上下文继续往下传播 Trace,实现“端到端”的链路串联。
- gin 侧用
注意事项与最佳实践
-
1. 每个服务只初始化一次 TracerProvider
- 每个示例服务的
main函数里,都调用了一次jaeger.InitTracer(ctx, "xxx-service"),并在退出时调用shutdown。 - 真实生产环境下也应遵循“每进程一次初始化”的原则,不要在每个请求中重复创建。
- 每个示例服务的
-
2. 不要双重埋点同一条出站请求
- 你可以选择:
- 使用
NewReqxOtelHook(示例代码当前使用) - 或者使用
otelhttp.NewTransport来包装底层http.RoundTripper
- 使用
- 但 不要同时使用两种方式,否则可能在同一次 HTTP 调用上生成两个 Client Span。
- 你可以选择:
-
3. 始终使用请求上下文
ctx- 示例中所有
.Do(ctx)调用都传入了从 gin / go-restful handler 中获取的c.Request.Context()或req.Request.Context()。 - 这是保证 Trace 能够串起来的关键:Span 和日志都会挂载在正确的调用链上。
- 示例中所有
-
4. TraceID 的获取与利用
- 在
NewReqxOtelHook中,你可以通过span.SpanContext().TraceID()拿到当前 Trace 的 ID,进而:- 记录到结构化日志中,方便排查问题时在日志与 Jaeger 之间对照
- 写入
X-Trace-ID等自定义 HTTP 头,给不支持 OpenTelemetry 的下游系统使用
- 在
-
5. 端口与路径规划
- 所有服务的端口在示例中已经错开,方便在本机直接运行多进程:
- gateway:
18080 - user:
18081 - order:
18082 - payment:
18083
- gateway:
- 路径命名尽量贴近真实业务接口风格,便于你在自己的项目中直接参考迁移。
- 所有服务的端口在示例中已经错开,方便在本机直接运行多进程:
如何运行与观察效果(简要)
- 启动本地 Jaeger(例如使用 Docker 或已有环境)。
- 依次在不同终端启动:
user、payment、order、gateway四个服务。 - 通过浏览器或
curl调用网关接口,例如:curl "http://localhost:18080/api/checkout/123" - 打开 Jaeger UI,按
service=gateway-service或具体 TraceID 查询,就可以看到完整的多服务调用链。
Click to show internal directories.
Click to hide internal directories.