Zenoh:互联网计算领域的下一个重大突破

Zenoh:互联网计算领域的下一个重大突破

Zenoh 官方在 What is Zenoh? 开篇直接指出,Zenoh 将是互联网计算领域的下一个重大突破。

从技术角度看,Zenoh 是一种发布 / 订阅 / 查询协议(pub/sub/query protocol),它把流动中的数据(data in motion)、静态数据(data at rest)和计算(computations)统一到同一套通信抽象中。换一种更形象的说法,Zenoh 可以被理解为一种"数据解放"协议:它试图从多个维度打破数据被位置、传输方式和系统边界束缚的状态。

项目 环境
OS Ubuntu 24.04 (wsl) && Ubuntu 22.04 (virtual box)
Examples eclipse-zenoh/zenoh-cpp 官方 examples

目录

  • [0. 什么是 Zenoh](#0. 什么是 Zenoh)
    • [Zenoh 的诞生背景](#Zenoh 的诞生背景)
    • 现有协议的问题
    • [Zenoh 的优势](#Zenoh 的优势)
  • [1. Zenoh 提出的核心概念](#1. Zenoh 提出的核心概念)
    • [1.1 Key、Value 和 Key Expression](#1.1 Key、Value 和 Key Expression)
    • [1.2 Data in Motion、Data at Rest 和 Computations](#1.2 Data in Motion、Data at Rest 和 Computations)
    • [1.3 Put、Pub/Sub、Get/Queryable](#1.3 Put、Pub/Sub、Get/Queryable)
    • [1.4 Peer、Client 和 Router](#1.4 Peer、Client 和 Router)
    • [1.5 Scouting、Endpoint 和 Router Connect](#1.5 Scouting、Endpoint 和 Router Connect)
    • [1.6 Liveliness、Storage 和性能示例](#1.6 Liveliness、Storage 和性能示例)
  • [2. 用 C++ examples 展示 Zenoh 能力](#2. 用 C++ examples 展示 Zenoh 能力)
    • [2.1 准备 C++ examples](#2.1 准备 C++ examples)
    • [2.2 实验 1:一次性写入和订阅](#2.2 实验 1:一次性写入和订阅)
    • [2.3 实验 2:持续数据流](#2.3 实验 2:持续数据流)
    • [2.4 实验 3:查询和计算响应](#2.4 实验 3:查询和计算响应)
    • [2.5 实验 4:观察 Data at Rest](#2.5 实验 4:观察 Data at Rest)
    • [2.6 实验 5:Scouting、Router 和显式 Endpoint](#2.6 实验 5:Scouting、Router 和显式 Endpoint)
    • [2.7 实验 6:通过 REST 读写 Zenoh 数据](#2.7 实验 6:通过 REST 读写 Zenoh 数据)
  • [3. 常见问题](#3. 常见问题)
  • 总结
  • 参考

0. 什么是 Zenoh

Zenoh 这个名字包含两层含义:

  • 哲学含义:致敬埃利亚的芝诺(前苏格拉底哲学家,以关于运动和无限的悖论闻名)和基提翁的芝诺(斯多葛学派创始人,代表极简、克制和有纪律的设计)。
  • 缩写含义:Zero Endpoint Network Overhead Handover,表达的是在每一层都尽量减少不必要开销的设计目标。

Zenoh 的诞生背景

Zenoh 由 Angelo Corsaro 构思和设计。当时他担任 PrismTech CTO,同时也是 OMG Data Distribution Service(DDS)规范工作组的联合主席。在参与军工、航天和智慧城市等超大规模分布式系统项目时,他开始思考一种能够跨越不同系统尺度和网络边界的数据通信方式。

现有协议的问题

在 Zenoh 官方的叙述中,当时还没有一种协议可以覆盖分布式系统的完整跨度:

  • DDS 在流动数据(data in motion)的发布 / 订阅场景中提供了很好的位置透明性,但既难以向上扩展到互联网尺度,也难以向下覆盖微控制器。
  • CoAP 天然偏向云中心和 client/server 模型。
  • MQTT 存在所谓的 broker paradox:即使两台设备位于同一个本地网络,通信仍然可能需要绕到远端云 broker 再返回。

这些系统就像一个东拼西凑的"数字怪物",各种协议片段被硬生生缝在一起,缺乏统一、透明的架构。

Zenoh 的优势

Zenoh 官方把它的优势概括为几个方向:

  • 从云到微控制器的通信跨度:Zenoh 面向从服务器级硬件和网络,到嵌入式微控制器和受限网络的统一通信场景。官方用"释放数据"来形容这种能力:数据可以在 microcontroller、edge、cloud、data-center 之间纵向和横向流动,开发者也不必为了打通企业系统和嵌入式系统而拼接多套通信技术。
  • 面向数据的位置透明性:发布 / 订阅让"流动中的数据"(data in motion)具备位置透明性,订阅者只表达兴趣,不需要知道发布者在哪里。Zenoh 进一步把这种思想扩展到"静态数据"(data at rest):查询可以不关心数据库的实际位置,由 Zenoh 选择网络中合适的数据源执行查询。
  • 易用和高性能设计:官方强调 Zenoh 从设计之初就面向易用和高性能,目标是在适用范围内提供较高吞吐、较低延迟,并减少为追求性能而写出脆弱、难维护代码的需求。
  • 能效设计:Zenoh 官方指出通信本身会消耗大量能源,因此协议设计时关注能效;这一点体现在 4-6 bytes 的最小 wire overhead,以及对通信局部性的利用上。
  • 面向下一代应用 :官方列举了 Robotics、Autonomous Vehicles、Internet Gaming、Telecommunications 等方向。在机器人领域,Zenoh 被用于 robot-to-robot communication、互联网尺度监控管理和实时遥操作;在自动驾驶与移动出行方向,CARMAIndy Autonomous Challenge 等项目也采用了 Zenoh。

总结来说,Zenoh 试图把互联网尺度的发布 / 订阅与地理分布式查询结合到同一套协议中。对机器人、车联网、边缘计算和云端监控这类系统来说,跨主机、跨网络边界、跨边缘与云端的数据流动,正是越来越常见的通信需求。

1. Zenoh 提出的核心概念

Zenoh 的核心不是把某一种已有协议重新包装,而是把数据流、存储查询和远端计算放进同一个命名空间和通信模型里。下面几个概念贯穿后面的实验。

1.1 Key、Value 和 Key Expression

Zenoh 使用 key 标识数据,例如:

text 复制代码
xiaoha/robot/zenoh/temp
robot/alpha/camera/front
factory/line1/status

value 是 key 对应的数据内容。key expression 则用于表达对一组 key 的兴趣,例如:

text 复制代码
xiaoha/robot/zenoh/**
robot/*/camera/**
factory/**/status

这种写法让发布者、订阅者、查询者和存储组件可以围绕同一套数据命名空间工作。发布 / 订阅关注"数据发生了变化",查询关注"现在有什么数据",二者不再是完全割裂的两套系统。

1.2 Data in Motion、Data at Rest 和 Computations

Zenoh 官方 overview 把它的目标概括为统一三类对象:

  • data in motion:正在流动的数据,典型接口是 pub/sub。
  • data at rest:已经存在于存储里的数据,典型接口是 get/query。
  • computations:由 queryable 暴露出的计算或服务能力,查询请求到达后可以动态生成回复。

在传统系统里,这三类能力经常由不同协议和组件分别承担:消息总线负责流数据,数据库负责静态数据,RPC 或服务框架负责计算。Zenoh 试图用同一套 key expression 和通信抽象把它们连起来。

1.3 Put、Pub/Sub、Get/Queryable

后面的 C++ examples 会反复用到几类操作:

操作 含义 实验入口
put 写入一个 key/value,并通知匹配的订阅者 z_put
pub/sub 持续发布和订阅数据流 z_pub / z_sub
get/queryable 发起查询,由匹配的 queryable 回复 z_get / z_queryable
storage 在进程内保存数据,再通过查询取回 z_storage

这里有一个重要区别:订阅者看到的是"之后发生的数据变化",查询者看到的是"当前可查询的数据或计算结果"。Zenoh 把二者放在同一套 key 空间里,减少了应用层在多种协议之间搬运数据的工作。

1.4 Peer、Client 和 Router

Zenoh deployment 文档把部署形态分成几类:

  • peer:应用之间直接发现和通信,适合简单局域网实验。
  • client:应用连接到 router,由 router 帮助路由数据。
  • router :负责连接多个应用或多个网络区域,跨主机、跨网段、跨区域时通常需要显式配置 endpoint。

默认局域网实验可以先使用 peer 模式。跨主机或跨网络边界时,更推荐显式启动 zenohd,再让应用通过 -e tcp/<router-ip>:7447 连接到 router。

1.5 Scouting、Endpoint 和 Router Connect

Zenoh 通过 scouting 发现网络中的其他 Zenoh 节点。局域网内可以依赖自动发现;跨网段、云主机、容器或防火墙环境中,自动发现往往不可靠,此时需要显式 endpoint:

text 复制代码
tcp/192.168.1.10:7447

本文仓库保留了两个 JSON5 router 示例:

text 复制代码
src/06_zenoh/config/router_host_a.example.json5
src/06_zenoh/config/router_host_b.example.json5

Host A 监听 tcp/0.0.0.0:7447,Host B 通过 connect/endpoints 连接 Host A。这种模式适合把两个网络区域接起来。

1.6 Liveliness、Storage 和性能示例

Zenoh examples 里还包含 liveliness、storage、roundtrip、throughput 等能力入口。本文把它们作为"观察 Zenoh 能力边界"的实验工具,不把一次本地测试结果扩展成通用性能结论。性能对比需要固定硬件、网络、消息大小、QoS、版本和统计方法。

2. 用 C++ examples 展示 Zenoh 能力

本节只使用 eclipse-zenoh/zenoh-cpp 官方 examples,不在仓库中额外维护 C++ 源码。zenoh-cpp README 说明,C++ API 是基于 zenoh-czenoh-pico 的 header-only C++ bindings,构建 examples 需要 C++17 编译器,并至少启用一个后端。

2.1 准备 C++ examples

安装基础工具:

bash 复制代码
sudo apt update
sudo apt install -y git cmake g++ pkg-config

获取官方仓库:

bash 复制代码
cd ~
git clone https://github.com/eclipse-zenoh/zenoh-cpp.git
cd zenoh-cpp
git checkout 1.9.0
git submodule update --init --recursive

本文的命令和实验结果以 1.9.0 为准。按 zenoh-cpp README 的说明,examples 默认使用 zenoh-c 后端,因此需要先安装 zenoh-c。下面把 zenoh-c 安装到用户目录 $HOME/.local,避免写入系统路径:

bash 复制代码
# 安装 Rust/Cargo
sudo apt install -y curl build-essential cmake ninja-build pkg-config git ca-certificates
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"

# 编译并安装 zenoh-c
cd ~/zenoh-cpp
cmake -S zenoh-c -B build-zenoh-c \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX="$HOME/.local"
cmake --build build-zenoh-c --target install -j"$(nproc)"

# 配置并编译 zenoh-cpp examples
cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX="$HOME/.local" \
  -DCMAKE_PREFIX_PATH="$HOME/.local" \
  -DZENOHCXX_ZENOHC=ON \
  -DZENOHCXX_ZENOHPICO=OFF
cmake --build build --target examples -j"$(nproc)"

examples 通常会生成在:

text 复制代码
build/examples/zenohc
build/examples/zenohpico

如果只启用了 zenoh-c 后端,后续命令中的 ./z_sub./z_put 等程序位于:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
ls

不同版本的 examples 名称和参数可能有少量变化,运行任意示例前可先查看帮助:

bash 复制代码
./z_sub --help
./z_put --help

跨主机 router 实验还需要 zenohd。它不是 zenoh-cpp examples 的一部分,可按 Zenoh 官方 Getting Started / Deployment 文档安装或下载对应平台的 Zenoh 二进制包。安装后先确认:

bash 复制代码
zenohd --version

2.2 实验 1:一次性写入和订阅

第一个终端启动订阅者,订阅 xiaoha/robot/zenoh/**

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_sub -k 'xiaoha/robot/zenoh/**'

第二个终端写入一个 key/value:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_put -k 'xiaoha/robot/zenoh/hello' -p 'Hello Zenoh'

预期现象:

text 复制代码
z_sub 收到 xiaoha/robot/zenoh/hello 对应的数据

这个实验展示的是 Zenoh 的 key expression 匹配能力。订阅者并不关心发布者在哪里,只表达"对 xiaoha/robot/zenoh/** 感兴趣";只要写入的 key 落在这个范围内,就能被订阅者接收。

2.3 实验 2:持续数据流

第一个终端继续订阅:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_sub -k 'xiaoha/robot/zenoh/stream'

第二个终端启动持续发布:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_pub -k 'xiaoha/robot/zenoh/stream' -p 'robot status'

预期现象:

text 复制代码
z_sub 持续收到 xiaoha/robot/zenoh/stream 上的新数据

这个实验对应 data in motion:数据不断产生、不断流动,订阅者按照 key expression 接收后续更新。传感器数据、状态上报、遥操作指令等都可以抽象成这类数据流。

2.4 实验 3:查询和计算响应

这个实验模拟客户端向机器人查询电池状态。第一个终端代表机器人端,启动 queryable,并把电池百分比设置为 85%

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_queryable \
  -k 'xiaoha/robot/zenoh/battery/status' \
  -p '85%'

第二个终端代表客户端,查询机器人当前的电池状态:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_get -s 'xiaoha/robot/zenoh/battery/status'

客户端预期收到:

text 复制代码
Received ('xiaoha/robot/zenoh/battery/status' : '85%')

机器人端也会打印收到查询并返回 85% 的日志。这个实验对应 computations:查询命中 queryable 后,回复不一定来自数据库,也可以由程序动态计算生成。

2.5 实验 4:观察 Data at Rest

第一个终端启动进程内 storage 示例,保存 xiaoha/robot/zenoh/storage/** 范围内的数据:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_storage -k 'xiaoha/robot/zenoh/storage/**'

第二个终端写入数据:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_put -k 'xiaoha/robot/zenoh/storage/temp1' -p '25.0'
./z_put -k 'xiaoha/robot/zenoh/storage/temp2' -p '26.0'

第三个终端查询已保存的数据:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_get -s 'xiaoha/robot/zenoh/storage/**'

预期现象:

text 复制代码
z_get 可以取回前面写入的 xiaoha/robot/zenoh/storage/**

这个实验对应 data at rest:数据不只是被订阅者实时看到,也可以被存储组件保存下来,再通过查询接口读取。Zenoh 官方强调的位置透明性,也包括这类"查询时不需要关心数据实际存在哪里"的场景。

需要注意,z_storage 官方示例使用进程内存保存数据,程序退出后数据会丢失;它用于演示 storage/query 语义,不代表磁盘持久化。需要持久化时,应使用 zenoh-plugin-storage-manager 及对应的 filesystem、RocksDB、S3 等 backend。

2.6 实验 5:Scouting、Router 和显式 Endpoint

单机实验里,peer discovery 往往已经足够。跨主机实验更适合使用 router。本实验在两台独立主机上各启动一个 Zenoh router,并使用 JSON5 文件描述 router 之间的连接关系:

其中,z_putz_sub 都以 client 模式连接本机 router;Router B 再通过显式 endpoint 主动连接 Router A。数据链路不依赖跨主机 multicast scouting。

txt 复制代码
我的环境:
Host A: 192.168.3.70
Host B: 192.168.3.82

Host A 的 config/router_host_a.example.json5 监听所有网络接口上的 TCP 7447 端口。在项目根目录启动 Router A:

bash 复制代码
# zenohd的安装参考:https://zenoh.io/docs/getting-started/installation/
# 终端1
zenohd -c src/06_zenoh/config/router_host_a.example.json5

在 Host B 启动 Router B:

bash 复制代码
# 终端2
zenohd -c src/06_zenoh/config/router_host_b.example.json5

Router B 会监听本机 TCP 7447 端口,同时主动连接 tcp/<HOST_A_IP>:7447

接下来验证跨 router 发布 / 订阅。Host B 启动订阅者,并以 client 模式连接本机 Router B:

bash 复制代码
# 终端3
cd ~/zenoh-cpp/build/examples/zenohc
./z_sub -m client -e tcp/127.0.0.1:7447 -k 'xiaoha/robot/zenoh/**'

Host A 写入数据,并以 client 模式连接本机 Router A:

bash 复制代码
# 终端4
cd ~/zenoh-cpp/build/examples/zenohc
./z_put -m client -e tcp/127.0.0.1:7447 \
  -k 'xiaoha/robot/zenoh/router' \
  -p 'through two routers'

预期现象:

这个实验展示了 router 和显式 endpoint 的作用:Router A 接受来自 Router B 的连接,Router B 负责把本地 client 接入 Router A 所在的网络区域。即使跨主机自动发现不可用,两个 router 仍然可以按照 JSON5 配置建立稳定的数据路径。

2.7 实验 6:通过 REST 读写 Zenoh 数据

Zenoh 官方 REST 插件把 HTTP PUTDELETE 映射到 Zenoh pub/sub,把 HTTP GET 映射到 query/reply。这样,浏览器、脚本或不方便集成 Zenoh SDK 的程序也可以通过 HTTP 访问同一个 Zenoh 数据空间。

安装 zenohd、REST 插件和 curl

bash 复制代码
sudo apt update
sudo apt install -y zenohd zenoh-plugin-rest curl

第一个终端启动 Zenoh router,并在 TCP 8000 端口启用 REST API:

bash 复制代码
zenohd --rest-http-port 8000
使用 REST PUT 发布数据

第二个终端启动 C++ subscriber,并以 client 模式连接本机 router:

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_sub -m client -e tcp/127.0.0.1:7447 \
  -k 'xiaoha/robot/zenoh/rest/**'

第三个终端通过 HTTP PUT 发布机器人在线状态:

bash 复制代码
curl -i -X PUT \
  -H 'Content-Type: text/plain' \
  --data 'online' \
  http://127.0.0.1:8000/xiaoha/robot/zenoh/rest/status

REST API 返回 HTTP 200 OK,C++ subscriber 应收到:

text 复制代码
>> [Subscriber] Received PUT ('xiaoha/robot/zenoh/rest/status' : 'online')

这说明 REST 写入会进入 Zenoh pub/sub 数据流,普通 Zenoh subscriber 不需要知道数据来自 HTTP 客户端。

使用 REST GET 查询电池状态

第四个终端启动实验 3 使用过的 C++ queryable,并返回固定电量 85%

bash 复制代码
cd ~/zenoh-cpp/build/examples/zenohc
./z_queryable -m client -e tcp/127.0.0.1:7447 \
  -k 'xiaoha/robot/zenoh/battery/status' \
  -p '85%'

第三个终端改用 HTTP GET 查询电池状态:

bash 复制代码
curl -s \
  http://127.0.0.1:8000/xiaoha/robot/zenoh/battery/status

在 Zenoh 1.9.0 环境中,返回结果如下:

json 复制代码
[{"key":"xiaoha/robot/zenoh/battery/status","value":"ODUl","encoding":"zenoh/bytes","timestamp":null}]

REST API 的 JSON 响应会把 bytes 类型的 payload 编码为 Base64;其中 ODUl 解码后就是 85%。这个实验说明 REST client 可以查询由 C++ queryable 提供的数据,HTTP 与 C++ API 访问的是同一套 Zenoh key space。

3. 常见问题

1. 找不到 z_subz_put 等程序

先确认 examples 已经构建成功:

bash 复制代码
cd ~/zenoh-cpp/build
cmake --build . --target examples
find examples -maxdepth 3 -type f -executable | sort

zenoh-cpp README 说明 examples 会放在 build/examples/zenohcbuild/examples/zenohpico 目录下。实际目录取决于构建时启用的后端。

2. cmake 找不到后端库

zenoh-cpp 是 C++ bindings,使用时需要 zenoh-czenoh-pico 后端。使用本文默认的 zenoh-c 后端时,先确认它已经安装到 $HOME/.local,再重新配置:

bash 复制代码
cmake -S . -B build \
  -DCMAKE_INSTALL_PREFIX="$HOME/.local" \
  -DCMAKE_PREFIX_PATH="$HOME/.local" \
  -DZENOHCXX_ZENOHC=ON \
  -DZENOHCXX_ZENOHPICO=OFF
cmake --build build --target examples

如果改用 zenoh-pico 后端,需要先安装开发包,并明确关闭默认启用的 zenoh-c

bash 复制代码
sudo apt install -y libzenohpico-dev
cmake -S . -B build-pico \
  -DZENOHCXX_ZENOHC=OFF \
  -DZENOHCXX_ZENOHPICO=ON
cmake --build build-pico --target examples

3. 跨主机收不到数据

先确认 Host A 已使用 JSON5 配置启动 Router A:

bash 复制代码
zenohd -c src/06_zenoh/config/router_host_a.example.json5

再确认 Host B 能访问 Router A 的端口:

bash 复制代码
nc -vz <HOST_A_IP> 7447

如果网络不通,检查防火墙、虚拟机网络模式和路由,并确认 router_host_b.example.json5 中的 Host A IP 已替换为实际地址。网络可达但仍然收不到数据时,继续检查 Router B 的启动日志,以及 z_putz_sub 是否使用 -m client -e tcp/127.0.0.1:7447 连接了各自的本地 router。

4. z_get 没有返回数据

z_get 需要匹配到 queryable 或 storage。仅运行 z_put 并不一定意味着后续查询能取回数据,除非同时运行了 storage 示例或其他 queryable。可以先用最小组合验证:

bash 复制代码
./z_queryable -k 'xiaoha/robot/zenoh/battery/status' -p '85%'
./z_get -s 'xiaoha/robot/zenoh/battery/status'

总结

本文围绕 Zenoh 的核心概念与基础通信能力展开,并通过 zenoh-cpp 官方 examples 验证了一次性写入、持续发布 / 订阅、查询响应、数据存储、跨主机 Router 通信以及 REST/C++ 互操作。通过这些实验可以看到,Zenoh 使用统一的 key expression 和通信模型,将 data in motion、data at rest 与 computations 连接在同一套数据空间中。

本文重点关注 Zenoh 的功能与通信模型,未对不同协议的性能作定量比较。若希望进一步了解 Zenoh 与 MQTT、Kafka、DDS 在吞吐量和时延方面的表现,可参考国立台湾大学发布的论文 《A Performance Study on the Throughput and Latency of Zenoh, MQTT, Kafka, and DDS》。后续文章将继续介绍 Zenoh 在 ROS 2 中的适配实现 rmw_zenoh 及其实际使用方式。

参考