ZigBolt: ทำไมเราถึงสร้าง Aeron ของตัวเองด้วย Zig และทำความเร็วได้ 20 นาโนวินาทีต่อข้อความ

วิธีและเหตุผลที่เราสร้างระบบรับส่งข้อความ ultra-low-latency สำหรับ HFT ตั้งแต่ต้นใน Zig ไม่มี JVM ไม่มี GC ไม่มีเรื่องน่าประหลาดใจ SPSC ring buffer ที่ 20 ns, IPC ที่ 30 ns, codec ที่ 0 ns พร้อม benchmark

ZigBolt — ultra-low latency messaging 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 — มาใช้ในภาษาที่:

  1. ไม่มี runtime overhead (ไม่มี GC, ไม่มี safepoints)
  2. อนุญาตให้สร้างโค้ดที่ compile time (comptime)
  3. ผสานกับ C libraries ได้อย่างง่ายดาย (DPDK, io_uring)
  4. 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: ตัวเลข ไม่ใช่คำพูด

ZigBolt Benchmark Results

ผลลัพธ์ benchmark จริงจาก 10 ล้านรอบ (Apple Silicon / macOS):

SPSC Ring Buffer

ขนาดข้อความp50p99p99.9Throughput
8 bytes20 ns30 ns120 ns42.8M msg/s
32 bytes30 ns50 ns150 ns28.5M msg/s
64 bytes50 ns60 ns320 ns17.6M msg/s
256 bytes30 ns50 ns50 ns29.5M msg/s

IPC Channel (shared memory)

ขนาดข้อความp50p99p99.9Throughput
64 bytes30 ns40 ns40 ns35.7M msg/s
256 bytes40 ns40 ns170 ns27.4M msg/s
1024 bytes90 ns260 ns900 ns9.9M msg/s

LogBuffer (Aeron-style triple-buffered)

ขนาดข้อความp50p99p99.9Throughput
32 bytes30 ns40 ns320 ns33.6M msg/s
64 bytes30 ns30 ns160 ns38.0M msg/s
256 bytes30 ns40 ns60 ns31.1M msg/s

WireCodec (comptime zero-copy)

การดำเนินการLatencyThroughput
Encode (32 bytes)0 nsinlined memcpy
Decode (32 bytes)~0.4 ns2.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: ความเรียบง่ายคือคุณธรรม

SPSC Ring Buffer

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 แทนการสร้างโค้ด

WireCodec — compile-time codec

ในโลก 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++?

เราเลือกระหว่างสี่ตัวเลือก นี่คือการเปรียบเทียบที่ตรงไปตรงมา:

เกณฑ์ZigC/C++RustJava (Aeron)
GC / runtime overheadไม่มีไม่มีไม่มีJVM safepoints, GC
Comptime code generationNativeMacros/templatesproc macrosไม่มี
C interop (DPDK, io_uring)Trivial @cImportNativeFFI/bindgenJNI overhead
SIMD@Vector, built-inIntrinsicspacked_simd (unstable)Vectorization hints
Cross-compilationBuilt-inCMake hellcargo targetN/A
Build timeวินาทีนาที (C++)นาทีวินาที + JVM startup
Hidden control flowไม่มีExceptions, implicit castsPanics ใน unwrapExceptions

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

ลองใช้งาน

Build จาก source (zig build), รัน benchmarks (zig build bench), เชื่อมต่อผ่าน FFI จากทุกภาษา ถ้าคุณมี Zig 0.15.1 และเวลาสองสามนาที — ลอง ping-pong benchmark และเปรียบเทียบกับโซลูชันปัจจุบันของคุณ


ลิงก์:


การอ้างอิง

@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 ไม่มีเรื่องน่าประหลาดใจ}
}