優步分布式追蹤技術再度精進
對于希望監視復雜的 微服務架構 系統的組織,分布式追蹤正在快速成為一種不可或缺的工具。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和副本。
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。
分布式追蹤系統在谷歌和推ter等主要技術公司獲得的成功意味著這些公司中廣泛使用的RPC框架、Stubby和 Finagle 是行之有效的。
同理,Tchannel自帶的追蹤能力也是一個重大的飛躍。我們部署的后端原型系統已經開始從數十種服務中收集追蹤信息。隨后我們使用Tchannel構建了更多服務,但在生產環境中全面推廣和廣泛使用依然有些困難。該后端原型以及所使用的 Riak / Solr 存儲系統無法妥善縮放以適應Uber的流量,同時很多查詢功能依然無法與Zipkin UI實現足夠好的互操作。盡管新構建的服務大量使用了Tchannel,Uber依然有大量服務尚未在RPC過程中使用Tchannel,實際上承擔核心業務的大部分服務都沒有使用Tchannel。這些服務主要是通過四大編程語言(Node.js、Python、Go和Java)實現的,在進程間通信方面使用了多種不同的框架。這種異構的技術環境使得Uber在分布式追蹤系統的構建方面會面臨比谷歌和推ter更嚴峻的挑戰。
在紐約市構建的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 框架。
我們正在將后端和UI代碼遷移至GitHub,并計劃盡快將Jaeger的源代碼全部公開。如果你對這個過程感興趣,可以關注 主代碼庫 。我們歡迎大家為此做貢獻,也很樂于看到更多人嘗試使用Jaeger。雖然我們對目前的進展很滿意,但Uber的分布式追蹤工作還有很長的路要走。
來自:http://www.infoq.com/cn/articles/evolving-distributed-tracing-at-uber-engineering