Go应用监控最佳实践:编译时插桩,低成本高性能,阿里云instgo工具助力高效监控。
原文标题:编译时插桩,Go应用监控的最佳选择
原文作者:阿里云开发者
冷月清谈:
SDK方案,以OpenTelemetry Go SDK为例,需要在代码中手动埋点,处理span上下文传递和版本兼容性问题,维护成本高。
eBPF方案利用Linux内核的虚拟机实现监控,但受限于权限、内核版本,且在复杂应用场景下难以单独使用,性能开销也较大。
编译时插桩方案,利用Go toolexec能力,在编译前插入监控代码,最终生成包含监控能力的二进制文件。该方案无需修改业务逻辑,性能消耗低,且支持多种插件和监控能力。阿里云的instgo工具,只需修改编译命令即可接入,方便快捷。
怜星夜思:
2、文章中提到的编译时插桩方案,有没有什么缺点?在实际应用中需要注意哪些问题?
3、eBPF方案文中提到性能略逊于Agent,那如果结合其他技术,比如BPF CO-RE(Compile Once – Run Everywhere),是否能弥补这个缺点?
原文内容
阿里妹导读
-
SDK方案
-
eBPF方案
-
编译期自动注入方案
SDK方案
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()。
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 instgo
chmod +x instgo
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./instgo go build main.go
通过wget下载instgo编译工具,只需要简单修改在go build前添加instgo即可完成监控能力注入。
总结
本文讲解了阿里云编译器团队和可观测团队为了实现Go应用监控为什么选择编译时插桩的原因,同时还介绍了其他的监控方案,以及它们的优缺点。我们相信阿里云Go Agent(Instgo)是一个非常强大的工具,可以帮助我们实现针对Go应用更好的APM能力,同时还能保持应用程序的安全性和可靠性。
[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]
云上公网架构设计和安全管理
云上公网的设计可以帮助企业更加统一、安全地管理自己的云上互联网出入口,同时可以实现统一监控运维和公网的成本优化。
点击阅读原文查看详情。