写点什么

编译时插桩,Go 应用监控最佳选择

  • 2025-01-07
    浙江
  • 本文字数:3924 字

    阅读完需:约 13 分钟


可观测性是以系统的指标、日志、链路追踪、持续剖析四大数据支柱为基础,从宏观到微观,通过不同数据之间互相关联,衍生出如数据监控、问题分析、系统诊断等一系列的能力。



Java[1]可以通过字节码增强的技术实现无侵入的应用监控(开源社区有非常多的无侵入 Agent 实现方案,技术非常成熟),可以轻松获取到关键监控数据,相比 Java,Go 因为语言的特点,应用运行的时候已经被编译成一个二进制文件,无法再做类似 Java 字节码增强的方式进行动态插桩,在应用监控领域的生态并不完善,可观测的四大数据支柱无法通过无侵入的方式来实现,使得用户的接入成本变高,当前针对 Go 应用的可观测能力,有 3 种解决方案:


  • SDK 方案

  • eBPF 方案

  • 编译期自动注入方案


以下分别来介绍这几个方案以及对应的开源实现:

SDK 方案

在可观测领域,随着 OpenTracing 被 OTel 收编,目前被广泛使用的 SDK 就是 OTel Go SDK[2],通过在业务代码的每个需要的地方进行手动增加埋点,如下所示:


package main
import ( "context" "fmt" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/trace" "io" "net/http")
func init() { tp := trace.NewTracerProvider() otel.SetTracerProvider(tp)}
func main() { for { tracer := otel.GetTracerProvider().Tracer("") ctx, span := tracer.Start(context.Background(), "Client/User defined span") otel.GetTextMapPropagator() req, err := http.NewRequestWithContext(ctx, "GET", "http://otel-server:9000/http-service1", nil) if err != nil { fmt.Println(err.Error()) continue } client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println(err.Error()) continue } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { fmt.Println(err.Error()) continue } fmt.Println(string(b)) span.SetAttributes(attribute.String("client", "client-with-ot")) span.SetAttributes(attribute.Bool("user.defined", true)) span.End() }}
复制代码


先定义好一个 TraceProvider,然后在发起请求的地方获取 tracer,使用 tracer.Start 创建一个 span,然后发起请求,在请求结束后使用 span.End()。


这是一个简单的 http 的请求,如果是复杂的业务应用,会涉及多个调用,比如调用 redis、mysql、mq、es 等中间件,需要在每个调用的地方都进行埋点,同时还需要处理好 span Context 的传递、baggage 的传递,以及及时调用 span End。


OTel 的 spanContext 都是通过 context 进行传递,如下所示:


func testContext() {  tracer := otel.Tracer("app-tracer")  opts := append([]trace.SpanStartOption{}, trace.WithSpanKind(trace.SpanKindServer))  rootCtx, rootSpan := tracer.Start(context.Background(), getRandomSpanName(), opts...)  if !rootSpan.SpanContext().IsValid() {    panic("invalid root span")  }
go func() { opts1 := append([]trace.SpanStartOption{}, trace.WithSpanKind(trace.SpanKindInternal)) _, subSpan1 := tracer.Start(rootCtx, getRandomSpanName(), opts1...) defer func() { subSpan1.End() }() }()
go func() { opts2 := append([]trace.SpanStartOption{}, trace.WithSpanKind(trace.SpanKindInternal)) _, subSpan2 := tracer.Start(rootCtx, getRandomSpanName(), opts2...) defer func() { subSpan2.End() }() }() rootSpan.End()}
复制代码


上述的 2 个新创建的协程里面使用了 rootCtx,这样 2 个协程里面创建的 span 会是 rootSpan 的子 span,在业务代码中也需要类似的方式进行传递,如果不正确传递 context 会导致调用链路无法串联在一起,也可能会造成链路错乱。


同时 OpenTelemetry Go SDK 目前保持着 2 周到 4 周会发布一个版本


https://github.com/open-telemetry/opentelemetry-go/releases,更新速度非常快,经常会有前后不兼容的情况,业务升级 OTel Go SDK 会导致代码也需要进行修改,成本非常高。

eBPF 方案

eBPF(扩展的伯克利数据包过滤器)作为 Linux 内核中的一个高效且灵活的虚拟机,允许开发者自定义运行程序,并通过特定接口将这些程序加载到内核空间执行。这一特性使得 eBPF 成为了构建各类系统监控解决方案的理想选择之一。



近年来,基于 eBPF 技术开发的各种开源项目如雨后春笋般涌现出来,其中包括


  • pixie(https://github.com/pixie-io/pixie)

  • beyla(https://github.com/grafana/beyla)

  • opentelemetry-go-instrumentation(https://github.com/open-telemetry/opentelemetry-go-instrumentation)

  • deepflow(https://github.com/deepflowio/deepflow)


等知名项目。它们共同致力于利用 eBPF 的强大能力来实现诸如性能分析(Profiling)、网络流量监测(Network Monitoring)、度量指标收集(Metric Collection)及分布式追踪(Distributed Tracing)等功能。


eBPF 可以通过在不同位置的挂载点完成对数据流的抓取,比如 tracepoint、kprobe 等,也可以使用 uprobe 针对用户态函数进行 hook,以协议解析为例,随着业务复杂度的提升以及不同使用场景的要求,用户态的协议非常多,有 RPC 类型的 http、https、grpc、dubbo 等,还有中间件的 mysql、redis、es、mq、ck 等,要通过 eBPF 抓取的数据完成数据解析并实现指标的统计难度非常大。


以使用 eBPF 监控 Go 应用为例,因其独特的并发模型而广泛采用异步处理机制,若想精确地进行跨协程上下文传递或深入到应用程序内部进行细粒度的跟踪,则通常还需要额外引入 SDK 来进行辅助支持,完成不同协程之间的上下文传递。


尽管上述项目在功能上存在一定程度的相似性,但由于 eBPF 自身的一些限制因素,比如 eBPF 通常仅限于具有提升权限的 Linux 环境,同时针对内核的版本有要求,对于某些应用场景尤其是涉及到复杂应用层逻辑追踪时,单独依靠 eBPF 往往难以达到理想效果。


就性能开销而言,eBPF 相对于进程内的 Agent 稍显落后,因为 uprobe 的触发需要在用户空间和内核之间进行上下文切换,这对于访问量特别大的一些接口难以承受。

编译时插桩方案

在这个方案前我们在 eBPF 方案做了非常多的探索,希望使用 eBPF 一劳永逸的解决非 Java 语言的各种监控问题,特别是 Go 应用(在当前除了 Java 外使用最广泛的语言),经过长时间的探索,发现无法达成如 Java 一样实现完全无死角的监控能力,这也正让我们开始思考通过其他方式解决这个问题,基于 Go toolexec 能力,编译时插桩实现 Go 的应用监控变得可行。


Go 应用的编译流程如下:



使用简单的 go build 即可获得最终可以执行的二进制文件,go build 的过程通过以下的:



在经过词法分析、语法分析后生成一些.a 的中间态文件,最终通过 Link 的方式将.a 文件生成为二进制文件。通过这个步骤可以看出我们可以在编译前端到编译后端中间进行 hook 的操作,因此我们将对应的编译流程改成如下方式:



通过 AST 语法树分析,查找到监控的埋点,根据提前定义好的埋点规则,在编译前插入需要的监控代码,然后经过完成的 Go 编译流程将代码注入到最终的二进制中,这个方案与程序员手写代码完全没有区别,由于经过了完整的编译流程,不会产生一些不可预料的错误。


使用阿里云可观测 Go Agent 能力,只需要下载一个编译工具 instgo,然后修改一下编译语句即可快速接入,如下所示:


当前的编译语句:


当前的编译语句:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
使用Aliyun Go Agent:
wget "http://arms-apm-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/instgo/instgo-linux-amd64" -O instgochmod +x instgoCGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./instgo go build main.go
复制代码


通过 wget 下载 instgo 编译工具,只需要简单修改在 go build 前添加 instgo 即可完成监控能力注入。


我们可以在插入的代码中实现跟 Java 应用监控完全一样的监控能力,如链路追踪、指标统计、持续剖析、动态配置、代码热点、日志 Trace 关联等等,在插件丰富度上我们支持了 40+的常见插件[4],包含了 RPC 框架、DB、Cache、MQ、Log 等,在性能上,5%的消耗即可支持 1000 qps[5],通过动态开关控制、Agent 版本灰度等实现生产的可用性和风险控制能力。

总结


本文讲解了阿里云编译器团队和可观测团队为了实现 Go 应用监控为什么选择编译时插桩的原因,同时还介绍了其他的监控方案,以及它们的优缺点。我们相信阿里云 Go Agent(Instgo)是一个非常强大的工具,可以帮助我们实现针对 Go 应用更好的 APM 能力,同时还能保持应用程序的安全性和可靠性。


为了推广编译时注入的方案,同时为 Go 开发者提供更多的选择,提升效率,我们的 Agent 进行了开源[7],欢迎大家加入我们的钉钉群(开源群:102565007776,商业化群:35568145),共同提升编译时插桩在 Go 应用监控的能力。


参考链接:


[1]https://github.com/open-telemetry/opentelemetry-java


[2]https://github.com/open-telemetry/opentelemetry-go


[3] 监控 Golang 应用:


https://help.aliyun.com/zh/arms/application-monitoring/user-guide/monitoring-the-golang-applications/


[4] ARMS 应用监控支持的 Golang 组件和框架:


https://help.aliyun.com/zh/arms/application-monitoring/developer-reference/go-components-and-frameworks-supported-by-arms-application-monitoring


[5] Golang 探针性能压测报告:


https://help.aliyun.com/zh/arms/application-monitoring/developer-reference/golang-probe-performance-pressure-test-report


[6]为Go应用无侵入地添加任意代码


[7]https://github.com/alibaba/opentelemetry-go-auto-instrumentation

用户头像

阿里云云原生 2019-05-21 加入

还未添加个人简介

评论

发布
暂无评论
编译时插桩,Go 应用监控最佳选择_阿里云_阿里巴巴云原生_InfoQ写作社区