链路跟踪归根到底只是一种理念和策略,简单的说就是在2次关联调用之间传递特定透传信息的能力。从组件设计的角度说其实关心的是是下面的几个特性:
典型的例子就是Java系的方案,总的来说java是一种编译语言,但是得意于虚拟机和字节码的实现方式,Java实际上是具有动态语言的特性的。
这类实现的基本思路就是在利用java-agent拦截具体类加载过程,在特定的类加载过程加入自定义的代码来实现trace的能力。
这类方案的主要缺点是的只能用于java,但是只要是java技术栈的实现就几乎可以无任何限制的接入,对java技术栈的公司来说是非常有效。接入成本也非常低,只要在启动命令中指定参数就可以了,无论是部署脚本还是构建镜像都很方便。剩下另一个一个缺点就是改字节码本身还是有一定风险的。不过总体来说稳定性还是有保障的。 skywalking
并不是所有公司内部都是java的,其他语言并没有改字节码这种骚操作,或者认为这种方式太过粗暴该怎么办呢?
这种情况下基本的思路就是抽象出协议层面的概念,让各个组件的实现内部支持链路跟踪的实现,trace日志组件的信息汇集也由组件完成。如果有业务方有特殊需要接入链路跟踪系统也需要可以依照相同的约定与trace进行交互。此外还考虑需要和各类开源的组件相适配。
这种情况下,协议层面的设计就显得很重要,是需要各方都认同和理解的方案,协议本身的完备性就是非常重要的。就目前来说最为著名的就是opentracing的规范,基本上可以视为链路跟踪领域的事实上的标准。
从个人来看我这种方式是更优的策略,而且在大公司的内部,推行标准化的编程规范也是必要的,但是这也是双刃剑,trace的实现依赖于标准化的程度,因为链路这种东西只要中间断过一次就无法达到链路跟踪的效果了。
另一个问题是即使标准化也是有限度的,比如跨线程的信息传递绝大多少公司内部的标准化就很难做。这样做出来的功能其实还是不如字节码增强来的简单有效。 jaeger 、 CAT 、 SOFATracer 、 zipkin 、 dapper
当然随着近几年容器化和service mesh的推进,基于servicemesh的方案也是可以做链路跟踪的。通过sidecare劫持流量,可以构建出不依赖具体语言或者rpc的链路跟踪系统,从模型上看确实是更为理想的模型,不过如何让运行时的程序内部也感知到链路跟踪也是一个问题,同时mesh的各种方案截止现阶段其实还是处在探索和实践阶段并没有完美的解决方案。 jaeger
现有的链路跟踪的模型大多参考了dapper的实现,opentracing的规范也对模型设计有很大的影响。opentracing的语义。下面大部分内容都摘取自这两部分内容
简单的说一次外部调用可以被多个内部请求组合完成,这一过程可以被描述成一个树的形式,而每次调用被定义为一个span。整个调用可以被称为一个trace,可以用一个唯一id标识。
span是构成调用树的最小单元。通常来说包括下面几个部分,通常也会被一个唯一id标识:
Causal relationships between Spans in a single Trace [Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C is a `ChildOf` Span A) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ (Span G `FollowsFrom` Span F) 复制代码
在如上的链路跟踪调用树比较直观了解。只有2个地方需要建的解释下:
一个是span中的引用,其实我觉得这应用更理解不同,在实现上大部分不太可能找到所有的子span的引用,或者没有必要。大部分情况下其实使用parent-span-id的概念来构造。一个引用的类型,
其实这里我其实更想讨论的是如何界定span的范围,但以我个人的来看并不太赞成将异步场景都串联起来。主要基于2点考虑
一是trace很多时候用于性能分析或者依赖分析之类的场景,在这2种比较典型的场景下其实将异步场景串联起来并没有太大的意义,反而不利于后续的数据分析工作
另一原因是即使不使用spanid本身的结构串联也并不意味着丢失了关联信息,因为trace本身是有信息透传能力的,我们完全可以构造一个类似logid的概念或者业务上有意义的数据,进行透传即使链路本身不再同一个trace下,但是信息依然是可以透传的。
透传的实质上就是在两个上下文之间完成spancontext的构造。总的来说需要做2件事情,一件事情用尽可能无侵入的方式传递spanContext用于重建span新型。另一件就是在当前的context构造的spanwapper中构造一个新的span并推入栈顶,当然也可以根据情况来选择是否构造新的span。下图展示了一个跨线程传递的例子。
在透传的场景下其实参数是可选的,大部分场景下,trace只针对跨runtime的请求处理,内部跨线程不会创建信息的span,这种情况下不会构造新的span,如果是rpc调用则会生成新的span。
另一个需要讨论的是SpanContext:SpanContext其实在不同场景下都有不同的概念。这里更多的指的是用于下游恢复链路的构造部分和需要透传的参数。 根据上述内容,很容易理解一个span其实是需要对端构造的2条日志才能完整的构造。这2条日志会被日志被各自实例收集起来各自上传,仅仅有少量的参数会随rpc之类的介质透传给下游用于恢复链路或者参数透传,总的来说参数透传成3个部分:
这里面描述的是通用的分布式链路追踪的模块设计类型,不同的系统可能在不同的地方有所取舍。但总体来说遵循
链路跟踪有很多实现形式,从我个人的理解来看需要做2层抽象,一层是增强点层面的。 具体实现上代码模块还是分了很多有意思的部分代码模块拆分成了多个部分
代码层面其实要做2层抽象,
logid是不是traceid?两种有什么区别? 严格来说log-id不是trace-id,但是也可以是。trace本质上是提供透传信息的能力,logid常用语串联日志信息,所以大部分场景下logid都是trace的透传能力在系统间透传的,在系统内部往往是threadlocal或者context的概念保存。 初次之外,之前我们还讨论过一种有意思的问题就是异步场景下的串联。根据之前的内容我们其实讨论过,trace本身由于起止时间的限制虽然可以用于异步场景,但是这样会给信息分析带来很多麻烦,在实际中我其实更倾向于将traceid定义在同步调用的scope内,在异步场景下,比如异步rpc,或者消息队列场景下,重新构造logId。
这里还有一个问题没有解释就是如何实现字节码增强的,基本原理是用的java-agent。java虽然是编译性的语言但是由于jvm和classloader的存在,java具有一定的动态特性。java的实际运行逻辑实际上是取决于jvm中的字节码,比如大多数javaer怀念的事务管理的注解,本质上上就给某些方法或者成员变量打上标记,运行过程中生成一个代理类同时在原有方法的基础上添加一些事务管理的模板。不论是生成新的代理类或者改变原有的类的字节码,从而实现动态代理。 我们回到trace的使用场景下,其实我们也是希望对字节码实现增强,理论上说也是可以基于自定义的类加载去制作动态代理实现的,但是一个主要的问题是没有办法控制所有的类加载器,其实trace希望的是在某个方法上实现wapper而并不关心具体的类加载是哪个。java本身提供了一种java-agent机制可以实现拦截所有的类加载过程,或者在运行过程中重载某个类的后门,显然更适合我们的场景。使用中只要是实现了对应接口,并打包成jar就可以。java-agent提供了2中方式一种是作为启动参数与jvm-runtime同时启动。或者在jvm实例启动之后,作为队列进程启动并attchment到jvm进程上。 这里不详细讨论代码实现的方式,因为网上例子很多,这里想说的其实是一套常用的java-agent使用的设计方式,这样对理解其他开源设计也有很多帮助。
一个典型的java-agent相关的模块很多情况下包括上面几个部分,
java-agent应用其实非常广,这里可以举几个列子;
首先介绍下环境隔离的概念:大部分的开发模式都是基于giflow的。如果只有一套环境的话就会有多个代码合并的蛋疼问题,如果建多套环境的话成本有很高(比如独立的数据库,Nginx、注册中心、redis,以及大量的依赖服务),那有没有一种方式既可以创建创建多套环境又避免蛋疼的产品问题呢?
环境染色的方案就是trace在各个中间件请求下解析出入口环境信息,并且将其作为参数透传,各个中间件组件配合该信息将trace路由到对应集群上的方案:举一个简单的例子如下所示:
基本的思路就是这样,所有集群都有一套基准环境,通常部署master分支,而本次分支涉及到的变革部署一套feature集群。。所有的公共组件都用同一套,比如Nginx、注册中心、数据库、kafka集群等等。但是应用用到的资源会略有区别,比如说注册中心上带有集群的环境信息,rds可以建一个带有环境名后缀的影子表。kafka建有对应环境名后缀的topic。 用户请求的时候使用相同的域名但是待上具有环境名的header,app在接受请求的时候会解析header并将入口环境信息放入baaage中,该信息会随链路下传。
对于rpc,客户端做路由的时候会根据环境信息优先选取特定子环境的集群,如果没有则调用基准环境,基准环境中的应用在调用的时候也可以根据相同的规则优先调用子环境。即使调用穿过了中间件比如队列,则传递的消息也负有环境信息,trace也可以根据信息解析出路由规则并进行透传。不过为了避免消息被基准环境的app消费还是需要建特定子环境的topic。