来源 | 火山引擎可观测团队
分布式架构和微服务的普及极大提升了系统的扩展性和灵活性,但也为可观测性(Observability)带来了数据分散、链路追踪复杂和故障传播三大挑战。由于请求可能跨越多个服务、节点甚至云区域,传统的单体监控方式难以关联完整的调用链,导致故障定位效率低下。同时,传统开发框架与监控工具之间也往往缺乏深度适配,需要开发者额外投入时间实现数据打点和链路追踪。
随着大语言模型(LLM)越来越多采用分布式架构,应用的可观测性问题进一步升级,繁多的组件、复杂的编排、模型效果评估等为观测带来更大的挑战。本文将基于字节跳动开源的企业级中间件集合 CloudWeGo ****和火山引擎应用性能监控全链路版 APMPlus,介绍基于字节跳动内部实践的一站式开发与观测解决方案。
什么是 CloudWeGo
CloudWeGo 是字节跳动开源的一套企业级云原生微服务架构中间件集合,专注于微服务通信与治理、大模型应用的构建,具有高性能、高扩展性、高可靠性的特点。CloudWeGo 旗下包括多个重点子项目 Kitex、Hertz、Netpoll、Volo,多个小而美的子项目 Thriftgo、Frugal、Fastpb、Pilota、Motore、cwgo、Dynamicgo、Shmipc、sonic-rs 等。
随着大语言模型(LLM)技术的快速发展,应用场景从内容生成到智能交互不断扩展,对开发框架的性能和扩展性提出了更高的要求。2025 年,字节跳动服务框架团队开源了专为 LLM 应用开发而设计的新框架 Eino,这是字节跳动内部大模型应用的首选全代码开发框架,已有包括豆包、抖音、扣子等多条业务线、数百个服务接入使用。作为 CloudWeGo 生态中的最新成员,Eino 的开源进一步扩展了 CloudWeGo 的生态边界,使其不仅适用于传统的微服务场景,还能够赋能大模型驱动的智能化应用开发。
什么是 APMPlus
APMPlus 是火山引擎提供的针对应用服务的品质、性能以及自定义埋点的 APM 服务,通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控能力,助力企业发现多类异常问题并及时告警,具有以下核心能力:
- 异常问题发现与报警:帮助开发者快速定位性能瓶颈和故障点。
- 丰富的归因能力:支持堆栈分析、调度分析、维度分析、埋点分析等多种监控视角。
- 链路追踪与日志查询:结合调用链路和单点日志,快速排查问题。
- 灵活报表能力:通过趋势分析掌握系统健康状况。
在字节跳动内部,APMPlus 支撑着今日头条、抖音、飞书等多项产品应用稳定性。凭借同源的产品解决方案和优质的服务,APMPlus 也在汽车、互联网、金融等多个行业取得了众多客户的信任。
目前,APMPlus 服务端监控已经针对 CloudWeGo 的多个框架进行了深度适配,包括 Kitex、Hertz 和最新的 Eino 框架。开发者可以轻松实现服务端监控和链路追踪,开启更智能、更便捷的监控体验:
- 自动化监控集成:在 Kitex、Hertz 和 Eino 等框架中深度集成 OpenTelemetry,无需额外配置即可生成性能指标和调用链路数据。
- 统一数据展示:所有监控数据可直接在 APMPlus 平台上可视化呈现,帮助开发者实时掌握系统状态。
- 快速问题定位:通过调用链路追踪和性能分析,显著提升故障排查效率。
这种深度适配不仅帮助开发者专注于业务逻辑的实现,还能轻松掌握应用的运行状态,真正实现开发与观测的无缝结合。我们在 Kitex、Hertz 和 Eino 中集成了 OpenTelemetry,支持指标和链路的打点,通过标准化的可观测数据协议,确保与 APMPlus 的无缝连接。
Kitex 接入
Kitex 是 Golang 微服务 RPC 框架,具有 高性能、高可扩展 的特点,支持多消息协议(Thrift/Protobuf/gRPC)、多消息类型(PingPong/Oneway/Streaming)、服务治理、代码生成以及完备的开源社区生态。
步骤一:初始化 Kitex
步骤一:初始化 Kitex
Kitex 通过 OpenTelemetry SDK 以中间件方式集成,生成并上报链路、指标数据。
Kitex 示例代码仅展示 Opentelemetry 集成相关的核心逻辑,而运行 demo 所需的 thrift 源文件、生成代码可以从社区的 hello-demo 获取。
服务端接入:
package main
import (
"context"
"log"
"log/slog"
api "github.com/cloudwego/kitex-examples/hello/kitex_gen/api/hello"
"github.com/cloudwego/kitex/server"
"github.com/kitex-contrib/obs-opentelemetry/provider"
"github.com/kitex-contrib/obs-opentelemetry/tracing"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
"go.opentelemetry.io/otel/log/global"
sdklog "go.opentelemetry.io/otel/sdk/log"
)
var otelLogger *slog.Logger
func main() {
ctx := context.Background()
// 1. 初始化 otel provider
p := provider.NewOpenTelemetryProvider(
provider.WithInsecure(),
)
defer p.Shutdown(ctx)
svr := api.NewServer(
new(HelloImpl),
// 2. 初始化 tracing 中间件
server.WithSuite(tracing.NewServerSuite()),
)
// 3. 初始化 otel log provider
lp := initLog(ctx)
defer lp.Shutdown(ctx)
// 4. 使用第三方 logger 打印日志,日志会自动上报至后台服务
otelLogger.Info("init logger successfully")
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
func initLog(ctx context.Context) *sdklog.LoggerProvider {
logExporter, err := otlploggrpc.New(ctx, otlploggrpc.WithInsecure())
if err != nil {
panic("failed to initialize exporter")
}
lp := sdklog.NewLoggerProvider(
sdklog.WithProcessor(
sdklog.NewSimpleProcessor(logExporter),
),
)
global.SetLoggerProvider(lp)
// init otelLogger, then use it directly anywhere in your app to record your log, and the
// log content will be sent to apmplus backend.
otelLogger = otelslog.NewLogger("slog")
return lp
}
客户端接入:
package main
import (
"context"
"log"
"time"
"github.com/cloudwego/kitex-examples/hello/kitex_gen/api"
"github.com/cloudwego/kitex-examples/hello/kitex_gen/api/hello"
"github.com/cloudwego/kitex/client"
"github.com/kitex-contrib/obs-opentelemetry/provider"
"github.com/kitex-contrib/obs-opentelemetry/tracing"
)
func main() {
ctx := context.Background()
// 1. 初始化 otel provider
p := provider.NewOpenTelemetryProvider(
provider.WithInsecure(),
)
defer p.Shutdown(ctx)
client, err := hello.NewClient(
"hello",
client.WithHostPorts("0.0.0.0:8888"),
// 2. 初始化 tracing 中间件
client.WithSuite(tracing.NewClientSuite()),
)
if err != nil {
log.Fatal(err)
}
for {
req := &api.Request{Message: "my request"}
resp, err := client.Echo(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second * 10)
}
}
步骤二:数据上报
Kitex 支持两种数据上报方式:
方式一:直接上报至 ApmPlus 服务端
在服务端,APMPlus 支持 OpenTelemetry、Jaeger、Prometheus 等接入协议,用户可以无需部署独立的 collector 就可将数据直接上报至 APMPlus。并通过环境变量设置服务名、数据上报地址等信息后启动 Kitex 服务。
- 获取鉴权信息
- 设置环境变量并启动 Kitex 应用
上报地址明细:
方式二:Otel Collector 转发
APMPlusOpenTelemetryCollector 是 APMPlus 基于OpenTelemetryCollector 二次开发的数据采集器。您可以在集群中安装 Opentelemetry Collector 做数据转发。
- 安装 vke-addon:APMPlusOpenTelemetryCollector
- 选择即将部署应用的集群
- 组件管理 > 监控 > apmplus-opentelemetry-collector 安装
- 设置环境变量启动 Kitex 应用
Hertz 接入
步骤一:初始化 Hertz
Hertz 是 Golang 微服务 HTTP 框架,具有 高性能、高可扩展 的特点,支持多协议(HTTP 1.1/HTTP 2/HTTP 3/Websocket)、网络层(Netpoll/go net)按需切换、丰富的 Web 中间件能力、丰富的服务治理能力、代码生成以及完备的开源社区生态。
服务端接入代码示例:
Hertz 以中间件方式集成 OpenTelemetry SDK,生成并上报链路和指标数据。
package main
import (
"context"
"log/slog"
"time"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
"github.com/hertz-contrib/obs-opentelemetry/provider"
hertztracing "github.com/hertz-contrib/obs-opentelemetry/tracing"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
"go.opentelemetry.io/otel/log/global"
sdklog "go.opentelemetry.io/otel/sdk/log"
)
var otelLogger *slog.Logger
func main() {
// 1. 初始化 otel metrics/traces provider
ctx := context.Background()
p := provider.NewOpenTelemetryProvider(
provider.WithInsecure(),
)
defer p.Shutdown(ctx)
// 2. 初始化 hertz tracer 中间件
tracer, cfg := hertztracing.NewServerTracer()
h := server.Default(tracer)
h.Use(hertztracing.ServerMiddleware(cfg))
// 3. 初始化 otel log provider
lp := initLog(ctx)
defer lp.Shutdown(ctx)
h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
// 4. 使用 otelLogger 记录日志
otelLogger.InfoContext(c, "/ping called", "time", time.Now().Unix())
ctx.JSON(consts.StatusOK, time.Now().Unix())
})
h.Spin()
}
func initLog(ctx context.Context) *sdklog.LoggerProvider {
logExporter, err := otlploggrpc.New(ctx, otlploggrpc.WithInsecure())
if err != nil {
panic("failed to initialize exporter")
}
lp := sdklog.NewLoggerProvider(
sdklog.WithProcessor(
sdklog.NewSimpleProcessor(logExporter),
),
)
global.SetLoggerProvider(lp)
// init otelLogger, then use it directly anywhere in your app to record your log, and the
// log content will be sent to apmplus backend.
otelLogger = otelslog.NewLogger("slog")
return lp
}
客户端接入代码示例:
package main
import (
"context"
"fmt"
"time"
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/hertz-contrib/obs-opentelemetry/provider"
hertztracing "github.com/hertz-contrib/obs-opentelemetry/tracing"
)
func main() {
// 1. 初始化 otel metrics/traces provider
p := provider.NewOpenTelemetryProvider(
provider.WithInsecure(),
)
defer p.Shutdown(context.Background())
// 2. 初始化 hertz tracer 中间件
c, _ := client.NewClient()
c.Use(hertztracing.ClientMiddleware())
for {
ctx := context.Background()
_, b, err := c.Get(ctx, nil, "http://0.0.0.0:8888/ping?foo=bar")
if err != nil {
hlog.CtxErrorf(ctx, err.Error())
}
fmt.Println("request sent return:", string(b))
<-time.After(time.Second * 10)
}
}
步骤二:数据上报
数据上报与 Kitex 的使用方式相同。
Eino 接入
Eino 提供了一个强调简洁性、可扩展性、可靠性与有效性,且更符合 Go 语言编程惯例的 LLM 应用开发框架,提供丰富的组件和工具生态,可在 AI 应用开发周期中的不同阶段,规范、简化和提效,帮助开发者以最快的速度实现最有深度的大模型应用。
步骤一:Eino 应用初始化
以 Eino 官方示例 eino-assistant 来介绍开发部署 eino 应用以及集成 APMPlus 的过程,通过 APMPlus Callback,Eino 可以将链路和指标数据上报至 APMPlus。
代码示例:
import (
"github.com/cloudwego/eino-ext/callbacks/apmplus"
"github.com/cloudwego/eino/callbacks"
...
)
func main() {
cbh, showdown, err := apmplus.NewApmplusHandler(&apmplus.Config{
Host: "apmplus-cn-beijing.volces.com:4317",
AppKey: "appkey-xxx",
ServiceName: "eino-app",
Release: "release/v0.0.1",
})
if err != nil {
log.Fatal(err)
}
callbacks.InitCallbackHandlers([]callbacks.Handler{cbh})
// 等待所有 trace 和 metrics 上报完成后退出
showdown(context.Background())
}
步骤二:Eino 应用部署
-
方式一:本地部署
- 在 .env 配置 APMPlus 认证信息 APMPLUS_APP_KEY
- 在 .env 配置大模型的接入点和 API Key
- 本地部署 Redis docker-compose up -d
- 启动应用 go run cmd/einoagent/main.go
-
方式二:火山引擎部署
- 登录 火山方舟控制台 大模型平台,创建两个模型,分别做为 Embedding 和 Chat。
- 登录 火山引擎数据库控制台,创建 Redis 集群,并获取私网地址。
- 登录 容器服务控制台,部署 Dockerfile 应用,并配置 APMPlus、方舟模型接入点、以及 Redis 参数。
- 在容器服务中,创建一个公网 LoadBalancer 类型的 Service,并获取公网访问地址。
- 通过浏览器访问 Eino-Assistant 应用,与之进行交流。
开发者可以通过 APMPlus 的全链路监控控制台,直观地查看基于 CloudWeGo 框架构建的应用的调用链 (Traces) 、性能指标( Metrics ) 以及运行时状态等关键数据。
黄金指标
通过上报的 OpenTelemetry 观测数据,APMPlus 会自动清洗并提取服务的 RED 黄金指标(请求率、错误率、响应时间),帮助用户全面掌握应用的运行状况。具体来说:
- 请求量( QPS ) :实时监控服务的请求处理能力,确保高峰期的稳定性。
- 错误率:精准定位异常请求,快速发现问题根源。
- 响应耗时:分析服务的延迟表现,为性能优化提供数据支持。
大模型指标
针对 Eino 应用,APMPlus 提供了与大语言模型(LLM)交互相关的专属指标,帮助用户深入了解 AI 应用的性能表现。具体包括:
- 大模型调用次数:统计应用与底层 LLM 的交互频率,反映模型的使用强度。
- Token 使用量:全面记录用户输入和模型输出的 Token 总量,为资源消耗和成本分析提供支持。
- 大模型响应耗时:监测应用调用 LLM 的整体响应时间,为优化调用效率提供依据。
- TTFT(Time To First Token ) :衡量用户提交查询后模型开始输出首个 Token 的时间,突出低延迟响应的重要性,尤其在实时交互场景中至关重要。
- TPOT(Time Per Output ****Token ) :计算模型输出首个 Token 后,后续每个 Token 的平均生成时间。这一指标直接影响用户对模型响应速度的体验感知。
这些指标不仅能够帮助开发者优化 AI 应用的性能,还能提升用户体验,确保模型交互的高效性和流畅性。
服务拓扑
服务拓扑图用于直观地展示服务间的上下游调用关系,通过图结构清晰地表征应用内部及外部服务的交互情况。它能够帮助用户快速了解服务之间的依赖关系、调用路径以及流量分布,为故障定位和性能优化提供重要支持。
运行时指标
应用集成了 OpenTelemetry Runtime 指标,可输出关键运行时指标,包括组件的内存水位、GC(垃圾回收)详情、Golang 协程数量等。这些指标为开发者提供了深入了解应用资源消耗情况的能力,帮助快速定位性能瓶颈或潜在问题。
链路查询
APMPlus 提供强大的链路查询功能,支持用户查看链路列表并深入分析具体的调用链路。用户可以通过以下操作快速定位和分析链路性能:
[链路列表查询] :展示应用的调用链路列表,便于用户筛选目标链路。
[单链路详情查看] :单击目标 Trace ID,可深入查看对应链路的详细信息,包括:
- 火焰图:直观展示链路的性能热点,帮助快速定位耗时操作。
- 调用列表:以表格形式呈现链路的调用详情,便于逐步分析。
- 服务拓扑图:展示链路的上下游调用关系,帮助理解服务间的交互。
随着技术的不断演进,开发与可观测的需求也在发生深刻变化。未来,APMPlus 和 CloudWeGo 将在微服务架构与大模型应用领域进一步深度结合,为开发者提供更强大的工具支持和全链路赋能。
更全面的支持
CloudWeGo 的生态正在快速扩展,除了核心框架 Kitex、Hertz 和 Eino 等,未来还将有更多组件和工具加入生态体系。APMPlus 计划进一步加强对这些框架的支持,并进一步降低开发者的监控集成成本,让可观测能力成为 CloudWeGo 生态的内生特性。
LLM 应用观测深入发展
针对 LLM 应用,会更加结合其自身的特点,提升模型交互相关指标的丰富度,开发针对性的归因分析工具,同时会与底层模型平台打通,获取模型推理过程中的关键指标(如推理耗时、资源使用情况、生成过程的中间状态),与应用层指标联动,帮助应用优化模型调用策略,提升生成结果质量。
相关链接
[1] www.volcengine.com/product/apmplus
[2] www.volcengine.com/product/prometheus