ZigBolt: ทำไมเราถึงสร้าง Aeron ของตัวเองด้วย Zig และทำความเร็วได้ 20 นาโนวินาทีต่อข้อความ
Lock-free ring buffers, zero-copy codecs, Raft cluster — ทั้งหมดใน Zig บริสุทธิ์ ทั้งหมด open source
หากคุณทำงานด้านการเทรดเชิงอัลกอริทึมหรือ market making คุณรู้ดีถึงราคาของทุกๆ ไมโครวินาที context switch ที่ไม่จำเป็นแม้แค่ครั้งเดียว — และออร์เดอร์ของคุณจะมาถึงเป็นที่สอง GC pause ของ JVM แค่ครั้งเดียว — และ market maker อีกฝั่งก็อัปเดตราคาไปแล้ว ในโลกที่เงินถูกวัดด้วยนาโนวินาที โครงสร้างพื้นฐานด้านการรับส่งข้อความไม่ใช่แค่ท่อธรรมดาระหว่างบริการ — มันคือความได้เปรียบเชิงแข่งขัน
เราสร้าง ZigBolt — ระบบรับส่งข้อความสำหรับการเทรดความถี่สูงที่เขียนด้วย Zig ตั้งแต่ต้น ไม่มี JVM ไม่มี garbage collector ไม่มี Media Driver ไม่มี XML config และเราทำได้ 20 นาโนวินาที p50 latency บน SPSC ring buffer และ 30 นาโนวินาที บน IPC ผ่าน shared memory
บทความนี้ครอบคลุมว่าทำไมเราถึงต้องการมัน มันทำงานอย่างไรภายใน และทำไมถึงเลือก Zig
TL;DR
- ZigBolt — ระบบรับส่งข้อความ open-source (MIT) สำหรับ HFT ใน Zig บริสุทธิ์
- 20 ns p50 บน SPSC, 30 ns p50 บน IPC — เร็วกว่าตัวเลขที่ Aeron เผยแพร่
- Zero-copy codec ทำงานที่ 0 ns (สร้างโค้ดที่ compile time, runtime เป็นแค่การ cast pointer)
- ไม่มี GC, ไม่มี JVM, ไม่มี Media Driver — library ฝังตัวโดยตรงในแอปพลิเคชันของคุณ
- Raft cluster, archive, sequencer — ครบหมด
- FFI bindings สำหรับ Rust, Python, Go, TypeScript, C — ทำงานในภาษาใดก็ได้ที่คุณชอบ
ปัญหา: ทำไม Aeron ถึงดีแต่ยังไม่พอ
Aeron จาก Real Logic คือมาตรฐาน de facto สำหรับการรับส่งข้อความ low-latency ในตลาดทุน บริษัท HFT หลายสิบแห่งใช้งาน มันผ่านการทดสอบในสนามจริงแล้ว และมีสถาปัตยกรรมที่ยอดเยี่ยม แต่ Aeron มีปัญหาพื้นฐาน และชื่อของมันคือ JVM
JVM Safepoints: ศัตรูที่มองไม่เห็น
แม้คุณจะระมัดระวังวางข้อมูลทั้งหมดใน off-heap memory แม้คุณจะปิดการใช้งาน GC ergonomics และตั้งค่า GuaranteedSafepointInterval=300000 — JVM ก็ยังหยุด thread ทั้งหมดที่ safepoint เป็นครั้งคราว นี่ไม่ใช่ bug แต่เป็นการตัดสินใจเชิงสถาปัตยกรรม: JVM ต้องการ safepoints สำหรับ deoptimization, biased locking, และ stack walking
ในทางปฏิบัติมันดูแบบนี้: thread ของคุณส่งข้อความที่ p50 = 200 ns และทันใดนั้น p99.9 พุ่งไปถึง 50 us ไม่มีเหตุผลที่ชัดเจน เพราะ JVM thread ตัวหนึ่งตัดสินใจว่าถึงเวลาแล้ว
Media Driver: Hop พิเศษที่ไม่จำเป็น
Aeron ทำงานผ่าน Media Driver — กระบวนการแยกต่างหาก (หรือ JVM แบบ embedded) ที่กำหนดเส้นทางข้อความระหว่าง publisher และ subscriber ผ่าน shared memory ซึ่งให้การแยกส่วนที่ดีแต่เพิ่ม hop พิเศษอย่างน้อยหนึ่งอัน:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
ทุก hop หมายถึงนาโนวินาทีพิเศษ cache miss พิเศษ และความไม่แน่นอนพิเศษ
SBE: ขั้นตอน Build แยกต่างหาก
Simple Binary Encoding — codec FIX มาตรฐานสำหรับข้อความทางการเงิน ในระบบนิเวศ Aeron มันเป็นยูทิลิตี้ Java แยกต่างหากที่สร้างโค้ดจาก XML schema เป็น dependency แยก ขั้นตอน build แยก และปัญหาแยกออกไปอีกชุด
ทางออก: ZigBolt
เราถามตัวเองว่า: ถ้าเราเอาแนวคิดที่ดีที่สุดของ Aeron — triple-buffered log, lock-free ring buffers, Raft cluster — มาใช้ในภาษาที่:
- ไม่มี runtime overhead (ไม่มี GC, ไม่มี safepoints)
- อนุญาตให้สร้างโค้ดที่ compile time (comptime)
- ผสานกับ C libraries ได้อย่างง่ายดาย (DPDK, io_uring)
- Compile เป็น binary ขนาด ~100 KB
ภาษานั้นคือ Zig
สถาปัตยกรรม
┌─────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────┘
เจ็ดชั้น แต่ละชั้นใช้งานได้อิสระ ต้องการแค่ SPSC ring buffer สำหรับ IPC ระหว่างสองกระบวนการ? เอาไปได้เลย ต้องการ cluster เต็มรูปแบบพร้อม Raft consensus และ archiving? ก็มีเช่นกัน
Benchmarks: ตัวเลข ไม่ใช่คำพูด

ผลลัพธ์ benchmark จริงจาก 10 ล้านรอบ (Apple Silicon / macOS):
SPSC Ring Buffer
| ขนาดข้อความ | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 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 |
IPC Channel (shared memory)
| ขนาดข้อความ | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 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 (Aeron-style triple-buffered)
| ขนาดข้อความ | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 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)
| การดำเนินการ | Latency | Throughput |
|---|---|---|
| Encode (32 bytes) | 0 ns | inlined memcpy |
| Decode (32 bytes) | ~0.4 ns | 2.7 billion msg/s |
ใช่ คุณอ่านถูก: การ encode ใช้เวลาศูนย์นาโนวินาที เพราะ WireCodec(T) ตรวจสอบ struct ที่ compile time และแปลง encode/decode เป็น @memcpy ธรรมดาหรือการ cast pointer Runtime overhead = ศูนย์
เพื่อเปรียบเทียบ: Aeron อ้างว่า IPC RTT (round-trip) ~250 ns latency ทางเดียวของเราคือ 30 ns แม้นับ round-trip เราก็ยังเร็วกว่า 4 เท่า
มันทำงานอย่างไรภายใน
Lock-Free SPSC: ความเรียบง่ายคือคุณธรรม

Single-producer single-consumer ring buffer คือโครงสร้างข้อมูล lock-free ที่ง่ายที่สุดและเร็วที่สุด ผู้เขียนเลื่อน head ผู้อ่านเลื่อน tail ไม่จำเป็นต้องใช้ CAS — acquire/release atomics เพียงพอ
เคล็ดลับสำคัญคือการใส่ padding ระหว่าง cache line ถ้า head และ tail อยู่ใน cache line เดียวกัน การอัปเดต counter ตัวใดตัวหนึ่งจะทำให้ cache ของ core อีกตัวใช้ไม่ได้ (false sharing) วิธีแก้:
// Head (write position) — on its own cache line
head: std.atomic.Value(usize) align(128) = .init(0),
// 128 bytes padding — guaranteed isolation
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (read position) — on its own cache line
tail: std.atomic.Value(usize) align(128) = .init(0),
128 bytes ไม่ใช่ 64 — เพราะบน Apple Silicon (และชิป ARM หลายตัว) hardware prefetcher สามารถทำงานกับคู่ของ cache line เราเล่นให้ปลอดภัย
WireCodec: Comptime แทนการสร้างโค้ด

ในโลก Java/C++ binary codec ต้องการขั้นตอนแยกต่างหาก: เขียน schema เรียก code generator รับโค้ด แล้ว compile ใน Zig ทั้งหมดนี้เกิดขึ้นที่ 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 — just a 32-byte memcpy. Inlines to 1-2 instructions.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// Decode — pointer cast. Zero copies.
const tick = Codec.decode(buf[0..Codec.wire_size]);
Zig compiler ตรวจสอบที่ comptime:
- struct เป็น packed (ไม่มี padding holes)
- ขนาดเป็นผลคูณของ 8 bytes (alignment สำหรับ SIMD)
- ทุก field เป็น primitive types
ถ้ามีอะไรผิดพลาด — compile error ไม่ใช่ runtime exception ตี 3 ใน production
IPC ผ่าน Shared Memory
สองกระบวนการ map ไฟล์เดียวกันใน /dev/shm Publisher เขียนไปยัง ring buffer subscriber อ่าน ไม่มี socket ไม่มี system call บน hot path:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber (another process)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
เส้นทางทั้งหมดจาก publish() ไปจนถึงการเรียก handler_fn ใน subscriber — 30 นาโนวินาทีสำหรับข้อความ 64 bytes
ความน่าเชื่อถือแบบ NAK-Based สำหรับ UDP
สำหรับ network transport ZigBolt ใช้การส่งซ้ำที่ขับเคลื่อนโดย receiver ผู้รับติดตามช่องว่างใน sequence numbers ผ่าน bitmap และส่ง NAK (negative acknowledgement) ไปยังผู้ส่ง บวกกับ AIMD congestion control — slow start และ congestion avoidance แบบ TCP — เพื่อหลีกเลี่ยงการท่วม network
Raft Cluster: เมื่อคุณต้องการความสอดคล้อง
สำหรับกรณีที่การสูญเสียข้อความเป็นสิ่งที่รับไม่ได้ (เช่น matching engine) ZigBolt รวม Raft consensus เต็มรูปแบบ:
- Leader election พร้อม timeout ที่ปรับได้ (150-300 ms)
- Log replication — leader จำลองทุกข้อความไปยัง followers
- Write-ahead log พร้อมการตรวจสอบ CRC32 และการกู้คืนจาก crash
- Snapshots — เพื่อไม่ให้ WAL โตขึ้นตลอดไป
Archive: บันทึกและเล่นซ้ำ
ข้อความทั้งหมดสามารถบันทึกลงใน archive บน disk แบบ segmented ได้ จากนั้น — เล่นซ้ำจากตำแหน่งใดก็ได้ตามเวลาหรือ sequence number การบีบอัดสไตล์ LZ4 แบบ built-in โดยไม่มี external dependency Index แบบ sparse สำหรับการค้นหาภายใน segments ที่รวดเร็ว
Total-Order Sequencer
สำหรับ market making ข้ามหลาย venue มันสำคัญมากที่ทุก event มี global order Sequencer รับ N input streams และรวมเป็นหนึ่ง โดยกำหนด sequence numbers ที่เพิ่มขึ้นแบบ monotonic ทุกผู้เข้าร่วมเห็น sequence ของ events เดียวกัน
ทำไม Zig ไม่ใช่ Rust/C/C++?
เราเลือกระหว่างสี่ตัวเลือก นี่คือการเปรียบเทียบที่ตรงไปตรงมา:
| เกณฑ์ | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / runtime overhead | ไม่มี | ไม่มี | ไม่มี | JVM safepoints, GC |
| Comptime code generation | Native | Macros/templates | proc macros | ไม่มี |
| C interop (DPDK, io_uring) | Trivial @cImport |
Native | FFI/bindgen | JNI overhead |
| SIMD | @Vector, built-in |
Intrinsics | packed_simd (unstable) | Vectorization hints |
| Cross-compilation | Built-in | CMake hell | cargo target | N/A |
| Build time | วินาที | นาที (C++) | นาที | วินาที + JVM startup |
| Hidden control flow | ไม่มี | Exceptions, implicit casts | Panics ใน unwrap |
Exceptions |
Zig ให้เราในสิ่งที่ไม่เหมือนใคร: ประสิทธิภาพระดับ C + ความปลอดภัยระหว่างการพัฒนา + comptime metaprogramming (codecs, lookup tables, protocol state machines — ทั้งหมดสร้างที่ compile time) + การผสานกับ DPDK, liburing, ef_vi ผ่าน @cImport ได้อย่างง่ายดาย
และ binary ของ Zig มีน้ำหนัก ~100 KB เทียบกับ 20+ MB สำหรับโซลูชันที่ใช้ JVM
Bindings: ทำงานในภาษาของคุณ
ZigBolt compile เป็น shared library ที่มี C-ABI และเรามี bindings สำเร็จรูปสำหรับห้าภาษา:
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)
บวกกับ Go และ C ธรรมดา ช่อง shared memory เดียวกันเข้าถึงได้จากทุกภาษาพร้อมกัน — publisher ใน Zig, subscriber ใน Python, monitoring ใน Go ทุกคนอ่าน mmap region เดียวกัน
SBE Codec: ข้อความที่เข้ากันได้กับ FIX
สำหรับ protocol ทางการเงิน ZigBolt รวม SBE (Simple Binary Encoding) codec เต็มรูปแบบพร้อม compile-time schemas ประเภทข้อความที่ built-in:
- NewOrderSingle — การส่งออร์เดอร์
- ExecutionReport — รายงานการซื้อขาย
- MarketDataIncrementalRefresh — การอัปเดตข้อมูลตลาดแบบ incremental
- MassQuote — การ quote จำนวนมาก
- Heartbeat — การตรวจสอบการเชื่อมต่อ
- Logon — การยืนยันตัวตน
ไม่มี external code generator ไม่มี XML ทุกอย่างอธิบายด้วย Zig structs และตรวจสอบที่ compile time
Wire Protocol: ความเข้ากันได้กับ Aeron
ZigBolt ใช้งาน Aeron-compatible wire protocol flyweights:
- DataHeaderFlyweight — data frames
- StatusMessage — flow control
- NAK — negative acknowledgement
- Setup, RTT, Error — service frames
หมายความว่า ZigBolt สามารถอยู่ร่วมกับโครงสร้างพื้นฐาน Aeron ที่มีอยู่ได้ การ migration ไม่จำเป็นต้องเป็น big bang
อะไรต่อไป
ZigBolt ปัจจุบันอยู่ที่เวอร์ชัน 0.2.1 core มีความเสถียร benchmarks สามารถทำซ้ำได้ bindings ทำงานได้ กำลังจะมาเร็วๆ นี้:
- io_uring backend — zero-copy network transport บน Linux 6.0+ (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — kernel bypass สำหรับเมื่อทุกไมโครวินาทีมีความสำคัญ
- Multi-Raft — sharding ตาม instrument/strategy
- Columnar archive — Apache Arrow/Parquet integration สำหรับ analytics
- Hugepage support — pre-faulted 2MB/1GB hugepages เพื่อลด TLB misses
ลองใช้งาน
- เว็บไซต์: zigbolt-landing.vercel.app
- เอกสาร: zigbolt-landing.vercel.app/getting-started/introduction/
- Source code: github.com/suenot/zigbolt
- License: MIT
Build จาก source (zig build), รัน benchmarks (zig build bench), เชื่อมต่อผ่าน FFI จากทุกภาษา ถ้าคุณมี Zig 0.15.1 และเวลาสองสามนาที — ลอง ping-pong benchmark และเปรียบเทียบกับโซลูชันปัจจุบันของคุณ
ลิงก์:
- ZigBolt Landing: 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: Why We Built Our Own Aeron in Zig and Hit 20 Nanoseconds Per Message},
year = {2026},
url = {https://marketmaker.cc/th/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {วิธีและเหตุผลที่เราสร้างระบบรับส่งข้อความ ultra-low-latency สำหรับ HFT ตั้งแต่ต้นใน Zig ไม่มี JVM ไม่มี GC ไม่มีเรื่องน่าประหลาดใจ}
}
ผู้เขียน
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.