Golang Go语言中如果没有 ctx 传递,如何让 Trace ID 连贯传递呢?

发布于 1周前 作者 vueper 来自 Go语言

RT 对于 Go (这类)没有 ThreadLocal 的语言,如果在服务方法调用之间缺漏了 ctx context.Context 的传递,Trace 的信息就无法传递下去。

通常来说对于平台、工具方来说(例如做 Trace 平台的人),想推动所有业务都去改造是相当困难的,请教一下了解的 V 友有什么办法能 “改善” 这种情况?


Golang Go语言中如果没有 ctx 传递,如何让 Trace ID 连贯传递呢?
29 回复

用一个全局的 map ,key 是 goroutine 的唯一 Id ,value 是 RequestId

更多关于Golang Go语言中如果没有 ctx 传递,如何让 Trace ID 连贯传递呢?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我想了一下,这种方案可以在流量进来和流量出去的时候为(可能是空白的) ctx 补充 trace id ,但是印象中:
1. goroutine 没有唯一 ID ,之前了解到是故意这样设计的,不知道这种情况后来有没有改进
2. goroutine 之间的 trace id (可能)不容易传递

其实更想了解有没有一些无侵入,业务不感知的方案,能够达到同 goroutine 内补全 trace id 的效果(也就是可以不考虑问题 2 )

流量进来从堆栈拿 gid 出去再判断。这是魔法

错误往上抛, 最后在中间件里面打印日志, 中间件里面可以访问 ctx 拿到 trace_id

不是,哥们,那你这个 trace id 如何传递给下一个调用方呢?

记下魔法了,跟 1 楼其实是一样的。让我们猜猜业务方愿不愿意改 orz

ctx 肯定要传啊😂

这个东西就跟你和刚用 Redis 的人讨论可不可以使用 KEYS 一样的,不可能要求所有人立刻就知道这个东西重要不重要,可行与否,以及是否强制要传。等发现问题要改的时候往往是很难改得动,所以才会有很强的需求寻找如何优雅解决,而不是反推他们 “全部做对”(当然,全部做对肯定是最好最好的,不能因为难就否认)。

如果真不想全改,可以改一些有网络请求的组件,比如数据库,http client, redis 之类的,调用方只要升级核心组件即可,其他方法有好多其实也可以不用传递,毕竟没有网络请求。如果真想全部 trace, 建议加上 ctx, 长期来看还是有好处的,长痛不如短痛。

得传递 ctx🌚

负责过类似的工作,手动传最稳定、可靠。要是担心的话在测试环境拦截出口、入口日志,分析是否有缺失的,有缺失的就告警出来。

我倾向于显示传递,其实五楼推荐的办法是合适的,业务抛出业务相关的数据。中间件里输出信息的时候带上 reqId 。但是那种业务上要传的比如 tenant id 就没办法了。显式的传好了。



谢谢两位朋友的建议,是个可以实施并且可以慢慢推的方式,可能落地起来还需要搭配一些宣讲一点点改善

五楼这个是不好的解决方案,视角很局限,我们想解决的是业务间传递的问题,而对于单一的服务,如果它自己有这样的需求,它肯定有办法独立解决,向上抛错误也好,补齐 ctx 也罢。

而在跨团队协作里面推动所有人完成一件事是比较困难的,一个单元没解决好,大家都没办法拿到最好的结果。

另外回归到方案本身,它是不太有实践价值的,试想你现在只在 request log 显示了 trace id ,其他所有日志都没 trace id ,你如何串联所有日志?那,这个 trace id 只出现在 request log 的意义又是什么,岂不是跟没有 id 一样了。

这个需求其实很普遍,一搜遍地都是,但是绝大部分框架都没提供一个方案,Java 我不是很熟悉不说了。nodejs 这边 async context 没进标准,nodejs 自己的 async hooks 可靠性不够,这个 feature 本身推进的也慢。可能业界并不觉得这是一个很值得解决的问题。。不过从业务的角度来说有这个东西确实会很方便。

有个办法,拿到 goroutine 的 id ,不知道还有没有效

如果想要 threadLocal 这种效果只能用魔法拿到 gid ,不过好像不太稳定
我们开始项目小也没有考虑过 traceId ,后面项目中函数参数不管用不用的到默认第一个就是 context ,再后来做电商生态等原因回归了 java 。

无它,规范耳。

另外一个就是放在 mesh 层做,对业务透明。

mesh 层如何将 in & out 的流量进行关联呢?
例如我是服务 B ,收到了一个服务 A 的请求,处理,然后向服务 C 发一个请求,mesh 的基础设施如何保证发给 C 的请求携带的 TraceID 与 A 发给我的 TraceID 一样呢?

直接告诉他们你们 latency 很高,需要优化。自己去分析哪儿是瓶颈,他们就想到用了

要有 tid ,老老实实显式传递。不单是 tid 的问题,还有 log 的问题。 最佳实战就是 ctx ,携带 log 和 tid

之前也遇到过这个问题,用魔法取 gid 不可取,只能通过传递变量的方式进行,其实这里也有两种实现:
1 、到底是传递 context ,很多框架用这种,但是实际的 Log 方法参数的时候要带上 ctx ,并且如果需要携带的 trace 多的话感觉效率不高
2 、传递 log 变量呢,有一些框架用这种,第一眼看到也觉得很新奇,个人感觉用 log 变量传递的话能更高效的携带各种 trace ,并且 Log 方法参数和普通日志库都一致

我们之前在业务团队的时候倾向于用 ctx 传递,因为第三方库的方法签名不可控不可改,但是基本都保留了 ctx 。如果用 log 传递,或者说把 log 变量放在结构体内传递,它是对 ctx 的隐式使用,而且是 hardcode 的使用,无法分离出来给第三方方法用(或者说成本不如方法 1 )

而这个帖子,其实不是想讨论如何改成传递 ctx ,而是说不发生任何业务修改的情况下如何拦截采集这种信息。因为想改动的人不是业务团队,也就是不是写这个代码的人,而是平台提供方,如何在极低成本(例如,只修改 cmd/main.go 方法里面的一两行)的目标下完成改造,(尽可能)让所有调用、日志都携带 trace 信息。

另外我们之前选用方法 1 做业务层面的改造时,log.Info(string) 改成了 log.Info(context.Context, string),这个没啥好办法,都是全局的字符串替换修改调用方的,修改成了 context.TODO() 传入,然后等未来慢慢补全,实际落地下来感觉效果还可以的,而且有利于整体习惯养成。用 log 传递那是不是又不用设计 ctx 在方法签名里了?实际上这在 go 里是个不好的实践,相当于用 log 传递的方式,节约了 log.Info 调用的改造成本,但是继续保留了不传 ctx 的坏习惯,个人认为不可取。

#26 你看下 https://skywalking.apache.org/docs/skywalking-go/next/en/concepts-and-designs/key-principles/ 能否满足你的要求,看文档 skywalking go 使用起来貌似没有需要业务方做大量的改动,你你参考下他的实现

很高兴在这里看到有人提及 skywalking go ,我也是其中的一位 contributor ,我觉得混合编译及代码生成是个很有创意的 idea ,因为项目比较新,短期内也没办法直接推广到公司用,未来看看其他使用方的反馈

在Golang中,如果没有通过显式传递context.Context(通常简写为ctx)来传递Trace ID,实现其连贯传递确实需要一些技巧。以下是一些常用的方法:

  1. 全局变量或单例模式: 可以使用全局变量或单例模式来存储Trace ID。但这种方法在多线程或高并发环境下可能会导致Trace ID混乱,因此不推荐在复杂系统中使用。

  2. 利用请求上下文(如HTTP请求): 如果是在Web服务中,可以将Trace ID存储在HTTP请求的头信息或请求上下文中。通过中间件在请求处理链中自动提取和设置Trace ID。

  3. 使用协程本地存储(Goroutine Local Storage, GLS): Go标准库没有直接提供GLS功能,但可以使用第三方库如github.com/opentracing/opentracing-go/log中的Field机制,或者golang.org/x/net/context/ctxhttp(尽管它主要用于与HTTP请求绑定context)。更现代的解决方案是使用go.opentelemetry.io/otel/trace包中的全局Tracer和SpanContext,但这也通常与context结合使用。

  4. 日志框架的集成: 一些日志框架支持在日志输出中自动添加Trace ID,通过配置这些框架可以在日志上下文中自动获取和设置Trace ID。

虽然不直接传递context可以实现Trace ID的连贯传递,但推荐的做法仍然是尽量使用context来传递请求范围内的元数据,包括Trace ID,这样可以更好地保证线程安全性和上下文一致性。

回到顶部