ZigBolt: Mengapa Kami Membangun Aeron Versi Kami Sendiri di Zig dan Mencapai 20 Nanodetik Per Pesan
Ring buffer bebas-kunci, codec zero-copy, kluster Raft — semuanya dalam Zig murni, semuanya open source.
Jika Anda bekerja di bidang algorithmic trading atau market making, Anda tahu betapa mahalnya setiap mikrodetik. Satu context switch tambahan — dan order Anda tiba di urutan kedua. Satu jeda GC JVM — dan market maker di sisi lain sudah memperbarui kuotasinya. Di dunia di mana uang diukur dalam nanodetik, infrastruktur messaging bukan sekadar pipa membosankan antara layanan — melainkan keunggulan kompetitif.
Kami membangun ZigBolt — sistem messaging untuk high-frequency trading yang ditulis dalam Zig. Dari nol. Tanpa JVM, tanpa garbage collector, tanpa Media Driver, tanpa konfigurasi XML. Dan kami mencapai latensi p50 20 nanodetik pada SPSC ring buffer dan 30 nanodetik pada IPC melalui shared memory.
Artikel ini membahas mengapa kami membutuhkannya, bagaimana cara kerjanya di dalam, dan mengapa Zig.
TL;DR
- ZigBolt — sistem messaging open-source (MIT) untuk HFT dalam Zig murni
- 20 ns p50 pada SPSC, 30 ns p50 pada IPC — lebih cepat dari angka yang dipublikasikan Aeron
- Codec zero-copy berjalan di 0 ns (generasi waktu kompilasi, runtime hanya pointer cast)
- Tanpa GC, tanpa JVM, tanpa Media Driver — library langsung terintegrasi ke dalam aplikasi Anda
- Kluster Raft, arsip, sequencer — semuanya termasuk
- Binding FFI untuk Rust, Python, Go, TypeScript, C — bekerja dalam bahasa apa pun yang Anda sukai
Masalahnya: Mengapa Aeron Bagus tapi Belum Cukup
Aeron dari Real Logic adalah standar de facto untuk messaging latensi rendah di pasar modal. Puluhan perusahaan HFT menggunakannya, sudah teruji battle-tested, dan memiliki arsitektur yang sangat baik. Namun Aeron memiliki masalah mendasar, dan namanya adalah JVM.
JVM Safepoint: Musuh Tak Terlihat
Meskipun Anda dengan hati-hati menempatkan semua data di off-heap memory, meskipun Anda menonaktifkan GC ergonomics dan menetapkan GuaranteedSafepointInterval=300000 — JVM tetap sesekali menghentikan semua thread di safepoint. Ini bukan bug, ini adalah keputusan arsitektur: JVM membutuhkan safepoint untuk deoptimisasi, biased locking, dan stack walking.
Dalam praktiknya terlihat seperti ini: thread Anda mengirim pesan di p50 = 200 ns, dan tiba-tiba p99.9 melonjak ke 50 us. Tanpa alasan yang jelas. Karena salah satu thread JVM memutuskan sudah waktunya.
Media Driver: Lompatan Ekstra
Aeron bekerja melalui Media Driver — proses terpisah (atau JVM tertanam) yang merutekan pesan antara publisher dan subscriber melalui shared memory. Ini memberikan isolasi yang baik tetapi menambahkan setidaknya satu lompatan ekstra:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
Setiap lompatan berarti nanodetik ekstra, cache miss ekstra, ketidakpastian ekstra.
SBE: Langkah Build Terpisah
Simple Binary Encoding — codec FIX standar untuk pesan finansial. Dalam ekosistem Aeron, ini adalah utilitas Java terpisah yang menghasilkan kode dari skema XML. Dependensi terpisah, langkah build terpisah, serangkaian masalah terpisah.
Solusinya: ZigBolt
Kami bertanya pada diri sendiri: bagaimana jika kami mengambil ide-ide terbaik Aeron — triple-buffered log, ring buffer bebas-kunci, kluster Raft — dan mengimplementasikannya dalam bahasa yang:
- Tidak memiliki overhead runtime (tanpa GC, tanpa safepoint)
- Memungkinkan pembuatan kode saat kompilasi (comptime)
- Terintegrasi secara trivial dengan library C (DPDK, io_uring)
- Dikompilasi menjadi biner ~100 KB
Bahasa tersebut adalah Zig.
Arsitektur
┌─────────────────────────────────────────────────────────┐
│ 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, masing-masing dapat digunakan secara independen. Hanya butuh SPSC ring buffer untuk IPC antara dua proses? Ambil saja. Butuh kluster penuh dengan konsensus Raft dan pengarsipan? Juga ada.
Benchmark: Angka, Bukan Kata-Kata

Hasil benchmark nyata selama 10 juta iterasi (Apple Silicon / macOS):
SPSC Ring Buffer
| Ukuran Pesan | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 8 byte | 20 ns | 30 ns | 120 ns | 42,8M msg/s |
| 32 byte | 30 ns | 50 ns | 150 ns | 28,5M msg/s |
| 64 byte | 50 ns | 60 ns | 320 ns | 17,6M msg/s |
| 256 byte | 30 ns | 50 ns | 50 ns | 29,5M msg/s |
IPC Channel (shared memory)
| Ukuran Pesan | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 64 byte | 30 ns | 40 ns | 40 ns | 35,7M msg/s |
| 256 byte | 40 ns | 40 ns | 170 ns | 27,4M msg/s |
| 1024 byte | 90 ns | 260 ns | 900 ns | 9,9M msg/s |
LogBuffer (triple-buffered bergaya Aeron)
| Ukuran Pesan | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 32 byte | 30 ns | 40 ns | 320 ns | 33,6M msg/s |
| 64 byte | 30 ns | 30 ns | 160 ns | 38,0M msg/s |
| 256 byte | 30 ns | 40 ns | 60 ns | 31,1M msg/s |
WireCodec (zero-copy comptime)
| Operasi | Latensi | Throughput |
|---|---|---|
| Encode (32 byte) | 0 ns | memcpy terinline |
| Decode (32 byte) | ~0,4 ns | 2,7 miliar msg/s |
Ya, Anda tidak salah baca: encoding membutuhkan nol nanodetik. Karena WireCodec(T) memvalidasi struct pada waktu kompilasi dan mengubah encode/decode menjadi @memcpy biasa atau pointer cast. Overhead runtime = nol.
Sebagai perbandingan: Aeron mengklaim IPC RTT (round-trip) ~250 ns. Latensi satu arah kami adalah 30 ns. Bahkan dihitung round-trip, kami 4x lebih cepat.
Cara Kerjanya di Dalam
SPSC Bebas-Kunci: Kesederhanaan sebagai Kebajikan

Ring buffer single-producer single-consumer adalah struktur data bebas-kunci yang paling sederhana dan tercepat. Penulis menggerakkan head, pembaca menggerakkan tail, tidak perlu CAS — acquire/release atomics sudah cukup.
Trik kuncinya adalah padding cache-line. Jika head dan tail berada di cache line yang sama, setiap pembaruan pada satu counter akan membatalkan cache untuk core lainnya (false sharing). Solusinya:
// Head (posisi tulis) — pada cache line tersendiri
head: std.atomic.Value(usize) align(128) = .init(0),
// Padding 128 byte — isolasi terjamin
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (posisi baca) — pada cache line tersendiri
tail: std.atomic.Value(usize) align(128) = .init(0),
128 byte, bukan 64 — karena pada Apple Silicon (dan banyak chip ARM) hardware prefetcher dapat bekerja dengan pasangan cache line. Kami bermain aman.
WireCodec: Comptime Menggantikan Pembuatan Kode

Di dunia Java/C++, codec biner memerlukan langkah terpisah: tulis skema, jalankan generator kode, dapatkan kode, kompilasi. Di Zig, semua ini terjadi pada waktu kompilasi:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// Encode — hanya memcpy 32 byte. Diinline menjadi 1-2 instruksi.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// Decode — pointer cast. Nol salinan.
const tick = Codec.decode(buf[0..Codec.wire_size]);
Kompiler Zig memvalidasi saat comptime:
- Struct bersifat packed (tanpa lubang padding)
- Ukuran adalah kelipatan 8 byte (alignment untuk SIMD)
- Semua field adalah tipe primitif
Jika ada yang salah — error kompilasi, bukan exception runtime jam 3 pagi di produksi.
IPC via Shared Memory
Dua proses memetakan file yang sama di /dev/shm. Publisher menulis ke ring buffer, subscriber membaca. Tanpa socket, tanpa system call di hot path:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber (proses lain)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
Seluruh jalur dari publish() hingga pemanggilan handler_fn di subscriber — 30 nanodetik untuk pesan 64 byte.
Reliabilitas Berbasis NAK untuk UDP
Untuk transport jaringan, ZigBolt menggunakan retransmisi yang digerakkan oleh receiver. Receiver melacak celah dalam sequence number melalui bitmap dan mengirimkan NAK (negative acknowledgement) ke pengirim. Ditambah kontrol kemacetan AIMD — slow start bergaya TCP dan penghindaran kemacetan — untuk menghindari banjir jaringan.
Kluster Raft: Ketika Anda Butuh Konsistensi
Untuk kasus-kasus di mana kehilangan pesan tidak dapat diterima (misalnya, matching engine), ZigBolt menyertakan konsensus Raft penuh:
- Pemilihan pemimpin dengan timeout yang dapat dikonfigurasi (150-300 ms)
- Replikasi log — pemimpin mereplikasi setiap pesan ke follower
- Write-ahead log dengan validasi CRC32 dan pemulihan dari crash
- Snapshot — agar WAL tidak bertumbuh selamanya
Arsip: Rekam dan Putar Ulang
Semua pesan dapat direkam ke arsip on-disk yang tersegmentasi. Kemudian — putar ulang dari posisi mana pun berdasarkan waktu atau sequence number. Kompresi bergaya LZ4 bawaan tanpa dependensi eksternal. Indeks sparse untuk pencarian cepat dalam segmen.
Sequencer Total-Order
Untuk market making di beberapa venue, sangat penting bahwa semua event memiliki urutan global. Sequencer mengambil N stream input dan menggabungkannya menjadi satu, menetapkan sequence number yang selalu meningkat monoton. Setiap peserta melihat urutan event yang sama.
Mengapa Zig, Bukan Rust/C/C++?
Kami memilih di antara empat kandidat. Berikut perbandingan jujurnya:
| Kriteria | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / overhead runtime | Tidak ada | Tidak ada | Tidak ada | JVM safepoint, GC |
| Pembuatan kode comptime | Native | Macro/template | proc macro | Tidak ada |
| C interop (DPDK, io_uring) | @cImport trivial |
Native | FFI/bindgen | Overhead JNI |
| SIMD | @Vector, bawaan |
Intrinsic | packed_simd (tidak stabil) | Petunjuk vektorisasi |
| Cross-compilation | Bawaan | CMake hell | cargo target | N/A |
| Waktu build | Detik | Menit (C++) | Menit | Detik + startup JVM |
| Control flow tersembunyi | Tidak ada | Exception, implicit cast | Panic di unwrap |
Exception |
Zig memberi kami kombinasi unik: performa setara C + keamanan saat pengembangan + metaprogramming comptime (codec, lookup table, mesin state protokol — semuanya dihasilkan saat kompilasi) + integrasi trivial dengan DPDK, liburing, ef_vi via @cImport.
Dan biner Zig berbobot ~100 KB. Berbanding 20+ MB untuk solusi berbasis JVM.
Binding: Bekerja dalam Bahasa Anda
ZigBolt dikompilasi menjadi shared library dengan C-ABI, dan kami memiliki binding siap pakai 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)
Plus Go dan C biasa. Channel shared memory yang sama dapat diakses dari semua bahasa secara bersamaan — publisher di Zig, subscriber di Python, monitoring di Go. Semuanya membaca region mmap yang sama.
Codec SBE: Pesan Kompatibel FIX
Untuk protokol finansial, ZigBolt menyertakan codec SBE (Simple Binary Encoding) penuh dengan skema waktu kompilasi. Tipe pesan bawaan:
- NewOrderSingle — pengajuan order
- ExecutionReport — laporan eksekusi
- MarketDataIncrementalRefresh — pembaruan data pasar inkremental
- MassQuote — kuotasi massal
- Heartbeat — pemeriksaan konektivitas
- Logon — autentikasi
Tanpa generator kode eksternal, tanpa XML. Semuanya dideskripsikan dengan struct Zig dan divalidasi pada waktu kompilasi.
Protokol Wire: Kompatibilitas Aeron
ZigBolt mengimplementasikan flyweight protokol wire yang kompatibel dengan Aeron:
- DataHeaderFlyweight — frame data
- StatusMessage — kontrol aliran
- NAK — negative acknowledgement
- Setup, RTT, Error — frame layanan
Ini berarti ZigBolt dapat berdampingan dengan infrastruktur Aeron yang sudah ada. Migrasi tidak harus dilakukan sekaligus.
Apa Selanjutnya
ZigBolt saat ini berada di versi 0.2.1. Core-nya stabil, benchmark dapat direproduksi, binding berfungsi. Segera hadir:
- Backend io_uring — transport jaringan zero-copy di Linux 6.0+ (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — kernel bypass untuk saat setiap mikrodetik berarti
- Multi-Raft — sharding berdasarkan instrumen/strategi
- Arsip kolumnar — integrasi Apache Arrow/Parquet untuk analitik
- Dukungan hugepage — 2MB/1GB hugepage yang di-pre-fault untuk meminimalkan TLB miss
Coba Sekarang
- Website: zigbolt-landing.vercel.app
- Dokumentasi: zigbolt-landing.vercel.app/getting-started/introduction/
- Kode sumber: github.com/suenot/zigbolt
- Lisensi: MIT
Build dari sumber (zig build), jalankan benchmark (zig build bench), hubungkan via FFI dari bahasa apa pun. Jika Anda memiliki Zig 0.15.1 dan beberapa menit — coba benchmark ping-pong dan bandingkan dengan solusi Anda saat ini.
Tautan:
- ZigBolt Landing: zigbolt-landing.vercel.app
- GitHub: github.com/suenot/zigbolt
- Aeron (sebagai perbandingan): github.com/real-logic/aeron | ikhtisar Aeron kami
- Bahasa Zig: ziglang.org
- Marketmaker.cc: marketmaker.cc
Kutipan
@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/id/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {Bagaimana dan mengapa kami membangun sistem messaging ultra-latensi-rendah untuk HFT dari nol di Zig. Tanpa JVM, tanpa GC, tanpa kejutan.}
}
Penulis
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.