# 16 | 如何搭建一套适合你的服务追踪系统? [专栏第8期](http://time.geekbang.org/column/article/15273)我给你讲了服务追踪系统的原理以及实现,简单回顾一下服务追踪系统的实现,主要包括三个部分。 * 埋点数据收集,负责在服务端进行埋点,来收集服务调用的上下文数据。 * 实时数据处理,负责对收集到的链路信息,按照traceId和spanId进行串联和存储。 * 数据链路展示,把处理后的服务调用数据,按照调用链的形式展示出来。 如果要自己从0开始实现一个服务追踪系统,针对以上三个部分你都必须有相应的解决方案。首先你需要在业务代码的框架层开发调用拦截程序,在调用的前后收集相关信息,把信息传输给到一个统一的处理中心。然后处理中心需要实时处理收集到链路信息,并按照traceId和spanId进行串联,处理完以后再存到合适的存储中。最后还要能把存储中存储的信息,以调用链路图或者调用拓扑图的形式对外展示。 可以想象这个技术难度以及开发工作量都不小,对于大部分中小业务团队来说,都十分具有挑战。不过幸运的是,业界已经有不少开源的服务追踪系统实现,并且应用范围也已经十分广泛,对大部分的中小业务团队来说,足以满足对服务追踪系统的需求。 业界比较有名的服务追踪系统实现有阿里的鹰眼、Twitter开源的OpenZipkin,还有Naver开源的Pinpoint,它们都是受Google发布的Dapper论文启发而实现的。其中阿里的鹰眼解决方案没有开源,而且由于阿里需要处理数据量比较大,所以鹰眼的定位相对定制化,不一定适合中小规模的业务团队,感兴趣的同学可以点击本期文章末尾“拓展阅读”进行学习。 下面我主要来介绍下开源实现方案OpenZipkin和Pinpoint,再看看它们有什么区别。 ## OpenZipkin OpenZipkin是Twitter开源的服务追踪系统,下面这张图展示了它的架构设计。 ![](https://static001.geekbang.org/resource/image/69/33/699916c60cd31a2b8d7ab0335038cf33.png) (图片来源:[https://zipkin.io/public/img/architecture-1.png](https://zipkin.io/public/img/architecture-1.png)) 从图中看,OpenZipkin主要由四个核心部分组成。 * Collector:负责收集探针Reporter埋点采集的数据,经过验证处理并建立索引。 * Storage:存储服务调用的链路数据,默认使用的是Cassandra,是因为Twitter内部大量使用了Cassandra,你也可以替换成Elasticsearch或者MySQL。 * API:将格式化和建立索引的链路数据以API的方式对外提供服务,比如被UI调用。 * UI:以图形化的方式展示服务调用的链路数据。 它的工作原理可以用下面这张图来描述。 ![](https://static001.geekbang.org/resource/image/4c/d9/4c036659e0d14176215686f1a1129ed9.png) (图片来源:[https://zipkin.io/pages/architecture.html](https://zipkin.io/pages/architecture.html)) 具体流程是,通过在业务的HTTP Client前后引入服务追踪代码,这样在HTTP方法“/foo”调用前,生成trace信息:TraceId:aa、SpanId:6b、annotation:GET /foo,以及当前时刻的timestamp:1483945573944000,然后调用结果返回后,记录下耗时duration,之后再把这些trace信息和duration异步上传给Zipkin Collector。 ## Pinpoint Pinpoint是Naver开源的一款深度支持Java语言的服务追踪系统,下面这张图是它的架构设计。 ![](https://static001.geekbang.org/resource/image/d8/a4/d8b526a56b633c34364924a2d00905a4.png) (图片来源:[http://naver.github.io/pinpoint/1.7.3/images/pinpoint-architecture.png](http://naver.github.io/pinpoint/1.7.3/images/pinpoint-architecture.png)) Pinpoint主要也由四个部分组成。 * Pinpoint Agent:通过Java字节码注入的方式,来收集JVM中的调用数据,通过UDP协议传递给Collector,数据采用Thrift协议进行编码。 * Pinpoint Collector:收集Agent传过来的数据,然后写到HBase Storgage。 * HBase Storage:采用HBase集群存储服务调用的链路信息。 * Pinpoint Web UI:通过Web UI展示服务调用的详细链路信息。 它的工作原理你可以看这张图。 ![](https://static001.geekbang.org/resource/image/87/95/8730864e70d666267e40e1cc4d622195.png) (图片来源:[http://naver.github.io/pinpoint/1.7.3/images/td\_figure6.png](http://naver.github.io/pinpoint/1.7.3/images/td_figure6.png)) 具体来看,就是请求进入TomcatA,然后生成TraceId:TomcatA^ TIME ^ 1、SpanId:10、pSpanId:-1(代表是根请求),接着TomatA调用TomcatB的hello方法,TomcatB生成TraceId:TomcatA^ TIME ^1、新的SpanId:20、pSpanId:10(代表是TomcatA的请求),返回调用结果后将trace信息发给Collector,TomcatA收到调用结果后,将trace信息也发给Collector。Collector把trace信息写入到HBase中,Rowkey就是traceId,SpanId和pSpanId都是列。然后就可以通过UI查询调用链路信息了。 ## 选型对比 根据我的经验,考察服务追踪系统主要从下面这几个方面。 **1\. 埋点探针支持平台的广泛性** OpenZipkin和Pinpoint都支持哪些语言平台呢? OpenZipkin提供了不同语言的Library,不同语言实现时需要引入不同版本的Library。 官方提供了C#、Go、Java、JavaScript、Ruby、Scala、PHP等主流语言版本的Library,而且开源社区还提供了更丰富的不同语言版本的Library,详细的可以点击[这里](https://zipkin.io/pages/existing_instrumentations)查看;而Pinpoint目前只支持Java语言。 所以从探针支持的语言平台广泛性上来看,OpenZipkin比Pinpoint的使用范围要广,而且开源社区很活跃,生命力更强。 **2\. 系统集成难易程度** 再来看下系统集成的难易程度。 以OpenZipkin的Java探针Brave为例,它只提供了基本的操作API,如果系统要想集成Brave,必须在配置里手动里添加相应的配置文件并且增加trace业务代码。具体来讲,就是你需要先修改工程的POM依赖,以引入Brave相关的JAR包。 ``` io.zipkin.brave brave-bom ${brave.version} pom import ``` 然后假如你想收集每一次HTTP调用的信息,你就可以使用Brave在Apache Httpclient基础上封装的httpClient,它会记录每一次HTTP调用的信息,并上报给OpenZipkin。 ``` httpclient =TracingHttpClientBuilder.create(tracing).build(); ``` 而Pinpoint是通过字节码注入的方式来实现拦截服务调用,从而收集trace信息的,所以不需要代码做任何改动。Java字节码注入的大致原理你可以参考下图。 ![](https://static001.geekbang.org/resource/image/4a/75/4a27448c52515020c1f687e8e3567875.png) (图片来源:[http://naver.github.io/pinpoint/1.7.3/images/td\_figure3.png](http://naver.github.io/pinpoint/1.7.3/images/td_figure3.png)) 我来解释一下,就是JVM在加载class二进制文件时,动态地修改加载的class文件,在方法的前后执行拦截器的before()和after()方法,在before()和after()方法里记录trace()信息。而应用不需要修改业务代码,只需要在JVM启动时,添加类似下面的启动参数就可以了。 ``` -javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar -Dpinpoint.agentId= -Dpinpoint.applicationName=