在Uber Engineering发展分布式跟踪

0
在Uber Engineering发展分布式跟踪

分布式跟踪正迅速成为组织用来监控其复杂的、microservice-based架构.在Uber Engineering,我们的开源分布式跟踪系统Jaeger在整个2016年看到了大规模的内部采用,集成到数百个微服务,现在每秒记录数千个痕迹。在我们开始新的一年的时候,下面是我们如何走到今天的故事,从研究现成的解决方案,比如Zipkin我们为什么从拉式架构转变为推式架构,以及分布式跟踪在2017年将如何继续发展。

从巨石到微服务

随着Uber的业务呈指数级增长,我们的软件架构也变得越来越复杂。一年多前,也就是2015年秋天,我们有大约500个微服务。截至2017年初,我们有超过2000家。这部分是由于越来越多的业务功能——面向用户的功能UberEATS而且UberRUSH-以及欺诈检测、数据挖掘和地图处理等内部功能。复杂性增加的另一个原因是远离大的独立应用程序到分布式微服务体系结构。

正如经常发生的那样,进入微服务生态系统会带来自身的挑战。其中之一是对系统的可见性的丧失,以及服务之间现在发生的复杂交互。超级的工程师要知道我们的技术直接影响着人们的生活。系统的可靠性是最重要的,但没有可观察性是不可能的。传统的监控工具诸如度量和分布式日志记录仍然有它们的位置,但是它们常常不能提供可见性在服务.这就是分布式跟踪蓬勃发展的地方。

跟踪超级的开端

优步第一个广泛使用的追踪系统被称为Merckx,以世界上最快的自行车手在他的时间。Merckx很快回答了有关Uber单片Python后端的复杂问题。它会发出类似于“查找用户登录位置的请求”的查询而且这个请求花了两秒钟多而且只使用了某些数据库而且一个交易可能被保持打开超过500毫秒。分析数据被组织到一个块树中,每个块表示一个特定的操作或远程调用,类似于OpenTracing API.用户可以对输入的数据流运行特别查询卡夫卡使用命令行工具。他们还可以使用web UI来查看预定义的摘要,这些摘要总结了API端点和的高级行为芹菜任务。

Merckx将调用图建模为一个块树,每个块表示应用程序中的一个操作,例如数据库调用RPC,甚至是像解析JSON这样的库函数。

Merckx工具自动应用于Python中的许多基础设施库,包括HTTP客户端和服务器、SQL查询、复述,调用,甚至JSON序列化。该工具记录了关于每个操作的某些性能指标和元数据,例如HTTP调用的URL,或数据库调用的SQL查询。它还捕获了数据库事务保持打开的时间、访问了哪些数据库碎片和副本等信息。

Merckx体系结构是来自Kafka中的工具数据流的拉模型。

Merckx的主要缺点是它的设计针对的是Uber的单一API时代。Merckx缺乏分布式上下文传播的概念。它记录了SQL查询、Redis调用,甚至对其他服务的调用,但无法深入到一个级别以上。Merckx的另一个有趣的限制是,许多高级功能,如数据库事务跟踪,实际上只能在以下条件下工作uWSGI,因为Merckx数据存储在全局的线程本地存储中。一旦Uber开始采用龙卷风作为Python服务的异步应用程序框架,线程本地存储无法表示Tornado的IOLoop上运行在同一线程中的多个并发请求。我们开始意识到,在不依赖全局变量或全局状态的情况下,有一个可靠的故事来保持请求状态并正确地传播它是多么重要。

接下来,在TChannel中跟踪

2015年初,我们开始了TChannel一种用于RPC的网络多路复用和帧协议。协议的设计目标之一是衣冠楚楚的样式的分布式跟踪作为一级公民内置到协议中。为了这个目标,TChannel协议规范定义跟踪领域作为二进制格式的一部分。

Spanid:8 parentid:8 traceid:8 traceflags:1

类型 描述
spanid
int64 它确定了电流跨度
parentid
int64 之前的跨度
traceid
int64 由原请求者指定
traceflags
uint8 位标志字段

中的二进制格式的一部分显示跟踪字段TChannel协议规范

除了协议规范之外,我们还发布了几个用不同语言实现协议的开源客户端库。这些库的设计原则之一是有一个概念请求上下文中期望应用程序从服务器端点传递到下游调用站点。例如,在tchannel-go,使签名使用JSON编码的出站调用要求context作为第一个参数:

func (c *客户端)调用(ctx上下文,方法字符串,参数,resp接口{})错误{..}

TChannel库鼓励应用程序开发人员在编写代码时考虑到分布式上下文传播。

通过编组连线表示和内存中上下文对象之间的跟踪上下文,以及围绕服务处理程序和出站调用创建跟踪跨度,客户机库内置了对分布式跟踪的支持。在内部,跨度以一种几乎相同的格式表示Zipkin跟踪系统,包括zipkin特定注释的使用,如“cs”(客户端发送)和“cr”(客户端接收)。TChannel使用跟踪报告器接口将收集的跟踪跨度发送到进程外的跟踪系统后端。这些库附带一个默认的报告器实现,该实现使用TChannel本身和Hyperbahn(发现和路由层)将以Thrift格式发送跨到收集器集群。

TChannel客户端库让我们接近了Uber需要的工作分布跟踪系统,提供了以下构建块:

  • 跟踪上下文的进程间传播,与请求一起在带内传播
  • 记录跟踪跨度的工具API
  • 跟踪上下文的进程内传播
  • 将跟踪数据从进程外报告到跟踪后端的格式和机制

唯一缺少的部分是跟踪后端本身。跟踪上下文的连线格式和报告器使用的缺省Thrift格式都设计得非常简单,可以将TChannel与Zipkin后端集成起来。然而,当时向Zipkin发送跨度的唯一方法是via抄写员,而Zipkin支持的唯一性能数据存储是卡珊德拉.那时,我们对这两种技术都没有直接的操作经验,所以我们构建了一个原型后端,将一些自定义组件与Zipkin UI结合起来,形成一个完整的跟踪系统。

tchannel生成跟踪的原型后端体系结构是一个带有自定义收集器、自定义存储和开源Zipkin UI的推模型。

分布式跟踪系统在其他主要技术公司(如谷歌和Twitter)的成功是建立在RPC框架、Stubby和欺瞒分别在这些公司广泛使用。

同样,TChannel中的开箱即用跟踪功能是向前迈出的一大步。部署的后端原型立即开始接收来自几十个服务的跟踪。使用TChannel构建了更多的服务,但全面的产品推出和广泛采用仍然存在问题。原型后端及其Riak/Solr基于存储的存储在扩展到Uber的流量时存在一些问题,一些查询功能无法与Zipkin UI正确地互操作。尽管新服务迅速采用了TChannel,但Uber仍然有大量的服务没有使用TChannel进行RPC;事实上,大多数负责运行核心业务功能的服务在没有TChannel的情况下运行。这些服务是用四种主要编程语言(Node.js、Python、Go和Java)实现的,使用各种不同的框架进行进程间通信。这种技术环境的异质性使得在Uber部署分布式跟踪比在谷歌和Twitter等地方要困难得多。

纽约的积家大厦

Uber NYC工程组织成立于2015年初,有两个主要团队:基础设施方面的Observability和产品方面的Uber Everything(包括UberEATS和UberRUSH)。由于分布式跟踪是生产监视的一种形式,它很适合可观察性。

我们组建了分布式跟踪团队,有两名工程师和两个目标:将现有的原型转化为一个全面的生产系统,让所有Uber微服务都可以使用并采用分布式跟踪。我们还需要项目的代号。命名是一种计算机科学中的两个难题因此,我们花了几周的时间,就追踪、侦探和狩猎等主题的词汇进行头脑风暴,直到我们确定了Jaeger这个名字(yuh -gər),在德语中是猎人或狩猎随从的意思。

NYC团队已经有了运行Cassandra集群的操作经验,这是由Zipkin后端直接支持的数据库,因此我们决定放弃基于Riak/Solr的原型。我们重新实现了Go中的收集器,以接受TChannel流量,并将其以与Zipkin兼容的二进制格式存储在Cassandra中。这允许我们使用Zipkin web和查询服务而不需要任何修改,而且还提供了通过自定义标记搜索轨迹的缺失功能。我们还在每个收集器中内置了一个可动态配置的乘法因子,以使入站流量相乘n用于使用生产数据对后端进行压力测试的时间。

早期的积家架构仍然依赖于Zipkin UI和Zipkin存储格式。

第二项业务是使跟踪对所有不使用TChannel进行RPC的现有服务可用。接下来的几个月,我们用Go、Java、Python和Node.js构建客户端库,以支持任意服务的插装,包括基于http的服务。尽管Zipkin后端相当有名和流行,但它在插装方面缺乏一个很好的故事,特别是在Java/之外Scala生态系统。我们考虑了各种开源工具库,但它们是由不同的人维护的,无法保证在线上的互操作性,通常使用完全不同的api,大多数都需要Scribe或Kafka作为报告跨越的传输工具。我们最终决定编写自己的库,对其进行集成测试以实现互操作性,支持我们需要的传输,最重要的是,用不同的语言提供一致的工具API。我们所有的客户端库从一开始就被构建为支持OpenTracing API。

我们在客户端库的第一个版本中构建的另一个新特性是能够轮询跟踪后端采样策略.当服务接收到没有跟踪元数据的请求时,跟踪工具通常通过生成一个新的随机跟踪ID为该请求启动一个新的跟踪。然而,大多数生产跟踪系统,特别是那些必须处理Uber规模的系统,并不会分析每一个跟踪或将其记录在存储中。这样做会产生从服务到跟踪后端的大量流量,可能比服务处理的实际业务流量大好几个数量级。相反,大多数跟踪系统只对小部分的痕迹进行采样,并且只对这些采样的痕迹进行分析和记录。做出抽样决策的精确算法就是我们所说的抽样策略。抽样策略的例子包括:

  • 样品的一切。这在测试中很有用,但是在生产中很昂贵!
  • 一种概率方法,即以一定的固定概率对给定的轨迹进行随机采样。
  • 一种速率限制方法,即每个时间单位采样X个迹。例如,一个变体漏桶算法可能被使用。

大多数现有的与zipkin兼容的工具库都支持概率抽样,但它们希望在初始化时配置抽样率。这种方法在大规模使用时会导致几个严重的问题:

  • 给定的服务很难了解到采样率对到跟踪后端的总体流量的影响。例如,即使服务本身具有中等的每秒查询(QPS)速率,它也可能调用另一个具有非常高的扇出因子的下游服务,或者使用大量的工具,从而导致大量的跟踪跨度。
  • 在优步,商业流量显示出强烈的每日季节性;更多的人在高峰时间乘车。固定的抽样概率对于非高峰流量可能太低,而对于高峰流量可能太高。

Jaeger客户端库中的轮询特性就是为了解决这些问题而设计的。通过将有关适当抽样策略的决策转移到跟踪后端,我们使服务开发人员不必猜测适当的抽样率。这还允许后端在流量模式改变时动态调整采样率。下图显示了从收集器到客户端库的反馈循环。

最初版本的客户端库仍然使用TChannel将跟踪跨度发送到进程外,直接将它们提交给收集器,因此这些库依赖于Hyperbahn进行发现和路由。这种依赖为工程师为他们的服务采用跟踪带来了不必要的摩擦,无论是在基础设施级别,还是在他们必须拉入服务的额外库的数量上依赖地狱

我们通过实现jaeger-agentSidecar进程,作为基础设施组件部署到所有主机,就像收集指标的代理一样。类中封装了所有路由和发现依赖项jaeger-agent我们重新设计了客户端库,以便将跟踪跨度报告到本地UDP对环回接口上的代理进行端口和轮询,以获取抽样策略。新客户机只需要基本的网络库。这个架构上的改变是向我们使用跟踪后采样的愿景迈进了一步:在代理的内存中缓冲跟踪。

目前的Jaeger架构:用Go实现的后端组件,支持OpenTracing标准的四种语言的客户端库反应的web前端,以及基于后处理和聚合的数据管道Apache火花

交钥匙分布式跟踪

Zipkin UI是我们在Jaeger中使用的最后一个第三方软件。为了与UI兼容,必须在Cassandra中以Zipkin Thrift格式存储跨度,这限制了我们的后端和数据模型。特别是,Zipkin模型不支持OpenTracing标准和我们的客户端库中的两个重要特性键值日志API和以更一般的有向无环图而不仅仅是跨度树表示的跟踪。我们决定冒险一试,在后端更新数据模型,并编写一个新的UI。如下所示,新的数据模型本机支持键值日志记录和跨度引用。它还通过避免在每个span中进程标记重复来优化发送出进程的数据量:

Jaeger数据模型本机支持键值日志记录和跨度引用。

我们目前正在完成后端管道到新的数据模型和一个新的、更好优化的Cassandra模式的升级。为了利用新的数据模型,我们在Go中实现了一个新的Jaeger-query服务和一个用React构建的全新web UI。UI的最初版本主要复制了Zipkin UI的现有功能,但它的架构易于扩展,可以使用新功能和组件,也可以作为React组件本身嵌入到其他UI中。例如,用户可以选择许多不同的视图来可视化跟踪结果,例如跟踪持续时间的直方图或跟踪中服务的累计时间:

积家UI显示跟踪搜索结果。在右上角,持续时间与时间的散点图给出了结果和向下钻取能力的可视化表示。

另一个例子是,可以根据特定的用例查看单个跟踪。默认呈现是一个时间序列;其他视图包括有向无环图或关键路径图:

Jaeger UI显示单个跟踪的详细信息。在屏幕的顶部是跟踪的小地图图,它支持在大型跟踪中更容易地导航。

通过用Jaeger自己的组件替换我们体系结构中剩下的Zipkin组件,我们将Jaeger定位为一个交钥匙的端到端分布式跟踪系统。

我们相信,仪表库是Jaeger固有的一部分是至关重要的,以保证它们与Jaeger后端的兼容性和通过持续集成测试彼此之间的互操作性。(这种保证在Zipkin生态系统中是不存在的。)特别是,跨所有支持的语言(目前是Go、Java、Python和Node.js)和所有支持的线路传输(目前是HTTP和TChannel)的互操作性作为每个拉请求的一部分进行测试crossdock框架,由Uber Engineering RPC团队编写。中可以找到Jaeger客户机集成测试的详细信息jaeger-client-gocrossdock库。目前,所有Jaeger客户端库都是可用的开源

我们正在将后端和UI代码迁移到Github,并计划很快提供完整的Jaeger源代码。如果你对这方面的进展感兴趣,请观看主要存储库.我们欢迎投稿,并希望看到其他人给Jaeger一个尝试。尽管我们对迄今为止的进展感到满意,但Uber的分布式跟踪还远远没有结束。

尤里·什库罗(Yuri Shkuro)是Uber纽约工程办公室的软件工程师,他很可能正在勤奋地研究积家(Jaeger)和其他软件Uber Engineering的开源贡献现在。

编辑更新2017年4月15日:Jaeger现在正式开源了,附带了生成的文档

2020年4月23日更新:2017年9月,积家加入了云原生计算基金会作为一个孵化项目,2019年10月,它逐渐发展成为基金会作为顶级项目。

评论