高效定位 Go 应用问题:Go 可观测性功能深度解析

作者:古琦

背景

自 2024 年 6 月 26 日,阿里云 ARMS 团队正式推出面向 Go 应用的可观测性监控功能以来,我们与程序语言及编译器团队携手并进,持续深耕技术优化与功能拓展。这一创新性的解决方案旨在为开发者提供更为全面、深入且高效的应用性能监控体验,助力企业在数字化转型中实现卓越的系统稳定性与性能表现。

从商业化版本的首次亮相至今,我们已历经五次重大版本迭代及若干次精细化的小版本更新。相较于初始版本,系统性能实现了翻倍提升,同时在功能层面亦展现出前所未有的丰富性与灵活性。新增特性包括但不限于智能化应用诊断、高度可定制的扩展能力、灵活的应用开关机制、接口全量采样以及代码热点分析等模块。这些功能的引入不仅显著提升了系统的实用性,也赢得了广大用户的广泛认可与积极反馈。而基于编译时插桩(Compile-time Instrumentation)的技术路径,更被实践证明是 Go 语言应用监控领域的一次突破性创举,堪称当前最优解。

为进一步赋能用户在复杂场景下快速定位与解决问题,我们结合近期发布的一系列全新功能,精心梳理了一套从接入到问题发现、再到问题排查与精准定位的最佳实践指南。

应用接入

通过 ARMS 提供的 Instgo 工具,只需要在 go build 前添加 instgo 命令,无需用修改一行代码,通过编译时插桩的方式实现监控能力注入[1]。

instgo go build {arg1} {arg2} {arg3}

智能告警

应用接入到 ARMS 后,可以在应用列表查看到应用的名称,点击进去查看到应用详情,包括了请求数、错误数、延迟等指标,还提供了每个接口的指标、以及依赖的接口指标,为了快速发现问题,可以通过配置应用的告警来第一时间发现问题。

可以创建对应的告警,如最近 1 分钟调用响应时间大于等于 500ms 就报警。

应用详情

通过监控告警第一时间发现问题后,到对应服务的详情查看这个接口的平均耗时非常长,即知道了告警是由于这个接口导致的。

查看对应的调用链,可以按耗时排列,找到耗时最长的调用链:

点击查看调用链详情,可以看到它的子 span 调用时间都非常短,可以确定是这个接口本身慢导致的,而不是其他对外请求导致的。

应用诊断

通过上述应用详情找到了请求慢的接口后,如何确认这时候的问题呢,我们可以通过应用诊断来发现问题,在应用监控中除了指标、链路、日志外,Profiling 的数据成为了应用监控的四大支柱之一。

通过 Profiling 数据能快速发现性能的瓶颈,ARMS Go 可观测提供了 CPU、内存、代码热点三个 Profiling 功能,用于快速发现应用性能问题。

ARMS 的持续剖析能力跟通过类似 https://github.com/grafana/pyroscope 或者 go 提供的 pprof 等工具相比,ARMS 提供的 Profiling 能力可以做到随开随关,通过应用设置-持续性能剖析设置即可进行开关设置,无需重启,直接生效。

CPU Profiling

CPU Profiling 用于收集和分析 Go 应用程序中的 CPU 使用情况,了解你的程序在运行时有多少时间花费在各个函数上。通过分析这些数据,开发者可以识别出程序中最耗费 CPU 时间的部分,ARMS 提供的 CPU Profiling 数据会采集每分钟的 CPU  运行情况,通过下面的火焰图即可找到当前执行时间最长的函数。

除了每分钟的数据之外,还提供了 CPU Profiling 数据的对比功能,对比前后 CPU 的消耗的不同,确定性能瓶颈。

内存 Profiling

跟 CPU Profiling 一样,内存 Profiling 也提供了对比的功能,可以对比前后不同时刻内存分配的情况,找到内存分配的热点。

除了通过内存 Profiling 找到内存分配热点外,还可以通过 Runtime 监控,找到每个时刻 Goroutines 数量、以及堆对象的数量来看某个时刻是否异常,是否因为流量突增导致的数量增加。

代码热点

在出现应用请求超时、响应慢的时候,为了快速定位到性能问题,从提供服务找到出现响应慢的接口,跳转到调用链,从调用链分析看出来对应接口在某些请求中响应的时间超出正常值很多,这时候如果还要进一步定位到这个请求执行过程中响应慢的函数是哪个,则无法通过单纯的调用链分析获取到,代码热点就是用来解决这个问题。点开对应的 Trace,通过放大镜即可查看当前的调用 Profiling[2]:

可以看到 main 中的 onCpu 函数消耗时间长达 0.62 秒,这样去排查这个函数的问题即可。

自定义扩展

通过上述方式可以查看到大部分问题,我们还提供了自定义扩展的功能[3],通过一个规则+一段待注入的代码组成,通过 Go Agent 的能力,在编译时完成代码的插桩,而不需要去修改原始代码,这个功能的优势是对于一些非项目开发人员可以在不修改原始代码的情况下完成相关功能实现。以下是我们经常会碰到的通过自定义扩展可以解决的问题:

日志打印

为了快速定位问题或者业务需求,日志可以记录非常详细的信息,比如函数的出入参数、Http 的返回的 body、sql 的请求语句参数等,以下是介绍打印 sql 请求的语句、参数:

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

package hookimport ("database/sql""fmt""github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
)func sqlQueryOnEnter(call api.CallContext, db *sql.DB, query string, args ...interface{}) {fmt.Println("sql is ", query)fmt.Println("sql arg is", args)
}

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

package mainimport ("context""database/sql""fmt"_ "github.com/go-sql-driver/mysql"
)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)maliciousAnd := "'foo' AND 1 = 1"injectedSql := fmt.Sprintf("SELECT * FROM userx WHERE id = '0' AND name = %s", maliciousAnd)db.Query(injectedSql, "abc")
}

第三步,在 Demo 文件夹下编写下面的 conf.json 配置,告诉工具我们想要将 hook 代码注入到 database/sql:😦*DB).Query()。

[{"ImportPath": "database/sql","Function": "Query","ReceiverType": "*DB","OnEnter": "sqlQueryOnEnter","Path": "/path/to/hook" # Path修改为hook代码的本地路径
}]

第四步,切换到 Demo 目录,使用 instgo 工具编译并执行程序,以验证 SQL 注入保护的效果。

$ ./instgo set --rule=./conf.json
$ 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
$ ./instgo go build .
$ ./demo

可以看到,使用 instgo 工具编译出的二进制文件成功检测到了潜在的 SQL 注入攻击,并打印出了相应日志:

sql is  SELECT * FROM userx WHERE id = '0' AND name = 'foo' AND 1 = 1
sql arg is [abc]

记录Span

ARMS 链路追踪记录的 span 信息都是对开源的 SDK 进行埋点获取的,用户在业务中如果有关心的函数需要记录可以通过自定义插件的功能,记录当前函数的 span。

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

package hookimport ("context""fmt""github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api""go.opentelemetry.io/otel""go.opentelemetry.io/otel/attribute"
)func requestDbOnEnter(call api.CallContext) {tracer := otel.GetTracerProvider().Tracer("")_, span := tracer.Start(context.Background(), "Client/User defined span")span.SetAttributes(attribute.String("client", "client-with-ot"))span.SetAttributes(attribute.Bool("user.defined", true))span.End()fmt.Println(span.SpanContext().SpanID().String())
}

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

package mainimport ("demo/common"_ "github.com/go-sql-driver/mysql"_ "go.opentelemetry.io/otel"
)func main() {common.RequestDb()
}

common 文件夹下增加 common.go 如下:

package commonimport ("context""database/sql""fmt"_ "github.com/go-sql-driver/mysql"
)func RequestDb() {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)maliciousAnd := "'foo' AND 1 = 1"injectedSql := fmt.Sprintf("SELECT * FROM userx WHERE id = '0' AND name = %s", maliciousAnd)db.Query(injectedSql, "abc")
}

第三步,在 Demo文件夹下编写下面的 conf.json 配置,告诉工具我们想要将 hook 代码注入到 common/RequestDb()。

[{"ImportPath": "demo/common","Function": "RequestDb","ReceiverType": "","OnEnter": "requestDbOnEnter","Path": "/path/to/hook" # Path修改为hook代码的本地路径
}]

第四步,切换到 Demo 目录,使用 instgo 工具编译并执行程序,以验证 SQL 注入保护的效果。

$ ./instgo set --rule=./conf.json
$ 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
$ ./instgo go build .
$ ./demo

可以看到,使用 instgo 工具编译出的二进制文件成功创建了 span,并打印出了相应 trace spanId:

0000000000000000

如果上报 span 到服务端,则可以看到自定义的 span。

流量回放

除了简单的打印日志和创建 Span 外,还可以对生产的请求进行录制,用于开发和测试阶段回归,提高测试质量,减少线上故障,以下是介绍通过对 Http 的请求、返回进行记录,将这些数据可以记录到日志或者数据库中,用于下次测试回归。

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

package hookimport ("encoding/json""fmt""github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api""io""net/http"
)func httpClientOnEnter(call api.CallContext, t *http.Transport, req *http.Request) {if req == nil {return}h, _ := json.Marshal(req.Header)fmt.Println("http request header is ", string(h))if req.GetBody == nil {return}requestBody, err := req.GetBody()if err != nil {return}defer requestBody.Close()requestData, err := io.ReadAll(requestBody)if err != nil {return}fmt.Println("http request body is ", string(requestData))
}

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

package mainimport ("bytes""context""encoding/json""net/http""time""unicode"
)func hello(w http.ResponseWriter, r *http.Request) {_, err := w.Write([]byte("Hello Http!"))if err != nil {panic(err)}
}func setupHttp() {http.Handle("/http-service1", http.HandlerFunc(hello))err := http.ListenAndServe(":9114", nil)if err != nil {panic(err)}
}// 定义一个结构体用于构造 JSON 数据
type RequestBody struct {Name  string `json:"name"`Email string `json:"email"`
}func requestServer() {ctx := context.Background()reqBody := RequestBody{Name:  "Alice",Email: "alice@example.com",}// 将结构体序列化为 JSON 格式jsonData, err := json.Marshal(reqBody)if err != nil {return}req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost:9114/http-service1", bytes.NewBuffer(jsonData))if err != nil {panic(err)}req.Header.Add("Content-Type", "application/json")req.Header.Add("test-key", "log")req.Header.Add("hello", "arms")client := &http.Client{}resp, err := client.Do(req)if err != nil {panic(err)}defer resp.Body.Close()
}func Is(s string) bool {for i := 0; i < len(s); i++ {if s[i] > unicode.MaxASCII {return false}}return true
}
func main() {go setupHttp()time.Sleep(3 * time.Second)requestServer()
}

第三步,在 Demo文件夹下编写下面的 conf.json 配置,告诉工具我们想要将 hook 代码注入到 net/http:😦*Transport).RoundTrip()。

[{"ImportPath": "net/http","Function": "RoundTrip","ReceiverType": "*Transport","OnEnter": "httpClientOnEnter","OnExit": "","Path": "/path/to/hook" # Path修改为hook代码的本地路径
}]

第四步,切换到 Demo 目录,使用 instgo 工具编译并执行程序,以验证 SQL 注入保护的效果。

$ ./instgo set --rule=./conf.json
$ ./instgo go build .
$ ./demo

可以看到,使用 instgo 工具编译出的二进制文件成功获取到了请求的 header 和 body,并打印出了相应日志:

http request header is  {"Content-Type":["application/json"],"Hello":["arms"],"Test-Key":["log"]}
http request body is  {"name":"Alice","email":"alice@example.com"}

通过自定义插件打印了日志,或者通过已有代码的日志也可以进行快速查看问题,我们提供了 TraceID 和 SpanID 关联到日志的能力[4]。

按需全采

针对一些重要的接口如果需要全采样,可以通过应用设置-采样设置配置接口名称,也可以通过前缀、后缀匹配来配置,这样这个接口的请求都会被采样到,避免被丢掉。

后续

为了进一步提升系统的可观测性与诊断能力,我们正致力于引入一系列高级性能分析工具,包括 Goroutine Profiling(协程剖析)、Mutex Profiling(互斥锁剖析)、Block Profiling(阻塞剖析)以及 Go Trace(Go语言运行轨迹追踪)。这些功能将为开发者提供更深入的洞察力,帮助他们在复杂的应用场景中精准定位性能瓶颈与潜在问题。

与此同时,我们将扩展对前沿技术的支持,特别是与大语言模型(LLM)相关的插件生态。例如,我们将集成 langchaingo 这一高效的语言处理框架,并引入 dify 的创新组件,如 dify-sandbox(沙盒环境)和 dify-plugin-daemon(插件守护进程),以满足开发者在多样化场景下的需求。

我们还计划推出一套在线调试工具,旨在为用户打造一个实时、交互式的问题诊断平台。通过这一平台,开发者可以快速定位并解决复杂问题,从而大幅缩短故障排查时间,提升系统的稳定性和可靠性。我们相信,这些能力的引入将为开发者带来前所未有的便捷体验,同时推动技术生态的进一步繁荣与发展。

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

相关链接:

[1] instgo 工具介绍:

https://help.aliyun.com/zh/arms/application-monitoring/developer-reference/instgo-tool-introduction

[2] 代码热点:

https://help.aliyun.com/zh/arms/application-monitoring/user-guide/use-hotspot-code-to-diagnose-slow-calls-in-go-applications

[3] 自定义扩展:

https://help.aliyun.com/zh/arms/application-monitoring/use-cases/use-golang-agent-to-customize-scalability

[4] Go 应用日志 Trace 关联:

https://help.aliyun.com/zh/arms/application-monitoring/use-cases/associate-trace-ids-with-business-logs-for-a-go-application

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/899960.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

构造超小程序

文章目录 构造超小程序1 编译器-大小优化2 编译器-移除 C 异常3 链接器-移除所有依赖库4 移除所有函数依赖_RTC_InitBase() _RTC_Shutdown()__security_cookie __security_check_cookie()__chkstk() 5 链接器-移除清单文件6 链接器-移除调试信息7 链接器-关闭随机基址8 移除异常…

大语言模型开发框架——LangChain

什么是LangChain LangChain是一个开发由语言模型驱动的应用程序的框架&#xff0c;它提供了一套工具、组件和接口&#xff0c;可以简化构建高级语言模型应用程序的过程。利用LangChain可以使应用程序具备两个能力&#xff1a; 上下文感知 将语言模型与上下文&#xff08;提示…

自动化释放linux服务器内存脚本

脚本说明 使用Linux的Cron定时任务结合Shell脚本来实现自动化的内存释放。 脚本用到sync系统命令 sync的作用&#xff1a;sync 是一个 Linux 系统命令&#xff0c;用于将文件系统缓存中的数据强制写入磁盘。 在你执行reboot、poweroff、shutdown命令时&#xff0c;系统会默认执…

Python Websockets库深度解析:构建高效的实时Web应用

引言 在现代Web开发中&#xff0c;实时通信已经成为许多应用的核心需求。无论是聊天应用、在线游戏、金融交易平台还是协作工具&#xff0c;都需要服务器和客户端之间建立持久、双向的通信通道。传统的HTTP协议由于其请求-响应模式&#xff0c;无法有效满足这些实时交互需求。…

【实用技巧】电脑重装后的Office下载和设置

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言下载设置总结互动致谢参考目录导航 前言 在数字化办公时代&#xff0c;Windows和…

Node.js 技术原理分析系列 —— Node.js 调试能力分析

Node.js 技术原理分析系列 —— Node.js 调试能力分析 Node.js 作为一个强大的 JavaScript 运行时环境,提供了丰富的调试能力,帮助开发者诊断和解决应用程序中的问题。本文将深入分析 Node.js 的调试原理和各种调试技术。 1. Node.js 调试原理 1.1 V8 调试器集成 Node.js…

【图论】最短路径问题总结

一图胜千言 单源最短路径 正权值 朴素Dijkstra dijkstra算法思想是维护一个永久集合U&#xff0c;全部点集合V。 循环n -1次 从源点开始&#xff0c;在未被访问的节点中&#xff0c;选择距离源点最近的节点 t。 以节点 t 为中间节点&#xff0c;更新从起点到其他节点的最短…

【最佳实践】win11使用hyper-v安装ubuntu 22/centos,并配置固定ip,扫坑记录

文章目录 场景查看本机的win11版本启用hyper-vhyper-v安装ubuntu22虚拟机1.准备好个人的 iso文件。2. hyper-v 快速创建3.编辑设置分配内存自定义磁盘位置设置磁盘大小连接网络修改虚拟机名称自定义检查点位置 和智能分页件位置虚拟机第一次连接给ubuntu22配置固定ip遇到过的坑…

自然语言处理(25:(终章Attention 1.)Attention的结构​)

系列文章目录 终章 1&#xff1a;Attention的结构 终章 2&#xff1a;带Attention的seq2seq的实现 终章 3&#xff1a;Attention的评价 终章 4&#xff1a;关于Attention的其他话题 终章 5&#xff1a;Attention的应用 目录 系列文章目录 前言 Attention的结构 一.seq…

Git 命令大全:通俗易懂的指南

Git 命令大全&#xff1a;通俗易懂的指南 Git 是一个功能强大且广泛使用的版本控制系统。对于初学者来说&#xff0c;它可能看起来有些复杂&#xff0c;但了解一些常用的 Git 命令可以帮助你更好地管理代码和协作开发。本文将介绍一些常用的 Git 命令&#xff0c;并解释它们的…

基于yolov11的棉花品种分类检测系统python源码+pytorch模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv11的棉花品种分类检测系统是一种高效、准确的农作物品种识别工具。该系统利用YOLOv11深度学习模型&#xff0c;能够实现对棉花主要品种&#xff0c;包括树棉&#xff08;G. arboreum&#xff09;、海岛棉&#xff08;G. barbadense&#xff09;、草棉&a…

论文:Generalized Category Discovery with Clustering Assignment Consistency

论文下载&#xff1a; https://arxiv.org/pdf/2310.19210 一、基本原理 该方法包括两个阶段:半监督表示学习和社区检测。在半监督表示学习中&#xff0c;使用了监督对比损失来充分地推导标记信息。此外&#xff0c;由于对比学习方法与协同训练假设一致&#xff0c;研究引入了…

Java高级JVM知识点记录,内存结构,垃圾回收,类文件结构,类加载器

JVM是Java高级部分&#xff0c;深入理解程序的运行及原理&#xff0c;面试中也问的比较多。 JVM是Java程序运行的虚拟机环境&#xff0c;实现了“一次编写&#xff0c;到处运行”。它负责将字节码解释或编译为机器码&#xff0c;管理内存和资源&#xff0c;并提供运行时环境&a…

MySQL 5.7 Online DDL 技术深度解析

14.13.1 在线DDL操作 索引操作主键操作列操作生成列操作外键操作表操作表空间操作分区操作 索引操作 下表概述了对索引操作的在线DDL支持情况。星号表示有附加信息、例外情况或依赖条件。有关详细信息&#xff0c;请参阅语法和使用说明。 操作原地执行重建表允许并发DML仅修…

kafka 报错消息太大解决方案 Broker: Message size too large

kafka-configs.sh --bootstrap-server localhost:9092 \ --alter --entity-type topics \ --entity-name sim_result_zy \ --add-config max.message.bytes10485880 学习营课程

HarmonyOS:ComposeTitleBar 组件自学指南

在日常的鸿蒙应用开发工作中&#xff0c;我们常常会面临构建美观且功能实用的用户界面的挑战。而标题栏作为应用界面的重要组成部分&#xff0c;它不仅承载着展示页面关键信息的重任&#xff0c;还能为用户提供便捷的操作入口。最近在参与的一个项目里&#xff0c;我就深深体会…

前端面试题之CSS中的box属性

前几天在面试中遇到面试官问了一个关于box的属性面试题&#xff0c;平时都是直接AI没有仔细去看过。来说说CSS中的常用box属性&#xff1a; 1. box-sizing box-sizing 属性定义了元素的宽度和高度是否包括内边距&#xff08;padding&#xff09;和边框&#xff08;border&…

前端开发时的内存泄漏问题

目录 &#x1f50d; 什么是内存泄漏&#xff08;Memory Leak&#xff09;&#xff1f;&#x1f6a8; 常见的内存泄漏场景1️⃣ 未清除的定时器&#xff08;setInterval / setTimeout&#xff09;2️⃣ 全局变量&#xff08;变量未正确释放&#xff09;3️⃣ 事件监听未清除4️⃣…

Java 基础-30-单例设计模式:懒汉式与饿汉式

在软件开发中&#xff0c;单例设计模式&#xff08;Singleton Design Pattern&#xff09;是一种常用的设计模式&#xff0c;它确保一个类只有一个实例&#xff0c;并提供一个全局访问点。这种模式通常用于管理共享资源&#xff08;如数据库连接池、线程池等&#xff09;或需要…

为 MinIO AIStor 引入模型上下文协议(MCP)服务器

Anthropic 最近宣布的模型上下文协议 &#xff08;MCP&#xff09; 将改变我们与技术交互的方式。它允许自然语言通信替换许多任务的复杂命令行语法。不仅如此&#xff0c;语言模型还可以总结传统工具的丰富输出&#xff0c;并以人类可读的形式呈现关键信息。MinIO 是世界领先的…