从Zipkin到Jaeger,Uber的分布式追踪之道tchannel

uber 的 tchannel 的模式是更优雅的实现模式

从Zipkin到Jaeger,Uber的分布式追踪之道

做者|Yuri Shauro前端

编辑|大愚若智算法

对于但愿监视复杂的微服务架构系统的组织,分布式追踪正在快速成为一种不可或缺的工具。Uber工程团队的开源分布式追踪系统Jaeger自2016年起,在公司内部实现了大范围的运用,已经集成于数百个微服务中,目前每秒钟已经能够记录数千条追踪数据。新年伊始,咱们想向你们介绍一下这一切是如何实现的,从咱们最开始使用现成的解决方案,如Zipkin,到咱们从拉取转换为推送架构的缘由,以及2017年有关分布式追踪的发展计划。数据库

从总体式到微服务架构编程

随着Uber的业务飞速增加,软件架构的复杂度也与日俱增。大概一年多前,2015年秋季,咱们有大约500个微服务,2017年初这一数量已增加至超过2000个。这样的增幅部分是因为业务该功能的增长,例如面向用户的UberEATS和UberRUSH等功能,以及相似欺诈检测、数据挖掘、地图处理等内部功能的增长。此外随着咱们从大规模总体式应用程序向着分布式微服务架构迁移,也形成了复杂度的增长。后端

迁移到微服务生态老是会遇到独特的挑战。例如丧失对系统的能见度,服务之间开始产生复杂的交互等。Uber工程团队很清楚,咱们的技术会对你们的生活产生直接影响,系统的可靠性相当重要,但这一切都离不开“可观测性”这一前提。传统的监视工具,例如度量值和分布式日志依然发挥着本身的做用,但这类工具每每没法提供跨越不一样服务的能见度。分布式追踪应运而生。服务器

Uber最初的追踪系统网络

Uber最初普遍使用的追踪系统叫作Merckx,这一名称源自全球速度最快的自行车骑行选手。Merckx很快就帮助咱们了解了有关Uber基于Python的总体式后端的不少问题。咱们能够查询诸如“查找已登陆用户的请求,而且请求的处理时间超过2秒钟,而且使用了某一数据库来处理,而且事务维持打开状态的时间超过500ms”这样的问题。全部待查询的数据被组织成树状块,每一个块表明某一操做或某个远程调用,这种组织方式相似于OpenTracing API中“Span”这个概念。用户能够在Kafka中使用命令行工具针对数据流执行即席查询,也可使用Web界面查看预约义的摘要,这些信息均从API端点的高级别行为和Celery任务中摘要汇总而来。架构

Merckx使用了一种相似于树状块的调用图,每一个块表明应用程序中的一个操做,例如数据库调用、RPC,甚至库函数,例如解析JSON。并发

Merckx的编排调度可自动应用于使用Python编写的一系列基础架构库,包括HTTP客户端和服务器、SQL查询、Redis调用,甚至JSON的序列化。这些编排调度可记录有关每次操做的某些性能度量值和元数据,例如HTTP调用的URL,或数据库调用的SQL查询。此外还能记录其余信息,例如数据库事务维持打开状态的时长,访问了哪些数据库Shard和副本。app

Merckx架构使用了拉取模式,可从Kafka的指令数据中拉取数据流。

Merckx最大的不足在于其设计主要面向Uber使用总体式API的年代。Merckx缺少分布式上下文传播的概念,虽然能够记录SQL查询、Redis调用,甚至对其余服务的调用,但没法进一步深刻。Merckx还有另外一个有趣的局限:由于Merckx数据存储在一个全局线程本地存储中,诸如数据库事务追踪等大量高级功能只能在uWSGI下使用。随着Uber开始使用Tornado(一种适用于Python服务的异步应用程序框架),线程本地存储没法体现Tornado的IOLoop中同一个线程内运行的大部分并发请求。咱们开始意识到不借助全局变量或全局状态,转为经过某种方式保存请求状态,并进行恰当的传播的重要性。

随后,使用TChannel进行追踪

2015年初,咱们开始开发TChannel,这是一种适用于RPC的网络多路复用和框架协议。该协议的设计目标之一是将相似于Dapper的分布式追踪能力融入协议中,并为其提供最优秀的支持。为了实现这一目标,TChannel协议规范将追踪字段直接定义到了二进制格式中。

spanid:8 parentid:8 traceid:8 traceflags:1

字段 类型 描述
spanid int64 用于识别当前span
parentid int64 前一个span
traceid int64 负责分配的原始操做方
traceflags uint8 位标志字段

追踪字段做为二进制格式的一部分已包含在TChannel协议规范中。

除了协议规范,咱们还发布了多个开源客户端库,用于以不一样语言实现该协议。这些库的设计原则之一是让应用程序须要用到的请求上下文这一律念可以从服务器端点贯穿至下游的调用站点。例如在tchannel-go中,让出站调用使用JSON进行编码的签名须要经过第一个参数提供上下文:

func (c *Client) Call(ctx Context, method string, arg, resp interface{}) error {..}

Tchannel库使得应用程序开发者在编写本身的代码时始终将分布式上下文传播这一律念铭记于心。

经过将所传输内容以及内存中的上下文对象之间的追踪上下文进行安排,并围绕服务处理程序和出站调用建立追踪Span,客户端库内建了对分布式追踪的支持。从内部来看,这些Span在格式上与Zipkin追踪系统几乎彻底相同,也使用了Zipkin所定义的注释,例如“cs”(Client Send)和“cr”(Client Receive)。Tchannel使用追踪报告程序(Reporter)接口将收集到的进程外追踪Span发送至追踪系统的后端。该技术自带的库默认包含一个使用Tchannel自己和Hyperbahn实现的报告程序以及发现和路由层,借此将Thrift格式的Span发送至收集器群集。

Tchannel客户端库已经比较近似于咱们所须要的分布式追踪系统,该客户端库提供了下列构建块:

  • 追踪上下文的进程间传播以及带内请求

  • 经过编排API记录追踪Span

  • 追踪上下文的进程内传播

  • 将进程外追踪数据报告至追踪后端所需的格式和机制

该系统惟独缺乏了追踪后端自己。追踪上下文的传输格式和报表程序使用的默认Thrift格式在设计上均可以很是简单直接地将Tchannel与Zipkin后端集成,然而当时只能经过Scribe将Span发送至Zipkin,而Zipkin只支持使用Cassandra格式的数据存储。此外当时咱们对这些技术没什么经验,所以咱们开发了一套后端原型系统,并结合Zipkin UI的一些自定义组件构建了一个完整的追踪系统。

后端原型系统架构:Tchannel生成的追踪记录推送给自定义收集器、自定义存储,以及开源的Zipkin UI。

分布式追踪系统在谷歌和Twitter等主要技术公司得到的成功意味着这些公司中普遍使用的RPC框架、Stubby和Finagle是行之有效的。

同理,Tchannel自带的追踪能力也是一个重大的飞跃。咱们部署的后端原型系统已经开始从数十种服务中收集追踪信息。随后咱们使用Tchannel构建了更多服务,但在生产环境中全面推广和普遍使用依然有些困难。该后端原型以及所使用的Riak/Solr存储系统没法妥善缩放以适应Uber的流量,同时不少查询功能依然没法与Zipkin UI实现足够好的互操做。尽管新构建的服务大量使用了Tchannel,Uber依然有大量服务还没有在RPC过程当中使用Tchannel,实际上承担核心业务的大部分服务都没有使用Tchannel。这些服务主要是经过四大编程语言(Node.js、Python、Go和Java)实现的,在进程间通讯方面使用了多种不一样的框架。这种异构的技术环境使得Uber在分布式追踪系统的构建方面会面临比谷歌和Twitter更严峻的挑战。

在纽约市构建的Jaeger

Uber纽约工程组织始建于2015年上半年,主要包含两个团队:基础架构端的Observability以及产品(包括UberEATS和UberRUSH)端的Uber Everything。考虑到分布式追踪其实是一种形式的生产环境监视,所以更适合交由Observability团队负责。

咱们组建了分布式追踪团队,该团队由两个工程师组成,目标也有两个:将现有的原型系统转换为一种能够全局运用的生产系统,让分布式追踪功能能够适用并适应Uber的微服务。咱们还须要为这个项目起一个开发代号。为新事物命名其实是计算机科学界两大老大难问题之一,咱们花了几周时间集思广益,考虑了追踪、探测、捕获等主题,最终决定命名为Jaeger(?yā-g?r),在德语中这个词表明猎手或者狩猎过程当中的帮手。

纽约团队在Cassandra群集方面已经具有运维经验,该数据库直接为Zipkin后端提供着支持,所以咱们决定弃用基于Riak/Solr的原型。为了接受TChannel流量并将数据以兼容Zipkin的二进制格式存储在Cassandra中,咱们用Go语言从新实现了收集器。这样咱们就能够无需改动,直接使用Zipkin的Web和查询服务,并经过自定义标签得到了本来不具有的追踪记录搜索功能。咱们还为每一个收集器构建了一套可动态配置的倍增系数(Multiplication factor),借此将入站流量倍增n次,这主要是为了经过生产数据对后端系统进行压力测试。

Jaeger的早期架构依然依赖Zipkin UI和Zipkin存储格式。

第二个业务需求但愿让追踪功能能够适用于未使用TChannel进行RPC的全部现有服务。随后几个月咱们使用Go、Java、Python和Node.js构建了客户端库,借此未包括HTTP服务在内各种服务的编排提供支持。尽管Zipkin后端很是著名而且流行,但依然缺少足够完善的编排能力,尤为是在Java/Scala生态系统以外的编排能力。咱们考虑过各类开源的编排库,但这些库是由不一样的人维护的,没法确保互操做性,而且一般还使用了彻底不一样的API,大部分还须要使用Scribe或Kafka做为报表Span的传输机制。所以咱们最终决定自行编写库,这样能够经过集成测试更好地保障互操做性,能够支持咱们须要的传输机制,更重要的是,能够用不一样的语言提供一致的编排API。咱们的全部客户端库从一开始均可支持OpenTracing API。

在初版客户端库中,咱们还增长了另外一个新颖的功能:能够从追踪后端轮询采样策略。当某个服务收到不包含追踪元数据的请求后,所编排的追踪功能一般会为该请求启动一个新的追踪,并生成新的随机追踪ID。然而大部分生产追踪系统,尤为是与Uber的缩放能力有关的系统没法对每一个追踪进行“描绘”(Profile)或将其记录在本身的存储中。这样作会在服务与后端系统之间产生难以招架的大流量,甚至会比服务所处理的实际业务流量大出好几个数量级。咱们改成让大部分追踪系统只对小比例的追踪进行采样,并只对采样的追踪进行“描绘”和记录。用于进行采样决策的算法被咱们称之为“采样策略”。采样策略的例子包括:

  • 采样一切。主要用于测试用途,但生产环境中使用会形成难以承受的开销!

  • 基于几率的采样,按照固定几率对特定追踪进行随机采样。

  • 限速采样,每一个时间单位对X个追踪进行采样。例如可能会使用漏桶(Leaky bucket)算法的变体。

大部分兼容Zipkin的现有编排库可支持基于几率的采样,但须要在初始化过程当中对采样速率进行配置。以咱们的规模,这种方式会形成一些严重的问题:

  • 每一个服务对不一样采样速率对追踪后端系统总体流量的影响知之甚少。例如,就算服务自己使用了适度的每秒查询数(QPS)速率,也可能调用扇出(Fanout)因素很是高的其余下游服务,或因为密集编排致使产生大量追踪Span。

  • 对于Uber来讲,天天不一样时段的业务流量有着明显规律,峰值时期乘客更多。固定不变的采样几率对非峰值时刻可能显得太低,但对峰值时刻可能显得太高。

Jaeger客户端库的轮询功能按照设计能够解决这些问题。经过将有关最恰当采样策略的决策转交给追踪后端系统,服务的开发者再也不须要猜想最适合的采样速率。然后端能够按照流量模式的变化动态地调整采样速率。下方的示意图显示了从收集器到客户端库的反馈环路。

初版客户端库依然使用TChannel发送进程外追踪Span,会将其直接提交给收集器,所以这些库须要依赖Hyperbahn进行发现和路由。对于但愿在本身的服务中运用追踪能力的工程师,这种依赖性形成了没必要要的摩擦,这样的摩擦存在于基础架构层面,以及须要在服务中额外包含的库等方面,进而可能致使依赖性地域。

为了解决这种问题,咱们实现了一种jaeger-agent边车(Sidecar)进程,并将其做为基础架构组件,与负责收集度量值的代理一块儿部署到全部宿主机上。全部与路由和发现有关的依赖项都封装在这个jaeger-agent中,此外咱们还从新设计了客户端库,可将追踪Span报告给本地UDP端口,并能轮询本地回环接口上的代理获取采样策略。新的客户端只须要最基本的网络库。架构上的这种变化向着咱们先追踪后采样的愿景迈出了一大步,咱们能够在代理的内存中对追踪记录进行缓冲。

目前的Jaeger架构:后端组件使用Go语言实现,客户端库使用了四种支持OpenTracing标准的语言,一个基于React的Web前端,以及一个基于Apache Spark的后处理和聚合数据管道。

统包式分布式追踪

Zipkin UI是咱们在Jaeger中使用的最后一个第三方软件。因为要将Span以Zipkin Thrift格式存储在Cassandra中并与UI兼容,这对咱们的后端和数据模型产生了必定的限制。尤为是Zipkin模型不支持OpenTracing标准和咱们的客户端库中两个很是重要的功能:键-值日志API,以及用更为通用的有向无环图(Directed acyclic graph)而非Span树所表明的追踪。所以咱们毅然决定完全革新后端所用的数据模型,并编写新的UI。以下图所示,新的数据模型可原生支持键-值日志和Span的引用,此外还对发送到进程外的数据量进行了优化,避免进程标签在每一个Span上重复:

Jaeger数据模型可原生支持键-值日志和Span引用。

目前咱们正在将后端管道全面升级到新的数据模型,以及全新的,更为优化的Cassandra架构。为了充分利用新的数据模型,咱们还用Go语言实现了一个全新的Jaeger查询服务,并用React实现了一套全新的Web UI。最第一版本的UI主要重现了Zipkin UI的原有功能,但在设计上更易于经过扩展提供新的功能和组件,并能做为React组件嵌入到其余UI。例如,用户能够选择用多种不一样视图对追踪结果进行可视化,例如追踪时段内的直方图,或服务在追踪过程当中的累积时间:

Jaeger UI显示的追踪信息搜索结果。右上角显示的时刻和持续时间散点图用可视化方式呈现告终果,并提供了向下挖掘能力。

另外一个例子,能够根据不一样用例查看同一条追踪记录。除了使用默认的时序渲染方式,还能够经过其余视图渲染为有向无环图或关键路径图:

Jaeger UI显示了一条追踪记录的详情。界面顶部是一条追踪记录的迷你地图示意图,借此可在更大规模的追踪记录中进行更轻松的导航。

经过将架构中剩余的Zipkin组件替代为Jaeger本身的组件,咱们将Jaeger完全变为一种统包式的端到端分布式追踪系统。

咱们认为编排库是Jaeger固有的一部分,这样能够确保与Jaeger后端的兼容性,以及经过持续集成测试保障相互之间的互操做性。(Zipkin生态系统作不到这些。)尤为是跨越全部可支持语言(目前支持Go、Java、Python和Node.js)和可支持的传输方式(目前支持HTTP和TChannel)实现的互操做性会在每一个Pull请求中测试,并用到了Uber工程部门RPC团队所开发的Crossdock框架。Jaeger客户端集成测试的详细信息请参阅jaeger-client-go crossdock代码库。目前全部Jaeger客户端库都已开源:

  • Go

  • Java

  • Node.js

  • Python

咱们正在将后端和UI代码迁移至GitHub,并计划尽快将Jaeger的源代码所有公开。若是你对这个过程感兴趣,能够关注主代码库。咱们欢迎你们为此作贡献,也很乐于看到更多人尝试使用Jaeger。虽然咱们对目前的进展很满意,但Uber的分布式追踪工做还有很长的路要走。

Yuri Shkuro是Uber纽约工程部办公室的全职软件工程师,目前正全力从事Jaeger和其余Uber工程团队开源项目。