深入探索RUM与全链路追踪:优化数字体验的利器
背景介绍
随着可观测技术的持续演进,多数企业已广泛采用APM、Tracing及Logging解决方案,以此强化业务监控能力,尤其在互联网行业,产品的体验直接关系着用户的口碑,决定了市场命运,使得RUM(真实用户监控)日益受到重视。然而,在面对由后端服务故障引起的体验问题时(例如,后端接口延迟引发的APP白屏或页面加载缓慢),如何有效的关联RUM、APM监控数据以及Tracing上下文,辅助问题排查以及影响面评估,成为一大挑战。解决这一问题的关键在于如何实现从用户端到服务端的全链路打通,而RUM作为贴近用户的监测起点,天然适合担当此角色。本文旨在探讨端到端链路打通的解决方案,并分享RUM与端到端链路集成的最佳实践。
端到端链路打通的难点
技术架构复杂,多端、跨语言、跨团队场景多
一个典型的互联网应用,通常会包含用户终端(Web & 小程序/Android/iOS)、网关代理层(ALB/MSE/Ingress/Nginx)、后端服务(Java/Go/Python)以及中间件(数据库、消息、缓存)等部分,涵盖了前、后端开发以及中间件、运维团队,实现全链路打通,往往会面临以下问题:
- 不同的链路追踪工具,支持的主流语言、框架不一致,对跨端场景不友好;
支持的语言 | |
---|---|
OpenTelemetry | Java, Python, Go, JavaScript, .NET, Ruby, PHP, Erlang, Swift, Rust, C++ 等 |
SkyWalking | Java, .NET, Node.js, PHP, Python, Go, Ruby, Lua, OAP 等 |
Zipkin | Java, Node.js, Ruby, Go, Scala, Python 等 |
Jaeger | Java, Python, Go, C++, C#, Node.js 等 |
- 生产环境实施,需要前后端开发人员、中间件以及运维同学通力协作,接入成本较高;
- 链路打通之后,如何与RUM、APM等监控数据、以及日志打通,方便问题排查与定界;
不同协议无法兼容,生产环境难以平滑切换
针对端到端链路打通场景,目前,主流的链路追踪项目,比如:OpenTelemetry、Zipkin、Jaeger、Skywalking等,都有定义各自的链路传播协议:
- OpenTelemetry:w3c透传协议
- Skywalking:sw8(v3)透传协议
- ZipKin:b3/b3multi透传协议
- Jaeger:jaeger透传协议
但是,不同协议间存在兼容性问题,比如:OpenTelemetry和Skywalking就无法相互兼容,而且不同厂商和开源项目对各透传协议的支持力度也不一致:
W3C | b3/b3multi | Jaeger | OpenTracing | sw8 | |
---|---|---|---|---|---|
Opentelemetry | ✓ | ✓ | ✓ | ✓ | ✗ |
Skywalking | ✗ | ✗ | ✗ | ✗ | ✓ |
Zipkin | ✗ | ✓ | ✗ | ✗ | ✗ |
Jaeger | ✓ | ✓ | ✓ | ✗ | ✗ |
因此,通常情况下,想要串联起完整的调用链路,就要求后端系统必须采用相同或者兼容的Trace协议,前端应用也需要引入对应的SDK,并且,中间链路各个环节,比如:网关代理层,也必须保证协议Header的透传。
基于OTel与W3C的端到端链路解决方案
关注可观测领域的同学应该知道,近些年行业发展的一个显著趋势,是不断向标准化和开源生态方向整合,上文提到的OpenTelemetry项目和W3C Trace Context标准,都是这一趋势的代表项目,以下通过链路透传场景、链路透传协议以及跨协议兼容几个方面介绍基于OTel和W3C Trace Context的端到到链路解决方案。
链路透传场景
OpenTelemetry 使用一种称为“传播器”(Propagators)的机制来实现在不同环境和协议中 Trace 上下文的透传,确保在一个分布式系统中能够追踪完整的请求链路。无论是进程内还是进程间的通信,其核心都是通过特定的格式在请求头中携带必要的追踪信息。下面是 OpenTelemetry 如何在不同场景下实现 Trace 上下文透传的方案介绍:
进程内透传
- 单线程场景:在单线程环境下,由于所有操作都在同一个线程上执行,因此可以直接通过局部变量(比如在Java语言中,通常会采用ThreadLocal)来存储当前Span信息,当新的操作开始时,可以将当前Scope的Span作为Parent Span,从而传递了Trace上下文;
- 多线程/异步场景:在多线程异步编程场景,则需要在任务提交或异步调用时显式的携带Span上下文,比如:OpenTelemetry就提供了API(如:context.with(currentSpan))来创建一个带有特定Span的新Context,并在此Context的作用域内执行代码,这样,即使是异步执行,也能确保Trace上下文可以被正确的传递和应用;
进程间透传
- HTTP场景:通常是将Trace上下文编码到HTTP请求头中,比如:上文提到的W3C Trace Context标准,就采用了
traceparent
、tracestate
两个header来传递Trace上下文信息,客户端在发起请求时,会自动将当前的Trace信息添加到HTTP头中;服务端接收到请求后,通过相应的传播器解析这些头部,恢复或延续Trace上下文。 - RPC和其他自定义协议场景:对于非HTTP协议,如gRPC、MQTT等,原理类似,也是通过协议允许的头部或元数据字段来携带Trace上下文信息。OpenTelemetry提供了多种传播器(如JaegerPropagator、B3Propagator、W3CBaggagePropagator等),可以根据具体协议的要求选择合适的传播器来序列化和反序列化Trace上下文。
- 消息队列场景:在消息队列场景中,通常将Trace ID、Span ID等信息作为消息的属性或元数据随消息一起发送,接收方可以从消息中提取这些信息并恢复上下文。
- 数据库场景:目前主流的数据库,比如:MySQL、PG等,底层协议层面尚未提供相应扩展机制,因此绝大数链路追踪工具,包括:OpenTelemetry,均采用了客户端插桩的方式,仅在应用侧记录耗时、以及执行SQL等关键信息。
链路透传协议
这里重点介绍下W3C Trace Contxt,也是目前国内外使用最多的一个协议标准,W3C Trace Context是W3C组织所推出的一个规范,旨在规范分布式追踪中跟踪信息的传播格式,除了HTTP场景以外,也支持二进制、以及消息等场景(目前还处于Draft状态),详见W3C官网
W3C Trace Context(HTTP Protocol)
Trace Context规范主要定义了两个HTTP头部字段:traceparent
和tracestate
traceparent
:采用扩展的巴科斯范式 (ABNF) 定义,由四个部分组成:
- version:2位十六进制数字,表示当前traceparent头部字段的版本,如:00;
- trace-id:32位十六进制数字,用于表示整个Trace链路的唯一ID,如:ec95e5a118ce450eac82ab9ec530b287;
- parent-id:16位十六进制数字,用于表示当前请求或操作的唯一ID,如:a7be58f9cd8dd80d;
- trace-flags:2位十六进制数字,用于控制追踪标志,包含采样、追踪级别等,如:01;
tracestate
:是对traceparent
字段的扩展,用于携带额外的、服务间可能需要的追踪状态信息,并且是 traceparent 字段的伴随标头。
链路传播器
Propagator | 协议标准 |
---|---|
tracecontext | W3C Trace Context |
baggage | W3C Baggage |
b3 | B3 |
b3multi | B3Multi |
jaeger | Jaeger |
opentracing | OpenTracing |
xray | AWS X-Ray |
OpenTelemetry项目几乎已经支持了除sw8以外大多数透传协议,并且还内置了一些国内外云厂商的协议传播器,同时Opentelemetry也支持自定义Propagator,我们可以组合不同的Propagator,也可以基于Opentelemetry的TextMapPropagator
实现一个自己的Propagator。
RUM集成端到端链路的最佳实践
为什么RUM适合作为链路入口
前面提到,RUM作为用户请求的入口,在解决链路打通问题上,天生就具备优势。一个比较直观的解法,就是直接在RUM端侧生成链路追踪的TraceID,然后通过透传协议,以HTTP Header的形式将Trace上下文传递给后端,后端应用就可以基于协议Header,来初始化Trace上下文,并在后端系统调用中进行传递。
透传格式名称 | 格式 | 备注 |
---|---|---|
tracecontext | traceparent : {version}-{trace-id}-{parent-id}-{trace-flags} tracestate: rum={version}&{appType}&{pid}&{sessionId} | 相关文档 |
b3 | b3: {TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} | 相关文档 |
b3multi | X-B3-TraceId: {TraceId} X-B3-SpanId: {SpanId} X-B3-ParentSpanId: {ParentSpanId} X-B3-Sampled: {SamplingState} | 相关文档 |
jaeger | uber-trace-id : {trace-id}:{span-id}:{parent-span-id}:{flags} | 相关文档 |
sw8 | sw8: {sample}-{trace-id}-{segment-id}-{0}-{service}-{instance}-{endpoint}-{peer} | 相关文档 |
相比直接在端侧集成开源协议SDK,RUM集成链路追踪还具有以下优势:
- 优势一:可以将用户体检监控中的错误、缓慢、以及用户会话数据,与链路追踪数据联动,实现端到端分析,比如:某个用户请求,在端侧看可能很慢,但是后端链路显示耗时并不长,此时,结合RUM与后端调用链数据,最终发现是DNS、网络层耗时较长;
- 优势二:无需在端侧集成开源协议SDK,也无需关心端侧链路数据上报的问题,尤其对于一些存在多个后端服务域名,并且协议还不相同的应用,可以在RUM产品中为不同域名设置不同的透传协议,一次接入即可实现一站式监控体验,极大降低了接入成本;
RUM与Trace数据模型的融合
目前主流的RUM开源项目以及国内外云厂商,数据模型上基本都是以用户、会话作为核心,以Event的方式记录前端用户的页面加载、资源请求(包含API与静态资源),同时也会包含请求错误、JS错误、崩溃、卡顿、自定义错误等异常数据,通过API请求,我们可以将RUM数据与后端调用链数据进行关联,从而获得从端侧用户到后端服务的完整链路,而RUM Event数据模型和Trace Span数据模型本身其实也是可以相互转换的
Rum Resource Fields | Otel Span Fields |
---|---|
rum.resource.trace_id | traceId |
rum.resource.trace.carrier(W3C traceparent ) | spanId |
rum.resource.name | spanName |
rum.resource.timestamp | startTime |
rum.resource.duration | duration |
rum.resource.net.ip | ip |
rum.resource.status_code | spanStatus |
rum.resource.trace.carrier(W3C tracepstate ) | tracestate |
rum.resource.ip、rum.sessionid | resource |
rum.user.id、rum.session.id、rum.view.name | attributes |
RUM与端到端链路集成的两种方案
方案一:RUM转Span,构建完整Trace链路
RUM转Trace的方案,通常是在端侧应用中接入RUM探针,通过RUM进行协议透传,同时记录Trace上下文信息,并在RUM数据接收侧,将RUM Event数据转换为标准的Trace Span数据,并将RUM相关信息(如:user、session、view等)注入到Span Attributes中,这么做的好处是:我们可以在RUM与Trace中实现互联互通,从而在线上问题排查中,可以方便的进行根因定位,并直观的评估对用户侧产生的影响。
方案二:Span转RUM,基于OTel的扩展机制构建
Span转RUM的方案,则是在端侧应用中接入OTel SDK,然后通过OTel提供的扩展机制,在OTel Collector中实现一个自定义的rum exporter
,将OTel SDK上报的Span数据转换为RUM Event数据,当然,你也可以在端侧同时引入RUM与OTel的SDK,然后通过OTel SDK中提供SpanProcessor
进行扩展,像开源RUM项目Sentry就采用的是这种方案。
但是这个方案对于RUM的数据模型有一定要求,最好的方式就是OTel能够支持RUM数据模型,目前OTel社区也有相关的小组,正在往这个方向努力,具体可以参考Github上这个Issue:https://github.com/open-telemetry/oteps/issues/169
RUM集成端到端链路的实际应用
全链路洞察
RUM与Trace链路打通后,一个最直观的应用场景就是全链路洞察,可以实现故障根因的快速定界,无需跳转产品和页面,这一点对于一些角色职责分离的大型团队比较有价值。
影响面分析
另外一个比较重要的应用场景,就是当后端系统出现问题时,可以记录故障期间用户侧的所有操作,同时结合调用链可以方便的定位出哪些请求受到了后端故障影响,从而精准地定位出故障的影响面,包含受影响的客户列表、终端设备、运营商、地域等信息。在某些情况下,还可以帮助我们判断线上问题处理优先级。
总结展望
本文主要介绍了基于OpenTeletemetry与W3C协议构建端到端全链路的解决方案,同时探讨了RUM与端到端链路集成的最佳实践,希望可以为大家在生产环境落地应用提供一些参考。实际上,除了上面介绍到的全链路洞察根因定位,以及影响面分析外,RUM与全链路追踪的应用场景还有很多,比如:对于一些生产环境难以复现的问题,可以结合RUM的会话重放功能,进行问题复现等,对于解决线上疑难问题,优化用户体验,绝对是一大利器。
参考文章
- https://opentelemetry.io/docs/
- https://www.w3.org/TR/trace-context/
- https://w3c.github.io/trace-context-protocols-registry/
- https://docs.google.com/document/d/16Vsdh-DM72AfMg_FIt9yT9ExEWF4A_vRbQ3jRNBe09w/edit?pli=1
- https://develop.sentry.dev/sdk/telemetry/traces/opentelemetry/#step-1-implement-the-sentryspanprocessor-on-your-sdk