← Kembali ke artikel
March 27, 2026
5 menit baca

ZigBolt: Mengapa Kami Membangun Aeron Versi Kami Sendiri di Zig dan Mencapai 20 Nanodetik Per Pesan

#zigbolt
#zig
#hft
#latensi-rendah
#messaging
#aeron
#ipc
#open-source

ZigBolt — messaging dengan latensi ultra-rendah 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:

  1. Tidak memiliki overhead runtime (tanpa GC, tanpa safepoint)
  2. Memungkinkan pembuatan kode saat kompilasi (comptime)
  3. Terintegrasi secara trivial dengan library C (DPDK, io_uring)
  4. 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 ZigBolt

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

SPSC Ring Buffer

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

WireCodec — codec waktu kompilasi

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

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:


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.}
}
Penafian: Informasi yang disediakan dalam artikel ini hanya untuk tujuan edukasi dan informasi serta tidak merupakan nasihat keuangan, investasi, atau trading. Trading mata uang kripto mengandung risiko kerugian yang signifikan.

Penulis

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Selangkah Lebih Maju dari Pasar

Berlangganan newsletter kami untuk wawasan AI trading eksklusif, analisis pasar, dan pembaruan platform.

Kami menghormati privasi Anda. Berhenti berlangganan kapan saja.