ZigBolt: Tại Sao Chúng Tôi Tự Xây Aeron Bằng Zig Và Đạt Được 20 Nanosecond Mỗi Tin Nhắn
Bộ đệm vòng không khóa, codec zero-copy, cụm Raft — tất cả trong Zig thuần túy, tất cả mã nguồn mở.
Nếu bạn làm việc trong giao dịch thuật toán hoặc tạo lập thị trường, bạn biết giá trị của từng microsecond. Một lần chuyển ngữ cảnh thêm — và lệnh của bạn đến thứ hai. Một lần tạm dừng GC của JVM — và nhà tạo lập thị trường ở phía kia đã cập nhật báo giá. Trong một thế giới mà tiền được đo bằng nanosecond, cơ sở hạ tầng nhắn tin không phải là đường ống nhàm chán giữa các dịch vụ — đó là lợi thế cạnh tranh.
Chúng tôi đã xây dựng ZigBolt — một hệ thống nhắn tin cho giao dịch tần số cao được viết bằng Zig. Từ đầu. Không JVM, không bộ thu gom rác, không Media Driver, không cấu hình XML. Và chúng tôi đạt được độ trễ p50 20 nanosecond trên bộ đệm vòng SPSC và 30 nanosecond trên IPC qua bộ nhớ dùng chung.
Bài viết này đề cập đến lý do tại sao chúng tôi cần nó, cách thức hoạt động bên trong, và tại sao Zig.
TL;DR
- ZigBolt — hệ thống nhắn tin mã nguồn mở (MIT) cho HFT bằng Zig thuần túy
- p50 20 ns trên SPSC, p50 30 ns trên IPC — nhanh hơn các con số đã công bố của Aeron
- Codec zero-copy chạy ở 0 ns (tạo ra tại compile-time, runtime chỉ là ép kiểu con trỏ)
- Không GC, không JVM, không Media Driver — thư viện nhúng trực tiếp vào ứng dụng của bạn
- Cụm Raft, archive, sequencer — tất cả đã bao gồm
- FFI bindings cho Rust, Python, Go, TypeScript, C — làm việc bằng bất kỳ ngôn ngữ nào bạn thích
Vấn Đề: Tại Sao Aeron Tuyệt Vời Nhưng Chưa Đủ
Aeron từ Real Logic là tiêu chuẩn thực tế cho nhắn tin độ trễ thấp trong thị trường vốn. Hàng chục công ty HFT sử dụng nó, đã được thử nghiệm thực chiến, và có kiến trúc xuất sắc. Nhưng Aeron có một vấn đề cơ bản, và tên của nó là JVM.
Safepoints JVM: Kẻ Thù Vô Hình
Ngay cả khi bạn cẩn thận đặt tất cả dữ liệu vào bộ nhớ off-heap, ngay cả khi bạn đã vô hiệu hóa GC ergonomics và đặt GuaranteedSafepointInterval=300000 — JVM vẫn thỉnh thoảng dừng tất cả các luồng tại một safepoint. Đây không phải là bug, đó là quyết định kiến trúc: JVM cần safepoints để deoptimization, biased locking, và stack walking.
Trong thực tế nó trông như thế này: luồng của bạn gửi tin nhắn ở p50 = 200 ns, và đột nhiên p99.9 tăng vọt lên 50 us. Không có lý do rõ ràng. Vì một trong các luồng JVM đã quyết định đến lúc rồi.
Media Driver: Một Hop Thêm
Aeron hoạt động thông qua một Media Driver — một tiến trình riêng (hoặc JVM nhúng) định tuyến tin nhắn giữa publisher và subscriber qua bộ nhớ dùng chung. Điều này mang lại sự cô lập tốt nhưng thêm ít nhất một hop thêm:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
Mỗi hop có nghĩa là thêm nanosecond, thêm cache miss, thêm tính không thể đoán trước.
SBE: Một Bước Build Riêng Biệt
Simple Binary Encoding — codec FIX tiêu chuẩn cho tin nhắn tài chính. Trong hệ sinh thái Aeron, đó là một tiện ích Java riêng biệt tạo ra code từ các schema XML. Một phụ thuộc riêng biệt, một bước build riêng biệt, một tập hợp vấn đề riêng biệt.
Giải Pháp: ZigBolt
Chúng tôi tự hỏi: nếu chúng tôi lấy những ý tưởng tốt nhất của Aeron — triple-buffered log, bộ đệm vòng không khóa, cụm Raft — và thực hiện chúng trong một ngôn ngữ:
- Không có overhead runtime (không GC, không safepoints)
- Cho phép tạo code tại compile time (comptime)
- Tích hợp dễ dàng với thư viện C (DPDK, io_uring)
- Biên dịch thành binary ~100 KB
Ngôn ngữ đó là Zig.
Kiến Trúc
┌─────────────────────────────────────────────────────────┐
│ Publisher/Subscriber API (typed generic wrappers) │
├─────────────────────────────────────────────────────────┤
│ Transport Layer (channel factory & lifecycle) │
├─────────────────────────────────────────────────────────┤
│ IPC Channel (shared memory) │ UDP Channel (network) │
├─────────────────────────────────────────────────────────┤
│ WireCodec (comptime, zero-copy) │ SBE Encoder/Decoder │
├─────────────────────────────────────────────────────────┤
│ Ring Buffers (SPSC/MPSC) │ LogBuffer (triple-buffered) │
├─────────────────────────────────────────────────────────┤
│ Archive (replay) │ Sequencer (total order) │ Raft (HA) │
└─────────────────────────────────────────────────────────┘
Bảy lớp, mỗi lớp có thể sử dụng độc lập. Chỉ cần một bộ đệm vòng SPSC cho IPC giữa hai tiến trình? Lấy nó đi. Cần một cụm đầy đủ với đồng thuận Raft và lưu trữ? Cũng có ở đó.
Benchmarks: Con Số, Không Phải Lời Nói

Kết quả benchmark thực tế trên 10 triệu lần lặp (Apple Silicon / macOS):
Bộ Đệm Vòng SPSC
| Kích Thước Tin Nhắn | p50 | p99 | p99.9 | Thông Lượng |
|---|---|---|---|---|
| 8 bytes | 20 ns | 30 ns | 120 ns | 42.8M msg/s |
| 32 bytes | 30 ns | 50 ns | 150 ns | 28.5M msg/s |
| 64 bytes | 50 ns | 60 ns | 320 ns | 17.6M msg/s |
| 256 bytes | 30 ns | 50 ns | 50 ns | 29.5M msg/s |
Kênh IPC (bộ nhớ dùng chung)
| Kích Thước Tin Nhắn | p50 | p99 | p99.9 | Thông Lượng |
|---|---|---|---|---|
| 64 bytes | 30 ns | 40 ns | 40 ns | 35.7M msg/s |
| 256 bytes | 40 ns | 40 ns | 170 ns | 27.4M msg/s |
| 1024 bytes | 90 ns | 260 ns | 900 ns | 9.9M msg/s |
LogBuffer (triple-buffered theo phong cách Aeron)
| Kích Thước Tin Nhắn | p50 | p99 | p99.9 | Thông Lượng |
|---|---|---|---|---|
| 32 bytes | 30 ns | 40 ns | 320 ns | 33.6M msg/s |
| 64 bytes | 30 ns | 30 ns | 160 ns | 38.0M msg/s |
| 256 bytes | 30 ns | 40 ns | 60 ns | 31.1M msg/s |
WireCodec (comptime zero-copy)
| Thao Tác | Độ Trễ | Thông Lượng |
|---|---|---|
| Encode (32 bytes) | 0 ns | memcpy nội tuyến |
| Decode (32 bytes) | ~0.4 ns | 2.7 tỷ msg/s |
Đúng vậy, bạn đọc đúng: encoding mất không nanosecond. Vì WireCodec(T) xác thực struct tại compile time và biến encode/decode thành một @memcpy hoặc ép kiểu con trỏ thông thường. Overhead runtime = không.
Để so sánh: Aeron khẳng định IPC RTT (round-trip) ~250 ns. Độ trễ một chiều của chúng tôi là 30 ns. Ngay cả tính round-trip, chúng tôi nhanh hơn 4 lần.
Cách Hoạt Động Bên Trong
SPSC Không Khóa: Sự Đơn Giản Là Đức Hạnh

Bộ đệm vòng single-producer single-consumer là cấu trúc dữ liệu không khóa đơn giản và nhanh nhất. Người ghi di chuyển head, người đọc di chuyển tail, không cần CAS — acquire/release atomics là đủ.
Mẹo chính là đệm cache-line. Nếu head và tail ngồi trong cùng một cache line, mọi cập nhật cho một bộ đếm sẽ vô hiệu hóa cache cho core kia (false sharing). Cách sửa:
// Head (vị trí ghi) — trên cache line riêng của nó
head: std.atomic.Value(usize) align(128) = .init(0),
// Đệm 128 bytes — đảm bảo cô lập
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (vị trí đọc) — trên cache line riêng của nó
tail: std.atomic.Value(usize) align(128) = .init(0),
128 bytes, không phải 64 — vì trên Apple Silicon (và nhiều chip ARM) hardware prefetcher có thể làm việc với các cặp cache line. Chúng tôi chơi an toàn.
WireCodec: Comptime Thay Vì Tạo Code

Trong thế giới Java/C++, binary codec yêu cầu một bước riêng biệt: viết schema, chạy code generator, lấy code, biên dịch. Trong Zig, tất cả điều này xảy ra tại compile time:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// Encode — chỉ là memcpy 32 byte. Nội tuyến thành 1-2 lệnh.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// Decode — ép kiểu con trỏ. Không có copy.
const tick = Codec.decode(buf[0..Codec.wire_size]);
Trình biên dịch Zig xác thực tại comptime:
- Struct được packed (không có lỗ padding)
- Kích thước là bội số của 8 bytes (căn chỉnh cho SIMD)
- Tất cả các trường là kiểu primitive
Nếu có gì đó sai — lỗi biên dịch, không phải exception runtime lúc 3 giờ sáng trong production.
IPC qua Bộ Nhớ Dùng Chung
Hai tiến trình ánh xạ cùng một file trong /dev/shm. Publisher ghi vào bộ đệm vòng, subscriber đọc. Không có socket, không có system call trên hot path:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber (tiến trình khác)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
Toàn bộ đường đi từ publish() đến lời gọi handler_fn trong subscriber — 30 nanosecond cho tin nhắn 64 byte.
Độ Tin Cậy Dựa Trên NAK cho UDP
Đối với vận chuyển mạng, ZigBolt sử dụng retransmission do receiver điều khiển. Receiver theo dõi các khoảng trống trong số thứ tự qua bitmap và gửi NAK (negative acknowledgement) đến sender. Thêm vào đó là điều khiển tắc nghẽn AIMD — slow start và congestion avoidance giống TCP — để tránh làm ngập mạng.
Cụm Raft: Khi Bạn Cần Tính Nhất Quán
Đối với các trường hợp mà mất một tin nhắn là không thể chấp nhận (ví dụ: matching engine), ZigBolt bao gồm đồng thuận Raft đầy đủ:
- Bầu chọn leader với timeout có thể cấu hình (150-300 ms)
- Sao chép log — leader sao chép mỗi tin nhắn đến followers
- Write-ahead log với xác thực CRC32 và phục hồi sau crash
- Snapshots — để WAL không tăng trưởng mãi mãi
Archive: Ghi và Phát Lại
Tất cả tin nhắn có thể được ghi vào archive trên đĩa được phân đoạn. Sau đó — phát lại từ bất kỳ vị trí nào theo thời gian hoặc số thứ tự. Nén tích hợp kiểu LZ4 không có phụ thuộc bên ngoài. Chỉ mục thưa thớt để tra cứu nhanh trong các phân đoạn.
Sequencer Thứ Tự Toàn Cục
Đối với tạo lập thị trường trên nhiều sàn giao dịch, điều quan trọng là tất cả các sự kiện có thứ tự toàn cục. Sequencer lấy N luồng đầu vào và hợp nhất chúng thành một, gán số thứ tự tăng đơn điệu. Mỗi người tham gia thấy cùng một chuỗi sự kiện.
Tại Sao Zig, Không Phải Rust/C/C++?
Chúng tôi chọn giữa bốn ứng viên. Đây là so sánh trung thực:
| Tiêu Chí | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / runtime overhead | Không | Không | Không | JVM safepoints, GC |
| Tạo code comptime | Có sẵn | Macros/templates | proc macros | Không |
| C interop (DPDK, io_uring) | @cImport đơn giản |
Có sẵn | FFI/bindgen | Overhead JNI |
| SIMD | @Vector, tích hợp |
Intrinsics | packed_simd (không ổn định) | Gợi ý vector hóa |
| Cross-compilation | Có sẵn | CMake địa ngục | cargo target | N/A |
| Thời gian build | Giây | Phút (C++) | Phút | Giây + khởi động JVM |
| Luồng điều khiển ẩn | Không | Exceptions, ép kiểu ngầm | Panics trong unwrap |
Exceptions |
Zig cho chúng tôi sự kết hợp độc đáo: hiệu suất cấp C + an toàn trong quá trình phát triển + metaprogramming comptime (codec, bảng tra cứu, máy trạng thái giao thức — tất cả được tạo ra tại compile time) + tích hợp dễ dàng với DPDK, liburing, ef_vi qua @cImport.
Và binary Zig nặng ~100 KB. So với 20+ MB cho giải pháp dựa trên JVM.
Bindings: Làm Việc Bằng Ngôn Ngữ Của Bạn
ZigBolt biên dịch thành thư viện dùng chung với C-ABI, và chúng tôi có sẵn bindings cho năm ngôn ngữ:
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();
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)
Ngoài ra còn có Go và C thuần túy. Cùng một kênh bộ nhớ dùng chung có thể truy cập từ tất cả các ngôn ngữ đồng thời — publisher trong Zig, subscriber trong Python, monitoring trong Go. Tất cả đều đọc cùng một vùng mmap.
Codec SBE: Tin Nhắn Tương Thích FIX
Đối với các giao thức tài chính, ZigBolt bao gồm codec SBE (Simple Binary Encoding) đầy đủ với schema compile-time. Các loại tin nhắn tích hợp:
- NewOrderSingle — gửi lệnh
- ExecutionReport — báo cáo thực hiện
- MarketDataIncrementalRefresh — cập nhật dữ liệu thị trường gia tăng
- MassQuote — báo giá hàng loạt
- Heartbeat — kiểm tra kết nối
- Logon — xác thực
Không có code generator bên ngoài, không có XML. Mọi thứ được mô tả bằng struct Zig và được xác thực tại compile time.
Giao Thức Wire: Tương Thích Aeron
ZigBolt thực hiện các flyweight giao thức wire tương thích Aeron:
- DataHeaderFlyweight — khung dữ liệu
- StatusMessage — điều khiển luồng
- NAK — negative acknowledgement
- Setup, RTT, Error — khung dịch vụ
Điều này có nghĩa là ZigBolt có thể cùng tồn tại với cơ sở hạ tầng Aeron hiện có. Việc di chuyển không phải là một big bang.
Tiếp Theo Là Gì
ZigBolt hiện đang ở phiên bản 0.2.1. Core ổn định, benchmarks có thể tái tạo, bindings hoạt động. Sắp ra mắt:
- Backend io_uring — vận chuyển mạng zero-copy trên Linux 6.0+ (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — kernel bypass khi mỗi microsecond đều quan trọng
- Multi-Raft — sharding theo công cụ/chiến lược
- Archive dạng cột — tích hợp Apache Arrow/Parquet cho phân tích
- Hỗ trợ Hugepage — hugepage 2MB/1GB pre-faulted để giảm thiểu TLB miss
Thử Ngay
- Website: zigbolt-landing.vercel.app
- Docs: zigbolt-landing.vercel.app/getting-started/introduction/
- Mã nguồn: github.com/suenot/zigbolt
- Giấy phép: MIT
Build từ nguồn (zig build), chạy benchmarks (zig build bench), kết nối qua FFI từ bất kỳ ngôn ngữ nào. Nếu bạn có Zig 0.15.1 và vài phút — hãy thử benchmark ping-pong và so sánh với giải pháp hiện tại của bạn.
Liên Kết:
- ZigBolt Landing: zigbolt-landing.vercel.app
- GitHub: github.com/suenot/zigbolt
- Aeron (để so sánh): github.com/real-logic/aeron | tổng quan Aeron của chúng tôi
- Ngôn ngữ Zig: ziglang.org
- Marketmaker.cc: marketmaker.cc
Trích Dẫn
@software{soloviov2026zigbolt,
author = {Soloviov, Eugen},
title = {ZigBolt: Why We Built Our Own Aeron in Zig and Hit 20 Nanoseconds Per Message},
year = {2026},
url = {https://marketmaker.cc/vi/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {Cách chúng tôi xây dựng hệ thống nhắn tin cực thấp độ trễ cho HFT từ đầu bằng Zig. Không JVM, không GC, không bất ngờ.}
}
Tác Giả
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.