在纽约市构建的Jaeger

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

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

纽约团队在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的后处理和聚合数据管道。