Jaeger TChannel ——protocol

cdh0805010118發表於2018-07-16

Jaeger 的 RPC 框架為 TChannel,它是基於 Thrift 協議,雖然現在市面上的 RPC 產品大多采用 Google GRPC,uber jaeger 官方說了,未來會考慮增加 grpc 的支援,但是不是太緊急.

TChannel 設計目標

TChannel 設計目標,共六點:

  1. 多語言支援
  2. 高效能地快速決策路由轉發
  3. 請求和響應的無序性,以便慢速請求不會阻止後續更快的請求
  4. 可選的校驗和
  5. 能夠在 endpoints 之間支援多種協議的資料傳輸(例如:HTTP+JSON 和 Thrift)

首先我們瞭解下 TChannel 的官方文件:

protocol

在微服務中,設計 RPC 服務框架時,經常遇到的問題,三個:

  1. 服務註冊與服務發現—— 我怎麼發現服務端有哪些服務,以及哪些服務是可用的?
  2. 容錯性—— 當 RPC 服務框架的 server 出現問題時,怎麼迅速隔離?另一個問題,如果當客戶端呼叫服務端發生錯誤時,是立即拋異常,還是進行容忍,再次嘗試等
  3. 加入追蹤能力 Dapper—— 如何識別和監控整個系統的瓶頸?

對於分散式追蹤系統的整體概貌,我覺得有個京東金融的視訊講得很好,大家可以認真看看

京東金融分散式服務跟蹤實踐

設計目標

TChannel 目標就是解決這三個問題,提供一個智慧路由協議來讓 client 發現可用的 server 的方案,例如:Hyperbahn

再次考慮微服務面臨的三個問題:

  1. 服務註冊與服務發現:所有的服務通過路由網格進行服務註冊。消費者通過服務名找到服務,它無需知道 IP 和埠,也就是通過服務名,就可以訪問 server
  2. 容錯性:路由網格能夠跟蹤並計算一些指標,如:失敗率,服務正常可用時間等。它能夠智慧地檢測服務的健康狀況,並從可用的 server 列表中移除它,並採用策略嘗試新增回來;
  3. request 跟蹤:trace 跟蹤是在 rpc 框架設計中必須優先考慮的

首先,在考慮設計微服務時,我們需要整合 client 和 server 之間的邏輯,並推送核心特性到微服務架構中,這樣應用就再無需升級更新庫

Consolidating logic between producers and consumers also allows us to push core features to our entire SOA without requiring applications to update libraries.

另外,在開發這個協議和路由網格時,我們要記住並遵循以下目標:

  1. 這個協議必須是支援多語言實現的,並且這樣做是很容易的。特別是 JavaScript、Python 和 Go。
  2. 非同步行為是最基本的要求,我們遵循傳統的 request/response 模型,且這個模型支援無序響應,不阻塞的。慢請求一定不能阻止子請求;
  3. 大的 request/response 可能或者一定要被分割成多個片段傳送 (例如:支援streaming請求)
  4. TChannel 能夠通過任意的序列化方案。例如:JSON、Thrift。其中,Thrift 是 IDL,型別安全和驗證保證的首選。
  5. 路由網格需要一個高效能轉發路徑,它能夠快速做出路由決策,選擇最合適的路由。
  6. request/response 整合的可選校驗和

欄位長度約束

在這個協議中,所有數字值都是無符號的和大端的,並且所有的字串都是 UTF-8。所有的鍵值對在 http header 中都是字串。

下面描述欄位長度和統計和 finagle-mux 是一樣的 schema:

  1. size:4 body:10定義了這個 field 大小是 4 個位元組,跟著就是 body 欄位大小為 10 個位元組。
  2. key~4表示這個key有 4 個位元組,跟著就是資料流
  3. key~4keysize:4 key:keysize的簡寫

Groups 有括號組成,對於多次重複,group 的重複計數為{*}, 或者對於 n 次重複,則為{n}。

訊息流

TChannel 是一個雙向流的 request/response 協議。在 peers 之間,無論哪一方發起的建立請求,連線都是等效的。甚至可能希望在同一對 peers 之間具有多個連線。訊息 ID 列表限定在一個連線上(TODO:不太理解)。傳送一個請求到一個連線上,並從另一個連線上響應,這是無效的。無論出於何種原因,可以將一個請求傳送到一個連線上,然後將一個後續請求傳送到另一個連線上。

ps: 上面的意思是,TChannel建立了Client/Server的多個TCP連線,如果A Client傳送請求到a連線上, 則Server對A的響應,一定是走a連線回路。至於後續的請求,對於業務方認為是一次新的連線建立,隨便選擇連線

一個訊息對應共享 ID 的所有幀。

  1. 一對"init req"和"init res"幀共享同一個訊息 ID ;
  2. 對於"call req", "call req continue", "call res", "call res continue"和"errors"幀可以共享同一個訊息 ID。
  3. 一對"ping req"和"ping res"幀共享同一個訊息 ID。

初始化一個新的連線步驟:

  1. node A 初始化一個 TCP 連線到 node B。 client:node A;server:node B
  2. node B 建立了 TCP 連線,但是要等到 node A 傳送訊息"init req"時,node B 才會傳送訊息給 A;
  3. node A 傳送想要版本號的訊息"init req",直到 node B 響應訊息"init res"之前都不會再傳送任何訊息;
  4. node B 傳送帶著選中的版本 V 訊息"init res",node B 現在可以傳送帶有版本 V 的請求;
  5. node A 接收到訊息"init res",node A 現在可以傳送帶著版本 V 的請求

每一個訊息都被封裝在幀中,這個幀處理業務資料本身還有額外通用的附加資訊。幀資訊的一部分是一個 ID, 當傳送請求訊息時,請求者選擇此 ID。當響應到一個請求時,這個響應節點使用請求發過來的訊息 ID。

每一幀都有一個型別,它描述了這個幀的 body 格式。依賴於這個幀型別,有些 body 是無資料的。具體如下所示:

訊息為"call req"的幀型別有個欄位是"ttl",它用於表示這個請求的生命週期。transmitter管理這個ttl以考慮傳輸花費時間或者故障轉移時間,僅僅在(deadline propagation purposes 截止傳播...)傳送這個ttl給接收者。這個允許接收者知道他們還有多少時間來完成這個請求的剩餘資料傳輸。只要ttl不為0,則表示這個請求還可以繼續完成或者重新傳輸。

一個服務例項 A1,通過服務路由 R1,進入到服務例項 B1,然後返回。這裡有一些訊息流的細節,下面的這些不是每個欄位的完整細節,但是他們可以說明一些不太明顯的行為。

這個例子中的訊息流:A1 ——> R1 ——> B1 ——> R1 ——> A1

A1 傳送訊息為"call req"(訊息型別:0x03) 給 R1:

  1. 為這個幀選擇訊息 ID:M1。這個將用於匹配此請求的響應;M1 的範圍在 A1 和 R1 之間,以及這個連線上;
  2. 生成一個唯一 traceid,這個 traceid 可以傳播到任意依賴的請求中。且只有在呼叫鏈中最邊緣的服務時才應該這樣做。(ps: 這個就是呼叫鏈發起的地方,也即開端)。這個唯一 traceid 目的是建立一個 span 樹,並覆蓋所有 RPCs
  3. 生成一個唯一的 spanid,表示當前節點處理的工作
  4. 為這個請求設定一個最大允許時間 ttl。如果在這個時間內,響應端沒有返回資料,則 client 節點會取消這個請求
  5. 服務 A1 不需要任何頭部,因此預設也是這樣使用的
  6. 服務 A1 發起帶有服務 B1 的請求,訊息體"call req", 在服務 B1 需要進行資料校驗,防篡改

R1 接收到來自服務 A1 的"call req"訊息,傳送"call req"(0x03) 給服務 B1:

  1. 為這個幀選擇訊息 ID:M2;
  2. 拷貝來自流入的訊息 traceid 到新訊息的 traceid 部分;
  3. 拷貝來自流入的訊息 spanid 到新訊息的 parentid 部分;
  4. 生成唯一的 spanid
  5. 拷貝來自流入的訊息 ttl 到新的訊息 ttl 部分
  6. 拷貝服務名、引數和校驗資料到新的訊息

B1 接收來自 R1 的訊息"call req", 傳送"call res"(0x04) 給 R1:

  1. 為這個幀使用訊息 ID:M2;
  2. 傳送來自應用響應的引數和校驗和

R1 接收來自 B1 的訊息"call res", 傳送"call res"(0x04) 給 A1:

  1. 匹配流入的訊息 ID:M2,訊息內容為"call req";
  2. 為新訊息使用訊息 ID:M1;
  3. 拷貝流入的訊息引數和校驗和;

A1 接收來自 R1 的訊息"call res":

  1. 匹配流入的訊息 ID:M1,且已存在的訊息為"call req"
  2. 傳送引數給應用程式;

所有型別的幀統一使用如下的結構表示:(這裡的數字單位:位元組)

Position 內容
0-7 size:2 type:1 reserved:1 id:4
8-15 reserved:8
16+ payload: - base on type

size:2

size:2 表示這個幀的最大範圍 2 個位元組:64kb-1,包括幀的頭部和資料部分。注意儘管有些其他欄位也是指定 16 個位元大小。實現時必須注意不能超過這個幀的大小值:64kb-1

type:1

資料負載型別,有效型別如下所示:

code 型別名稱 描述
0x01 init req 建立連線的第一個訊息
0x02 init res init req 訊息的響應
0x03 call req RPC 方法的請求
0x04 call res RPC 方法的響應
0x13 call req continue RPC 請求的分片
0x14 call res continue RPC 響應的分片
0xc0 cancel 取消未完全的呼叫請求/轉發請求(body 為空)
0xc1 claim 宣告/取消 冗餘請求
0xd0 ping req 這個用於健康檢查 (body 為空)
0xd1 ping res 響應 (body 為空)
0xff error TChannel 框架內部錯誤

成幀層對所有有效載荷都是通用的。它有意圖的限制幀大小為 64kb, 以便允許跨共享 TCP 連線更好地交錯幀 (interleaving of frames)。為了處理更多的操作實現,這將需要解碼 payload 有效載荷的上層協議

reserved:1

預留 1 個位元組,作為未來協議的擴充套件,目前暫不用

id:4

訊息 ID 佔 4 個位元組,它由請求的傳送端選擇,然後響應時返回同樣的訊息 ID。這個訊息 ID 僅僅是對於這個傳送者有效的。這類似於 TCP 在每個方向上具有序列號的方式。連線的每一端可能發生選擇訊息 ID 重疊現象,這個是 ok 的,因為他們是具有方向的

id代表最頂端的訊息 ID。請求幀和響應幀都是相同訊息 ID,用於訊息匹配。在請求被分解為片段序列之後,可以在多個請求或者響應幀上使用單個訊息 ID,如下所述:

訊息 ID 的有效值:00xFFFFFFFE。這個0xFFFFFFFF值被保留,用於協議錯誤響應

reserved:8

將來用於協議擴充套件,暫不用

payload

由協議幀的 type 欄位決定 body 的 0~N 個位元組內容。payload 的長度等於總幀大小-16 位元組

有關每一種 payload, 具體詳見下文

Payloads

init req(0x01)

schema: version: 2 nh: 2 (key~2 value~2){nh}

這個一定是建立連線的第一個訊息。它用於協商通用協議版本和描述兩端的服務名稱。將來,我們可能使用這個來協商身份認證和授權。

version

version是佔用兩個位元組的數字。當前這個指定版本協議為 2.如果新版本被要求,一個常用版本也是可以協商的。

headers

這裡有些鍵值對。對於版本號是 2 的版本,下面是必須要求的:

名稱 格式 描述
host_port address:port remote server
process_name 任意字串 這個例項的附加資訊,用於日誌
tchannel_language 任意字串 例如:"node", "python", "go"
tchannel_language_version 任意字串 語言版本號
tchannel_version 任意字串 當前使用的 tchannel 版本號

後面四個引數都是指呼叫方,也就是 client。

考慮到向後相容,實現應該忽略這五個之外的 key-value 列表

對於無法監聽新連線或者無意義的連線,實現應傳送host_port: 0.0.0.0。此特殊值告訴接收實現使用 getpeername(2) 或者等效 API 的結果來唯一標識此連線。它還告訴接收者這個連線之外的地址是無效的,因此不應該將其轉發到其他節點。

init res(0x02)

schema: version:2 nh:2 (key~2 value~2){nh}

請求建立時,client request 選擇了一個版本號,那麼 server 響應時也會使用這個版本號。這個 key-values 鍵值對同上

call req(0x03)

schema:

flags: 1 ttl: 4 tracing: 25
service~1 nh: 1(hk~1 hv~1){nh}
csumtype: 1(csum:4){0, 1} arg1~2 arg2~2 arg3~2

這個"call req" 是最主要的 RPC 機制,元組{arg1, arg2, arg3|在連線建立之後通過資料傳輸傳送到 server 中

不管是 client 直接連線到 server,還是通過 router 連線到的 server,這個服務名總是被指定的。這個支援顯式路由模型以及選擇將某些請求通過 router 委託給另一個服務,這兩者是等同的。

路由轉發可以在不理解訊息體中 body 的情況下,router payload。

一個"call req"可以被分成多個幀。這樣第一個幀的 type 為” call req“,接下來在其他幀叫做"call req continue"

flags:1

使用控制片段指令,有效值:

flag 描述
0x01 多幀片段
0x02 是請求流

如果 flag 沒有設定,它表示訊息 ID 是唯一或者最後一幀

如果 flag 設定為0x02, 表示流請求超時語義 (到第一響應幀的時間) 而不是非流語義 (時間到最後響應幀)。(原文:If the streaming flag is set, then streaming request timeout semantics (time to first response frame) apply instead of the non-streaming semantics (time to last response frame).)

flag=0x02, 這個幀只能是CallRequest或者CallResponse幀。如果這個幀設為CallRequestCont或者CallResponseCont幀,這是一個協議錯誤和無效 Cont 幀。(Cont:continue)

ttl:4

幀的生成時間 (Time To Live), 單位:毫秒。路由中轉時應該考慮酌情減少這個值。在tcp中,ttl表示路由跳轉數,每經過一個路由ttl減一, 這個 ttl 最小為 0。當 ttl 為 0 時,則這個請求永遠不會被髮送出去,同時生成一個 error 響應

tracing:25

Tracing 有效負載,詳見 tracing 部分

service~1

UTF-8 字串定義了應該被路由到的目標服務名,佔用 1 位元組

nh:1

傳輸頭部,在"Transport Headers"部分詳細描述

csumtype:1(csum:4){0,1}

Checksum 詳見"checksums"部分

arg1~2 arg2~2 arg3~2

這三個 args 的含義取決於每一端的系統。arg1, arg2 和 arg3 的格式沒有被指定,且每個 arg 都佔用兩個位元組。 就 TChannel 而言,這些事不透明的二進位制 blob。

這個arg1最大值可表示 16kb

未來版本可能允許 callers 指定具體的服務例項,去執行這個請求,或者將一定比例的所有流量路由到例項子集以進行 canary 分析的機制。

call res(0x04)

schema:

flags:1 code:1 tracing:25
nh:1 (hk~1 hv~1){nh}
csumtype:1 (csum:4){0,1} arg1~2 arg2~2 arg3~2

這個與call req(0x03)類似,不同在於:

  1. 增加了code欄位
  2. 沒有ttl欄位
  3. 沒有service欄位

所有公共欄位與"call req"都有相同的定義,請參閱上面的詳細資訊部分。對於 arg1 來說,"call req"與"call res"具有相同的值是沒有必要的;按照慣例,現有的實現將 arg1 保留以用於"call res"訊息

arg1最大值為 16kb

code:1

響應碼:

code 名稱 描述
0x00 OK 一切都是 ok 的
0x01 Error 應用程式錯誤,詳見 args

code 為非零值沒有暗示這個請求是否需要重試

實現應該用 unix 樣式的零/非零邏輯,以便將來對其他"not ok"程式碼安全

cancel(0xC0)

schema: ttl:4 tracing:25 why~2

這個訊息強制對一個"call req"的原響應,必須帶有一個 error 型別0x02

這個 cancel 訊息的 ID 必須能夠匹配"call req"幀。

注意,這個訊息 IDs 作用於一個連線上,cancel 訊息可能會出發一個或者多個依賴的訊息發生 cancel。

why欄位用於描述為何 cancel,僅僅用於日誌記錄

應該要注意的是,response 和 cancellation 訊息可以在連線中相互傳遞,因此我們需要能夠在 cancel 後處理響應。我們還需要能夠處於類似的原因處理重複的響應。如有必要,該網路的邊緣需要實施自己的重複資料刪除策略。

call req continue(0x13)

schema: flags:1 csumtype:1 (csum:4){0,1}{continuation})

這個幀繼續"call req"。具體描述見"fragmentation"部分

"flags"與"call req"型別有相同定義: 控制 flag

call res continue(0x14)

schema: csumtype:1 (csum:4){0, 1} {continuation}

這個幀繼續一個"call res",具體見"fragmentation"部分

claim(0xC1)

schema: ttl:4 tracing:25

此訊息用於宣告或者取消冗餘請求。當請求被髮送到多個節點,它開始工作或者工作完成時,根據選項,他們將告訴其他節點關,以減少額外的工作。此宣告訊息從工作節點到其他工作節點。這個宣告的請求由其完整的 zipkin 跟蹤資料引用,該資料由第一個請求的發起者選擇。

當一個節點 B 接收一個來自節點 A 的 claim 訊息,這個訊息帶有節點 B 不知道的 tracer T。B 在短時間內將會注意到這個。如果節點 B 接收到帶有 trace T 的請求延遲了,那麼節點 B 會靜默的忽略 tracer T。這個是對於大多數都是期望的,因為在傳送給 A 之後,且在傳送給 B 之前,路由將會增加一些延時。

在 Google 的"The Tail at Scale"論文中有實現,他們描述的"backup request with cross-server cancellation", 這個演講的 ppt 在這裡

在 page 39 頁,相關的視訊在這裡

這個 claim 不太理解,後續再看 ::TODO

ping req(0xD0)

這個訊息型別用於驗證協議在連線上是否正常執行。接收方將發回"ping res"訊息,但是預計應用程式不會看到此訊息。如果需要更加詳細的健康檢查和驗證檢查,可以使用"call req"和"clal res"訊息在更高階別實施這些檢查

此訊息型別沒有正文

ping res(0xD1)

總是對"ping req"的一個回應訊息。傳送這個訊息並不意味這服務就是健康的。它僅僅是驗證網路的連通性和協議的正常性

這個訊息型別沒有正文

error(0xFF)

schema: code:1 tracing:25 message~2

在這個協議上或者系統因為某種原因無法觸發 RPC 請求時,響應一個失敗訊息。應用級別的錯誤不再這裡體現,這裡的 error 是指 RPC 框架內部的錯誤。 應用或者業務的錯誤是儲存在"call res"訊息內指定的 args 異常資料中。

幀頭部的訊息 ID 應該是原始請求的訊息 ID,如果沒有訊息 ID 可用,則訊息 ID 設定為0xFFFFFFFF

code:1

內部響應碼:

code 名稱 描述
0x00 invalid 不使用
0x01 timeout 超時
0x02 cancelled 帶有 cancel 訊息
0x03 busy 節點忙,如果可以的話,這個請求可以進行重試
0x04 declined 因為某種原因拒絕(比如:負載、流控、黑名單等)
0x05 unexpected error 請求結果發生了異常錯誤
0x06 bad request 請求引數不匹配
0x07 network error 網路錯誤
0x08 unhealthy 節點不健康
0xFF fatal protocol error 協議不支援, 訊息 ID 為0xFFFFFFFF

tracing:25

Tracing payload, 見 tracing 部分。

message~2

這個訊息不打算向使用者展示。他面向錯誤日誌用以幫助工程師除錯日誌和錯誤。

Tracing

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

tracing 總位元組數 25 個。

欄位名 型別 描述
spanid int64 表示當前 span
parentid int64 當前 span 的父級 spanid
traceid int64 原始請求 traceid,也即呼叫鏈開端生成的跟蹤 id
traceflags uint8 bit flags 欄位

一個span是一個邏輯操作,如:call 或者 forward

這個traceid是一個完整呼叫鏈的追蹤,它在上下文傳播過程中始終保持不變的,無論經過多少服務

當一個請求進入到我們的系統時,且這個請求的 traceid 等於 0,則需要生成一個全域性唯一的 traceid,同時生成一個 spanid,如果節點處理請求的 client 帶有 span,則這個節點的 parentid 為上一個 spanid。

Trace flags:

flag 描述
0x01 為此請求開啟 trace

當這個 trace flag 沒有設定時,這個跟蹤資料仍然要求在這個幀出現,但是這個呼叫鏈資料將不會再 Zipkin 儲存中出現

Transport Headers

在傳輸和路由上,這些 headers 意圖控制一些東西,因此,期望付出的成本小。應用級別的 headers 是在協議的高層封裝的,也就是 payload 中封裝自己的業務資料協議。

重複 key 是不允許的,且應該爆出一個解析錯誤

Header 鍵不可以為 0,但是 header 值可以為 0

Header 鍵有一個最大長度 16 位元組;這個傳輸 header 的總數量最大允許 128 個鍵值對

schema: nh:1 (hk~1 hv~1){nh}

TChannel headers 有 0~N 個鍵值對,資料型別都是UTF-8的字串。

這個 headers 的數量填充到協議的第一個位元 (nh) 中

如果nh為 0,則表示這個頭部沒有位元

如果nh大於等於 1,則表示有多個 key/value 鍵值對

每一個鍵值對字串前面都有一個位元組編碼其長度。

例如,下面的 hex dump:

0103 6369 6402 6869 ..cid.hi

編碼後的鍵值對:("cid", "hi")

下面的表列出了有效傳輸 header 鍵,無論他們在"call req"或者"call res"中是有效還是無效的。下面詳細闡述:

name req res 描述
as Y Y arg schema
cas Y N Claim At Start
caf Y N Claim At Finish
cn Y N Caller Name
re Y N Retry Flags
se Y N Speculative Execution
fd Y Y Failure Domain
sk Y N Shard key
rd Y N Routing Delegate

Transport Header | (as —— Arg Scheme)

這個 header as是必須的

這描述了端點處理程式/協議檢查的 args 格式。這個最主要的 RPC 機制是使用"thrift", "http"和"json"被用於和其他系統的互動上。

下面表格描述as值和響應 arg1, arg2 和 arg3 形式:

  1. 對於as=thrift ,詳見 thrift arg scheme definition
  2. 對於as=sthrift, 詳見 streaming thrift arg scheme definition
  3. 對於as=json, 詳見 json arg scheme definition
  4. 對於as=http, 詳見 http arg scheme definition
  5. 對於as=raw, 詳見 raw arg scheme definition

Transport Header | (cas —— Claim At Start)

值為字串 "host:port"

這個請求也被髮送到另一個埠為host:port的例項。當工作開始時傳送一個 claim 訊息。

Trasport Header | (caf —— Claim At Finish)

值為字串 "host:port"

當 response 被髮送時,傳送 claim 訊息到埠為host:port的服務

Transport Header | ( cn —— Call Name)

這個cn是必須的

值是呼叫者的服務名。

Transport Header | (re —— Retry Flags)

Flags 是用 UTF-8 編碼的。每個 Flag 都是有單個字元表示。

flag 含義
n 不重試,直接暴露 error 錯誤. cancels 為 ct
c 連線錯誤時重試。這個具體重試機制沒有被 TChannel 指定。實際的重試是由路由層處理
t 超時重試

對於re的有效值如下:

value 描述
c 預設為連線錯誤時,重試
t 超時重試
n 不重試
ct 連線錯誤或者超時的重試
tc 連線錯誤或者超時的重試

Transport Header | (se —— Speculative Execution)

推測執行次數,編碼為單個數字 (<10). 表示執行請求的節點數

此版本協議中se的唯一有效值是 2.

將來我們可以將這個推測執行系統擴充套件到 2 個以上的節點。為了簡化和最大限度地減少混淆,我們有意將se限制為 2.

Transport Header | (fd —— Failure Domain)

描述對同一服務的一組相關請求的字串,如果他們失敗,則可能以相同的方式失敗。

例如,某些請求可能具有對另一個服務的下游依賴,而其他請求可能完全在所請求的服務內處理。這用於實現類似 Hystrix 的"斷路器"行為。

Transport Header | (sk —— Shard Key)

這個 Shard 鍵確定這個"call req"來自哪裡。如果你想要一個 TChannel 環,請求轉到特定節點,你可以設定一個 Shard 鍵 header

ringpop 使用此sk頭,將"call req"傳遞給特定的 TChannel 例項。

例如,你可以希望保留一些記憶體狀態,即快取,聚合。你可以使用sk來讀取並將呼叫請求轉發給具有該分片鍵所有權的特定程式

Transport Header | (rd —— Routeing Delegate)

這個路由委託請求頭必須設定為一個有效的服務名

當路由委託頭部設定為我們將要路由的服務名,去替代 call request 的服務名

我們期望在rd頭的服務名,能夠讓我們正確地找到目標服務

有關host:port的注意事項

這些host:port欄位全部都是 string 型別,它們可以提供其他未直連的實體可以回到正確的地址。

原文:While these host:port fields are indeed strings, the intention is to provide the address where other entities not directly connected can reach back to this exact entity.

在 IPv4 協議中,這些host:port欄位應該是 IPv4 地址。類似於127.0.0.1:8080

這個欄位不應該使用 hostname 或者 DNS 名稱,除非我們找到令人信服的理由來使用它們。

例子:

  1. 10.0.0.1:12345 —— IPv4 host:port
  2. [1fff:0:a88:8583:ac1f]:8001 —— IPv6 host:port

注意,IPv6 地址在地址部分也有冒號,因此實現應使用字串中的最後一個冒號將地址和埠分開。

Deadline 與 TTL

這個 TTL 是指呼叫服務完成一次呼叫的允許最大等待時間。一個例項只要滿足 deadline,就可以重試。在上下文傳播時,當呼叫鏈達到下游節點後,TTL 需要減掉這個請求已經耗費掉的時間。為網路延新增一些屬性可以幫助檢測網路節點的水位情況。

TTLs 不會在響應時帶上。它是有呼叫方跟蹤整個連線的時間消耗,並在分發新的子請求之間驗證新的 deadline。

Checksums

為了簡化在不同語言不同平臺下的實現,Checksums 是可選的。當 TCP 在連線內提供了 checksum,TChannel payloads 會意圖進行多 TCP 連線傳輸,增加併發量。通過在源頭進行 checksum,中轉和目的地都能夠進行 payload 校驗,防止資訊丟失或者篡改。驗證這些 checksums 和拒絕無效幀都是期望的,但不是必須的。

Checksums 通過三個引數進行計算,如下所示:

csum = func(arg1, 0);
csum = func(arg2, csum);
csum = func(arg3, csum);

當訊息被分片時,這個行為會有輕微的變化。具體見"fragmentation"

Checksum 型別:

type:1 scheme 值長度
0x00 none 0 bytes
0x01 crc-32 4 bytes
0x02 farmhash Fingerprint32 4 bytes
0x03 crc-32C 4 bytes

這裡不展開了。

Fragmentation

這裡有兩個重要且可能重疊的案例需要支援 Fragmentation:

  1. args 不適合單個幀的大訊息;建立請求時未知總大小的訊息,類似於 HTTP 的分塊編碼;
  2. 單個幀的最大大小有意限制為 64kb,以避免共享 TCP 連線上的行頭阻塞。此大小限制允許來自其他訊息的幀與較大訊息的幀交織。限制大小也會降低對任何中間節點的緩衝要求,並且在某些實現上可能支援固定大小分配的使用

所有的"call req"/"call res"在 checksum 之前,arg1, arg2 和 arg3 必須都在單個幀中

傳送這些欄位後,checksum args 被髮送,或者大多數 args 都在這個幀中。如果這個幀小於 64kb,這個幀是完整的,並且 “more fragments follow” 的 flag 不會被設定。然而,如果這個幀的空間不夠以適應 checksum 和 args,那麼這個訊息會分成多個子請求"call req continue". 並在最後一個子請求上, "more fragments follow"的 flag 不會被設定,這個訊息是完整的

可能一開始給定 args 還不能確定這個是否要進行分片操作,幀中欄位長度表示存在多少個 args。當超過該 arg 結尾的幀中有更多資料時,arg 被認為是完整的。如果 arg 碰巧在精確的幀邊界上結束,則下一個連續幀將以該 arg 的 0 位元組大小開始

Checksums 是對幀的內容計算的,這個內容是 args 資料。每個幀的 checksum 來自先前幀的累積校驗和 seed。這允許在將 arg 有效負載傳遞給應用程式之前檢測到損壞。

Example

傳送一個"call req", 帶有 4 個位元組的 arg1,2 個位元組的 arg2 和 8 個位元組的 arg3. 這個 args 以極小的片段形成流,在這個分片例子中是很容易理解的。如果一個生產者進行流式更新,這可能在現實世界發生這種情況。如果預先知道總髮送大小,則優選傳送最大幀。

幀 1 是"call req"(0x03) 帶有"more fragments remain"的 flag 設定。這個 ttl, tracing, traceflags, service 和 headers 都設定為合理的值。我們有 2 個位元組已達到 arg1,但是還有差 2 個位元組。我們計算帶有種子 0 的 arg1 2 個位元組 checksum,我們指定 2 個位元組的長度,這個幀最後 2 個位元組的資料流。這個接收解析這注意到這個 arg1 是不完整的。

幀 2 是一個"call req continue"(0x13) 帶有"more fragments remain"的 flag 設定,填充另外 2 個位元組的 arg1 現在可用了。且 arg2 兩個位元組已填充。這個 checksum 會計算這個幀的 arg1 和 arg2 使用前陣幀的種子 0xBEEF. 這個接收解析這知道,它需要繼續 arg1. Arg1 在這個幀中有兩個位元組,但是這個幀仍然需要更多的位元流,因此這個接收解析器知道 arg1 現在是完整的。這個幀的結尾處還有 arg2 的 2 個位元組。接受解析器知道 arg2 是不完整的,甚至我們知道在 arg2 中沒有更多的位元流了。這樣做是為了說明當 arg 在精確的框架邊界上結束時會發生什麼。

幀 3 是一個"call req continue"(0x23),這個"more fragments remain"的 flag 被清除了。arg2 是 0 位元,arg3 是 8 個位元組。這個 checksum 是以 arg3 的 8 個位元組和前面幀的 0xDEAD 作為種子計算的。這個接收解析器知道,arg2 是 0 個位元組,表示 arg2 傳輸完成了。arg3 是 8 個位元組,且相應的 flag 被清除了,說明這個幀已經是最後一個了,那麼 arg3 的資料也傳輸完成了。

type id payload 解析後的狀態
0x03 1 flags: 1=0x01, ttl 4:=0x2328, tracing:24 = 0x1,0x2,0x3, traceflags: 1= 0x1, service1 = 0x5 "svcA", nh: 1=0x1, hk1=0x1"k", hv1=0xA"abcdefghij", csumtype:1=0x2 csum:4=0xBEEF arg12=0x2<2 bytes> sending arg1
0x13 1 flags:1=0x1, csumtype:1=0x2 csum:4=0xDEAD arg12=0x2<2 bytes> arg22=0x2<2 bytes> sending arg2
0x23 1 flags:1=0x0, csumtype:1=0x2 csum:4=0xF00F arg22=0x0<0 bytes> arg32=0x8<8 bytes> complete

Streaming

後面有時間在翻譯::TODO

參考資料

tchannel official docs

更多原創文章乾貨分享,請關注公眾號
  • Jaeger TChannel ——protocol
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章