Elastic APM 接入实战
Elastic APM:应用性能监控接入实战
ELK 全家桶不只能查日志,还能做 APM(Application Performance Monitoring)。Elastic APM 提供了从应用埋点到可视化分析的端到端方案,且与 Elastic Stack 原生集成。本文从架构讲起,手把手带你接入一个 Go 应用。
1. APM 架构总览
┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ APM Agent │ ──> │ APM Server │ ──> │ Elasticsearch │ ──> │ Kibana │
│ (应用内置) │ │ (接收+处理) │ │ (存储) │ │ (APM UI) │
└─────────────┘ └─────────────┘ └──────────────┘ └──────────┘
| 组件 | 职责 | 部署方式 |
|---|---|---|
| APM Agent | 嵌入应用进程,自动采集性能数据 | Go module、Java agent JAR、npm 包 |
| APM Server | 接收 Agent 数据、校验、转发到 ES | 独立进程或 Fleet 管理 |
| Elasticsearch | 存储 trace、transaction、span 数据 | 同上 |
| Kibana APM UI | 服务地图、延迟分布、错误追踪 | Kibana 内置 APM 应用 |
APM Server 从 7.16 开始集成到 Fleet 中管理;8.x 中用 Fleet + Elastic Agent 替代了独立 APM Server,但架构思路一致。
2. Go 应用接入
Step 1:安装 APM Module
go get go.elastic.co/apm/v2
Step 2:在 main.go 中初始化
package main
import (
"net/http"
"go.elastic.co/apm/v2"
)
func main() {
// 默认从环境变量读取配置,也可以代码设置
// 环境变量:ELASTIC_APM_SERVER_URL、ELASTIC_APM_SERVICE_NAME 等
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", orderHandler)
// apmhttp.Wrap 自动为每个请求创建 Transaction
http.ListenAndServe(":8080", apmhttp.Wrap(mux))
}
环境变量配置:
export ELASTIC_APM_SERVER_URL=http://apm-server:8200
export ELASTIC_APM_SERVICE_NAME=order-service
export ELASTIC_APM_ENVIRONMENT=production
export ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.5
关键参数说明:
| 参数 | 含义 | 必填 |
|---|---|---|
ELASTIC_APM_SERVICE_NAME |
服务名称(在 Kibana 中标识) | 是 |
ELASTIC_APM_SERVER_URL |
APM Server 地址 | 是 |
ELASTIC_APM_ENVIRONMENT |
环境标签(production/staging/dev) | 推荐 |
ELASTIC_APM_TRANSACTION_SAMPLE_RATE |
采样率(0~1,默认 1.0) | 推荐(高 QPS 设 0.1~0.5) |
ELASTIC_APM_CAPTURE_BODY |
捕获请求体(off/errors/transactions/all) | 可选 |
Step 3:代码配置方式(替代环境变量)
import (
"go.elastic.co/apm/v2"
"go.elastic.co/apm/v2/transport"
)
func init() {
tracer, _ := apm.NewTracer(apm.TracerOptions{
ServiceName: "order-service",
ServiceEnvironment: "production",
ServerURL: "http://apm-server:8200",
TransactionSampleRate: 0.5,
CaptureBody: apm.CaptureBodyErrors,
})
tracer.SetDefaultTracer(tracer)
}
3. Trace 和 Span 的概念
APM 的核心理念来自分布式追踪(Distributed Tracing)。
| 概念 | 含义 | 示例 |
|---|---|---|
| Trace | 一个完整请求的端到端调用链 | 用户下单:Gateway -> Order -> Inventory -> DB |
| Transaction | 一个服务内部的一次完整处理 | Order 服务处理 POST /api/orders |
| Span | Transaction 内部的一个子操作 | 执行 SQL 查询、调用 Redis、发送 HTTP 请求 |
| Error | 异常或错误事件 | panic、HTTP 500 |
层级关系:
Trace (整个下单流程)
├── Transaction: POST /api/orders (Order 服务)
│ ├── Span: SELECT * FROM orders WHERE ... (MySQL, 45ms)
│ ├── Span: GET inventory:8080/api/stock (HTTP, 120ms)
│ └── Span: INCR order:counter (Redis, 3ms)
└── Transaction: GET /api/stock (Inventory 服务)
├── Span: SELECT stock FROM products (MySQL, 30ms)
└── Span: SETEX stock:lock (Redis, 5ms)
4. APM 自动捕获的能力
APM Agent 通过 apmhttp、apmsql、apmgorm 等子包自动拦截以下调用,只需 import 对应 wrapper:
| 类别 | 自动支持模块 |
|---|---|
| HTTP Server | net/http(apmhttp.Wrap)、gin(apmgin)、echo(apmecho)、gorilla/mux(apmmux) |
| HTTP Client | net/http(apmhttp.WrapClient) |
| 数据库 | database/sql(apmsql.Register)、GORM(apmgorm)、sqlx(apmsqlx) |
| Redis | go-redis(apmgoredis)、redigo |
| 消息队列 | Kafka(sarama)、RabbitMQ(amqp) |
| 缓存 | go-redis Cache |
| gRPC | google.golang.org/grpc(apmgrpc) |
自动捕获的通用 Span 标签:
| 标签 | 示例 |
|---|---|
db.type |
mysql、postgresql、redis |
db.statement |
SELECT * FROM users WHERE id = ? |
http.status_code |
200、404、500 |
http.method |
GET、POST |
http.url |
http://inventory:8080/api/stock |
5. 自定义 Transaction 和 Label
自动捕获无法覆盖所有业务场景。有些关键业务逻辑需要手动埋点。
自定义 Transaction
import (
"go.elastic.co/apm/v2"
)
// 启动一个自定义 Transaction
tx := apm.DefaultTracer().StartTransaction("OrderProcessing", "business")
defer tx.End()
// 业务逻辑...
err := orderService.ProcessOrder(orderID)
if err != nil {
tx.Result = "failed"
apm.CaptureError(tx.Context, err).Send()
return
}
tx.Result = "success"
自定义 Span
import (
"go.elastic.co/apm/v2"
)
span := tx.StartSpan("VerifyCoupon", "db.mysql.query", nil)
defer span.End()
span.Context.SetLabel("coupon_code", couponCode)
span.Context.SetLabel("user_id", userID)
// 查询优惠券有效性
valid, err := couponMapper.IsValid(couponCode)
if err != nil {
span.Result = "error"
apm.CaptureError(span.Context, err).Send()
return
}
if valid {
span.Result = "valid"
} else {
span.Result = "invalid"
}
添加业务 Label
// 在 Transaction 级别添加业务标签
tx.Context.SetLabel("order_id", orderID)
tx.Context.SetLabel("order_amount", amount)
tx.Context.SetLabel("channel", "wechat_pay")
tx.Context.SetLabel("user_level", "vip")
// User 信息也可以挂到 Transaction 上
tx.Context.SetUserID(userID)
tx.Context.SetUserEmail("user@test.com")
tx.Context.SetUsername("zhangsan")
6. APM 与日志关联(trace.id 注入日志)
这是 APM 最有价值的功能之一:把 trace ID 注入到应用日志中,这样从 Kibana APM UI 的一个 Trace 点进去,能直接跳转到关联日志。
使用 logrus + apmlogrus
import (
"github.com/sirupsen/logrus"
"go.elastic.co/apm/v2"
"go.elastic.co/ecslogrus"
)
func orderHandler(w http.ResponseWriter, r *http.Request) {
// 从 request context 获取 APM 注入的 trace ID
ctx := r.Context()
// logrus 配合 ECS 格式输出,自动带上 trace.id 和 transaction.id
logrus.WithContext(ctx).
WithField("order_id", "order_12345").
Info("开始处理订单")
// 业务逻辑...
err := orderService.ProcessOrder(ctx, orderID)
if err != nil {
logrus.WithContext(ctx).
WithError(err).
Error("订单处理失败")
}
}
日志输出效果(JSON / ECS 格式)
{
"@timestamp": "2024-06-15T14:32:01.123Z",
"log.level": "info",
"message": "开始处理订单",
"service.name": "order-service",
"trace.id": "a3b2f6d8e1c9",
"transaction.id": "4f7a9b1c2d3e",
"labels": { "order_id": "order_12345" }
}
APM module 会自动把 trace.id 和 transaction.id 写入 context,ecslogrus 在序列化时自动带上。如果用的是 zap,则配合 go.elastic.co/apm/module/apmzap/v2 使用。
在 Kibana APM UI 中打开 a3b2f6d8e1c9 这个 Trace,可以看到完整的调用链;点"关联日志"即可直接跳转到带 trace.id: a3b2f6d8e1c9 过滤条件的 Discover 页面。
7. 踩坑案例
案例 1:APM Agent 导致应用内存飙升
现象:接入 APM 后,应用 Heap 使用率从 60% 涨到 85%。
排查:transaction_sample_rate 默认 1.0,高并发下每个请求都生成 Transaction 和 Span 对象,大量 Span 堆积在内存中等上报。
解决:
export ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.1 # 高 QPS 场景设 10% 采样
export ELASTIC_APM_TRANSACTION_MAX_SPANS=500 # 限制单个 Transaction 最大 Span 数
export ELASTIC_APM_SPAN_MIN_DURATION=10ms # 短于 10ms 的 Span 不上报(减少噪音)
案例 2:生产环境忘记关 debug 日志
现象:APM Server 的磁盘空间被日志打满。
排查:Agent 配置了 ELASTIC_APM_LOG_LEVEL=DEBUG,每个 Span 的详细信息打印到文件。
解决:
export ELASTIC_APM_LOG_LEVEL=INFO # 生产必须用 INFO 以上
export ELASTIC_APM_LOG_FILE=/var/log/apm/elastic-apm.log
export ELASTIC_APM_LOG_FORMAT=json # JSON 格式便于分析
案例 3:APM Server 与 ES 版本不兼容
现象:APM Server 启动报错,日志显示 Index Template 创建失败。
排查:APM Server 7.17,ES 8.5,版本跨度太大导致 ILM Policy 结构不兼容。
解决:
| 组件 | 版本要求 |
|---|---|
| APM Agent | 尽量和 APM Server 主版本一致 |
| APM Server | 和 ES 主版本保持一致(7.x 配 7.x,8.x 配 8.x) |
8. Kibana APM UI 核心功能
接入数据后,Kibana 的 APM 应用提供以下视图:
| 页面 | 功能 |
|---|---|
| Services | 服务列表,显示延迟、吞吐量、错误率 |
| Service Map | 自动生成服务调用拓扑图 |
| Transactions | 单个服务的所有 Transaction 耗时分布 |
| Dependencies | 下游依赖(数据库、HTTP、缓存)延迟 |
| Traces | 单次请求的完整调用链瀑布图 |
| Errors | 错误分组、堆栈信息 |
| Service Overview | 综合仪表盘(P95 延迟、错误率、QPS) |
总结
APM 接入是一个逐步深化的过程:
- 入门的价值(1 天):
go get+ 几行apmhttp.Wrap,立刻看到服务地图、延迟分布、错误堆栈 - 深化的价值(1 周):自定义业务 Transaction、添加 Label、关联日志,把监控从"框架级"做到"业务级"
- 体系的价值(持续):APM + 日志 + 指标告警三位一体,形成一个完整的可观测性平台
不需要一开始就埋很多自定义点。先把自动捕获跑稳,然后在排查线上问题时,边排查边补埋点——这样做效率最高,也最贴近真实痛点。