Skip to content

Zenoh 協定

zenoh-dissector

如果想要學習 Zenoh Protocol,那我們一定需要使用 zenoh-dissector 這個 wireshark plugin 來看。

  • 先確保已經有安裝 wireshark
sudo add-apt-repository ppa:wireshark-dev/stable
sudo apt update
sudo apt install wireshark
  • GitHub Release page 下載對應自己作業系統的 library
  • 解壓縮後會看到 libzenoh_dissector.so 這個檔案,要放到對應的位置,這邊是以 4.6.x 為例,你要放到對應自己版本的資料夾
mkdir -p ~/.local/lib/wireshark/plugins/4.6/epan
cp libzenoh_dissector.so ~/.local/lib/wireshark/plugins/4.6/epan/libzenoh_dissector.so
  • 開啟 wireshark 之後,就可以輸入 zenoh 來查看 protocol
  • 我們可以用 Zenoh 最基本的範例來觀察,注意的是目前預設 Zenoh plugin 只會追蹤 port 7447,所以讓我們特別指定 listener 和 connector
./z_sub -l tcp/127.0.0.1:7447
./z_pub -e tcp/127.0.0.1:7447

Zenoh 協定

基本上 Zenoh 的協定可以分成兩種:

  • Scouting:用來發現不同節點的協定,預設會監聽 multicast 的位址 224.0.0.224:7446
  • Session / Data:用在一般傳輸上面,一般來說我們會使用 port 7447

如果用流程來理解,大致可以分成這幾段:

  • Scouting
  • Session Establishment
  • Session State Synchronization
  • Data Plane
  • Transport Maintenance
  • Multicast

Scouting

在 Zenoh 中,不同節點可以不需要輸入固定 IP 位址就能彼此通訊,這就是依靠 Scouting 的機制。 Scouting 會廣播自己的資訊給其他人,其他節點就可以依靠這個資訊來建立 Zenoh 的連線。

Scouting 的流程大概如下:

A                   B                   C
|       SCOUT       |                   |   (multicast/broadcast)
|─────────────────>|                   |
|         \──────────────────────────>|
|                   |                   |
|       HELLO       |                   |   (unicast, if B matches)
|<─────────────────|                   |
|                   |      HELLO        |   (unicast, if C matches)
|<──────────────────────────────────── |

A 會透過 multicast 或是 broadcast 發送 SCOUT 的訊息給其他人, 如果 B 或 C 有相對應的 Zenoh 服務,就會用 unicast 回傳 HELLO 的訊息給 A

參考:

除了回應 SCOUT 之外,節點也可以主動週期性送出 HELLO 來宣告自己的存在。 這樣即使沒有先收到 SCOUT,其他節點也有機會得知目前網路上的 Zenoh 節點。

A                   B                   C
|                   |                   |
|                   |      HELLO        |   (periodic multicast / broadcast)
|<─────────────────|                   |
|                   |                   |
|<──────────────────────────────────── |

參考:

Session Establishment

Session establishment 指的是 unicast 情境下,雙方怎麼從「知道彼此存在」進入可傳輸資料的狀態。

  • A 先發送 INIT SYN 給 B,B 回覆 INIT ACK,用來確認雙方的 protocol version、ZenohID 與參數
  • 接著 A 發送 OPEN SYN,B 回覆 OPEN ACK
  • 到這一步之後,session 才算正式建立
A                           B
|                           |
|  INIT SYN  (A=0)          |   propose version, ZID, parameters
|─────────────────────────>|
|          INIT ACK  (A=1)  |   accept + cookie
|<─────────────────────────|
|  OPEN SYN  (A=0)          |   echo cookie + propose lease / initial_sn
|─────────────────────────>|
|          OPEN ACK  (A=1)  |   confirm; session is now active
|<─────────────────────────|

參考:

Session State Synchronization

Zenoh 不只是建立 session 之後就開始送資料,還需要同步目前的 session 狀態。 這一層最重要的是 DECLAREINTEREST

DECLARE / INTEREST

DECLARE 用來宣告 key expression alias、subscriber、queryable、liveliness token。 比較晚加入的節點如果要知道現有狀態,就會送出 INTEREST

常見流程如下:

A                                 B
|                                 |
|  INTEREST                       |   ask for current declarations
|────────────────────────────────>|
|                                 |
|                 DECLARE (I=1)   |   D_KEYEXPR / D_SUBSCRIBER / ...
|<────────────────────────────────|
|                 DECLARE (I=1)   |   more declarations if needed
|<────────────────────────────────|
|                                 |
|                 DECLARE (I=1)   |   D_FINAL
|<────────────────────────────────|

可以把它理解成:

  • INTEREST:我要知道目前有哪些宣告
  • DECLARE(I=1):這些是對應的宣告內容
  • D_FINAL:這一批 current declarations 已經送完

參考:

為何只 declare subscriber / queryable

這裡有一個很值得特別說明的設計:Zenoh 會要求 session 內宣告 subscriberqueryabletoken,但不要求 publisherquerier 先 declare。 因為真正會影響路由決策的只有「誰想接收資料」與「誰能回答 query」,也就是 subscriberqueryable。 因此我們可以忽略 publisherquerier 來減少 control plane 設計的複雜度。

Data Plane

真正的資料傳輸主要可以拆成 PUSHQUERY / REPLY 兩條路徑。

PUSH

PUSH 是最常見的 publish path,body 會是 PUTDEL

Publisher                      Subscriber
|                               |
|  FRAME                        |
|    PUSH                       |   body = PUT or DEL
|──────────────────────────────>|

如果是 low-latency unicast session,PUSH 也可能不經過 FRAME,而是直接以 network message 傳送。

參考:

QUERY / RESPONSE / RESPONSE_FINAL

Query plane 和一般 pub/sub 不同,一個 REQUEST 可能對應多個 RESPONSE,最後再用 RESPONSE_FINAL 收尾。

Querier                         Queryable B              Queryable C
|                               |                        |
|  REQUEST                      |                        |
|──────────────────────────────>|                        |
|───────────────────────────────────────────────────────>|
|                               |                        |
|         RESPONSE              |                        |
|<──────────────────────────────|                        |
|                               |         RESPONSE       |
|<───────────────────────────────────────────────────────|
|                               |                        |
|         RESPONSE_FINAL        |                        |
|<──────────────────────────────|                        |

這個流程的重點是:

  • 一個 REQUEST 可以收到多個 RESPONSE
  • RESPONSE 的 body 會是 REPLYERR
  • RESPONSE_FINAL 表示這次 query 的回覆流結束

參考:

Transport Maintenance

Session 建好之後,transport 還需要持續維護這個連線,並在必要時處理大封包。

KEEP_ALIVE / CLOSE

最基本的維護流程如下:

A                           B
|                           |
|  FRAME / direct network   |   low-latency sessions may skip FRAME batching
|  messages                 |
|<────────────────────────>|
|  KEEP_ALIVE               |   every lease/4
|<────────────────────────>|
|                           |
|  CLOSE (when done)        |
|─────────────────────────>|

參考:

FRAGMENT

如果單一 payload 太大,Zenoh 可能無法把所有內容塞進同一個 FRAME,這時就會用 FRAGMENT 來切片。

A                           B
|                           |
|  FRAGMENT seq=n           |   first chunk
|─────────────────────────>|
|  FRAGMENT seq=n+1         |   next chunk
|─────────────────────────>|
|  FRAGMENT seq=n+2         |   final chunk
|─────────────────────────>|

在 wire format 上,FRAGMENT 裡承載的是 raw fragment bytes,而不是完整的 network message 結構。

參考:

Multicast

前面的 session establishment 主要是在講 unicast。 如果是 multicast transport,Zenoh 會使用 JOIN 而不是 INIT / OPEN

JOIN

JOIN 可以視為 multicast 情境下的 session announcement / parameter synchronization。

A                           B                   C
|                           |                   |
|          JOIN             |                   |   multicast
|─────────────────────────>|                   |
|           \────────────────────────────────>|
|                           |                   |

JOIN 會攜帶:

  • version
  • ZenohID
  • lease
  • next reliable sequence number
  • next best-effort sequence number
  • 可選的 resolution / batch size

因此它比較接近「我現在以這組 multicast transport 參數加入這個群組」的訊息,而不是一般 unicast 的 request / ack 握手。

參考:

Router 與 Router 的轉傳

如果我們的網路有多個 Zenoh Router,那他是如何彼此知道對有連接哪些節點的? 下面幾個名詞跟這個機制有關:

  • gossip:Router 和 Router 彼此交換資訊的機制
  • link-state:實際散播的內容
  • graph / tree / next hop:router 內部算出來的結果

gossip 的流程

zenoh 原始碼來看,gossip 沒有一個獨立的新封包型別,也沒有一套和 INIT / OPEN 類似的獨立 handshake。 它實際上是把 LinkStateList 包進 Transport OAMOAM_LINKSTATE body,然後送給其他 router / peer。

常見流程可以想成:

R1                           R2                           R3
|                            |                            |
|  OAM_LINKSTATE             |                            |
|───────────────────────────>|
|                            |                            |
|                            |  OAM_LINKSTATE            |   if gossip / multihop enabled
|                            |───────────────────────────>|

新 link 建立時,router 也會把目前已知的 node 狀態送給新鄰居。 如果開了 gossip autoconnect,而且 message 裡帶有 locators,實作還可能依此主動去連新的 peer / router。

參考:

  • Source / Gossip message creation: zenoh/src/net/protocol/gossip.rs
  • Source / Gossip receive path: zenoh/src/net/protocol/gossip.rs
  • Source / Gossip add_link behavior: zenoh/src/net/protocol/gossip.rs

link-state 才是 router graph 的原始資料。 每個 LinkState 會描述:

  • 這個 node 的 psid
  • sequence number
  • 可選的 zid
  • 可選的 locators
  • 它目前有哪些鄰居 links
  • 可選的 link weights

本地 router 收到 LinkStateList 後,會:

  1. 先把 remote psid 轉成本地可識別的 zid
  2. 更新本地 graph 裡的 node
  3. links 新增 / 更新 edge
  4. 再重新計算 routing tree

也就是說,router graph 不是從資料流量反推,而是直接從 link-state 重建。

參考:

  • Source / LinkState definitions: zenoh/src/net/protocol/linkstate.rs
  • Source / apply link-state to graph: zenoh/src/net/protocol/network.rs
  • Source / psid <-> zid mapping: zenoh/src/net/protocol/network.rs

如何避免 loop

避免 loop 的主因,不是 gossip 本身,而是 router 在 graph 建好之後會先算出:

  • tree
  • next hop

之後資料只沿著算好的 next hop 前進,不會任意 flood 到所有鄰居。 這就是為什麼實作上能避免 R1 -> R2 -> R3 -> R1 這種大圈。

NodeId 還是有用,但它比較像第二層保護:

  • 它記錄上一跳的 routing context
  • 用來避免把 message 立刻送回來源 session

所以可以把它理解成:

  • graph / tree / next hop
    • 防止大圈 loop
  • NodeId
    • 防止逐跳回彈

參考:

  • Source / compute trees: zenoh/src/net/protocol/network.rs
  • Source / routers network uses full link-state: zenoh/src/net/routing/hat/router/mod.rs
  • Source / compute routes after tree changes: zenoh/src/net/routing/hat/router/mod.rs
  • Source / pubsub next-hop lookup: zenoh/src/net/routing/hat/router/pubsub.rs
  • Source / query next-hop lookup: zenoh/src/net/routing/hat/router/queries.rs
  • Source / per-hop NodeId mapping: zenoh/src/net/routing/hat/router/mod.rs

參考: