ZigBolt:为什么我们用 Zig 从零打造了自己的 Aeron,实现了每条消息 20 纳秒延迟
无锁环形缓冲区、零拷贝编解码器、Raft 集群——全部用纯 Zig 编写,全部开源。
如果你从事算法交易或做市,就知道每一微秒的价值。多一次上下文切换,你的订单就会晚到一步。JVM 的一次垃圾回收暂停,对面的做市商就已经更新了报价。在一个以纳秒衡量利润的世界里,消息传递基础设施不是服务之间的无聊管道,而是核心竞争优势。
我们用 Zig 语言从零构建了 ZigBolt——一套面向高频交易的消息系统。没有 JVM,没有垃圾回收器,没有 Media Driver,没有 XML 配置文件。我们实现了 SPSC 环形缓冲区 20 纳秒 p50 延迟,以及共享内存 IPC 30 纳秒延迟。
这篇文章讲述的是:为什么需要做这件事,内部是如何实现的,以及为什么选择了 Zig。
概要
- ZigBolt — 开源(MIT 协议)的纯 Zig 高频交易消息系统
- 20 纳秒 p50(SPSC),30 纳秒 p50(IPC)——优于 Aeron 公布的性能指标
- 零拷贝编解码器耗时 0 纳秒(编译期代码生成,运行时仅为指针类型转换)
- 无 GC、无 JVM、无 Media Driver ——库直接嵌入应用程序
- Raft 集群、归档、排序器 ——全部内置
- FFI 绑定支持 Rust、Python、Go、TypeScript、C——用你习惯的语言即可
问题:Aeron 很好,但还不够
Aeron 是 Real Logic 开发的产品,堪称资本市场低延迟消息传递的事实标准。数十家高频交易公司在使用它,久经实战检验,架构设计优秀。但 Aeron 有一个根本性问题,那就是 JVM。
JVM safepoints:隐形的敌人
即使你小心地把所有数据放到了堆外内存,即使你关闭了 GC 自适应调节并设置了 GuaranteedSafepointInterval=300000,JVM 仍然会不时地在 safepoint 处暂停所有线程。这不是 bug,而是架构设计:JVM 需要 safepoints 来执行去优化、偏向锁、栈遍历等操作。
实际表现是这样的:你的线程以 p50 = 200 纳秒的速度发送消息,突然 p99.9 飙升到 50 微秒。没有任何明显原因。因为 JVM 的某个线程认为该执行 safepoint 了。
Media Driver:多余的一跳
Aeron 通过 Media Driver 工作——一个独立的进程(或嵌入式 JVM),通过共享内存在 publisher 和 subscriber 之间路由消息。这提供了漂亮的隔离性,但至少增加了一次额外的跳转:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
每多一跳,就多几纳秒延迟、多几次缓存未命中、多一份不可预测性。
SBE:额外的构建步骤
Simple Binary Encoding 是金融消息的标准 FIX 编解码器。在 Aeron 生态中,它是一个独立的 Java 工具,从 XML 模式生成代码。独立的依赖、独立的构建步骤、独立的问题集。
解决方案:ZigBolt
我们问了自己一个问题:如果把 Aeron 最好的设计理念——三重缓冲日志、无锁环形缓冲区、Raft 集群——用一种这样的语言重新实现会怎样:
- 没有运行时开销(无 GC、无 safepoints)
- 能在编译期生成代码(comptime)
- 与 C 库(DPDK、io_uring)集成极其简单
- 编译出的二进制文件仅约 100 KB
这种语言就是 Zig。
架构
┌─────────────────────────────────────────────────────────┐
│ Publisher/Subscriber API(类型化封装) │
├─────────────────────────────────────────────────────────┤
│ Transport Layer(通道工厂、生命周期管理) │
├─────────────────────────────────────────────────────────┤
│ IPC Channel(共享内存) │ UDP Channel(网络) │
├─────────────────────────────────────────────────────────┤
│ WireCodec(comptime 零拷贝)│ SBE Encoder/Decoder │
├─────────────────────────────────────────────────────────┤
│ Ring Buffers (SPSC/MPSC) │ LogBuffer(三重缓冲) │
├─────────────────────────────────────────────────────────┤
│ Archive(回放)│ Sequencer(全局有序)│ Raft(高可用) │
└─────────────────────────────────────────────────────────┘
七层架构,每一层都可以独立使用。只需要一个 SPSC 环形缓冲区在两个进程间做 IPC?直接用。需要完整的 Raft 共识集群加归档?也有。
基准测试:用数据说话

以下是在 1000 万次迭代(Apple Silicon / macOS)下的实测结果:
SPSC 环形缓冲区
| 消息大小 | p50 | p99 | p99.9 | 吞吐量 |
|---|---|---|---|---|
| 8 字节 | 20 纳秒 | 30 纳秒 | 120 纳秒 | 42.8M msg/s |
| 32 字节 | 30 纳秒 | 50 纳秒 | 150 纳秒 | 28.5M msg/s |
| 64 字节 | 50 纳秒 | 60 纳秒 | 320 纳秒 | 17.6M msg/s |
| 256 字节 | 30 纳秒 | 50 纳秒 | 50 纳秒 | 29.5M msg/s |
IPC Channel(共享内存)
| 消息大小 | p50 | p99 | p99.9 | 吞吐量 |
|---|---|---|---|---|
| 64 字节 | 30 纳秒 | 40 纳秒 | 40 纳秒 | 35.7M msg/s |
| 256 字节 | 40 纳秒 | 40 纳秒 | 170 纳秒 | 27.4M msg/s |
| 1024 字节 | 90 纳秒 | 260 纳秒 | 900 纳秒 | 9.9M msg/s |
LogBuffer(Aeron 风格三重缓冲)
| 消息大小 | p50 | p99 | p99.9 | 吞吐量 |
|---|---|---|---|---|
| 32 字节 | 30 纳秒 | 40 纳秒 | 320 纳秒 | 33.6M msg/s |
| 64 字节 | 30 纳秒 | 30 纳秒 | 160 纳秒 | 38.0M msg/s |
| 256 字节 | 30 纳秒 | 40 纳秒 | 60 纳秒 | 31.1M msg/s |
WireCodec(comptime 零拷贝)
| 操作 | 延迟 | 吞吐量 |
|---|---|---|
| 编码(32 字节) | 0 纳秒 | ∞(内联 memcpy) |
| 解码(32 字节) | ~0.4 纳秒 | 27 亿 msg/s |
没错,你没看错:编码耗时为零纳秒。因为 WireCodec(T) 在编译期验证结构体,并将 encode/decode 转化为普通的 @memcpy 或指针类型转换。运行时开销 = 零。
作为对比:Aeron 公布的 IPC RTT(往返延迟)约 250 纳秒。我们的单程延迟为 30 纳秒。即使按往返计算,我们也快了 4 倍。
内部原理
无锁 SPSC:以简洁为美

单生产者单消费者环形缓冲区是最简单、最快的无锁数据结构。写入端移动 head,读取端移动 tail,无需 CAS——acquire/release 原子操作就够了。
关键技巧是缓存行填充。如果 head 和 tail 位于同一缓存行,那么每次更新其中一个计数器都会使另一个核心的缓存失效(伪共享)。解决方案:
// Head(写入位置)——独占一个缓存行
head: std.atomic.Value(usize) align(128) = .init(0),
// 128 字节填充——确保隔离
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail(读取位置)——独占一个缓存行
tail: std.atomic.Value(usize) align(128) = .init(0),
128 字节而非 64 字节——因为在 Apple Silicon(及许多 ARM 架构)上,硬件预取器可能以缓存行对的方式工作。我们选择了更保守的方案。
WireCodec:用 comptime 取代代码生成

在 Java/C++ 的世界里,要使用二进制编解码器,你需要一个单独的步骤:编写模式 -> 运行代码生成器 -> 得到代码 -> 编译。在 Zig 里,这一切都在编译期完成:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// 编码——只是 32 字节的 memcpy。内联为 1-2 条指令。
Codec.encode(&msg, buf[0..Codec.wire_size]);
// 解码——指针类型转换。零拷贝。
const tick = Codec.decode(buf[0..Codec.wire_size]);
Zig 编译器在 comptime 阶段检查:
- 结构体是 packed 的(没有填充空洞)
- 大小是 8 字节的倍数(为 SIMD 对齐)
- 所有字段都是基本类型
如果有问题——编译错误,而不是凌晨三点生产环境的运行时异常。
基于共享内存的 IPC
两个进程映射 /dev/shm 中的同一个文件。Publisher 写入环形缓冲区,subscriber 读取。热路径上没有套接字、没有系统调用:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber(另一个进程)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
从 publish() 到 subscriber 中 handler_fn 被调用的完整路径——64 字节消息仅需 30 纳秒。
基于 NAK 的 UDP 可靠传输
对于网络传输,ZigBolt 使用接收端驱动的重传机制。接收端通过位图追踪 sequence number 中的间隙,并向发送端发送 NAK(否定确认)。加上 AIMD 拥塞控制——类似 TCP 的慢启动和拥塞避免——以防止网络过载。
Raft 集群:当一致性不可妥协
对于不能丢失消息的场景(例如撮合引擎),ZigBolt 内置了完整的 Raft 共识:
- Leader 选举,可配置超时时间(150-300 毫秒)
- 日志复制 ——leader 将每条消息复制到 followers
- 预写式日志(WAL),带 CRC32 校验和崩溃恢复
- 快照 ——防止 WAL 无限增长
归档:录制与回放
所有消息可以录制到磁盘上的分段归档中。之后可以按时间或 sequence number 从任意位置回放。内置 LZ4 风格压缩,无外部依赖。稀疏索引用于在段内快速查找。
全局有序排序器
对于在多个交易场所做市的场景,所有事件必须有全局顺序。排序器接收 N 个输入流并合并为一个,分配单调递增的 sequence number。每个参与者看到的是同一个事件序列。
为什么选 Zig 而不是 Rust/C/C++?
我们在四个候选语言中做了比较。以下是客观的对比:
| 指标 | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / 运行时开销 | 无 | 无 | 无 | JVM safepoints, GC |
| Comptime 代码生成 | 原生支持 | 宏/模板 | proc macros | 无 |
| C 互操作(DPDK, io_uring) | 简单的 @cImport |
原生 | FFI/bindgen | JNI 开销 |
| SIMD | @Vector,内置 |
Intrinsics | packed_simd(不稳定) | 向量化提示 |
| 交叉编译 | 内置 | CMake 地狱 | cargo target | 不适用 |
| 构建时间 | 秒级 | 分钟级(C++) | 分钟级 | 秒级 + JVM 启动 |
| 隐式控制流 | 无 | 异常、隐式类型转换 | unwrap 中的 panic |
异常 |
Zig 提供了独一无二的组合: C 级性能 + 开发安全性 + comptime 元编程(编解码器、查找表、协议状态机——全在编译期生成)+ 通过 @cImport 与 DPDK、liburing、ef_vi 轻松集成。
而且 Zig 二进制文件仅约 100 KB,相比 JVM 方案的 20+ MB。对于边缘部署和容器化,这很重要。
多语言绑定:用你熟悉的语言
ZigBolt 编译为带 C-ABI 的共享库,我们提供了五种语言的现成绑定:
TypeScript / Node.js
import { IpcChannel } from "@zigbolt/node";
const channel = IpcChannel.create({
name: "/my-market-data",
termLength: 1024 * 1024,
});
const msg = Buffer.from("BTC/USDT 42000.50", "utf-8");
channel.publish(msg, 1);
Rust
use zigbolt::IpcChannel;
let ch = IpcChannel::create("/my-channel", 64 * 1024).unwrap();
ch.publish(b"hello", 1).unwrap();
// Subscriber
let sub = IpcChannel::open("/my-channel", 64 * 1024).unwrap();
sub.poll(|data, msg_type_id| {
println!("got {} bytes, type={}", data.len(), msg_type_id);
}, 10);
Python
from zigbolt import IpcChannel
ch = IpcChannel.create("/market-data", term_length=1024*1024)
ch.publish(b"tick data here", msg_type_id=1)
还有 Go 和纯 C 绑定。同一个共享内存通道可以从所有语言同时访问——publisher 用 Zig,subscriber 用 Python,监控用 Go。所有人读取同一块 mmap 区域。
SBE 编解码器:FIX 兼容消息
对于金融协议,ZigBolt 内置了完整的 SBE(Simple Binary Encoding)编解码器,支持编译期模式。内置消息类型包括:
- NewOrderSingle — 提交订单
- ExecutionReport — 成交回报
- MarketDataIncrementalRefresh — 增量行情更新
- MassQuote — 批量报价
- Heartbeat — 心跳检测
- Logon — 身份认证
不需要外部代码生成器,不需要 XML。一切通过 Zig 结构体描述,在编译期完成验证。
Wire Protocol:兼容 Aeron
ZigBolt 实现了与 Aeron 兼容的 wire protocol flyweights:
- DataHeaderFlyweight — 数据帧
- StatusMessage — 流量控制
- NAK — 否定确认
- Setup、RTT、Error — 控制帧
这意味着 ZigBolt 可以与现有的 Aeron 基础设施共存。迁移不必一步到位。
未来规划
ZigBolt 目前版本为 0.2.1。核心稳定,基准测试可复现,绑定可用。近期计划包括:
- io_uring 后端 — Linux 6.0+ 上的零拷贝网络传输(IORING_OP_SEND_ZC)
- DPDK / AF_XDP — 内核旁路,适用于每微秒都至关重要的场景
- Multi-Raft — 按交易品种/策略分片
- 列式归档 — 集成 Apache Arrow/Parquet 以支持分析
- 大页支持 — 预分配的 2MB/1GB 大页以减少 TLB 缺失
立即体验
- 官网:zigbolt-landing.vercel.app
- 文档:zigbolt-landing.vercel.app/getting-started/introduction/
- 源码:github.com/suenot/zigbolt
- 许可证:MIT
从源码构建(zig build),运行基准测试(zig build bench),通过 FFI 从任何语言接入。如果你有 Zig 0.15.1 和几分钟时间——试试 ping-pong 基准测试,和你目前的方案比较一下。
链接:
- ZigBolt 官网:zigbolt-landing.vercel.app
- GitHub:github.com/suenot/zigbolt
- Aeron(供对比):github.com/real-logic/aeron | 我们的 Aeron 概述
- Zig 语言:ziglang.org
- Marketmaker.cc:marketmaker.cc
引用
@software{soloviov2026zigbolt,
author = {Soloviov, Eugen},
title = {ZigBolt: 为什么我们用 Zig 从零打造了自己的 Aeron,实现了每条消息 20 纳秒延迟},
year = {2026},
url = {https://marketmaker.cc/zh/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {详解我们如何用 Zig 语言从零构建了一套面向高频交易的超低延迟消息系统。无 JVM、无 GC、无意外。}
}
MarketMaker.cc Team
量化研究与策略