一、overview
有讚的自研版 NSQ 在高可用性以及負載均衡方面進行了改造,自研版的 nsqd 中引入了資料分割槽以及副本,副本儲存在不同的 nsqd 上,達到容災目的。此外,自研版 NSQ 在原有 Protocol Spec 基礎上進行了擴充,支援基於分割槽的訊息生產、消費,以及基於訊息分割槽的有序消費,以及訊息追蹤功能。
為了充分支援自研版 NSQ 新功能,在要構建 NSQ client 時,需要在相容原版 NSQ 的基礎上,實現額外的設計。本文作為《Building Client Libraries》的擴充,為構建有贊自研版 NSQ client 提供指引。
參考 NSQ 官方的構造 client 指南的結構,接下來的文章分為如下部分:
1 workflow 及配置 2 nsqd 發現服務 3 nsqd 建連 4 傳送/接收訊息 5 順序消費
本文根據有贊自研版 nsq 的新特性,對 nsq 文件[1]中構建 nsq client 的專題進行補充。在閱讀《Building Client Libraries》的基礎上閱讀本文,更有助於理解。
二、workflow 及配置
通過一張圖,瀏覽一下 nsq client 的工作流程。
client 作為訊息 producer 或者 consumer 啟動後,負責 lookup 的功能通過 nsqlookupd 進行 nsqd 服務發現。對於服務發現返回的 nsqd 節點,client 進行建連操作以及異常處理。nsq 連線建立後,producer 進行訊息傳送,consumer 則監聽埠接收訊息。同時,client 負責響應來自 nsqd 的心跳,以保持連線不被斷開。在和 nsqd 訊息通訊過程中,client 通過 lookup 發現,持續更新 nsq 叢集中 topic 以及節點最新資訊,並對連線做相應更新操作。當訊息傳送/消費結束時,client 負責關閉相應 nsqd 連線。文章在接下來討論這一流程中的關鍵步驟,對相應功能的實現做更詳細的說明。
2.1 配置 client
自研版 NSQ 改造自開源版 NSQ,繼承了開源版 NSQ 中的配置。[^1]中 Configuration 段落的內容適用於有贊自研版。唯一需要指出的地方是,開源版 nsq 將使用 nsqlookupd 作為 nsqd 服務發現的一個可選項,基於配置靈活性的考量,開源版 NSQ 允許 client 通過 nsqd 的地址直接建立連線,自研版 NSQ 由於支援動態負載,nsqd 之間的主從關係在叢集中發生切換的時候,需要依賴自研版的 nsqlookupd 將變更資訊反饋給 nsq client。基於此,使用 nsqlookupd 進行服務發現,在自研版 NSQ 中是一個“標配”。我們也將在下一節中對服務發現過程做詳細的說明。
三、nsqd 發現服務
開源版中,提供 nsqd 發現服務作為 nsqlookupd 的重要功能,用於向訊息的 consumer/producer 提供可消費/生產的 nsqd 節點。上文提到,區別於開源版本,自研版的 nsqlookupd 將作為服務發現的唯一入口。nsq client 負責呼叫 nsqlookupd 的 lookup 服務,並通過 poll,定時更新訊息在 nsq 叢集上的讀寫分佈資訊。根據 lookup 返回的結果,nsq client 對 nsqd 進行建連。
在自研版中訪問 lookup 服務的方式和開源版一樣簡單直接,向 nsqlookupd 的 http 埠 GET:/lookup?topic={topic_name} 即可完成。不同之處在於,自研版本 NSQ 的 lookup 服務中支援兩種新的查詢引數:
GET:/lookup?topic={topic_name}&access={r or w}&metainto=true
其中:access 用於區分 nsq client 的生產/消費請求。代表 producer 的 lookup 查詢中的引數為 access=w,consumer 的 lookup 查詢為 access=r。metainfo 引數用於提示 nsqlookup 是否返回查詢 topic 的分割槽後設資料,包括查詢 topic 的分割槽總數,以及副本個數。順序消費時,prodcuer 通過返回的分割槽後設資料來判斷 lookup 響應中返回的分割槽數是否完整,順序消費和生產的詳細部分我們將在傳送訊息章節中討論。client 在訪問 lookup 服務時,根據 prodcuer&consumer 角色的差別可以使用兩類查詢引數
Producer GET:/lookup?topic=test&access=w&metainfo=true
Consumer GET:/lookup?topic=test&access=r
在設計上,由於 metainfo 提供的資訊是為 producer 在順序消費場景下的生產,為了減少 nsqlookupd 服務壓力,代表 consumer 的 lookup 查詢無需攜帶 metainfo 引數。自研版 lookup 的響應和開原版本相容:
{ "status_code":200, "status_txt":"OK", "data":{ "channels":[ "BaseConsumer" ], "meta":{ "partition_num":1, "replica":1 }, "partitions":{ "0":{ "id":"XX.XX.XX.XX:33122", "remote_address":"X.X.X.X:33122", "hostname":"host-name", "broadcast_address":"XX.XX.XX.XX", "tcp_port":4150, "http_port":4151, "version":"0.3.7-HA.1.5.4.1", "distributed_id":"XX.XX.XX.XX:4250:4150:338437" } }, "producers":[ { "id":"XX.XX.XX.XX:33122", "remote_address":"XX.XX.XX.XX:33122", "hostname":"host-name", "broadcast_address":"XX.XX.XX.XX", "tcp_port":4150, "http_port":4151, "version":"0.3.7-HA.1.5.4.1", "distributed_id":"XX.XX.XX.XX:4250:4150:338437" } ] }}複製程式碼
3.1 lookup 流程
client 的 lookup 流程如下:
自研版 nsqlookupd 新增了 listlookup 服務,用於發現接入叢集中的 nsqlookupd。nsq client 通過訪問一個已配置的 nsqlookupd 地址,發現所有 nsqlookupd。獲得 nsqlookupd 後,nsq client 遍歷得到的 nsqlookupd 地址,進行 lookup 節點發現。nsq client 合併遍歷獲得的節點資訊並返回。nsq client 在訪問 listlookup 以及 lookup 服務失敗的場景下(如,訪問超時),nsq client 可以嘗試重試。lookup 超過最大重試次數後依然失敗的情況,nsq client 可以降低訪問該 nsqlookupd 的優先順序。client 定期查詢 lookup,保證 client 更新連線到有效的 nsqd。
四、nsqd 建連
自研版 nsqd 在建連時遵照[^1]中描述的建連步驟,通過 lookup 返回結果中 partitions 欄位中的{broadcastaddress}:{tcpport}建立 TCP 連線。自研版中,一個獨立的 TCP 連線對應一個 topic 的一個分割槽。consumer 在建連的時候需要建立與分割槽數量對應的 TCP 連線,以接收到所有的分割槽中的訊息。client 的基本建連過程依然遵守[^1]中的 4 步:
client 傳送 magic 標誌
client 傳送 IDENTIFY command 並處理返回結果
client 傳送 SUB command (指定目標的 topic 以及分割槽), 並處理返回結果
client 傳送 RDY 命令
client 通過自研版 NSQ 中擴充的SUB命令,連線到指定 topic 的指定分割槽。
SUB <topic> <channel_name> <topic_partition> \ntopic_name -- 消費的 topic 名稱topic_partition -- topic 的合法分割槽名channel_name -- channel複製程式碼
Response:
OK複製程式碼
Error response:
E_INVALIDE_BAD_TOPICE_BAD_CHANNELE_SUB_ORDER_IS_MUST 當前 topic 只支援順序消費複製程式碼
client 在建連過程中,向 lookup 返回的每一個 nsqd partition 傳送該命令。SUB 命令的出錯響應中,自研版本 NSQ 中加入了最後一個錯誤程式碼,當 client SUB 一個配置為順序消費的 topic 時,client 會收到該錯誤。相應的攜帶分割槽號的 PUB 命令格式為:
PUB <topic_name> <topic_partition>\n[ 4-byte size in bytes ][ N-byte binary data ]topic -- 合法 topic 名稱partitionId -- 合法分割槽名複製程式碼
Response:
OK複製程式碼
Error response:
E_INVALIDE_BAD_TOPICE_BAD_MESSAGEE_PUB_FAILEDE_FAILED_ON_NOT_LEADER 當前嘗試寫入的 nsqd 節點在副本中不是 leaderE_FAILED_ON_NOT_WRITABLE 當前的 nqd 節點禁止寫入訊息E_TOPIC_NOT_EXIST 向當前連線中寫入不存在的 topic 訊息複製程式碼
自研版本 NSQ 中加入了最後三個錯誤程式碼,分別用於提示當前嘗試寫入的 nsqd 節點在副本中不是 leader,以及當前的 nqd 節點禁止寫入。client 在接收到錯誤的時候,應該直接關閉 TCP 連線,等待 lookup 定時查詢更新 nsqd 節點資訊,或者立刻發起 lookup 查詢。如果沒有傳入 partition id, 服務端會選擇預設的 partition. 客戶端可以選擇 topic 的 partition 傳送演算法,可以根據負載情況選擇一個 partition 傳送,也可以固定的 Key 傳送到固定的 partition。client 在消費時,可以指定只消費一個 partition 還是消費所有 partition。每個 partition 會建立獨立的 socket 連線接收訊息。client 需要處理多個 partition 的 channel 消費問題。
五、傳送/接收訊息
主要討論生產者和消費者對訊息的處理。
5.1 生產者傳送訊息
client 的訊息生產流程如下:
建連過程中,對於訊息生產者,client 在接收到對於 IDENTITY 的響應之後,使用 PUB 命令向連線傳送訊息。client 在 PUB 命令後需要解析收到的響應,出現 Error response 的情況下,client 應當關閉當前連線,並向 lookup 服務重新獲取 nsqd 節點資訊。client 可以將 nsqd 連線通過池化,在生產時進行復用,連線池中指定 topic 的連線為空時,client 將初始化該連線,因失敗而關閉的連線將不返回連線池。
5.2 消費者接收訊息
client 的消費流程如下:
client 在和 nsqd 建立連線後,使用非同步方式消費訊息。nsqd 定時向連線中傳送心跳相應,用於檢查 client 的活性。client 在收到心跳幀後,向 nsqd 回應 NOP。當 nsqd 連續兩次傳送心跳未能收到回應後,nsqd 連線將在服務端關閉。參考[^1]中訊息處理章節的相關內容,client 在消費訊息的時候有如下情景:
訊息被順利消費的情況下,FIN 通過 nsqd 連線傳送;
訊息的消費失敗的情況下,client 需要通知 nsqd 該條訊息消費失敗。client 通過 REQ 命令攜帶延時引數,通知 nsqd 將訊息重發。如果消費者沒有指定延時引數,client 可以根據訊息的 attempt 引數,計算延時引數。client 可以允許消費者指定當某條訊息的消費次數超過指定的次數,client 是否可以將該條訊息丟棄,或者重新傳送至 nsqd 訊息佇列尾部去後,再 FIN;
訊息的消費需要更多的時間,client 傳送 TOUCH 命令,重置 nsqd 的超時時間。TOUCH 命令可以重複傳送,直到訊息超時,或者 client 傳送 FIN,REQ 命令。是否傳送 TOUCH 命令,決定權應當由消費者決定,client 本身不應當使用 TOUCH;
下發至 client 的訊息超時,此時 nsqd 將重發該訊息。此種情況下,client 可能重複消費到訊息。消費者在保證訊息消費的冪等性的同時,對於重發訊息 client 應當能夠正常消費;
5.3 訊息 ACK
自研版本 NSQ 中,對原有的訊息 ID 進行了改造,自研版本中的訊息 ID 長度依然為 16 位元組:
[8-byte internal id][8-byte trace id]複製程式碼
高位開始的 16 位元組,是自研版 NSQ 的內部 ID,後 16 位元組是該條訊息的 TraceID,用於 client 實現訊息追蹤功能。
六、順序消費
基於 topic 分割槽的訊息順序生產消費,是自研版 NSQ 中的新功能。自研版 NSQ 允許生產者通過 shardingId 對映將訊息傳送到固定 topic 分割槽。建立連線時,消費者在傳送 IDENTIFY 後,通過新的 SubOrder 命令連線到順序消費 topic。順序消費時,nsqd 的分割槽在接收到來自 client 的 FIN 確認之前,將不會推送下一條訊息。
在 nsqd 配置為順序消費的 topic 需要 nsq client 通過 SubOrder 進行消費。向順序消費 topic 傳送 Sub 命令將會收到錯誤資訊,同時連線將被關閉。
E_SUB_ORDER_IS_MUST複製程式碼
client 在啟動消費者前,可以通過配置指導 client 在 SUB 以及 SUBORDER 命令之間切換,或者基於 topic 進行切換。SUBORDER 在 TCP 協議的格式如下:
SUB_ORDER <topic_name> <channel_name> <topic_partition>\ntopic_name -- 進行順序訊息的 topic 名稱channel_name -- channel 名稱topic_partition -- topic 分割槽名稱複製程式碼
NSQ 新叢集中,訊息的順序生產/消費基於 topic 分割槽。訊息生產者通過指定 shardingID,向目標 partition 傳送訊息;訊息消費者通過指定分割槽 ID,從指定分割槽接收訊息。Client 進行順序消費時時,TCP 連線的 RDY 值相當於將被 NSQ 服務端指定為 1,在當前訊息 Finish 之前不會推送下一條。NSQ 伺服器端 topic 進行強制消費配置,當消費場景中日誌出現錯誤訊息時,說明該 topic 必須進行順序消費。
順序消費的場景由訊息生產這個以及訊息消費者兩方的操作完成:
訊息生產者通過 SUB_ORDER 命令,連線到 Topic 所在的所有 NSQd 分割槽;
訊息消費者通過設定 shardingID 對映,將訊息傳送到指定 NSQd 的指定 partition,進行生產;
6.1 順序消費場景下的訊息生產
client 在進行訊息生產時,將攜帶有相同 shardingID 的訊息投遞到同一分割槽中,分割槽的訊息則通過 lookup 服務發現。作為生產者,client 在 lookup 請求中包含 metainfo 引數,用於獲得 topic 的分割槽總數。client 負責將 shardingID 對映到 topic 分割槽,同時保證對映的一致性:具有相同的 shardindID 的訊息始終被投遞到固定的分割槽連線中。當 shardingID 對映到的 topic 分割槽對於 client 不可達時,client 結束髮送,告知生產者返回錯誤資訊,並立即更新 lookup。
6.2 順序消費場景下的訊息消費
client 在進行訊息消費時,通過 SUB_ORDER 命令連線到 topic 所有分割槽上。順序消費的場景中,當某條消費的訊息超時或 REQUEUE 後,nsq 將會立即將該條訊息下發。訊息超時或者超過指定重試次數後的策略由消費者指定,client 可以對於重複消費的訊息列印日誌或者告警。
七、訊息追蹤功能的實現
新版 NSQ 通過在訊息 ID 中增加 TraceID,對訊息在 NSQ 中的生命週期進行追蹤,client 通過 PUBTRACE 新命令將需要追蹤的訊息傳送到 NSQ,PUB_TRACE 命令將包含 traceID 和訊息位元組碼,格式如下:
PUB_TRACE <topic_name> <topic_partition>\n[ 4-byte size in bytes ][8-byte size trace id][ N-byte binary data ]複製程式碼
相較於 PUB 命令,PUB_TRACE 在訊息體中增加了 traceID 欄位,client 在實現時,傳遞 64 位整數。NSQ 對 PUB_TRACE 的響應格式如下:
OK(2-bytes)+[8-byte internal id]+[8-byte trace id from client]+[8-byte internal disk queue offset]+[4 bytes internal disk queue data size]複製程式碼
client 可通過配置或者動態開關,開啟或關閉訊息追蹤功能,讓生產者在 PUB 和 PUB_TRACE 命令之間進行切換。為了得到完整的 Trace 資訊,建議 client 在生產者端列印 PUB_TRACE 響應返回的資訊,在消費者端列印收到訊息的 TraceID 和時間。
八、總結
本文結合自研版 NSQ 新特性,討論了構建支援服務發現、順序消費、訊息追蹤等新特性的 client 過程中的一些實踐。
九、參考資料
[1] NSQ TCP Protocol Spec:http://nsq.io/clients/tcp_protocol_spec.html
[2] Building Client Libraries:http://nsq.io/clients/building_client_libraries.html
[3] NSQ-Client-Java in youzan:https://github.com/youzan/nsqJavaSDK
[4] NSQ in youzan:https://github.com/youzan/nsq