ZigBolt: Mengapa Kami Membina Aeron Kami Sendiri dalam Zig dan Mencapai 20 Nanosaat Per Mesej
Penimbal gelang bebas-kunci, codec sifar-salinan, kluster Raft — semua dalam Zig tulen, semua sumber terbuka.
Jika anda bekerja dalam dagangan algoritmik atau pembuat pasaran, anda tahu harga setiap mikrosaat. Satu peralihan konteks tambahan — dan pesanan anda tiba di tempat kedua. Satu jeda JVM GC — dan pembuat pasaran di sebelah lain sudah mengemas kini petikan. Dalam dunia di mana wang diukur dalam nanosaat, infrastruktur penghantar mesej bukan paip membosankan antara perkhidmatan — ia adalah kelebihan daya saing.
Kami membina ZigBolt — sistem penghantar mesej untuk dagangan frekuensi tinggi yang ditulis dalam Zig. Dari awal. Tiada JVM, tiada pengumpul sampah, tiada Media Driver, tiada konfigurasi XML. Dan kami mendapat kependaman p50 20 nanosaat pada penimbal gelang SPSC dan 30 nanosaat pada IPC melalui memori bersama.
Artikel ini merangkumi mengapa kami memerlukannya, bagaimana ia berfungsi di dalam, dan mengapa Zig.
TL;DR
- ZigBolt — sistem penghantar mesej sumber terbuka (MIT) untuk HFT dalam Zig tulen
- 20 ns p50 pada SPSC, 30 ns p50 pada IPC — lebih pantas daripada nombor yang diterbitkan Aeron
- Codec sifar-salinan berjalan pada 0 ns (penjanaan masa-kompil, masa jalan hanyalah penghantaran penuding)
- Tiada GC, tiada JVM, tiada Media Driver — perpustakaan tertanam terus ke dalam aplikasi anda
- Kluster Raft, arkib, penjujukan — semua disertakan
- Pengikatan FFI untuk Rust, Python, Go, TypeScript, C — bekerja dalam bahasa yang anda suka
Masalah: Mengapa Aeron Bagus Tetapi Tidak Mencukupi
Aeron dari Real Logic adalah piawaian de facto untuk penghantar mesej kependaman rendah di pasaran modal. Berpuluh-puluh firma HFT menggunakannya, ia telah teruji dalam pertempuran, dan mempunyai seni bina yang sangat baik. Tetapi Aeron mempunyai masalah asas, dan namanya adalah JVM.
Titik Selamat JVM: Musuh Tidak Kelihatan
Walaupun anda meletakkan semua data dengan teliti dalam memori off-heap, walaupun anda melumpuhkan ergonomik GC dan menetapkan GuaranteedSafepointInterval=300000 — JVM masih kadangkala menghentikan semua urutan di titik selamat. Ini bukan pepijat, ini adalah keputusan seni bina: JVM memerlukan titik selamat untuk penyahoptimuman, penguncian berat sebelah, dan penjelajahan tindanan.
Dalam amalan ia kelihatan seperti ini: urutan anda menghantar mesej pada p50 = 200 ns, dan tiba-tiba p99.9 melonjak ke 50 us. Tanpa sebab yang jelas. Kerana salah satu urutan JVM memutuskan sudah tiba masanya.
Media Driver: Lompatan Tambahan
Aeron berfungsi melalui Media Driver — proses berasingan (atau JVM tertanam) yang menghalakan mesej antara penerbit dan pelanggan melalui memori bersama. Ini memberikan pengasingan yang baik tetapi menambah sekurang-kurangnya satu lompatan tambahan:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
Setiap lompatan bermakna nanosaat tambahan, ketinggalan cache tambahan, ketidakpastian tambahan.
SBE: Langkah Binaan Berasingan
Simple Binary Encoding — codec FIX standard untuk mesej kewangan. Dalam ekosistem Aeron, ia adalah utiliti Java berasingan yang menjana kod dari skema XML. Kebergantungan berasingan, langkah binaan berasingan, set masalah berasingan.
Penyelesaian: ZigBolt
Kami bertanya kepada diri sendiri: bagaimana jika kami mengambil idea terbaik Aeron — log tiga-penimbal, penimbal gelang bebas-kunci, kluster Raft — dan melaksanakannya dalam bahasa yang:
- Tidak mempunyai overhed masa jalan (tiada GC, tiada titik selamat)
- Membolehkan penjanaan kod pada masa kompil (comptime)
- Berintegrasi dengan mudah dengan perpustakaan C (DPDK, io_uring)
- Mengkompil kepada binari ~100 KB
Bahasa itu adalah Zig.
Seni Bina
┌─────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────┘
Tujuh lapisan, setiap satunya boleh digunakan secara bebas. Hanya perlu penimbal gelang SPSC untuk IPC antara dua proses? Ambil sahaja. Perlu kluster penuh dengan konsensus Raft dan pengarkiban? Juga ada.
Penanda Aras: Nombor, Bukan Kata-kata

Keputusan penanda aras sebenar melebihi 10 juta lelaran (Apple Silicon / macOS):
Penimbal Gelang SPSC
| Saiz Mesej | p50 | p99 | p99.9 | Daya Pemprosesan |
|---|---|---|---|---|
| 8 bait | 20 ns | 30 ns | 120 ns | 42.8M msg/s |
| 32 bait | 30 ns | 50 ns | 150 ns | 28.5M msg/s |
| 64 bait | 50 ns | 60 ns | 320 ns | 17.6M msg/s |
| 256 bait | 30 ns | 50 ns | 50 ns | 29.5M msg/s |
Saluran IPC (memori bersama)
| Saiz Mesej | p50 | p99 | p99.9 | Daya Pemprosesan |
|---|---|---|---|---|
| 64 bait | 30 ns | 40 ns | 40 ns | 35.7M msg/s |
| 256 bait | 40 ns | 40 ns | 170 ns | 27.4M msg/s |
| 1024 bait | 90 ns | 260 ns | 900 ns | 9.9M msg/s |
LogBuffer (tiga-penimbal gaya Aeron)
| Saiz Mesej | p50 | p99 | p99.9 | Daya Pemprosesan |
|---|---|---|---|---|
| 32 bait | 30 ns | 40 ns | 320 ns | 33.6M msg/s |
| 64 bait | 30 ns | 30 ns | 160 ns | 38.0M msg/s |
| 256 bait | 30 ns | 40 ns | 60 ns | 31.1M msg/s |
WireCodec (sifar-salinan comptime)
| Operasi | Kependaman | Daya Pemprosesan |
|---|---|---|
| Enkod (32 bait) | 0 ns | memcpy diselaraskan |
| Dekod (32 bait) | ~0.4 ns | 2.7 bilion msg/s |
Ya, anda membaca dengan betul: pengekodan mengambil sifar nanosaat. Kerana WireCodec(T) mengesahkan struct pada masa kompil dan menukar enkod/dekod kepada @memcpy biasa atau penghantaran penuding. Overhed masa jalan = sifar.
Sebagai perbandingan: Aeron mendakwa IPC RTT (perjalanan pusingan) sekitar ~250 ns. Kependaman sehala kami adalah 30 ns. Walaupun mengira perjalanan pusingan, kami 4x lebih pantas.
Cara Ia Berfungsi Di Dalam
SPSC Bebas-Kunci: Kesederhanaan sebagai Kebajikan

Penimbal gelang pengeluar-tunggal pengguna-tunggal adalah struktur data bebas-kunci yang paling mudah dan paling pantas. Penulis menggerakkan head, pembaca menggerakkan tail, tiada CAS diperlukan — atomik perolehan/pelepasan mencukupi.
Helah utama adalah padding baris cache. Jika head dan tail berada dalam baris cache yang sama, setiap kemas kini kepada satu pembilang membatalkan cache untuk teras lain (perkongsian palsu). Pembaikannya:
// Head (kedudukan tulis) — pada baris cache sendiri
head: std.atomic.Value(usize) align(128) = .init(0),
// Padding 128 bait — pengasingan terjamin
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (kedudukan baca) — pada baris cache sendiri
tail: std.atomic.Value(usize) align(128) = .init(0),
128 bait, bukan 64 — kerana pada Apple Silicon (dan banyak cip ARM) pengambil awal perkakasan boleh bekerja dengan pasangan baris cache. Kami bermain selamat.
WireCodec: Comptime Berbanding Penjanaan Kod

Dalam dunia Java/C++, codec binari memerlukan langkah berasingan: tulis skema, jalankan penjana kod, dapatkan kod, kompil. Dalam Zig, semua ini berlaku pada masa kompil:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// Enkod — hanya memcpy 32-bait. Diselaraskan ke 1-2 arahan.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// Dekod — penghantaran penuding. Sifar salinan.
const tick = Codec.decode(buf[0..Codec.wire_size]);
Pengkompil Zig mengesahkan pada comptime:
- Struct adalah packed (tiada lubang padding)
- Saiz adalah gandaan 8 bait (penjajaran untuk SIMD)
- Semua medan adalah jenis primitif
Jika ada yang salah — ralat kompil, bukan pengecualian masa jalan pada pukul 3 pagi dalam pengeluaran.
IPC melalui Memori Bersama
Dua proses memetakan fail yang sama dalam /dev/shm. Penerbit menulis ke penimbal gelang, pelanggan membaca. Tiada soket, tiada panggilan sistem pada laluan panas:
// Penerbit
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Pelanggan (proses lain)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
Keseluruhan laluan dari publish() ke pemanggilan handler_fn dalam pelanggan — 30 nanosaat untuk mesej 64-bait.
Kebolehpercayaan Berasaskan NAK untuk UDP
Untuk pengangkutan rangkaian, ZigBolt menggunakan penghantaran semula yang didorong penerima. Penerima menjejaki jurang dalam nombor urutan melalui bitmap dan menghantar NAK (pengakuan negatif) kepada penghantar. Ditambah kawalan kesesakan AIMD — permulaan perlahan gaya TCP dan pengelakan kesesakan — untuk mengelakkan banjir rangkaian.
Kluster Raft: Apabila Anda Memerlukan Konsistensi
Untuk kes di mana kehilangan mesej tidak boleh diterima (contohnya, enjin padanan), ZigBolt menyertakan konsensus Raft penuh:
- Pemilihan pemimpin dengan tamat masa yang boleh dikonfigurasi (150-300 ms)
- Replikasi log — pemimpin mereplikasi setiap mesej kepada pengikut
- Log tulis-awal dengan pengesahan CRC32 dan pemulihan kerosakan
- Syot kilat — supaya WAL tidak berkembang selama-lamanya
Arkib: Rekod dan Putar Semula
Semua mesej boleh direkodkan ke arkib tersegmen di cakera. Kemudian — putar semula dari sebarang kedudukan mengikut masa atau nombor urutan. Pemampatan gaya LZ4 terbina dalam tanpa kebergantungan luaran. Indeks jarang untuk carian pantas dalam segmen.
Penjujukan Pesanan-Total
Untuk pembuat pasaran merentasi pelbagai tempat, adalah kritikal bahawa semua peristiwa mempunyai perintah global. Penjujukan mengambil N aliran input dan menggabungkannya menjadi satu, memberikan nombor urutan yang meningkat secara monoton. Setiap peserta melihat urutan peristiwa yang sama.
Mengapa Zig, Bukan Rust/C/C++?
Kami memilih antara empat calon. Berikut adalah perbandingan jujur:
| Kriteria | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| Overhed GC / masa jalan | Tiada | Tiada | Tiada | Titik selamat JVM, GC |
| Penjanaan kod comptime | Asli | Makro/templat | makro proc | Tiada |
| Interop C (DPDK, io_uring) | @cImport mudah |
Asli | FFI/bindgen | Overhed JNI |
| SIMD | @Vector, terbina dalam |
Intrinsik | packed_simd (tidak stabil) | Petunjuk vektorisasi |
| Penyusunan silang | Terbina dalam | Neraka CMake | cargo target | N/A |
| Masa binaan | Saat | Minit (C++) | Minit | Saat + permulaan JVM |
| Aliran kawalan tersembunyi | Tiada | Pengecualian, hantaran tersirat | Panik dalam unwrap |
Pengecualian |
Zig memberikan kami kombinasi unik: prestasi peringkat C + keselamatan semasa pembangunan + metaprograman comptime (codec, jadual carian, mesin keadaan protokol — semua dijana pada masa kompil) + integrasi mudah dengan DPDK, liburing, ef_vi melalui @cImport.
Dan binari Zig bersaiz ~100 KB. Berbanding 20+ MB untuk penyelesaian berasaskan JVM.
Pengikatan: Bekerja dalam Bahasa Anda
ZigBolt mengkompil kepada perpustakaan bersama dengan C-ABI, dan kami mempunyai pengikatan siap untuk lima bahasa:
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)
Ditambah Go dan C biasa. Saluran memori bersama yang sama boleh diakses dari semua bahasa serentak — penerbit dalam Zig, pelanggan dalam Python, pemantauan dalam Go. Mereka semua membaca kawasan mmap yang sama.
Codec SBE: Mesej Serasi FIX
Untuk protokol kewangan, ZigBolt menyertakan codec SBE (Simple Binary Encoding) penuh dengan skema masa-kompil. Jenis mesej terbina dalam:
- NewOrderSingle — penyerahan pesanan
- ExecutionReport — laporan pelaksanaan
- MarketDataIncrementalRefresh — kemas kini data pasaran tambahan
- MassQuote — petikan massa
- Heartbeat — semakan kesambungan
- Logon — pengesahan
Tiada penjana kod luaran, tiada XML. Semua diterangkan dengan struct Zig dan disahkan pada masa kompil.
Protokol Wire: Keserasian Aeron
ZigBolt melaksanakan flyweight protokol wire serasi Aeron:
- DataHeaderFlyweight — bingkai data
- StatusMessage — kawalan aliran
- NAK — pengakuan negatif
- Setup, RTT, Error — bingkai perkhidmatan
Ini bermakna ZigBolt boleh wujud bersama dengan infrastruktur Aeron yang sedia ada. Penghijrahan tidak perlu menjadi peledak besar.
Apa Seterusnya
ZigBolt kini berada pada versi 0.2.1. Teras adalah stabil, penanda aras boleh dihasilkan semula, pengikatan berfungsi. Akan datang tidak lama lagi:
- Backend io_uring — pengangkutan rangkaian sifar-salinan pada Linux 6.0+ (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — pintasan kernel untuk bila setiap mikrosaat dikira
- Multi-Raft — pecahan mengikut instrumen/strategi
- Arkib lajur — integrasi Apache Arrow/Parquet untuk analitik
- Sokongan Hugepage — hugepage 2MB/1GB yang dipra-lalai untuk meminimumkan kesilapan TLB
Cuba Ia
- Laman web: zigbolt-landing.vercel.app
- Dokumen: zigbolt-landing.vercel.app/getting-started/introduction/
- Kod sumber: github.com/suenot/zigbolt
- Lesen: MIT
Bina dari sumber (zig build), jalankan penanda aras (zig build bench), sambung melalui FFI dari mana-mana bahasa. Jika anda mempunyai Zig 0.15.1 dan beberapa minit — cuba penanda aras ping-pong dan bandingkan dengan penyelesaian semasa anda.
Pautan:
- ZigBolt Landing: zigbolt-landing.vercel.app
- GitHub: github.com/suenot/zigbolt
- Aeron (untuk perbandingan): github.com/real-logic/aeron | gambaran keseluruhan Aeron kami
- Bahasa Zig: ziglang.org
- Marketmaker.cc: marketmaker.cc
Petikan
@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/ms/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {Bagaimana dan mengapa kami membina sistem penghantar mesej ultra-rendah kependaman untuk HFT dari awal dalam Zig. Tiada JVM, tiada GC, tiada kejutan.}
}
Pengarang
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.