為Go應用無侵入地新增任意程式碼

阿里妹導讀
這篇文章旨在提供技術深度和實踐指南,幫助開發者理解並應用這項創新技術來提高Golang應用的監控與服務治理能力。在接下來的部分,我們將透過一些實際案例,進一步展示如何在不同場景中應用這項技術,提供更多實踐啟示。
作者|青風、古琦、牧思、如漫
背景
在Go語言的開發過程中,儘管該語言以卓越的效能和高效的編碼能力聞名,但在應用程式監控與服務治理方面,仍面臨顯著的成本與技術挑戰。傳統解決方案通常需要開發者手動調整原始碼,這不僅增加了工作量,還對現有架構產生了影響,使得無縫整合變得異常困難。特別是在複雜的異構系統中,實現全面而細緻的監控和服務最佳化幾乎成為既耗時又需要專家經驗的任務。此情此景下,尋求一種既能減少侵入性又能有效提升運維效率的方法論,成為了業界共同追求的目標。
為了解決這一問題,阿里雲ARMS團隊、編譯器團隊、MSE團隊攜手合作,共同釋出並開源[1]了Go語言的編譯期自動插樁技術[2]。這項技術以其零侵入的特點,為Golang應用提供了與Java監控能力媲美的解決方案。開發者無需對現有程式碼進行任何修改,只需簡單地將go build替換為新編譯命令,即可實現對Go應用的全面監控和治理。
在開源版本中,我們支援了16個主流開源框架(在商業化版中支援38個主流開源框架),同時考慮到使用者的多樣化需求,特別是使用了未在支援列表中的框架或高階定製需求,我們進一步推出了模組化插樁擴充套件功能。使用者只需透過簡單的JSON配置,即可零侵入注入自定義程式碼到任意目標函式,不需要修改原來程式碼倉庫的程式碼,透過模組化插樁擴充套件的方式即可完成程式碼注入,從而實現更細粒度的控制、監控、治理和安全。
模組化擴充套件原理
在正常情況下,go build命令會經過六個主要步驟:原始碼分析、型別檢查、語義分析、編譯最佳化、程式碼生成和連結,來編譯一個 Go 應用程式。然而,使用自動插樁工具後,在這些步驟之前會增加兩個步驟:預處理(Preprocess)和程式碼注入(Instrument)。
預處理
在這一階段,工具首先讀取使用者定義的 rule.json配置檔案,它詳細說明了需要在哪些框架或標準庫的哪些版本中插入自定義的 hook 程式碼。rule.json 配置檔案的內容完全由使用者控制,一個典型的示例如下:
[{"ImportPath": "google.golang.org/grpc","Function": "NewClient","OnEnter": "grpcNewClientOnEnter","OnExit": "grpcNewClientOnExit","Path": "/path/to/my/code"}]
這個配置表示希望在google.golang.org/grpc庫的NewClient函式入口和出口分別插入grpcNewClientOnEntergrpcNewClientOnExit這兩個程式碼段。需要插入的這兩個函式程式碼位於本地路徑/path/to/my/code
接下來工具會分析專案的第三方庫依賴,並將其與 rule.json 中的自定義的插樁規則進行匹配,同時提前配置這些規則所需的額外依賴。當所有預處理工作完成後,工具將攔截常規的編譯流程,在每個包的編譯過程前面額外加入一個程式碼注入階段。
程式碼注入
在程式碼注入階段,工具會根據 rule.json 的配置,為目標函式(如NewClient)插入蹦床程式碼(Trampoline Code)。蹦床程式碼的主要作用是作為邏輯上的跳板來處理異常和填充上下文,最終它會跳轉到使用者自定義的grpcNewClientOnEntergrpcNewClientOnExit函式,以完成監控資料的收集或服務流量的治理。由於蹦床程式碼是效能攸關的,我們在AST(抽象語法樹)層面還會對蹦床程式碼做一系列最佳化,確保它的開銷降到最低,關於最佳化部分感興趣的讀者可以訪問專案原始碼,這裡不再贅述。
透過以上步驟,工具有效地在保證程式碼功能完整性的前提下插入了使用者指定的程式碼邏輯,隨後,工具修改必要的編譯引數,然後執行常規編譯以生成最終的應用程式。
使用示例
在瞭解了上述原理之後,我們將通過幾個例子演示Go自動插樁的模組化擴充套件的使用方式。
1、記錄http請求的Header
以net/http為例,很多使用者都關心請求的引數、body用來定位問題,這裡我們使用自定義插樁的能力,介紹如何獲取請求的header和返回的header。
第一步,建立hook資料夾,使用go mod init hook初始化該資料夾,然後新增下面的hook.go程式碼,它是即將注入的程式碼:
package hookimport("encoding/json""fmt""github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api""net/http")// 注意:注入程式碼第一個引數必須是api.CallContext,後續引數和目標函式引數一致func httpClientEnterHook(call api.CallContext, t *http.Transport, req *http.Request){  header, _ := json.Marshal(req.Header)  fmt.Println("request header is ", string(header))}// 注意:注入程式碼第一個引數必須是api.CallContext,後續引數和目標函式返回值一致func httpClientExitHook(call api.CallContext, res *http.Response, err error) {  header, _ := json.Marshal(res.Header)  fmt.Println("response header is ", string(header))}
第二步,編寫下面的conf.json配置,告訴工具我們想要將hook程式碼注入到:
net/http::(*Transport).RoundTrip
[{"ImportPath":"net/http","Function":"RoundTrip","OnEnter":"httpClientEnterHook","ReceiverType": "*Transport","OnExit": "httpClientExitHook","Path": "/path/to/hook" # Path修改為hook程式碼的本地路徑}]
第三步,編寫測試Demo。建立資料夾並使用go mod init demo初始化,然後新增main.go
package mainimport("context""fmt""io/ioutil""log""net/http")func main(){// 定義請求的URL  req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://www.aliyun.com", nil)  req.Header.Set("otelbuild", "true")  client := &http.Client{}  resp, _ := client.Do(req)// 確保在函式結束時關閉響應的主體  defer resp.Body.Close()}
第四步,切換到demo目錄,使用otelbuild工具編譯並執行程式,以驗證效果。
$ ./otelbuild -rule=conf.json -- main.go$ ./main
可以看到如下輸出, 表示注入是成功的:
該示例可以在:
https://github.com/alibaba/opentelemetry-go-auto-instrumentation/tree/main/example/extension/netHttp中找到。
2、替換標準庫sort演算法
Golang標準庫中目前使用的排序演算法是pdqsort(Pattern-Defeating Quick Sort)[3],由計算機科學家Orson R. L. Peters發明。pdqsort會檢測輸入資料的特定模式,如部分排序、有序或反序排列,並選擇合適的策略來處理。例如,當資料接近有序時,pdqsort會切換到插入排序,它的名稱Pattern-Defeating也反映了它對特定資料模式的特殊最佳化。
假設你在創造新的快排演算法,或者發現在特定的工作負載下,另一種快排演算法如DualPivot Quick Sort速度更快,這時候藉助插樁工具可以非常簡單的替換標準庫排序演算法,快速驗證新演算法。
第一步,建立hook資料夾,使用go mod init hook初始化該資料夾,然後新增下面的hook.go程式碼,它是即將注入的程式碼:
package hookimport("github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api")func partition(arr []int, low, high int)(int, int){if arr[low] > arr[high] {    arr[low], arr[high] = arr[high], arr[low]  }  lp := low + 1  g := high - 1  k := low + 1  p := arr[low]  q := arr[high]for k <= g {if arr[k] < p {      arr[k], arr[lp] = arr[lp], arr[k]      lp++    } elseif arr[k] >= q {for arr[g] > q && k < g {        g--      }      arr[k], arr[g] = arr[g], arr[k]      g--if arr[k] < p {        arr[k], arr[lp] = arr[lp], arr[k]        lp++      }    }    k++  }  lp--  g++  arr[low], arr[lp] = arr[lp], arr[low]  arr[high], arr[g] = arr[g], arr[high]return lp, g}func dualPivotQuickSort(arr []int, low, high int) {if low < high {    lp, rp := partition(arr, low, high)    dualPivotQuickSort(arr, low, lp-1)    dualPivotQuickSort(arr, lp+1, rp-1)    dualPivotQuickSort(arr, rp+1, high)  }}func sortOnEnter(call api.CallContext, arr []int) {// 使用dual pivot qsort  dualPivotQuickSort(arr, 0, len(arr)-1)// 跳過原始的sort演算法  call.SetSkipCall(true)}
第二步,編寫下面的conf.json配置,告訴工具我們想要將hook程式碼注入到sort.Ints
[{"ImportPath":"sort","Function":"Ints","OnEnter":"sortOnEnter","Path":"/path/to/hook" # Path修改為hook程式碼的本地路徑}]
第三步,編寫測試Demo。建立資料夾並使用go mod init demo初始化,然後新增main.go。
package mainimport("fmt""sort")func main(){  arr := []int{6, 3, 7, 9, 4, 4}  sort.Ints(arr)  fmt.Printf("== %v\n", arr)}
第四步,切換到demo目錄,使用otelbuild工具編譯並執行程式,以驗證dual pivot quicksort效果。
$ ./otelbuild -rule=conf.json -- main.go$ ./main== [344679]
3、防止SQL程式碼注入
為了防止SQL程式碼注入,可以在database/sql::(*DB).Query()查詢中注入額外的程式碼,以檢查SQL語句是否存在注入風險並及時攔截。
第一步,建立hook資料夾,使用go mod init hook初始化該資料夾,然後新增下面的hook.go程式碼,它是即將注入的程式碼:
package hookimport("database/sql""errors""github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api""log""strings")func checkSqlInjection(query string) error {  patterns := []string{"--", ";", "/*", " or ", " and ", "'"}for _, pattern := range patterns {if strings.Contains(strings.ToLower(query), pattern) {return errors.New("potential SQL injection detected")    }  }return nil}func sqlQueryOnEnter(call api.CallContext, db *sql.DB, query string, args ...interface{}) {if err := checkSqlInjection(query); err != nil {log.Fatalf("sqlQueryOnEnter %v", err)  }}
第二步,編寫下面的conf.json配置,告訴工具我們想要將hook程式碼注入到database/sql::(*DB).Query()
[{"ImportPath": "database/sql","Function": "Query","ReceiverType": "*DB","OnEnter": "sqlQueryOnEnter","Path": "/path/to/hook" # Path修改為hook程式碼的本地路徑}]
第三步,編寫測試Demo。建立資料夾並使用go mod init demo初始化,然後新增main.go。
package mainimport("context""database/sql""fmt"  _ "github.com/go-sql-driver/mysql""os""time")func main(){  mysqlDSN := "test:test@tcp(127.0.0.1:3306)/test"  db, _ := sql.Open("mysql", mysqlDSN)    db.ExecContext(context.Background(), `CREATE TABLE IF NOT EXISTS usersx (id char(255), name VARCHAR(255), age INTEGER)`)    db.ExecContext(context.Background(), `INSERT INTO usersx (id, name, age) VALUE ( ?, ?, ?)`, "0", "foo", 10)    # SQL中注入惡意程式碼,抓取整個表的資訊    maliciousAnd := "'foo' AND 1 = 1"  injectedSql := fmt.Sprintf("SELECT * FROM userx WHERE id = '0' AND name = %s", maliciousAnd)  db.Query(injectedSql)}
第四步,切換到demo目錄,使用otelbuild工具編譯並執行程式,以驗證SQL注入保護的效果。
$ ./otelbuild -rule=conf.json -- main.go$ docker run -d -p 3306:3306 -p 33060:33060 -e MYSQL_USER=test -e MYSQL_PASSWORD=test -e MYSQL_DATABASE=test -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:8.0.36$ ./main
可以看到,使用otelbuild工具編譯出的二進位制檔案成功檢測到了潛在的sql注入攻擊,並打印出了相應日誌:
2024/11/0421:12:47 sqlQueryOnEnter potential SQL injection detected
該示例可以在:
https://github.com/alibaba/opentelemetry-go-auto-instrumentation/tree/main/example/extension/sqlinject中找到。
4、使請求具備流量防護能力
假設我們準備基於sentinel-golang給grpc-go unary請求增加流量防護的能力,也可以透過自動插樁的方式,在grpc client處透過注入中介軟體的方式來實現。
第一步,建立hook資料夾,使用go mod init hook初始化該資料夾,然後新增下面的hook.go程式碼,它是即將注入的程式碼:
package hookimport("context""google.golang.org/grpc"    sentinel "github.com/sentinel-golang/api""github.com/sentinel-golang/core/base"    pkgapi "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api")// 在 gRPC 客戶端入口新增流量防護中介軟體func newClientOnEnter(call pkgapi.CallContext, target string, opts ...grpc.DialOption){    opts = append(opts, grpc.WithChainUnaryInterceptor(unaryClientInterceptor))}// 基於 sentinel-golang 的流量防護中介軟體func unaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {    entry, blockErr := sentinel.Entry(        method,        sentinel.WithResourceType(base.ResTypeRPC),        sentinel.WithTrafficType(base.Outbound),    )    defer func() {if entry != nil {            entry.Exit()        }    }()if blockErr != nil {return blockErr    }return invoker(ctx, method, req, reply, cc, opts...)}
第二步,編寫下面的conf.json配置,告訴工具我們想要將hook程式碼注入到google.golang.org/grpc::NewClient
[{"ImportPath": "google.golang.org/grpc","Function": "NewClient","OnEnter": "newClientOnEnter","Path": "/path/to/hook"  # Path修改為hook程式碼的本地路徑}]
第三步,編寫測試Demo。建立資料夾並使用go mod init demo初始化,然後新增main.go。
package mainimport("context""fmt""google.golang.org/grpc"    pb "path/to/your/protobuf"// 替換為你的 proto 檔案路徑)func main(){// 連線到 GRPC 伺服器    conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())    client := pb.NewYourServiceClient(conn)// 傳送 gRPC 請求    response, _ := client.YourMethod(context.Background(), &pb.YourRequest{})    fmt.Println("Response: ", response)}
第四步,切換到demo目錄,使用otelbuild工具編譯並執行程式,以驗證效果。
$ ./otelbuild -rule=conf.json -- main.go$ ./main
如果希望給grpc-go stream請求增加防護規則也是同理。除此之外,如果希望使請求具備灰度路由、標籤路由、百分比路由等灰度釋出能力,還可以針對框架的負載均衡器進行按需增強,具有非常高的自主性和擴充套件性。
總結和展望
Golang編譯期自動插樁成功解決了微服務監控中繁瑣的手動埋點問題,並已商業化上線至阿里雲公有云,為客戶提供強大的監控能力。這項技術最初的設計初衷是為了讓使用者能夠在不改動現有程式碼的前提下輕鬆地插入監控程式碼,從而實現對應用程式效能狀態的即時監測與分析,但它的實際應用領域超越預期,包括服務治理、程式碼審計、應用安全、程式碼除錯等,甚至在許多未被探索的領域中也展現出潛力。
我們決定將這項創新方案開源,並捐贈給OpenTelemetry社群[4],目前已經達成貢獻意向,後續我們的程式碼將遷移到OpenTelemetry社群倉庫。開源不僅促進技術共享與提升,藉助社群的力量還可以持續探索該方案在更多領域上的可能。
最後誠邀大家試用我們的商業化產品[5][6],並加入我們的釘釘群(開源群:102565007776,商業化群:35568145),共同提升Go應用監控與服務治理能力。透過群策群力,我們相信能為Golang開發者社群帶來更加優質的雲原生體驗。
參考連結:
[1] Go自動插樁開源專案:https://github.com/alibaba/opentelemetry-go-auto-instrumentation
[3] Pattern-Defeating快排演算法論文:https://arxiv.org/pdf/2106.05123
[4] 在OpenTelemetry社群討論捐獻專案:https://github.com/open-telemetry/community/issues/1961
[5] 阿里雲ARMS Go Agent商業版:https://help.aliyun.com/zh/arms/tracing-analysis/monitor-go-applications/
[6] 阿里雲MSE Go Agent商業版:https://help.aliyun.com/zh/mse/getting-started/ack-microservice-application-access-mse-governance-center-golang-version
高可用及共享儲存 Web 服務
隨著業務規模的增長,資料請求和併發訪問量增大、靜態檔案高頻變更,企業需要搭建一個高可用和共享儲存的網站架構,以確保網站服務能夠7*24小時執行的同時,可保障資料一致性和共享性,並降低資料重複儲存的成本。
點選閱讀原文檢視詳情。

相關文章