通过 eBPF 探针接入 Go 应用

最近更新时间:2024-10-17 22:07:32

我的收藏
说明:
OpenTelemetry 是工具、API 和 SDK 的集合,用于检测、生成、收集和导出遥测数据(指标、日志和跟踪),帮助用户分析软件的性能和行为。关于 OpenTelemetry 的更多信息请参考 OpenTelemetry 官方网站
eBPF(扩展伯克利包过滤器)允许在内核空间运行安全的用户定义代码,而不需要修改内核源代码或加载内核模块。eBPF 与 OpenTelemetry 结合,使得用户无需在代码中手动埋点就可实现完整的监控能力。
腾讯云 eBPF 探针基于开源社区 opentelemetry-go-instrumentation 项目二次开发,功能和特性仍在快速迭代中。
本文将介绍如何通过腾讯云 eBPF 探针接入 Go 应用。

前提条件

说明:
eBPF 目前仅支持 Linux,内核版本需要在 4.19 及以上。
暂不支持 macOS 和 Windows 的系统(包括运行在这类宿主机中的 Linux 容器)。
Go 版本需在 1.18 及以上,支持的库和框架如下:
库/框架
版本
database/sql
go1.12 to go1.23.0
github.com/segmentio/kafka-go
v0.4.1 to v0.4.47
google.golang.org/grpc
v1.14.0 to v1.66.0
net/http
go1.12 to go1.22.6
注意:
基于net/http的 Web 框架,如 Gin 等都支持。

接入流程

步骤1:获取接入点和 Token

1. 登录 腾讯云可观测平台 控制台。
2. 在左侧菜单栏中选择应用性能监控,单击应用列表 > 接入应用
3. 在右侧弹出的数据接入抽屉框中,单击 Go 语言。
4. 接入 Go 应用页面,选择您所要接入的地域以及业务系统
5. 选择接入协议类型OpenTelemetry
6. 上报方式选择您所想要的上报方式,获取您的接入点Token
说明:
内网上报:使用此上报方式,您的服务需运行在腾讯云 VPC。通过 VPC 直接联通,在避免外网通信的安全风险同时,可以节省上报流量开销。
外网上报:当您的服务部署在本地或非腾讯云 VPC 内,可以通过此方式上报数据。请注意外网通信存在安全风险,同时也会造成一定上报流量费用。

步骤2:下载探针

linux/amd64 探针下载
linux/arm64 探针下载
赋予探针执行权限:
chmod +x otel-go-instrumentation

步骤3:接入并上报

1. 确认应用的运行路径

将需要接入的应用编译成二进制文件,确认应用启动时的完整路径

2. 运行探针

运行探针需要有系统的 root 权限,使用以下命令接入:
sudo OTEL_GO_AUTO_TARGET_EXE=</path/to/executable_binary> \\
OTEL_SERVICE_NAME=<serviceName> \\
OTEL_EXPORTER_OTLP_PROTOCOL=grpc \\
OTEL_TRACES_EXPORTER=otlp \\
OTEL_EXPORTER_OTLP_ENDPOINT=<endpoint> \\
OTEL_RESOURCE_ATTRIBUTES=token=<token>,host.name=<hostName> \\
./otel-go-instrumentation
对应字段的说明如下:
</path/to/executable_binary> :应用可执行文件的路径,这里一定要是绝对路径
<serviceName>:应用名,多个使用相同 serviceName 接入的应用进程,在 APM 中会表现为相同应用下的多个实例。应用名最长63个字符,只能包含小写字母、数字及分隔符“ - ”,且必须以小写字母开头,数字或小写字母结尾。
<endpoint>:前置步骤中拿到的接入点,注意这里必须添加http://前缀。
<token>:前置步骤中拿到业务系统 Token。
<hostName>:该实例的主机名,是应用实例的唯一标识,通常情况下可以设置为应用实例的 IP 地址。
下述内容以应用名为 myService,运行路径为/root,业务系统 Token 为 myToken,主机名为 192.168.0.10,接入点以https://ap-guangzhou.apm.tencentcs.com:4317为例,完整的启动命令为:
sudo OTEL_GO_AUTO_TARGET_EXE=/root/myService \\
OTEL_SERVICE_NAME=myService \\
OTEL_EXPORTER_OTLP_PROTOCOL=grpc \\
OTEL_TRACES_EXPORTER=otlp \\
OTEL_EXPORTER_OTLP_ENDPOINT=https://ap-guangzhou.apm.tencentcs.com:4317 \\
OTEL_RESOURCE_ATTRIBUTES=token=myToken,host.name=192.168.0.10 \\
./otel-go-instrumentation
了解更多接入方式,请参考 官方文档

3. 运行应用

运行应用,当发生接口调用时,探针会输出日志。如果应用停止运行,探针不需要停止,应用下次启动时会自动埋点。

4. 接入验证

接入侧
探针的日志输出中含有instrumentation loaded successfully字段表示接入成功:
{"level":"info","ts":1725609047.2234442,"logger":"go.opentelemetry.io/auto","caller":"cli/main.go:119","msg":"starting instrumentation..."}
{"level":"info","ts":1725609047.2235398,"logger":"Instrumentation.Manager","caller":"instrumentation/manager.go:195","msg":"loading probe","name":"net/http/server"}
{"level":"info","ts":1725609047.388379,"logger":"go.opentelemetry.io/auto","caller":"cli/main.go:115","msg":"instrumentation loaded successfully"}
APM 控制台
在发生接口调用的情况下,应用性能监控 > 应用列表 中将展示接入的应用,点击应用名称/ID 进入应用详情页,再选择实例监控,即可看到接入的应用实例。由于可观测数据的处理存在一定延时,如果接入后在控制台没有查询到应用或实例,请等待30秒左右。

接入错误排查

探针仅有一条日志输出:
{"level":"info","ts":1725609014.2038825,"logger":"go.opentelemetry.io/auto","caller":"cli/main.go:86","msg":"building OpenTelemetry Go instrumentation ...","globalImpl":false}
一般是如下两种情况:
1. 应用没有启动;
2. 应用的启动路径(OTEL_GO_AUTO_TARGET_EXE)错误。
探针日志报错:
traces export: failed to exit idle mode: dns resolver: missing address
一般是接入点没有添加http://前缀导致。

eBPF 接入代码示例

说明:
以下将给出多组使用 eBPF 探针接入 APM 的代码示例,帮助用户更方便地把业务的链路数据上报到 APM。
使用 eBPF 探针上报数据时,几乎不需要更改已有的业务代码,但是需要一些细节,例如代码中 context 的传递。

项目一:对外提供接口的数据库操作程序

本项目的主要功能是对外提供一个 HTTP 接口,调用此接口可以操作 sqlite 数据库。基于 Go 语言的标准库 net/httpdatabase/sql 实现。
package main
import (
"database/sql"
"fmt"
"net/http"
"os"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
)

const (
sqlQuery = "SELECT * FROM contacts"
dbName = "test.db"
tableDefinition = `CREATE TABLE contacts (
contact_id INTEGER PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT NOT NULL);`

tableInsertion = `INSERT INTO 'contacts'
('first_name', 'last_name', 'email', 'phone') VALUES
('Moshe', 'Levi', 'moshe@gmail.com', '052-1234567');`
)

type Server struct {
db *sql.DB
}
// 初始化数据库
func CreateDb() {
file, err := os.Create(dbName)
if err != nil {
panic(err)
}
err = file.Close()
if err != nil {
panic(err)
}
}

func NewServer() *Server {
CreateDb()
database, err := sql.Open("sqlite3", dbName)
if err != nil {
panic(err)
}
_, err = database.Exec(tableDefinition)
if err != nil {
panic(err)
}
return &Server{
db: database,
}
}
func (s *Server) queryDb(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
conn, err := s.db.Conn(ctx)
if err != nil {
panic(err)
}
// 注意这里一定要传递 `ctx` 或者 `req.Context()`,使 HTTP 请求和数据库的操作记录可以保持在一条链路中。
// 如果在这里没有传递请求的 context,写成了 `s.db.Exec(tableInsertion)`,就会导致链路中断,HTTP span 和 database span 出现在两条链路中!
_, err = s.db.ExecContext(ctx, tableInsertion)
if err != nil {
panic(err)
}
rows, err := conn.QueryContext(req.Context(), sqlQuery)
if err != nil {
panic(err)
}
logger.Info("queryDb called")
for rows.Next() {
var id int
var firstName string
var lastName string
var email string
var phone string
err := rows.Scan(&id, &firstName, &lastName, &email, &phone)
if err != nil {
panic(err)
}
fmt.Fprintf(w, "ID: %d, firstName: %s, lastName: %s, email: %s, phone: %s\\n", id, firstName, lastName, email, phone)
}
}
注意:
请关注代码中的注释说明,一定要正确传递 context 才能正确构造 span 之间的父子关系,保证链路不会中断。