CloudWeGo + APMPlus:打造从开发到可观测的一站式体验

大模型云原生可观测微服务治理

picture.image

来源 | 火山引擎可观测团队

分布式架构和微服务的普及极大提升了系统的扩展性和灵活性,但也为可观测性(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 也在汽车、互联网、金融等多个行业取得了众多客户的信任。

可观测集成: CloudWeGo + 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 服务。

  1. 获取鉴权信息

picture.image

  1. 设置环境变量并启动 Kitex 应用

picture.image

上报地址明细:

picture.image

方式二:Otel Collector 转发

APMPlusOpenTelemetryCollector 是 APMPlus 基于OpenTelemetryCollector 二次开发的数据采集器。您可以在集群中安装 Opentelemetry Collector 做数据转发。

  1. 安装 vke-addon:APMPlusOpenTelemetryCollector
  • 选择即将部署应用的集群
  • 组件管理 > 监控 > apmplus-opentelemetry-collector 安装

picture.image

  1. 设置环境变量启动 Kitex 应用

picture.image

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
  • 方式二:火山引擎部署

  1. 登录 火山方舟控制台 大模型平台,创建两个模型,分别做为 Embedding 和 Chat。

picture.image

  1. 登录 火山引擎数据库控制台,创建 Redis 集群,并获取私网地址。

picture.image

  1. 登录 容器服务控制台,部署 Dockerfile 应用,并配置 APMPlus、方舟模型接入点、以及 Redis 参数。

picture.image

  1. 在容器服务中,创建一个公网 LoadBalancer 类型的 Service,并获取公网访问地址。

picture.image

  1. 通过浏览器访问 Eino-Assistant 应用,与之进行交流。

picture.image

应用观测

开发者可以通过 APMPlus 的全链路监控控制台,直观地查看基于 CloudWeGo 框架构建的应用的调用链 (Traces)性能指标( Metrics 以及运行时状态等关键数据。

黄金指标

通过上报的 OpenTelemetry 观测数据,APMPlus 会自动清洗并提取服务的 RED 黄金指标(请求率、错误率、响应时间),帮助用户全面掌握应用的运行状况。具体来说:

  • 请求量( QPS :实时监控服务的请求处理能力,确保高峰期的稳定性。
  • 错误率:精准定位异常请求,快速发现问题根源。
  • 响应耗时:分析服务的延迟表现,为性能优化提供数据支持。

picture.image

大模型指标

针对 Eino 应用,APMPlus 提供了与大语言模型(LLM)交互相关的专属指标,帮助用户深入了解 AI 应用的性能表现。具体包括:

  • 大模型调用次数:统计应用与底层 LLM 的交互频率,反映模型的使用强度。
  • Token 使用量:全面记录用户输入和模型输出的 Token 总量,为资源消耗和成本分析提供支持。
  • 大模型响应耗时:监测应用调用 LLM 的整体响应时间,为优化调用效率提供依据。
  • TTFT(Time To First Token :衡量用户提交查询后模型开始输出首个 Token 的时间,突出低延迟响应的重要性,尤其在实时交互场景中至关重要。
  • TPOT(Time Per Output ****Token :计算模型输出首个 Token 后,后续每个 Token 的平均生成时间。这一指标直接影响用户对模型响应速度的体验感知。

这些指标不仅能够帮助开发者优化 AI 应用的性能,还能提升用户体验,确保模型交互的高效性和流畅性。

picture.image

服务拓扑

服务拓扑图用于直观地展示服务间的上下游调用关系,通过图结构清晰地表征应用内部及外部服务的交互情况。它能够帮助用户快速了解服务之间的依赖关系、调用路径以及流量分布,为故障定位和性能优化提供重要支持。

picture.image

运行时指标

应用集成了 OpenTelemetry Runtime 指标,可输出关键运行时指标,包括组件的内存水位、GC(垃圾回收)详情、Golang 协程数量等。这些指标为开发者提供了深入了解应用资源消耗情况的能力,帮助快速定位性能瓶颈或潜在问题。

picture.image

链路查询

APMPlus 提供强大的链路查询功能,支持用户查看链路列表并深入分析具体的调用链路。用户可以通过以下操作快速定位和分析链路性能:

[链路列表查询] :展示应用的调用链路列表,便于用户筛选目标链路。

picture.image

[单链路详情查看] :单击目标 Trace ID,可深入查看对应链路的详细信息,包括:

  • 火焰图:直观展示链路的性能热点,帮助快速定位耗时操作。
  • 调用列表:以表格形式呈现链路的调用详情,便于逐步分析。
  • 服务拓扑图:展示链路的上下游调用关系,帮助理解服务间的交互。

picture.image

picture.image

picture.image


未来展望

随着技术的不断演进,开发与可观测的需求也在发生深刻变化。未来,APMPlusCloudWeGo 将在微服务架构与大模型应用领域进一步深度结合,为开发者提供更强大的工具支持和全链路赋能。

更全面的支持

CloudWeGo 的生态正在快速扩展,除了核心框架 Kitex、Hertz 和 Eino 等,未来还将有更多组件和工具加入生态体系。APMPlus 计划进一步加强对这些框架的支持,并进一步降低开发者的监控集成成本,让可观测能力成为 CloudWeGo 生态的内生特性。

LLM 应用观测深入发展

针对 LLM 应用,会更加结合其自身的特点,提升模型交互相关指标的丰富度,开发针对性的归因分析工具,同时会与底层模型平台打通,获取模型推理过程中的关键指标(如推理耗时、资源使用情况、生成过程的中间状态),与应用层指标联动,帮助应用优化模型调用策略,提升生成结果质量。

相关链接

[1] www.volcengine.com/product/apmplus

[2] www.volcengine.com/product/prometheus

[3] www.volcengine.com/product/cloudmonitor

picture.image

picture.image

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
云原生环境下的日志采集存储分析实践
云原生场景下,日志数据的规模和种类剧增,日志采集、加工、分析的多样性也大大增加。面对这些挑战,火山引擎基于超大规模下的 Kubernetes 日志实践孵化出了一套完整的日志采集、加工、查询、分析、消费的平台。本次主要分享了火山引擎云原生日志平台的相关实践。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论