
阿里妹導讀
背景
為了解決這一問題,阿里雲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"
}]
接下來工具會分析專案的第三方庫依賴,並將其與 rule.json 中的自定義的插樁規則進行匹配,同時提前配置這些規則所需的額外依賴。當所有預處理工作完成後,工具將攔截常規的編譯流程,在每個包的編譯過程前面額外加入一個程式碼注入階段。
程式碼注入
在程式碼注入階段,工具會根據 rule.json 的配置,為目標函式(如NewClient)插入蹦床程式碼(Trampoline Code)。蹦床程式碼的主要作用是作為邏輯上的跳板來處理異常和填充上下文,最終它會跳轉到使用者自定義的grpcNewClientOnEnter和grpcNewClientOnExit函式,以完成監控資料的收集或服務流量的治理。由於蹦床程式碼是效能攸關的,我們在AST(抽象語法樹)層面還會對蹦床程式碼做一系列最佳化,確保它的開銷降到最低,關於最佳化部分感興趣的讀者可以訪問專案原始碼,這裡不再贅述。

透過以上步驟,工具有效地在保證程式碼功能完整性的前提下插入了使用者指定的程式碼邏輯,隨後,工具修改必要的編譯引數,然後執行常規編譯以生成最終的應用程式。
使用示例
在瞭解了上述原理之後,我們將通過幾個例子演示Go自動插樁的模組化擴充套件的使用方式。
1、記錄http請求的Header
以net/http為例,很多使用者都關心請求的引數、body用來定位問題,這裡我們使用自定義插樁的能力,介紹如何獲取請求的header和返回的header。
第一步,建立hook資料夾,使用go mod init hook初始化該資料夾,然後新增下面的hook.go程式碼,它是即將注入的程式碼:
package hook
import(
"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程式碼的本地路徑
}]
package main
import(
"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()
}
$ ./otelbuild -rule=conf.json -- main.go
$ ./main

https://github.com/alibaba/opentelemetry-go-auto-instrumentation/tree/main/example/extension/netHttp中找到。
2、替換標準庫sort演算法

package hook
import(
"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)
}
[{
"ImportPath":"sort",
"Function":"Ints",
"OnEnter":"sortOnEnter",
"Path":"/path/to/hook" # Path修改為hook程式碼的本地路徑
}]
package main
import(
"fmt"
"sort"
)
func main(){
arr := []int{6, 3, 7, 9, 4, 4}
sort.Ints(arr)
fmt.Printf("== %v\n", arr)
}
$ ./otelbuild -rule=conf.json -- main.go
$ ./main
== [344679]
3、防止SQL程式碼注入
package hook
import(
"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程式碼的本地路徑
}]
package main
import(
"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
該示例可以在:
4、使請求具備流量防護能力
package hook
import(
"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...)
}
[{
"ImportPath": "google.golang.org/grpc",
"Function": "NewClient",
"OnEnter": "newClientOnEnter",
"Path": "/path/to/hook" # Path修改為hook程式碼的本地路徑
}]
package main
import(
"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
總結和展望
[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小時執行的同時,可保障資料一致性和共享性,並降低資料重複儲存的成本。
點選閱讀原文檢視詳情。