编译时插桩,Go应用监控的最佳选择
阿里妹导读
SDK方案
eBPF方案
编译期自动注入方案
SDK方案
package mainimport ("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()。
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周会发布一个版本
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)
编译时插桩方案
在经过词法分析、语法分析后生成一些.a的中间态文件,最终通过Link的方式将.a文件生成为二进制文件。通过这个步骤可以看出我们可以在编译前端到编译后端中间进行hook的操作,因此我们将对应的编译流程改成如下方式:
通过AST语法树分析,查找到监控的埋点,根据提前定义好的埋点规则,在编译前插入需要的监控代码,然后经过完成的Go编译流程将代码注入到最终的二进制中,这个方案与程序员手写代码完全没有区别,由于经过了完整的编译流程,不会产生一些不可预料的错误。
当前的编译语句: 当前的编译语句: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即可完成监控能力注入。
总结
本文讲解了阿里云编译器团队和可观测团队为了实现Go应用监控为什么选择编译时插桩的原因,同时还介绍了其他的监控方案,以及它们的优缺点。我们相信阿里云Go Agent(Instgo)是一个非常强大的工具,可以帮助我们实现针对Go应用更好的APM能力,同时还能保持应用程序的安全性和可靠性。