Ruby Span Profiles 实战:用 Grafana Tempo 与 Pyroscope 将链路追踪和持续剖析关联起来
Ruby Span Profiles 实战:用 Grafana Tempo 与 Pyroscope 将链路追踪和持续剖析关联起来
本指南基于仓库中 examples/tracing/ruby 示例,完整讲解如何在 Ruby 应用中同时接入 OpenTelemetry 链路追踪与 Pyroscope 持续剖析,并通过 Grafana Tempo 数据源的 Trace-to-Profiles 能力,实现"从一条分布式链路直接跳转到对应代码行的火焰图"。读完本文,你将掌握该示例的整体架构、容器化启动方式、Ruby 侧埋点实现,以及 Tempo 数据源关联配置的全部细节。
示例架构:一套完整的可观测性演示环境
该示例通过 docker compose 一次性拉起 6 类组件(见 docker-compose.yml):
| 组件 | 说明 | 关键配置 |
|---|---|---|
us-east / eu-north / ap-south |
三个地域的 Ruby Rideshare 打车应用实例 | 同一镜像、不同 REGION 与 PYROSCOPE_LABELS |
tempo |
Trace 收集与存储(Grafana Tempo 2.10.8) | 通过 OTLP HTTP/gRPC 接收 span |
pyroscope |
持续剖析后端(grafana/pyroscope:latest) | 暴露 4040 端口接收 profile |
grafana |
可视化与数据源关联 | 预装 pyroscope 插件、匿名登录、预置数据源 |
load-generator |
流量生成器 | 持续向三个地域实例发送请求 |
其中 Grafana 服务通过环境变量 GF_FEATURE_TOGGLES_ENABLE=traceToProfiles tracesEmbeddedFlameGraph 显式开启两项功能开关,这是链路与火焰图双向跳转的前置条件;GF_PLUGINS_PREINSTALL_SYNC=grafana-pyroscope-app 则保证 Grafana 启动即具备 Pyroscope 数据源插件。Pyroscope 与 Tempo 数据源均由 grafana-provisioning/datasources 目录下的 YAML 自动预置,无需手工在 UI 中配置。
load-generator 构建自 examples/language-sdk-instrumentation/golang-push/rideshare 目录,启动后自动向 http://us-east:5000、http://eu-north:5000、http://ap-south:5000 三个地址循环发送流量。
构建与启动
在 examples/tracing/ruby 目录下依次执行:
# 拉取最新 pyroscope 与 grafana 镜像
docker pull grafana/pyroscope:latest
docker pull grafana/grafana:latest
# 安装 Ruby 依赖(pyroscope SDK 等)
bundle install
# 启动整套环境
docker compose up
启动后负载生成器会自动开始向所有地域实例发送请求,几分钟内 Grafana 中即可查询到 trace 与 profile 数据。示例应用镜像基于 ruby:3.3.9 构建(见 Dockerfile),通过 bundle install 安装依赖后以 ruby lib/server.rb 启动。
依赖清单与版本约束
Gemfile 中固定了与链路剖析关联直接相关的依赖:
| Gem | 版本 | 作用 |
|---|---|---|
pyroscope |
= 0.6.4 | Pyroscope Ruby SDK,负责 CPU 采样与标签 |
pyroscope-otel |
最新 | 提供 OpenTelemetry SpanProcessor,将 span 与剖析采样关联 |
opentelemetry-sdk |
最新 | OpenTelemetry Ruby SDK |
opentelemetry-exporter-otlp |
最新 | 通过 OTLP 协议将 span 导出到 Tempo |
sinatra / thin / puma |
~> 4.2 / ~> 2.0 / ~> 7.2 | 应用 Web 框架与服务器 |
Ruby 侧源码解析:剖析与追踪如何打通
srv/server.rb 是理解整套机制的核心文件,它同时完成了三件事:Pyroscope 剖析配置、OpenTelemetry 链路配置、以及两者之间的 span-profile 关联。
1. 配置 Pyroscope 剖析
app_name = ENV.fetch("PYROSCOPE_APPLICATION_NAME", "rideshare.ruby.push.app")
pyroscope_server_address = ENV.fetch("PYROSCOPE_SERVER_ADDRESS", "http://pyroscope:4040")
Pyroscope.configure do |config|
config.app_name = app_name
config.server_address = pyroscope_server_address
config.tags = {
"region": ENV["REGION"],
}
end
应用名默认为 rideshare.ruby.push.app,对应 docker-compose 中 OTEL_SERVICE_NAME: rideshare.ruby.push.app,两者保持一致才能让链路与剖析数据按服务名关联起来;region 标签区分三个地域实例。
2. 通过 SpanProcessor 实现剖析与链路关联
OpenTelemetry::SDK.configure do |c|
c.add_span_processor Pyroscope::Otel::SpanProcessor.new("#{app_name}.cpu", pyroscope_server_address)
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: 'http://tempo:4318/v1/traces'
)
)
)
end
这里注册了两个 span 处理器:Pyroscope::Otel::SpanProcessor 以 "#{app_name}.cpu" 作为剖析应用名,负责为每个 span 生成对应的剖析数据标记(即 span 属性 pyroscope.profile.id);BatchSpanProcessor 则把 span 批量通过 OTLP 导出到 Tempo 的 4318 端口,与 tempo/tempo.yml 中 otlp.protocols.http.endpoint: 0.0.0.0:4318 的接收配置对应。
3. 跨服务 trace 上下文传递
before do
if (traceparent = request.env['HTTP_TRACEPARENT'])
_version, trace_id_hex, parent_span_id_hex, _flags = traceparent.split('-')
carrier = { 'traceparent' => traceparent }
@extracted_context = OpenTelemetry.propagation.extract(carrier)
end
end
Rideshare 应用通过解析请求头中的 traceparent(W3C Trace Context 格式 version-traceid-spanid-flags)恢复上游链路上下文,再在路由处理中使用 OpenTelemetry::Context.with_current(@extracted_context) 包裹 tracer.in_span(...),从而使每个 HTTP 处理 span 挂载到负载生成器发起的父 trace 之下,形成完整的分布式调用链:
get "/bike" do
OpenTelemetry::Context.with_current(@extracted_context) do
tracer.in_span("BikeHandler") do |span|
order_bike(0.4)
"<p>Bike ordered</p>"
end
end
end
/bike、/scooter、/car 三个路由分别调用 order_bike、order_scooter、order_car,产生带业务语义的 span 名称。
4. 业务函数中的动态标签
在 lib/utility/utility.rb 中可以看到 Pyroscope 标签的另一种用法——按请求维度动态打标:
def find_nearest_vehicle(n, vehicle)
Pyroscope.tag_wrapper({ "vehicle" => vehicle }) do
# ... 模拟查找耗时
end
end
Pyroscope.tag_wrapper 会在代码块执行期间临时附加 vehicle 标签,剖析数据中即可按车型维度拆分性能,这是区分不同业务路径耗时的常用手段。
在 Grafana 中查看 Traces 与 Profiles
环境启动后,打开 Grafana 的 Explore 页面(该 URL 已预填好 TraceQL 搜索:按资源属性 service.name = rideshare.ruby.push.app 过滤、时间范围 now-6h)。选中一条 trace,点击带有剖析链接图标的 span,即可从链路直接跳转到对应的 Pyroscope 火焰图。
关于 root span 标记与采样间隔
默认情况下,只有 root span 会被打上剖析标记——即本地创建的第一个 span(对应服务端收到请求后创建的 handler span)。此类 span 会显示链接图标,且其属性中带有 pyroscope.profile.id,值对应关联剖析数据的 span ID。
需要特别注意的是:span 上存在 pyroscope.profile.id 属性并不一定意味着该 span 真的采集到了剖析数据。当 span 实际占用的 CPU 时间小于采样间隔时,可能没有收集到任何栈样本。本示例中采样间隔为 10ms,因此耗时极短的 span 即使有标记也可能没有对应火焰图,这是按固定时间间隔采样的持续剖析技术的固有特性,而非配置错误。
Grafana Tempo 数据源:Trace-to-Profiles 关联配置
要让 Grafana 能把 trace span 与剖析数据关联起来,Tempo 数据源必须配置两部分内容:剖析数据源(指向 Pyroscope)以及剖析查询使用的标签映射。本示例通过 grafana-provisioning/datasources/tempo.yml 自动完成预置:
apiVersion: 1
datasources:
- name: Tempo
type: tempo
uid: tempo
url: http://tempo:3200
jsonData:
tracesToProfiles:
customQuery: false
datasourceUid: "pyroscope"
profileTypeId: "process_cpu:cpu:nanoseconds:cpu:nanoseconds"
tags:
- key: "service.name"
value: "service_name"
关键字段说明:
| 字段 | 值 | 作用 |
|---|---|---|
tracesToProfiles.datasourceUid |
pyroscope |
关联的剖析数据源 UID,与 pyroscope.yml 中的 uid: pyroscope 对应 |
tracesToProfiles.profileTypeId |
process_cpu:cpu:nanoseconds:cpu:nanoseconds |
跳转时默认使用的剖析类型(CPU 采样) |
tracesToProfiles.tags |
service.name → service_name |
span 属性到 Pyroscope 标签的映射 |
标签映射是可选项,但强烈建议配置,因为它直接影响查询性能。本示例把 span 属性 service.name 映射为 Pyroscope 查询中的 service_name 标签,从而将剖析数据查找范围限定在对应服务内,保证跳转查询始终快速稳定。同时要注意:配置的标签必须真实存在于 span 属性或资源属性中,否则 Trace-to-Profiles 的 span 链接不会出现——这正是示例应用在 docker-compose.yml 中设置 OTEL_SERVICE_NAME: rideshare.ruby.push.app、并在 server.rb 中将 app_name 保持一致的原因。
演示场景:用人为缺陷验证剖析价值
示例代码还内置了一个"故障演练"逻辑,用于直观展示性能问题如何在火焰图中暴露。在 lib/utility/utility.rb 的 check_driver_availability 中:
# 每 4 分钟,eu-north 区域会被人为制造慢请求,仅用于演示
current_time = Time.now
current_minute = current_time.strftime('%M').to_i
force_mutex_lock = (current_minute * 4 % 8) == 0
mutex_lock(n) if ENV["REGION"] == "eu-north" and force_mutex_lock
eu-north 实例每隔约 4 分钟会周期性触发 mutex_lock(一段纯 CPU 忙等循环),人为放大该区域部分请求的 CPU 耗时。配合 Explore 页面中按 service.name 过滤、对比不同地域实例的火焰图,可以清晰看到 eu-north 区域在特定时间窗口内 mutex_lock 栈帧显著变宽——这正是"从链路跳转到火焰图、定位单行代码性能问题"的完整工作流。
小结
本示例完整覆盖了 Ruby 服务实现链路追踪与持续剖析关联的四个关键环节:SDK 依赖与版本选择(pyroscope + pyroscope-otel + OTel SDK)、server.rb 中的 SpanProcessor 双处理器配置与 traceparent 上下文恢复、Tempo 数据源的 tracesToProfiles 标签映射,以及通过 Pyroscope.tag_wrapper 实现的请求级动态标签。按此结构复制到生产环境时,只需替换应用名、服务名与标签键值,即可复用同一套"trace 直达火焰图"的可观测性链路。