編譯時插樁,Go應用監控的最佳選擇

阿里妹導讀
本文講解了阿里雲編譯器團隊和可觀測團隊為了實現Go應用監控選擇編譯時插樁的原因,同時還介紹了其他的監控方案以及它們的優缺點。
可觀測性是以系統的指標、日誌、鏈路追蹤、持續剖析四大資料支柱為基礎,從宏觀到微觀,透過不同資料之間互相關聯,衍生出如資料監控、問題分析、系統診斷等一系列的能力。
Java[1]可以透過位元組碼增強的技術實現無侵入的應用監控(開源社群有非常多的無侵入Agent實現方案,技術非常成熟),可以輕鬆獲取到關鍵監控資料,相比Java,Go因為語言的特點,應用執行的時候已經被編譯成一個二進位制檔案,無法再做類似Java位元組碼增強的方式進行動態插樁,在應用監控領域的生態並不完善,可觀測的四大資料支柱無法透過無侵入的方式來實現,使得使用者的接入成本變高,當前針對Go應用的可觀測能力,有3種解決方案:
  • SDK方案
  • eBPF方案
  • 編譯期自動注入方案
以下分別來介紹這幾個方案以及對應的開源實現:
SDK方案
在可觀測領域,隨著OpenTracing 被OTel 收編,目前被廣泛使用的SDK就是OTel Go SDK[2],透過在業務程式碼的每個需要的地方進行手動增加埋點,如下所示:
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()。
這是一個簡單的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
[7]https://github.com/alibaba/opentelemetry-go-auto-instrumentation
雲上公網架構設計和安全管理
雲上公網的設計可以幫助企業更加統一、安全地管理自己的雲上網際網路出入口,同時可以實現統一監控運維和公網的成本最佳化。   
點選閱讀原文檢視詳情。

相關文章