分布式追蹤系統架構與設計
先前的博客公告 中討論過為什么Knewton需要一個分布式追蹤系統,并且數值可以被添加到一個公司中。這個章節將會更加深入探討技術細節,我們如何實施分布式追蹤系統的。
總體結構與追蹤數據管理
我們的方案分為兩大部分:所有服務集成到追蹤庫中,分配一個內存塊來存儲與查看追蹤數據。我們選擇 Zipkin ,在推ter開發的一個可擴展的開源追蹤框架,用于存儲與查看追蹤數據。Zipkin通常以Finagle對的形式出現,但是,像上一節提及的一樣,我們排除了與我們現有基礎設施沖突的并發癥。Knewton構建追蹤庫,稱為TDist,從地面起,開始作為公司“黑客日”的實驗。
追蹤數據模型
就我們的方案而言,我們選擇使用Zipkin來匹配數據模型,輪流從 Dapper 大量借入。一個追蹤樹由一系列的跨度組成。跨度代表一個特殊的呼叫從服務器接收開始,到服務器發送,最后是客戶端接收。舉個例子,在服務器A和服務器B之間的呼叫與響應將會作為一個簡單的跨度:
每一個跨度(span)有三個ID:
-
Trace ID:一個軌跡中所有的跨度(span)共享同一個Trace ID。
-
Span ID:用以標示不同的跨度(span)。Span ID與Trace ID不一定相同。
-
Parent Span ID: 只有子跨度持有這個ID,根跨度沒有Parent Span ID。
下面的圖展示了在一個樹結構的調用中,上面三個ID是如何應用的。注意在整個樹結構中Trace ID是一致的。
更多詳情, 請參見 Dapper paper .
TDist
TDist是Knewton開發的一個Java庫。利用該庫我們可以追蹤所有的應用. TDist 目前支持 Thrift , HTTP, and Kafka, 還可以用來追蹤使用了注解(參見 Guice )的方法的調用。
對每一個線程服務或者對另外一個服務發起的請求都分配一個跨度(span),類庫會在后臺對跨度進行傳播和更新。收到一個請求(或者即將發出一個請求),追蹤數據會被添加到一個內部隊列中,DataManager將TraceID修改添加到處理請求的線程的名稱中。工作線程消費隊列,然后將追蹤數據發布到追蹤消息總線。Java ThreadLocal可以很方便的存儲和讀取線程范圍內的全局變量,我們在DataManager中使用了這種方法。
通常,線程將遠程調用或者報告回父線程這樣的實際的工作轉嫁給其他線程來做。因此,我們也實現了線程factory和executor,這樣就知道如何檢索父線程的追蹤數據,并將其分配給子線程,從而使得子線程也可以追蹤。
Zipkin
追蹤數據一旦經過TDist到達追蹤消息總線,Zipkin基礎設施會處理剩下的流程。多收集器實例,從消息總線中消費,存儲追蹤數據中每個記錄。一個分離的查詢集合與web服務,Zipkin的部分源代碼,為了追蹤依次查詢數據庫。我們為了使得事情變得簡單,決定參與查詢和web服務,并且也因為這種組合服務是內部的,并且有可預測的交通模式。但是,收集器是從查詢與web服務中分離的,因為越多Knewton服務集成到收集器,越多追蹤數據需要處理。
Zipkin UI
盒子外部,Zipkin在整個服務中提供一套簡單的UI給視圖追蹤。當在所有服務中,非常容易地打印視圖日志用于一個特殊的追蹤ID號,Zipkin UI在每次調用的持續時長中提供一個總體視圖,不需要查詢數百個日志語句。在一個特定的周期時間內,它也是一個有效的方式來辨別最大的或者最慢的追蹤。在發現這些異常值的情況下,允許我們標志出哪里重復調用其他服務,為了總體調用鏈而放慢我們的SLA。以下是Zipkin UI中的追蹤截圖:
當然,UI并不會撤銷。雖然它很容易看清楚個人痕跡,我們發現Zipkin UI 缺乏檢查匯總數據。比如說,目前還沒有方法獲取總的時間信息或者總的數據,稱之為端點,服務等等。
在整個發展過程中,推出Zipkin基礎設施,我們對Zipkin的開源做出了些許貢獻,感謝它的活躍以及成長社區的支持。
Splunk
正如上面所提到,當前處理請求的線程名也會變動,其Trace ID會追加在上面。因為這樣,我們在需要特定請求的時候,才能從所有啟用追蹤的服務上查詢日志。這使得調試更加方便,同時事實證明其用于事后分析、日志聚合、獨立問題的調試及解釋平臺的異常行為時也比較有用!
Thrift
Thrift是一個用于構建可拓展服務的跨平臺的RPC框架。在Thrift中,用戶可以定義一個服務、數據類型的規則,Thrift就會在許多不同的語言中編譯其規則,這時用戶就可以用想要的開發語言實現所生成的服務接口。Thrift同時自動生成客戶端代碼及用戶為服務所定義的數據結構。
在Knewton中Thrift是服務之間使用最普遍使用的RPC框架,我們服務的大多數通過使用此框架進行通信,所以在維護后端兼容性的時候支持它,對于此項目的成功性而言有著重大的影響。更準確的說,我們想讓未啟用追蹤的服務能與啟用追蹤的服務通信。
當我們開始研究給Thrift添加追蹤支持的時候,我們與不同的兩個方式進行實驗。第一種方式涉及到一個修改過的Thrift編譯器,而第二種涉及到修改后的序列協議及服務處理器。兩種方法都有其優缺點。
自定義編譯器
在這個方法中,我們體驗修改C++簡易編譯器來生成額外的服務接口,可以傳遞追蹤數據給用戶。 可能最著名例子就是Scrooge, 修改簡易的編譯器并不罕見。修改過的編譯器的其中一個優勢是,客戶端在它們代碼中交換較少的類實現,由于在生成的代碼中支持追蹤。客戶端也可以獲得來自服務接口的追蹤數據作為參考。
雖然我們沒有檢測,我們也可以認為這個方法將會更快速地受到應用,由于只有較少的類調用追蹤實現。但是,我們我么將會重編譯我們所有的簡易代碼,偏離開源版本,使得它在將來更難升級。我們也將會認識到,允許用戶訪問追蹤數據將缺乏渴望或者安全,并且數據管理可以更好地保證TDist的一致性。
自定義協議與服務處理器
最終我們應用這個方法到生產中。并不會維持一個自定義編譯器來大量降低我們開發成本。自定義協議與服務接口的最大缺陷是,我們不得不升級來節儉0.9.0(從0.7.0),利用一些特征將會使得它更加容易插入我們自定義協議與處理器的追蹤組件中。升級需要許多組織的協調。令人慶幸的是,更新的簡易版本是向后兼容舊版本的,我們可以在TDist工作,當Knewton服務被更新到新版本時。但是,在我們可以開始用我們的分布式追蹤方案來集成它們時,我們仍然不得不釋放所有的Knewton服務。
升級 Thrift
一般來說通過依賴管理工具升級依賴庫相對比較容易,但如果是那些 類似 Thrift的 RPC框架,或者一些有很深調用鏈的SOA框架,問題就會復雜很多。一個典型的服務通常會同時包含服務端和客戶端的代碼,而服務端的代碼往往會依賴其他的一些客戶端的 依賴庫 。所以,升級的時候需要從服務調用樹的葉子節點開始向上逐級升級,以避免服務調用的兼容性問題。因為服務提供方可能并不知道調用方是否可以檢測出那些附加的追蹤數據。
另外一個障礙是,一些服務會依賴 Thrift 0.7.0(譯者注:上文談到需要升級為0.9.0),比如 Cassandra客戶端依賴 Astyanax , Astyanax 依賴的一些第三方依賴庫會 反過來依賴 Thrift 0.7.0。對于 Astyanax,我們不得不通過 Maven 將依賴的JAR包屏蔽( shade )并且修改包名來避免新舊版本 Thrift庫之間的沖突。 整個升級過程必須迅速,并且沒有停機時間。為了不讓升級過程給 Knewton 的其他 團隊帶來 額外成本,分布式追蹤小組不得不實施并推動整個變更過程。
追蹤簡約組件:他是如何工作的
我們的簡約方案由自定義、向后兼容的協議與自定義服務處理器組成,提取跟蹤數據,放在路由到相應的RPC調用。我們的協議基本上在每個消息頭寫入追蹤數據。當RPC調用到達服務器,處理器將會識別并且標記呼入調用是否有追蹤數據,因此它可以恰到好處地響應。追蹤數據的調用也從中獲取響應,來自非集成服務的請求不會攜帶沒有響應的追蹤數據。這使得追蹤協議向后兼容,因為服務器傳出的協議不會寫追蹤數據,如果指示不是出自處理器所服務的請求。一個追蹤協議可以檢測有效載荷是否包含追蹤數據基于前面幾個字節。簡約追加協議ID到協議頭,假如讀取協議時發現前面幾個字節不顯示追蹤數據,緩存與有效載荷的存在作為一個非追蹤載荷來重讀。當正在讀取一個消息時,協議將會提取追蹤數據,并且使用數據管理器來保存它們至本地線程,用于RPC呼入調用的線程服務。假如該線程額外調用于其他服務的下游,追蹤數據將通過TDist從數據管理器自動地被提取出來,并且添加到外部消息。
下面是一個圖解,展示如何修改載荷來添加追蹤數據:
Kafka消息追蹤支持
當我們為kafka提供消息追蹤支持時,我們希望把Kafka服務(也被稱為brokers),做為一個黑箱看待。換言之,我們希望brokers不需要知道消息是否被消費(即brokers不需要知道消息是否通過它發送給消費者)因此我們不需要修改Kafka的源碼。我們采用與RPC 服務類似的處理方式,在升級生產者之前先升級消費者。消費者反向兼容并能檢查到一個包含追蹤信息的消息,以之前Thrift 協議描述的方式反序列化內容。為了能夠實現上述方式,我們需要客戶端封裝他們在追蹤系統中使用的序列化/反序列化實現,用于不包含追蹤信息的消息和包含追蹤信息的消息之間的讀寫轉換。
HTTP 請求追蹤支持
在Knewton內部的一些基礎構件中所有對外開放的節點都是基于HTTP的,我們需要以一種簡便的方式在HTTP請求中插入需要攜帶的追蹤信息。
這個實現起來很簡單,因為HTTP請求支持在消息頭中放入任意數據。根據 rfc2047 的第5章節中的內容,唯一的參考是在放置自定義的消息頭需要為他們加入前綴'X-'。
我們保持Zipkin傳統并且使用一下標題傳播信息:
-
X-B3-TraceId
-
X-B3-SpanId
-
X-B3-ParentSpanId
Knewton 的服務主要使用 Jetty HTTP Server 和 Apache HTTP Client 。所有依據這兩種中間件構建的項目都能以便捷的方式實現對HTTP消息頭的操作。
Jetty 服務請求被路由到一個 Servlet。作為這個路由的一部分, Jetty 允許請求和回應通過一系列過濾。我們覺得處理追蹤數據,這是最理想的。當任何傳入請求附帶跟蹤數據頭時,我們構造的跨數據會提交到 DataManager 。
與 Apache HTTP 客戶端一起,我們使用一個 HttpRequestInterceptor 和 HttpResponseInterceptor ,它被設計成與頭部內容能交互,并能修改他們。這些攔截器使用 DataManager, 并能從頭中讀出追蹤數據,反之亦然。
Guice
大多數人不熟悉 Guice ,這是一個谷歌開發的依賴關系管理框架。TDist 集成到我們現有的服務模塊,那么我們的客戶端將更簡潔、更少出錯,我們依靠的就是 Guice ,并且實現了多個模塊可以讓我們的客戶端更易于安裝。Guice 處理依賴于在對象實例化期間注入,在交換接口也能更簡潔。如果通過這篇文章你已經開始思考集成 TDist ,那么聽起來很復雜。很多時候,我們的客戶端需要安裝附加的 Guice 模塊,這些模塊將綁定到我們的追蹤實現上,實現現有的 Thrift 接口。這也意味著,我們的客戶端不能實例化任何我們追蹤啟動的構造。無論什么時候,一個 TDist 客戶端忽略綁定一些東西,Guice 都會在編譯時通知我們的客戶端。我們把很多心思放在如何制定我們的 Guice 模塊層次結構上,因此 TDist 不會與我們的客戶端沖突,我們都很小心,無論什么時候,我們都必須暴露元素到外部世界。
追蹤消息總線
所有我們的客戶服務在使用之前都會放置追蹤數據,追蹤消息總線是通過 Zipkin 收集器來持續收集的。 我們的兩個選項是 Kafka 和 Kinesis ,不過最終我們選擇了 Kinesis 。我們考慮到 Kafka 是因為 Knewton 已經穩定部署 Kafka 很多年。在那時,我們的 Kafka 集群一直使用我們的子事件總線,在生產環境中每秒產生超過300條消息。我們初步估計將超過 400,000 條的跟蹤信息,每秒只有部分進行集成。生產系統與儀表數據使我們緊張。Kinesis 似乎是一個有吸引力的替代,它將我們從 Kafka 服務器分離,這只是生產數據,而不是儀表上的數據。在實現的時候,Kinesis 是一個新的 AWS 服務,我們對它很熟悉。它的價格,吞吐能力,不用太多維護,這些促成了我們達成一致。總的來說,我們已經滿意它的性能和穩定性。它沒有 Kafka 那么快,但是 Kafka 數據產生的性質,它從產生到攝入 SLA 甚至要幾分鐘。自從我們部署追蹤消息總線到生產環境,我們也很能容易擴展大量 Kinesis, 且不會引起任何宕機。
追蹤數據的存儲
所有我們追蹤的數據都會被放在追蹤數據存儲。數據放在那里是有一個配置時間的,并且由 Zipkin 查詢服務顯示在 UI 上。Zipkin 提供了大量開箱即用的數據存儲,包括 Cassandra, Redis,MongoDB, Postgres 和 MySQL。我們對 Cassandra 和 DynamoDB 做過實驗,這主要是因為我們在 Knewton 中獲得的習以為常的知識,最終我們還是選擇了亞馬遜的 Elasticache Redis 。下面這些是我們做出這個決定的最重要原因。
-
花在生產上的時間,我們還沒鋪開就要交付了,并且,我們還要維護一個新的集群
-
成本
-
與 Zipkin 集成更簡單,代碼更少
-
在數據上支持TTLs
結 論
現在在 Knewton,我們的追蹤解決方案在整個環境中已經運行好多個月了。目前它已經被證明是非常有價值的。我們只有在開始擦除那個面的時候,我們才可以追蹤并且收集時間數據。我們有很多有趣的實現,并且在 Knewton 交付,之后,我們就理解了這個數據的價值。
來自:http://www.oschina.net/translate/distributed-tracing-design-architecture