在企业级业务系统日趋复杂的背景下,微服务架构逐渐成为了许多中大型企业的标配,它将庞大的单体应用拆分成多个子系统和公共的组件单元。这一理念带来了许多好处:复杂系统的拆分简化与隔离、公共模块的重用性提升与更合理的资源分配、大大提升了系统变更迭代的速度、更灵活的可扩展性以及在云计算中的适用性,等等。
但是微服务架构也带来了新的问题:拆分后每个用户请求可能需要数十个子系统的相互调用才能最终返回结果,如果某个请求出错可能需要逐个子系统排查定位问题;或者某个请求耗时比较高,但是很难知道时间耗在了哪个子系统中。全链路追踪系统就是为了解决微服务场景下的这些问题而诞生的。一般地,该系统由几大部分组成:
有赞目前的应用类型有很多种,已经支持追踪的语言有 Java、Node.js、PHP。那么,如何让跨不同语言的调用链路串到一起呢?有赞的链路追踪目前在使用的是Cat协议,业界也已经有比较成熟的开源协议:OpenTracing,OpenTracing是一个“供应商中立”的开源协议,使用它提供的各语言的API和标准数据模型,开发人员可以方便的进行链路追踪,并能轻松打通不同语言的链路。
Cat协议与OpenTracing协议在追踪思路和API上是大同小异的。基本思路是Trace和Span:Trace标识链路信息、Span标识链路中具体节点信息。一般在链路的入口应用中生成traceId和spanId,在后续的各节点调用中,traceId保持不变并全链路透传,各节点只产生自己的新的spanId。这样,通过traceId唯一标识一条链路,spanId标识链路中的具体节点的方式串起整个链路。一个简单的示意图如下:
实际每个节点上报的数据中还包含一些其他信息,比如:时间戳、服务标识、父子节点的id等等。
Java Agent一般通过在应用启动参数中添加 -javaagent
参数添加 ClassFileTransformer
字节码转换器。JVM在类加载时触发 JVMTI_EVENT_CLASS_FILE_LOAD_HOOK
事件调用添加的字节码转换器完成字节码转换,该过程时序如下:
Java Agent所使用的Instrumentation依赖JVMTI实现,当然也可以绕过Instrumentation直接使用JVMTI实现Agent。JVMTI 与 JDI 组成了 Java 平台调试体系(JPDA)的主要能力。
有赞的应用启动脚本都是由业务方各自维护,因此要在成百上千的应用启动脚本中添加 -javaagent
参数是个不小的工作量。Java 从 Java 6 开始提供了 JVMAttachAPI
,可以在运行时动态 attach 到某个JVM进程上。AttacheAPI
需要的进程pid参数,如果获取当前进程的pid是一个难点。不同的JVM版本有不同的方式,比较方便的是,字节码增强框架Byte-Buddy已经封装好了上述过程:JMX
或 java.lang.ProcessHandle
接口获取当前进程的pid,只需要使用 ByteBuddyAgent.install()
就能attach到当前进程。
Byte-Buddy是一个优秀的字节码增强框架,基于ASM实现,提供了比较高级的subClass()、redefine()、rebase()接口,并抽象了强大丰富的Class和Method匹配API。
有赞内部的框架和中间件组件已经进行统一托管,由专门的Jar包容器负责托管加载工作,几乎所有Java应用都接入了该Jar包容器,Jar包容器在应用的类加载之前启动。借助Jar包容器提供的入口,链路追踪的SDK在应用启动之前完成字节码转换器的装载工作,同时SDK也托管在该Jar包容器中,进而在实现应用无感知的追踪同时,又实现了全链路追踪SDK的透明升级。整个带起过程如下图所示:
应用无感知的自动化追踪与透明升级方式,大大提升了后续迭代的速度,应用接入成本降到0、追踪应用的比例接近100%,为后续发展带来了更多可能。与非透明方式的追踪对比如下:
优缺点 | 非透明方式 | Java Agent方式 |
---|---|---|
埋点方式 | dubbo Filter + spring 拦截器 + AOP + Javassist…… | 统一的接口和增强模型 |
接入成本 | 强依赖(Maven、Gradle)+ 手动配置 | 透明接入 |
升级方式 | 逐个应用升级 | 透明升级 |
请求在一个进程内部可能会有多个子调用,Trace信息在进程内部共享一般是通过 ThreadLocal
实现的,但是当有异步调用情形时,这部分调用可能就会在链路中丢失。我们需要做的是跨线程透传Trace信息,虽然 JDK 内置的 InheritableThreadLocal
支持父子线程传递,但是当面对线程池中线程复用的场景时还是不能满足需求。
比较常见的解决方案是 Capture/Replay
模型:在创建异步线程时将当前线程上下文进行 Capture
快照,并传递到子线程中,在子线程运行时先通过 Replay
回放设置传递过来快照信息到当前上下文。具体流程如图:
因为内部有通用的线程工具类 FrameworkRunnable
和 FrameworkCallable
,因此只需要对这两个工具类进行统一增强就可以满足大部分异步调用的追踪场景。另外提供了异步处理工具 AsyncUtil
,在使用了自定义线程时,使用 AsyncUtil.wrap()
对自定义线程进行包装即可实现 Capture/Replay
的过程。
ClassFileTransformer
中进行Classloader过滤;统一接入系统是有赞外部流量的接入代理系统。
因为链路有采样率设置,有时在测试或排查线上问题时不方便。为了支持链路100%采样,我们支持在前端页面请求的HTTP Header中设置类似 -XXDebug
参数,统一接入系统判断在HTTP请求头中包含特定的 -XXDebug
参数时,生成符合链路采样特征规则的traceId从而实现测试请求100%采样。
为了使业务系统在上报日志给天网日志系统时自动记录traceId,链路追踪SDK在开启追踪后会将traceId put
到 MDC
中,天网日志SDK在记录日志时会获取MDC中的traceId,并作为日志的描述数据一起上报,并进行索引。在日志查询时,支持按traceId查询。
同时天网日志系统对每次查询的结果数据页的日志traceId进行批量计算,判断哪些日志记录对应的请求在链路追踪系统中被采样,对采样过的日志记录的traceId替换成超链接,支持一键跳转到对应的链路详情页。
链路数据处理使用Spark Streaming任务准实时计算,处理过的数据进行ES索引,并存储在Hbase中。数据上报过程使用常见的本地数据上报agent + remote collector的方式,然后汇到kafka队列供实时任务处理,架构视图如下所示:
全链路追踪系统包含几大部分:链路采集SDK、数据处理服务、用户产品。SDK部分比较偏技术。数据处理更考验数据处理的吞吐能力和存储的容量,采用更高级的链路采样解决方案可以有效降低这部分的成本。用户产品主要考验的是设计者对用户需求的把握,全链路追踪可以做很多事情,产品上可以堆叠出很多功能,怎样能让用户快速上手,简洁而又易用是链路追踪产品设计的一大挑战。未来一段时间,有赞全链路追踪会围绕以下几个方面继续演进:
领取专属 10元无门槛券
私享最新 技术干货