ZigBolt: Zig ile Kendi Aeron'umuzu Neden Yaptık ve Mesaj Başına 20 Nanosaniyeye Nasıl Ulaştık
Kilit gerektirmeyen ring buffer'lar, sıfır kopyalı codec'ler, Raft kümesi — hepsi saf Zig ile, hepsi açık kaynak.
Algoritmik ticaret veya piyasa yapıcılığı alanında çalışıyorsanız, her mikrosaniyenin bedelini bilirsiniz. Bir ekstra bağlam geçişi — ve emriniz ikinci sıraya düşer. Bir JVM GC duraklaması — karşı taraftaki piyasa yapıcı kotayı çoktan güncellemiştir. Paranın nanosaniyelerle ölçüldüğü bir dünyada, mesajlaşma altyapısı servisler arasındaki sıradan bir boru değil — rekabetçi bir avantajdır.
Zig ile yüksek frekanslı ticaret için yazılmış bir mesajlaşma sistemi olan ZigBolt'u geliştirdik. Sıfırdan. JVM yok, garbage collector yok, Media Driver yok, XML yapılandırması yok. Ve SPSC ring buffer'da p50 gecikme olarak 20 nanosaniye, paylaşımlı bellek üzerinden IPC'de ise 30 nanosaniye elde ettik.
Bu makale, neden ihtiyaç duyduğumuzu, içeride nasıl çalıştığını ve neden Zig seçtiğimizi ele alıyor.
TL;DR
- ZigBolt — saf Zig ile yazılmış HFT için açık kaynaklı (MIT) mesajlaşma sistemi
- SPSC'de p50 20 ns, IPC'de p50 30 ns — Aeron'un yayımlanmış rakamlarından daha hızlı
- Sıfır kopyalı codec 0 ns'de çalışır (derleme zamanı üretimi, çalışma zamanı sadece bir işaretçi dönüşümüdür)
- GC yok, JVM yok, Media Driver yok — kütüphane doğrudan uygulamanıza gömülür
- Raft kümesi, arşiv, sıralayıcı — hepsi dahil
- Rust, Python, Go, TypeScript, C için FFI bağlantıları — istediğiniz dilde çalışabilirsiniz
Sorun: Aeron Neden Harika Ama Yeterli Değil?
Real Logic'in Aeron'u, sermaye piyasalarında düşük gecikmeli mesajlaşma için fiili standarttır. Düzinelerce HFT firması tarafından kullanılır, savaşta test edilmiştir ve mükemmel bir mimariye sahiptir. Ancak Aeron'un temel bir sorunu var: adı JVM.
JVM Güvenli Noktaları: Görünmez Düşman
Tüm verileri dikkatlice off-heap belleğe koymuş olsanız bile, GC ergonomiklerini devre dışı bırakıp GuaranteedSafepointInterval=300000 ayarlasanız bile — JVM zaman zaman tüm iş parçacıklarını bir güvenli noktada durdurmaya devam eder. Bu bir hata değil, mimari bir karardır: JVM, deoptimizasyon, önyargılı kilitleme ve yığın yürümesi için güvenli noktalara ihtiyaç duyar.
Pratikte şöyle görünür: iş parçacığınız p50 = 200 ns'de mesaj gönderir, ardından birdenbire p99.9 değeri 50 us'a fırlar. Görünür bir neden olmaksızın. Çünkü JVM iş parçacıklarından biri bunun zamanı olduğuna karar vermiştir.
Media Driver: Ekstra Bir Atlama
Aeron, bir Media Driver aracılığıyla çalışır — yayıncı ile abone arasındaki mesajları paylaşımlı bellek üzerinden yönlendiren ayrı bir süreç (veya gömülü JVM). Bu güzel bir yalıtım sağlar, ancak en az bir ekstra atlama ekler:
Aeron: Uygulama → shm → Media Driver → shm → soket → NIC
ZigBolt: Uygulama → ring buffer → io_uring → NIC
Her atlama, ekstra nanosaniyeler, ekstra önbellek kayıpları, ekstra öngörülemezlik anlamına gelir.
SBE: Ayrı Bir Derleme Adımı
Simple Binary Encoding — finansal mesajlar için standart FIX codec'i. Aeron ekosisteminde, XML şemalarından kod üreten ayrı bir Java yardımcı programıdır. Ayrı bir bağımlılık, ayrı bir derleme adımı, ayrı bir sorun seti.
Çözüm: ZigBolt
Kendimize şunu sorduk: Aeron'un en iyi fikirlerini — üçlü tamponlu log, kilit gerektirmeyen ring buffer'lar, Raft kümesi — alıp şu özelliklere sahip bir dilde uygulasaydık ne olurdu:
- Çalışma zamanı yükü yok (GC yok, güvenli noktalar yok)
- Derleme zamanında kod üretimi (comptime)
- C kütüphaneleriyle (DPDK, io_uring) önemsiz entegrasyon
- ~100 KB ikili dosyaya derlenebilir
Bu dil Zig.
Mimari
┌─────────────────────────────────────────────────────────┐
│ Yayıncı/Abone API'si (tiplenmiş genel sarmalayıcılar) │
├─────────────────────────────────────────────────────────┤
│ Taşıma Katmanı (kanal fabrikası ve yaşam döngüsü) │
├─────────────────────────────────────────────────────────┤
│ IPC Kanalı (paylaşımlı bellek) │ UDP Kanalı (ağ) │
├─────────────────────────────────────────────────────────┤
│ WireCodec (comptime, sıfır kopyalı) │ SBE Encoder/Decoder│
├─────────────────────────────────────────────────────────┤
│ Ring Buffer'lar (SPSC/MPSC) │ LogBuffer (üçlü tamponlu) │
├─────────────────────────────────────────────────────────┤
│ Arşiv (yeniden oynatma) │ Sıralayıcı (toplam sıra) │ Raft│
└─────────────────────────────────────────────────────────┘
Yedi katman, her biri bağımsız olarak kullanılabilir. İki süreç arasında IPC için sadece bir SPSC ring buffer mu gerekiyor? Alın. Raft mutabakatı ve arşivleme içeren tam bir kümeye mi ihtiyacınız var? O da var.
Kıyaslamalar: Söz Değil, Sayılar

10 milyon iterasyon üzerinde gerçek kıyaslama sonuçları (Apple Silicon / macOS):
SPSC Ring Buffer
| Mesaj Boyutu | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 8 bayt | 20 ns | 30 ns | 120 ns | 42,8M msg/s |
| 32 bayt | 30 ns | 50 ns | 150 ns | 28,5M msg/s |
| 64 bayt | 50 ns | 60 ns | 320 ns | 17,6M msg/s |
| 256 bayt | 30 ns | 50 ns | 50 ns | 29,5M msg/s |
IPC Kanalı (paylaşımlı bellek)
| Mesaj Boyutu | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 64 bayt | 30 ns | 40 ns | 40 ns | 35,7M msg/s |
| 256 bayt | 40 ns | 40 ns | 170 ns | 27,4M msg/s |
| 1024 bayt | 90 ns | 260 ns | 900 ns | 9,9M msg/s |
LogBuffer (Aeron tarzı üçlü tamponlu)
| Mesaj Boyutu | p50 | p99 | p99.9 | Throughput |
|---|---|---|---|---|
| 32 bayt | 30 ns | 40 ns | 320 ns | 33,6M msg/s |
| 64 bayt | 30 ns | 30 ns | 160 ns | 38,0M msg/s |
| 256 bayt | 30 ns | 40 ns | 60 ns | 31,1M msg/s |
WireCodec (comptime sıfır kopyalı)
| İşlem | Gecikme | Throughput |
|---|---|---|
| Kodlama (32 bayt) | 0 ns | satır içi memcpy |
| Kod çözme (32 bayt) | ~0,4 ns | 2,7 milyar msg/s |
Evet, doğru okudunuz: kodlama sıfır nanosaniye alır. Çünkü WireCodec(T), struct'ı derleme zamanında doğrular ve encode/decode işlemlerini düz bir @memcpy veya işaretçi dönüşümüne çevirir. Çalışma zamanı yükü = sıfır.
Karşılaştırma olarak: Aeron, ~250 ns'lik IPC RTT (gidiş-dönüş) iddia etmektedir. Bizim tek yönlü gecikmemiz 30 ns. Gidiş-dönüşü bile hesaba katsak 4 kat daha hızlıyız.
İçeride Nasıl Çalışır?
Kilit Gerektirmeyen SPSC: Erdem Olarak Basitlik

Tek üretici tek tüketici ring buffer'ı, en basit ve en hızlı kilit gerektirmeyen veri yapısıdır. Yazıcı head'i hareket ettirir, okuyucu tail'i hareket ettirir, CAS gerekmez — acquire/release atomic'ler yeterlidir.
Temel numara, önbellek satırı dolgulamasıdır. head ve tail aynı önbellek satırında oturuyorsa, bir sayaçtaki her güncelleme diğer çekirdek için önbelleği geçersiz kılar (yanlış paylaşım). Çözüm:
// Head (yazma konumu) — kendi önbellek satırında
head: std.atomic.Value(usize) align(128) = .init(0),
// 128 bayt dolgu — garantili yalıtım
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (okuma konumu) — kendi önbellek satırında
tail: std.atomic.Value(usize) align(128) = .init(0),
64 değil 128 bayt — çünkü Apple Silicon'da (ve birçok ARM çipinde) donanım ön getirici önbellek satırı çiftleriyle çalışabilir. Güvende olmak için bu şekilde yapıyoruz.
WireCodec: Kod Üretimi Yerine Comptime

Java/C++ dünyasında, ikili codec'ler ayrı bir adım gerektirir: şema yazın, kod üreticisini çalıştırın, kodu alın, derleyin. Zig'de tüm bunlar derleme zamanında gerçekleşir:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// Kodlama — sadece 32 baytlık bir memcpy. 1-2 talimata satır içi eklenir.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// Kod çözme — işaretçi dönüşümü. Sıfır kopya.
const tick = Codec.decode(buf[0..Codec.wire_size]);
Zig derleyicisi comptime'da doğrular:
- Struct paketlenmiştir (dolgu delikleri yok)
- Boyut 8 baytın katıdır (SIMD için hizalama)
- Tüm alanlar ilkel türlerdir
Bir şey yanlışsa — sabah 3'te üretimde bir çalışma zamanı istisnası değil, derleme hatası alırsınız.
Paylaşımlı Bellek Üzerinden IPC
İki süreç /dev/shm'deki aynı dosyayı eşler. Yayıncı ring buffer'a yazar, abone okur. Kritik yolda soket yok, sistem çağrısı yok:
// Yayıncı
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 MB
});
channel.publish(&msg_bytes, msg_type_id);
// Abone (başka bir süreç)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
publish()'dan abonedeki handler_fn çağrısına kadar tüm yol — 64 baytlık bir mesaj için 30 nanosaniye.
UDP için NAK Tabanlı Güvenilirlik
Ağ taşıması için ZigBolt, alıcı güdümlü yeniden iletim kullanır. Alıcı, bitmap aracılığıyla dizi numaralarındaki boşlukları takip eder ve gönderene NAK (olumsuz onay) gönderir. Ayrıca AIMD tıkanıklık kontrolü — TCP benzeri yavaş başlangıç ve tıkanıklıktan kaçınma — ağı doldurmamak için.
Raft Kümesi: Tutarlılığa İhtiyaç Duyduğunuzda
Bir mesajın kaybolmasının kabul edilemez olduğu durumlarda (örneğin bir eşleştirme motoru), ZigBolt tam Raft mutabakatını içerir:
- Yapılandırılabilir zaman aşımı (150-300 ms) ile lider seçimi
- Log replikasyonu — lider her mesajı takipçilere çoğaltır
- CRC32 doğrulaması ve çökme kurtarma ile önceden yazma logu
- WAL'ın sonsuza kadar büyümemesi için anlık görüntüler
Arşiv: Kaydet ve Yeniden Oynat
Tüm mesajlar, segmentlenmiş bir disk arşivine kaydedilebilir. Ardından — zaman veya dizi numarasına göre herhangi bir konumdan yeniden oynatma. Harici bağımlılıklar olmadan yerleşik LZ4 tarzı sıkıştırma. Segmentler içinde hızlı arama için seyrek dizin.
Toplam Sıralı Sıralayıcı
Birden fazla mekan üzerinde piyasa yapıcılığı için, tüm olayların küresel bir sıraya sahip olması kritik öneme sahiptir. Sıralayıcı N giriş akışını alır ve bunları tek bir akışa birleştirerek monoton artan dizi numaraları atar. Her katılımcı aynı olay dizisini görür.
Neden Zig, Rust/C/C++ Değil?
Dört aday arasında seçim yaptık. İşte dürüst bir karşılaştırma:
| Kriter | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / çalışma zamanı yükü | Yok | Yok | Yok | JVM güvenli noktaları, GC |
| Comptime kod üretimi | Yerel | Makrolar/şablonlar | proc makroları | Yok |
| C birlikte çalışma (DPDK, io_uring) | Önemsiz @cImport |
Yerel | FFI/bindgen | JNI yükü |
| SIMD | @Vector, yerleşik |
İç birimler | packed_simd (kararsız) | Vektörizasyon ipuçları |
| Çapraz derleme | Yerleşik | CMake cehennemi | cargo target | Yok |
| Derleme süresi | Saniyeler | Dakikalar (C++) | Dakikalar | Saniyeler + JVM başlangıcı |
| Gizli kontrol akışı | Yok | İstisnalar, örtük dönüşümler | unwrap'teki panikler |
İstisnalar |
Zig bize benzersiz bir kombinasyon sağladı: C düzeyinde performans + geliştirme sırasında güvenlik + comptime metaprogramlama (codec'ler, arama tabloları, protokol durum makineleri — hepsi derleme zamanında üretilir) + @cImport aracılığıyla DPDK, liburing, ef_vi ile önemsiz entegrasyon.
Ve bir Zig ikili dosyası ~100 KB ağırlığındadır. JVM tabanlı bir çözümün 20+ MB'ına karşılık.
Bağlantılar: Kendi Dilinizde Çalışın
ZigBolt, C-ABI ile paylaşımlı bir kütüphaneye derlenir ve beş dil için hazır bağlantılarımız var:
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)
Ayrıca Go ve sade C. Aynı paylaşımlı bellek kanalı tüm dillerden aynı anda erişilebilir — Zig'de yayıncı, Python'da abone, Go'da izleme. Hepsi aynı mmap bölgesini okur.
SBE Codec: FIX Uyumlu Mesajlar
Finansal protokoller için ZigBolt, derleme zamanı şemaları içeren tam bir SBE (Simple Binary Encoding) codec'i içerir. Yerleşik mesaj türleri:
- NewOrderSingle — emir gönderimi
- ExecutionReport — çalıştırma raporu
- MarketDataIncrementalRefresh — artımlı piyasa verisi güncellemesi
- MassQuote — toplu kotasyon
- Heartbeat — bağlantı kontrolü
- Logon — kimlik doğrulama
Harici kod üreticisi yok, XML yok. Her şey Zig struct'larıyla tanımlanır ve derleme zamanında doğrulanır.
Tel Protokol: Aeron Uyumluluğu
ZigBolt, Aeron uyumlu tel protokol flyweight'lerini uygular:
- DataHeaderFlyweight — veri çerçeveleri
- StatusMessage — akış kontrolü
- NAK — olumsuz onay
- Setup, RTT, Error — servis çerçeveleri
Bu, ZigBolt'un mevcut Aeron altyapısıyla bir arada var olabileceği anlamına gelir. Geçiş, büyük bir patlama olmak zorunda değildir.
Sırada Ne Var?
ZigBolt şu anda 0.2.1 sürümündedir. Çekirdek kararlıdır, kıyaslamalar yeniden üretilebilir, bağlantılar çalışır. Yakında:
- io_uring backend — Linux 6.0+'da sıfır kopyalı ağ taşıması (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — her mikrosaniyenin önemli olduğu durumlarda çekirdek atlama
- Multi-Raft — enstrüman/strateji bazında parçalama
- Sütunsal arşiv — analitik için Apache Arrow/Parquet entegrasyonu
- Büyük sayfa desteği — TLB kayıplarını en aza indirmek için önceden hata yapılmış 2MB/1GB büyük sayfalar
Deneyin
- Web sitesi: zigbolt-landing.vercel.app
- Belgeler: zigbolt-landing.vercel.app/getting-started/introduction/
- Kaynak kodu: github.com/suenot/zigbolt
- Lisans: MIT
Kaynaktan derleyin (zig build), kıyaslamaları çalıştırın (zig build bench), herhangi bir dilden FFI aracılığıyla bağlanın. Zig 0.15.1 ve birkaç dakikanız varsa — ping-pong kıyaslamasını deneyin ve mevcut çözümünüzle karşılaştırın.
Bağlantılar:
- ZigBolt Landing: zigbolt-landing.vercel.app
- GitHub: github.com/suenot/zigbolt
- Aeron (karşılaştırma için): github.com/real-logic/aeron | Aeron'a genel bakışımız
- Zig dili: ziglang.org
- Marketmaker.cc: marketmaker.cc
Atıf
@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/tr/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {Zig ile sıfırdan HFT için ultra düşük gecikmeli bir mesajlaşma sistemi nasıl ve neden geliştirdik. JVM yok, GC yok, sürpriz yok.}
}
Yazarlar
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.