可观测Go Agent如何实现无侵入Go应用监控
引言
随着Kubernetes和容器化技术的普及,Go语言不仅在云原生基础组件领域广泛应用,也在各类业务场景中占据了重要地位。如今,越来越多的新兴业务选择Golang作为首选编程语言。得益于丰富的RPC框架(如Gin、Kratos、Kitex等),Golang在微服务生态中愈加成熟,并被用于很多重要的开源项目,如OpenTelemetry Collector、ETCD、Prometheus、Istio等。
但是跟Java相比,Golang在微服务生态上依然处于劣势,相比Java 可以使用字节码增强的技术来实现无侵入的应用监控能力,Golang没有成熟的对应方案,当前,大多数面向Golang应用的监控能力主要是通过SDK方式接入,如OTel SDK,需要开放人员手动进行埋点,手动埋点的方案就会存在以下的两个问题:
- Trace需要每个调用点都需要进行埋点,同时要注意Trace上下文的传递,避免链路串联错误
- Metrics统计,需要针对每次调用都进行统计,同时注意指标发散的问题
- 工作量非常大,对业务侵入性,每增加一个接口就需要同步增加对应的埋点
为了解决上述问题,可观测Go Agent应运而生。
可观测Agent架构
Java有JVM提供的基于字节码增强的能力可以进行无侵入的埋点,Golang没有类似的能力,因此这里我们是通过编译期注入的方案,在编译期完成埋点的注入,架构如下所示:
熟悉Golang编译流程的同学会比较熟悉,Go应用程序编译的大概流程如下所示:
- 创建临时目录,类似的语句为
<font style="color:rgb(0, 0, 0);">mkdir -p $WORK/b088/</font>
。 - 查找依赖信息,类似的语句为
<font style="color:rgb(0, 0, 0);">cat >/var/folders/7c/xvg9tyv929d1mqbd44ygh8400000gp/T/go-build2899987616/b104/importcfg << 'EOF' # internal</font>
,importcfg中包含所有的依赖信息。 - compile编译出目标文件xxx.a
- 生成Link需要的配置文件,运行Link将上述目标文件转换为可执行文件
- 将可执行文件移到当前目录,删除临时目录
同时Go提供了-toolexec指定程序编译时候的工具,工具会在go build的时候介入编译过程,如下所示:
那Go Agent如何在编译期完成埋点的注入呢,我们从以下几个方面来进行介绍:
查找埋点
一般微服务的代码非常多如何找到需要插入的点呢,这里使用了语法树的能力,通过语法树分析出来每个.go文件中的语法,下面介绍一下语法树如何使用的:
- 使用Lexer词法分析器对源文件进行语法分析,生成一个Token
- Parser解析器通过检索分析生存AST语法树
Go本身有提供上述这些库,如下所示:
通过下面这个Demo进行AST的介绍:
分析后的结果如下所示:
其中ast.Ident 表示包名,ast.GenDecl表示函数以外的所有声明,如import、const、var、type等关键字,ast.FuncDecl代表函数声明和函数的内部参数等。
代码插入
通过上述的词法分析就可以得出当前Golang服务中的代码编写情况,然后修改这些分析出来的语法树,将监控相关的逻辑如生成span添加到语法树中。
我们在Agent中提供了一个代码插入的框架,以下是插入框架对应的API,其中可以标注进行埋点的规则,如针对哪个SDK、哪个版本范围、哪个函数、哪个类进行埋点,埋点的前后代码的是什么。
类似go redis的埋点如下:
其中afterNewFailOverRedisClient 就是我们想要插入到NewFailoverClient 函数中的代码,通过这个API我们非常方便的去定义我们的埋点方法,同时方便进行扩展。
混合编译
在查找到埋点的位置后,通过API完成埋点代码的插入,接下来就是进行混合编译阶段,编译过程中将插入的代码和已有的代码一起编译,编译完成后会生成对应的二进制文件,Go Agent编译代码的流程如下:
可观测Trace/Metrics能力
介绍完整个编译、插入流程后,我们将介绍一下在Go Agent中我们注入的Trace和Metrics能力,Trace、Metrics作为可观测领域最重要的2个部分(后续我们还会提供Profiling的能力),对应用的稳定性监控至关重要。
Trace
Trace埋点
Trace其实就是链路追踪,一次调用可以通过一条链路信息找到所有的调用的接口、延时等数据,如下所示:
在Go Agent中我们在每个调用的埋点的开始处调用tracer.Start()
在埋点结束时候调用span.End()
Trace上下文透传
在同一个应用的不同的埋点中如何保障trace的上下文传递不会丢失呢,这里我们在goroutine中增加一个tls context变量,goroutine是通过以下的结构体描述的
我们通过编译时注入的方式,在其中增加一个变量,trace_tls用于保存trace的上下文信息。
代码中我们直接使用tracer.start 时候,会通过埋点的方式去在g中查找trace的上下文信息,如果有新的goroutine创建,我们也会对newproc1 函数进行埋点,将父goroutine trace信息传递给子gouroutine,通过这样的方式确保了单条trace id对应的上下文都能串联在一起。
Metrics
指标的统计跟Trace类似,在每个埋点的地方对如调用次数、时间、错误、慢请求都进行记录,同时为了避免指标的发散带来的性能问题,我们通过指标收敛减少指标的数量, 通过下面两个收敛器完成指标收敛:
- 常规收敛器:负责根据输入规则直接转换输出,例如转换url,转换sql语句等。
- 限制收敛器:实现对收敛后的总值域大小的限制**,**其内部使用不同方式维护了一套有大小上限Limit的白名单。一般逻辑为,当白名单已满且要收敛的值不在白名单中时,触发收敛逻辑。
Go Agent Plugin
支持20个常见的微服务框架、中间件SDK等,同时对OTel SDK可以兼容。
Go Agent兼容性
OTel SDK兼容
OTel SDK的兼容我们支持从v1.6.0版本到v1.26.0版本,在代码中如果已经使用OTel SDK添加埋点逻辑,如下所示,在代码中使用tracer.Start创建了自定义的span:
Go Agent 在进行代码注入的时候,同样会hook OTel SDK的逻辑,在Agent上报的链路信息中也会包含用户代码自定义的逻辑:
Trace透传协议兼容
支持W3C、Jaeger、EagleEye、Zipkin协议透传。