《Shell 转 Go》
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

2. Golang Context 值传递的生产案例(01) - 链路追踪

Golang Context 值传递的生产案例(01): 链路追踪

如果在 公众号 文章发现状态为 已更新, 建议点击 查看原文 查看最新内容。

状态: 未更新

原文链接: https://typonotes.com/posts/2023/03/15/golang-context-in-action-trace/

看完本文

  1. 了解 链路追踪 和 OpenTelemetry 相关知识
  2. 了解 Context 值传递是如何在 链路追踪 的发展历程上登台亮相的。

之前在 Golang 上下文 Context 源码解析(1): 值传递 文章中举了一个例子说明讲解 Context 的值传递, 其中说到了 刘备-关羽-张飞 之间使用 Context 传递 曹操军队人数,

有朋友反馈说这个值应该是 业务参数 放在函数中作为 形参 传递, 难以理解为什么会放在 Context 中。

func Guanyu(n int) {
    // statement
}
func Zhangfei(n int) {
    // statement
}

因此, 这次我们通过生产实际应用, 来说一下 Context 的值传递。

链路追踪

想必大家也知道, 微服务治理是一个头疼的问题。 由于服务众多且部署分散, 因此通过 调用链路 排查问题就非常重要了。 实现这个的方案就叫 链路追踪 , 在微服务中非常重要。

可以说, 开源项目 OpenTelemetry 在链路追踪层面, 基本上已经是事实标准了。

Github 地址: https://github.com/open-telemetry

这里有一个官方一步步实现链路追踪的案例, 可以自己实现一下: https://github.com/open-telemetry/opentelemetry-go/tree/main/example/fib

Context 在 链路追踪 中的应用

现在假设有 6 个服务, SvcA 1-3 , SvcB 1-3, 这 6 个服务可能分布在不同机器上面。

第一阶段

我们知道他们之间存在调用关系, 但是任何证据支撑。 所以从外部看来, 他们之间就是完全独立的的 6 个服务。

由于时间紧, 任务重, 业务就这样上线了。

第二阶段

后来业务出了问题, 除了靠记忆, 就只能现场看代码找调用链路。 排查过程异常艰难。

于是有人提议说, 不如我们在每个请求中都带上 UUID, 并且打印到日志里面, 就知道哪些服务在某个请求下, 是有关系的。

于是就有了 TraceID

第三阶段

业务还是除了问题, 这次排查过程相比上次就轻松多了, 但是依然发现了一些 困惑 的问题。 虽然能从日志时间看到服务的调用顺序, 但是并不知道他们之间的调用关系, 没办法有效的把他们组织起来。

于是就有人说, 要不在日志中增加两个字段, 表明他们调用的父子关系吧

于是就有了 ParentSpanIDSpanID

使用 Context 携带字段

现在好了我们能通过 TraceID, SpanID, ParentSpanID 搞清楚服务之间的调用关系了。 但是要怎么传递他们呢?

这些字段是 肯定不能 直接放在参数里面,

  1. 这些字段不是业务参数, 放进去会 污染 函数或方法。
  2. 即使现在放进去了, 以后要维护 增加或减少 怎么办? 不可能每个地方服务都去改一次吧, 服务里面还有那么多函数。 从理论上来说, 这个就是不合理的设计。
func Guanyu(n int, TraceID string, SpanID string, ParentSpanID string) {
    // statement
}
func Zhangfei(n int, TraceID string, SpanID string, ParentSpanID string) {
    // statement
}

第四阶段

于是又有人说了, 要不我们统一放在 Context 里面吧,

  1. Context 本身就是上下文, 就具有传递性。
  2. 而且其设计理论上还可以存储无限多的数据。

这样我们就只需要在在每个函数或者方法中多添加一个 Context 参数就行了, 维护也方便。

func Guanyu(ctx context.Context, n int) {
    // statement
}
func Zhangfei(ctx context.Context, n int) {
    // statement
}

这样看起来, 就清爽很多了。 而且这种结构 是不是很熟悉