为Go应用无侵入地添加任意代码

简介: 这篇文章旨在提供技术深度和实践指南,帮助开发者理解并应用这项创新技术来提高Golang应用的监控与服务治理能力。在接下来的部分,我们将通过一些实际案例,进一步展示如何在不同场景中应用这项技术,提供更多实践启示。

欢迎扫描文末二维码,关注「阿里云开发者」公众号,了解更多技术干货,关于阿里的技术创新均呈现于此。


背景

在Go语言的开发过程中,尽管该语言以卓越的性能和高效的编码能力闻名,但在应用程序监控与服务治理方面,仍面临显著的成本与技术挑战。传统解决方案通常需要开发者手动调整源代码,这不仅增加了工作量,还对现有架构产生了影响,使得无缝集成变得异常困难。特别是在复杂的异构系统中,实现全面而细致的监控和服务优化几乎成为既耗时又需要专家经验的任务。此情此景下,寻求一种既能减少侵入性又能有效提升运维效率的方法论,成为了业界共同追求的目标。


为了解决这一问题,阿里云ARMS团队、编译器团队、MSE团队携手合作,共同发布并开源[1]了Go语言的编译期自动插桩技术[2]。这项技术以其零侵入的特点,为Golang应用提供了与Java监控能力媲美的解决方案。开发者无需对现有代码进行任何修改,只需简单地将go build替换为新编译命令,即可实现对Go应用的全面监控和治理。


在开源版本中,我们支持了16个主流开源框架(在商业化版中支持38个主流开源框架),同时考虑到用户的多样化需求,特别是使用了未在支持列表中的框架或高级定制需求,我们进一步推出了模块化插桩扩展功能。用户只需通过简单的JSON配置,即可零侵入注入自定义代码到任意目标函数,不需要修改原来代码仓库的代码,通过模块化插桩扩展的方式即可完成代码注入,从而实现更细粒度的控制、监控、治理和安全。


模块化扩展原理

在正常情况下,go build命令会经过六个主要步骤:源码分析、类型检查、语义分析、编译优化、代码生成和链接,来编译一个 Go 应用程序。然而,使用自动插桩工具后,在这些步骤之前会增加两个步骤:预处理(Preprocess)和代码注入(Instrument)。


image.png



预处理

在这一阶段,工具首先读取用户定义的 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(抽象语法树)层面还会对蹦床代码做一系列优化,确保它的开销降到最低,关于优化部分感兴趣的读者可以访问项目源码,这里不再赘述。


image.png


通过以上步骤,工具有效地在保证代码功能完整性的前提下插入了用户指定的代码逻辑,随后,工具修改必要的编译参数,然后执行常规编译以生成最终的应用程序。


使用示例

在了解了上述原理之后,我们将通过几个例子演示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代码的本地路径
}]


第三步,编写测试Demo。创建文件夹并使用go mod init demo初始化,然后添加main.go


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()
}


第四步,切换到demo目录,使用otelbuild工具编译并执行程序,以验证效果。


$ ./otelbuild -rule=conf.json -- main.go
$ ./main


可以看到如下输出, 表示注入是成功的:


image.png


该示例可以在:

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也反映了它对特定数据模式的特殊优化。


image.png


假设你在创造新的快排算法,或者发现在特定的工作负载下,另一种快排算法如DualPivot Quick Sort速度更快,这时候借助插桩工具可以非常简单的替换标准库排序算法,快速验证新算法。


第一步,创建hook文件夹,使用go mod init hook初始化该文件夹,然后新增下面的hook.go代码,它是即将注入的代码:


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++
    } else if 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 main

import (
  "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
== [3 4 4 6 7 9]


3、防止SQL代码注入

为了防止SQL代码注入,可以在database/sql::(*DB).Query()查询中注入额外的代码,以检查SQL语句是否存在注入风险并及时拦截。


第一步,创建hook文件夹,使用go mod init hook初始化该文件夹,然后新增下面的hook.go代码,它是即将注入的代码:


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代码的本地路径
}]


第三步,编写测试Demo。创建文件夹并使用go mod init demo初始化,然后添加main.go。


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/04 21: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 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...)
}


第二步,编写下面的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 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


如果希望给grpc-go stream请求增加防护规则也是同理。除此之外,如果希望使请求具备灰度路由、标签路由、百分比路由等灰度发布能力,还可以针对框架的负载均衡器进行按需增强,具有非常高的自主性和扩展性。


总结和展望

Golang编译期自动插桩成功解决了微服务监控中繁琐的手动埋点问题,并已商业化上线至阿里云公有云,为客户提供强大的监控能力。这项技术最初的设计初衷是为了让用户能够在不改动现有代码的前提下轻松地插入监控代码,从而实现对应用程序性能状态的实时监测与分析,但它的实际应用领域超越预期,包括服务治理、代码审计、应用安全、代码调试等,甚至在许多未被探索的领域中也展现出潜力。


我们决定将这项创新方案开源,并捐赠给OpenTelemetry社区[4],目前已经达成贡献意向,后续我们的代码将迁移到OpenTelemetry社区仓库。开源不仅促进技术共享与提升,借助社区的力量还可以持续探索该方案在更多领域上的可能。


最后诚邀大家试用我们的商业化产品[5][6],并加入我们的钉钉群(开源群:102565007776,商业化群:35568145),共同提升Go应用监控与服务治理能力。通过群策群力,我们相信能为Golang开发者社区带来更加优质的云原生体验。


参考链接:

[1] Go自动插桩开源项目https://github.com/alibaba/opentelemetry-go-auto-instrumentation

[2] 面向OpenTelemetry的Golang应用无侵入插桩技术

[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




来源  |  阿里云开发者公众号

作者 |  青风、古琦、牧思、如漫





相关文章
|
10天前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
14天前
|
存储 人工智能 调度
阿里云吴结生:高性能计算持续创新,响应数据+AI时代的多元化负载需求
在数字化转型的大潮中,每家公司都在积极探索如何利用数据驱动业务增长,而AI技术的快速发展更是加速了这一进程。
|
5天前
|
并行计算 前端开发 物联网
全网首发!真·从0到1!万字长文带你入门Qwen2.5-Coder——介绍、体验、本地部署及简单微调
2024年11月12日,阿里云通义大模型团队正式开源通义千问代码模型全系列,包括6款Qwen2.5-Coder模型,每个规模包含Base和Instruct两个版本。其中32B尺寸的旗舰代码模型在多项基准评测中取得开源最佳成绩,成为全球最强开源代码模型,多项关键能力超越GPT-4o。Qwen2.5-Coder具备强大、多样和实用等优点,通过持续训练,结合源代码、文本代码混合数据及合成数据,显著提升了代码生成、推理和修复等核心任务的性能。此外,该模型还支持多种编程语言,并在人类偏好对齐方面表现出色。本文为周周的奇妙编程原创,阿里云社区首发,未经同意不得转载。
|
10天前
|
人工智能 运维 双11
2024阿里云双十一云资源购买指南(纯客观,无广)
2024年双十一,阿里云推出多项重磅优惠,特别针对新迁入云的企业和初创公司提供丰厚补贴。其中,36元一年的轻量应用服务器、1.95元/小时的16核60GB A10卡以及1元购域名等产品尤为值得关注。这些产品不仅价格亲民,还提供了丰富的功能和服务,非常适合个人开发者、学生及中小企业快速上手和部署应用。
|
5天前
|
人工智能 自然语言处理 前端开发
用通义灵码,从 0 开始打造一个完整APP,无需编程经验就可以完成
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。本教程完全免费,而且为大家准备了 100 个降噪蓝牙耳机,送给前 100 个完成的粉丝。获奖的方式非常简单,只要你跟着教程完成第一课的内容就能获得。
|
21天前
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
3945 4
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
10天前
|
算法 安全 网络安全
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
2024阿里云11.11金秋云创季活动火热进行中,活动月期间(2024年11月01日至11月30日)通过折扣、叠加优惠券等多种方式,阿里云WoSign SSL证书实现优惠价格新低,DV SSL证书220元/年起,助力中小企业轻松实现HTTPS加密,保障数据传输安全。
530 3
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
|
9天前
|
数据采集 人工智能 API
Qwen2.5-Coder深夜开源炸场,Prompt编程的时代来了!
通义千问团队开源「强大」、「多样」、「实用」的 Qwen2.5-Coder 全系列,致力于持续推动 Open Code LLMs 的发展。
|
16天前
|
安全 数据建模 网络安全
2024阿里云双11,WoSign SSL证书优惠券使用攻略
2024阿里云“11.11金秋云创季”活动主会场,阿里云用户通过完成个人或企业实名认证,可以领取不同额度的满减优惠券,叠加折扣优惠。用户购买WoSign SSL证书,如何叠加才能更加优惠呢?
995 3
|
14天前
|
机器学习/深度学习 存储 人工智能
白话文讲解大模型| Attention is all you need
本文档旨在详细阐述当前主流的大模型技术架构如Transformer架构。我们将从技术概述、架构介绍到具体模型实现等多个角度进行讲解。通过本文档,我们期望为读者提供一个全面的理解,帮助大家掌握大模型的工作原理,增强与客户沟通的技术基础。本文档适合对大模型感兴趣的人员阅读。
447 18
白话文讲解大模型| Attention is all you need