ZigBolt: لماذا بنينا نظام Aeron الخاص بنا بلغة Zig وحققنا 20 نانوثانية لكل رسالة
مخازن حلقية بدون أقفال، مُرمِّزات بدون نسخ، مجموعة Raft — الكل بلغة Zig الخالصة، الكل مفتوح المصدر.
إذا كنت تعمل في التداول الخوارزمي أو صناعة السوق، فأنت تعرف قيمة كل ميكروثانية. تبديل سياق واحد إضافي — وأمر التداول الخاص بك يصل متأخراً. توقف واحد لجمع القمامة في JVM — وصانع السوق على الطرف الآخر قد حدّث أسعاره بالفعل. في عالم تُقاس فيه الأرباح بالنانوثانية، البنية التحتية للرسائل ليست أنبوباً مملاً بين الخدمات، بل هي ميزة تنافسية جوهرية.
بنينا ZigBolt — نظام رسائل للتداول عالي التردد بلغة Zig. من الصفر. بدون JVM، بدون جامع قمامة، بدون Media Driver، بدون ملفات تكوين XML. وحققنا زمن استجابة p50 يبلغ 20 نانوثانية على مخزن SPSC الحلقي و30 نانوثانية على IPC عبر الذاكرة المشتركة.
هذه المقالة تشرح لماذا كان هذا ضرورياً، وكيف يعمل من الداخل، ولماذا اخترنا Zig.
ملخص
- ZigBolt — نظام رسائل مفتوح المصدر (MIT) للتداول عالي التردد بلغة Zig الخالصة
- p50: 20 نانوثانية على SPSC، p50: 30 نانوثانية على IPC — أسرع من المواصفات المعلنة لـ Aeron
- مُرمِّز بدون نسخ يعمل بـ 0 نانوثانية (توليد الكود وقت الترجمة، وقت التشغيل مجرد تحويل مؤشر)
- بدون GC، بدون JVM، بدون Media Driver — المكتبة تُدمج مباشرة في التطبيق
- مجموعة Raft، أرشيف، مُرتِّب — كل شيء مضمّن
- روابط FFI لـ Rust وPython وGo وTypeScript وC — اعمل بلغتك المفضلة
المشكلة: Aeron ممتاز، لكنه غير كافٍ
Aeron من Real Logic هو المعيار الفعلي لنظام الرسائل منخفض زمن الاستجابة في أسواق رأس المال. تستخدمه عشرات شركات التداول عالي التردد، وقد أثبت كفاءته في الميدان، ويتمتع بمعمارية ممتازة. لكن لدى Aeron مشكلة جوهرية — JVM.
نقاط الأمان في JVM: العدو الخفي
حتى لو وضعت جميع البيانات بعناية في ذاكرة خارج الكومة، حتى لو عطّلت التعديل التلقائي لجمع القمامة وضبطت GuaranteedSafepointInterval=300000، فإن JVM لا تزال توقف جميع الخيوط عند نقاط الأمان من وقت لآخر. هذا ليس خطأ برمجياً، بل قرار معماري: تحتاج JVM إلى نقاط الأمان لإلغاء التحسين، والقفل المنحاز، واستعراض المكدس.
عملياً يبدو الأمر هكذا: خيطك يرسل رسائل بـ p50 = 200 نانوثانية، وفجأة يقفز p99.9 إلى 50 ميكروثانية. بدون سبب واضح. لأن أحد خيوط JVM قرر أن الوقت قد حان لنقطة أمان.
Media Driver: قفزة إضافية
يعمل Aeron عبر Media Driver — عملية منفصلة (أو JVM مدمجة) تُوجّه الرسائل بين الناشر والمشترك عبر الذاكرة المشتركة. هذا يوفر عزلاً جميلاً، لكنه يضيف قفزة إضافية واحدة على الأقل:
Aeron: App → shm → Media Driver → shm → socket → NIC
ZigBolt: App → ring buffer → io_uring → NIC
كل قفزة تعني نانوثوانٍ إضافية، وإخفاقات ذاكرة مخبئية إضافية، وعدم قابلية للتنبؤ.
SBE: خطوة بناء منفصلة
Simple Binary Encoding هو مُرمِّز FIX القياسي للرسائل المالية. في منظومة Aeron، هو أداة Java منفصلة تولّد الكود من مخططات XML. اعتماد منفصل، خطوة بناء منفصلة، مجموعة مشاكل منفصلة.
الحل: ZigBolt
سألنا أنفسنا: ماذا لو أخذنا أفضل أفكار Aeron — السجل ثلاثي التخزين المؤقت، المخازن الحلقية بدون أقفال، مجموعة Raft — ونفّذناها بلغة:
- ليس لها عبء وقت تشغيل (لا GC، لا نقاط أمان)
- تسمح بتوليد الكود وقت الترجمة (comptime)
- تتكامل بسهولة مع مكتبات C (مثل DPDK وio_uring)
- تُنتج ملفاً تنفيذياً بحجم ~100 كيلوبايت
تلك اللغة هي Zig.
المعمارية
┌─────────────────────────────────────────────────────────┐
│ Publisher/Subscriber API (أغلفة مُنمَّطة) │
├─────────────────────────────────────────────────────────┤
│ Transport Layer (مصنع القنوات، دورة الحياة) │
├─────────────────────────────────────────────────────────┤
│ IPC Channel (ذاكرة مشتركة) │ UDP Channel (شبكة) │
├─────────────────────────────────────────────────────────┤
│ WireCodec (comptime بدون نسخ) │ SBE Encoder/Decoder │
├─────────────────────────────────────────────────────────┤
│ Ring Buffers (SPSC/MPSC) │ LogBuffer (ثلاثي التخزين) │
├─────────────────────────────────────────────────────────┤
│ Archive (إعادة تشغيل) │ Sequencer (ترتيب كلي) │ Raft (HA) │
└─────────────────────────────────────────────────────────┘
سبع طبقات، كل منها يمكن استخدامها بشكل مستقل. تحتاج فقط مخزناً حلقياً SPSC للـ IPC بين عمليتين؟ خذه. تحتاج مجموعة كاملة مع إجماع Raft وأرشيف؟ موجود أيضاً.
اختبارات الأداء: أرقام لا كلام

هذه النتائج الفعلية على 10 ملايين تكرار (Apple Silicon / macOS):
مخزن SPSC الحلقي
| حجم الرسالة | p50 | p99 | p99.9 | الإنتاجية |
|---|---|---|---|---|
| 8 بايت | 20 نانوثانية | 30 نانوثانية | 120 نانوثانية | 42.8M msg/s |
| 32 بايت | 30 نانوثانية | 50 نانوثانية | 150 نانوثانية | 28.5M msg/s |
| 64 بايت | 50 نانوثانية | 60 نانوثانية | 320 نانوثانية | 17.6M msg/s |
| 256 بايت | 30 نانوثانية | 50 نانوثانية | 50 نانوثانية | 29.5M msg/s |
قناة IPC (ذاكرة مشتركة)
| حجم الرسالة | p50 | p99 | p99.9 | الإنتاجية |
|---|---|---|---|---|
| 64 بايت | 30 نانوثانية | 40 نانوثانية | 40 نانوثانية | 35.7M msg/s |
| 256 بايت | 40 نانوثانية | 40 نانوثانية | 170 نانوثانية | 27.4M msg/s |
| 1024 بايت | 90 نانوثانية | 260 نانوثانية | 900 نانوثانية | 9.9M msg/s |
LogBuffer (ثلاثي التخزين على نمط Aeron)
| حجم الرسالة | p50 | p99 | p99.9 | الإنتاجية |
|---|---|---|---|---|
| 32 بايت | 30 نانوثانية | 40 نانوثانية | 320 نانوثانية | 33.6M msg/s |
| 64 بايت | 30 نانوثانية | 30 نانوثانية | 160 نانوثانية | 38.0M msg/s |
| 256 بايت | 30 نانوثانية | 40 نانوثانية | 60 نانوثانية | 31.1M msg/s |
WireCodec (comptime بدون نسخ)
| العملية | زمن الاستجابة | الإنتاجية |
|---|---|---|
| ترميز (32 بايت) | 0 نانوثانية | ∞ (memcpy مضمّن) |
| فك الترميز (32 بايت) | ~0.4 نانوثانية | 2.7 مليار msg/s |
نعم، قرأت ذلك بشكل صحيح: الترميز يستغرق صفر نانوثانية. لأن WireCodec(T) يتحقق من البنية وقت الترجمة ويحوّل عمليات الترميز/فك الترميز إلى @memcpy عادي أو تحويل مؤشر. عبء وقت التشغيل = صفر.
للمقارنة: تعلن Aeron عن IPC RTT (ذهاباً وإياباً) بحوالي 250 نانوثانية. زمن الاستجابة أحادي الاتجاه لدينا هو 30 نانوثانية. حتى لو حسبنا الذهاب والإياب، نحن أسرع بـ 4 مرات.
كيف يعمل من الداخل
SPSC بدون أقفال: البساطة كفضيلة

المخزن الحلقي أحادي المنتج وأحادي المستهلك هو أبسط وأسرع بنية بيانات بدون أقفال. الكاتب يحرّك head، والقارئ يحرّك tail، ولا حاجة لـ CAS — العمليات الذرية acquire/release كافية.
الحيلة الأساسية هي حشو سطر الذاكرة المخبئية. إذا كان head وtail في نفس سطر الذاكرة المخبئية، فإن كل تحديث لأحد العدادين يُبطل ذاكرة النواة الأخرى (المشاركة الزائفة). الحل:
// Head (موضع الكتابة) — في سطر ذاكرة مخبئية خاص به
head: std.atomic.Value(usize) align(128) = .init(0),
// حشو 128 بايت — لضمان العزل
_pad0: [128 - @sizeOf(std.atomic.Value(usize))]u8 = .{0} ** ...,
// Tail (موضع القراءة) — في سطر ذاكرة مخبئية خاص به
tail: std.atomic.Value(usize) align(128) = .init(0),
128 بايت وليس 64 — لأنه في Apple Silicon (والعديد من معمارية ARM) قد يعمل الجلب المسبق العتادي بأزواج من سطور الذاكرة المخبئية. نحن نأخذ الاحتياط.
WireCodec: comptime بدلاً من توليد الكود

في عالم Java/C++، تحتاج مُرمِّزات ثنائية إلى خطوة منفصلة: كتابة المخطط ← تشغيل مولّد الكود ← الحصول على الكود ← الترجمة. في Zig، كل هذا يتم وقت الترجمة:
const TickMsg = packed struct {
symbol_id: u32,
price: i64,
quantity: u32,
side: u8,
_reserved: [3]u8,
timestamp: u64,
};
const Codec = WireCodec(TickMsg);
// الترميز — مجرد نسخ 32 بايت. يتحول إلى 1-2 تعليمة.
Codec.encode(&msg, buf[0..Codec.wire_size]);
// فك الترميز — تحويل مؤشر. صفر نسخ.
const tick = Codec.decode(buf[0..Codec.wire_size]);
مترجم Zig يتحقق في مرحلة comptime من:
- البنية packed (لا ثقوب حشو)
- الحجم من مضاعفات 8 بايت (محاذاة SIMD)
- جميع الحقول أنواع بدائية
إذا كان هناك خطأ — يظهر كخطأ ترجمة، وليس استثناء وقت تشغيل في الثالثة صباحاً على بيئة الإنتاج.
IPC عبر الذاكرة المشتركة
تقوم عمليتان بتعيين نفس الملف في /dev/shm. الناشر يكتب في المخزن الحلقي، والمشترك يقرأ. لا مقابس، لا استدعاءات نظام في المسار الحرج:
// Publisher
const channel = try IpcChannel.create("/market-data", .{
.term_length = 1024 * 1024, // 1 ميغابايت
});
channel.publish(&msg_bytes, msg_type_id);
// Subscriber (عملية أخرى)
const channel = try IpcChannel.open("/market-data", .{
.term_length = 1024 * 1024,
});
const count = channel.poll(handler_fn, 10);
المسار الكامل من publish() إلى استدعاء handler_fn في المشترك — 30 نانوثانية لرسالة بحجم 64 بايت.
موثوقية UDP المبنية على NAK
للنقل الشبكي، يستخدم ZigBolt إعادة الإرسال بقيادة المستقبِل. يتتبع المستقبِل الفجوات في أرقام التسلسل عبر خريطة بتات ويرسل NAK (إقرار سلبي) إلى المرسل. بالإضافة إلى التحكم في الازدحام AIMD — بدء بطيء وتجنب ازدحام شبيه بـ TCP — لمنع إغراق الشبكة.
مجموعة Raft: عندما تكون الاتساقية ضرورية
للحالات التي لا يمكن فيها فقدان رسالة (مثل محرك المطابقة)، يتضمن ZigBolt إجماع Raft كاملاً:
- انتخاب القائد بمهلة قابلة للتكوين (150-300 مللي ثانية)
- نسخ السجل — القائد ينسخ كل رسالة إلى التابعين
- سجل الكتابة المسبقة (WAL) مع التحقق بـ CRC32 واستعادة بعد الأعطال
- لقطات — لمنع نمو WAL بلا حدود
الأرشيف: التسجيل وإعادة التشغيل
يمكن تسجيل جميع الرسائل في أرشيف مُجزّأ على القرص. ثم إعادة التشغيل من أي موضع بحسب الوقت أو رقم التسلسل. ضغط مدمج بأسلوب LZ4 بدون اعتماديات خارجية. فهرس متناثر للبحث السريع داخل الأجزاء.
مُرتِّب الترتيب الكلي
لصناعة السوق على عدة بورصات، يجب أن يكون لجميع الأحداث ترتيب عالمي. يأخذ المُرتِّب N تدفقات إدخال ويدمجها في تدفق واحد، مُعيّناً أرقام تسلسل متزايدة رتابياً. كل مشارك يرى نفس تسلسل الأحداث.
لماذا Zig وليس Rust/C/C++؟
قارنّا بين أربعة مرشحين. إليك جدول المقارنة الموضوعي:
| المعيار | Zig | C/C++ | Rust | Java (Aeron) |
|---|---|---|---|---|
| GC / عبء وقت التشغيل | لا يوجد | لا يوجد | لا يوجد | نقاط أمان JVM، GC |
| توليد كود comptime | أصلي | ماكرو/قوالب | proc macros | لا يوجد |
| التوافق مع C (مثل DPDK وio_uring) | @cImport بسيط |
أصلي | FFI/bindgen | عبء JNI |
| SIMD | @Vector مدمج |
Intrinsics | packed_simd (غير مستقر) | تلميحات تسريع شعاعي |
| الترجمة المتقاطعة | مدمجة | جحيم CMake | cargo target | غير متاح |
| وقت البناء | ثوانٍ | دقائق (C++) | دقائق | ثوانٍ + بدء JVM |
| تدفق تحكم مخفي | لا يوجد | استثناءات، تحويلات ضمنية | panic في unwrap |
استثناءات |
قدّم Zig مزيجاً فريداً: أداء C + أمان أثناء التطوير + برمجة وصفية comptime (المُرمِّزات، جداول البحث، آلات حالة البروتوكول — كلها تُولَّد وقت الترجمة) + تكامل سلس مع DPDK وliburing وef_vi عبر @cImport.
وملف Zig التنفيذي يبلغ حوالي 100 كيلوبايت. مقابل أكثر من 20 ميغابايت للحلول المبنية على JVM. للنشر على الحافة والحاويات، هذا مهم.
الروابط اللغوية: اعمل بلغتك المفضلة
يُترجَم ZigBolt إلى مكتبة مشتركة بواجهة C-ABI، ولدينا روابط جاهزة لخمس لغات:
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();
// Subscriber
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 الخالصة. نفس قناة الذاكرة المشتركة يمكن الوصول إليها من جميع اللغات في نفس الوقت — الناشر بـ Zig، المشترك بـ Python، المراقبة بـ Go. الجميع يقرأ نفس منطقة mmap.
مُرمِّز SBE: رسائل متوافقة مع FIX
للبروتوكولات المالية، يتضمن ZigBolt مُرمِّز SBE (Simple Binary Encoding) كاملاً بمخططات وقت الترجمة. أنواع الرسائل المدمجة:
- NewOrderSingle — إرسال أمر
- ExecutionReport — تقرير التنفيذ
- MarketDataIncrementalRefresh — تحديث تدريجي لبيانات السوق
- MassQuote — عرض أسعار جماعي
- Heartbeat — فحص الاتصال
- Logon — المصادقة
لا حاجة لمولّد كود خارجي، لا حاجة لـ XML. كل شيء يُوصف ببُنى Zig ويُتحقَّق منه وقت الترجمة.
بروتوكول السلك: التوافق مع Aeron
يُنفّذ ZigBolt واجهات بروتوكول السلك المتوافقة مع Aeron:
- DataHeaderFlyweight — إطارات البيانات
- StatusMessage — التحكم في التدفق
- NAK — الإقرار السلبي
- Setup وRTT وError — إطارات التحكم
هذا يعني أن ZigBolt يمكنه العيش في نفس المنظومة مع بنية Aeron التحتية الحالية. لا يجب أن تكون الهجرة دفعة واحدة.
ما التالي
ZigBolt حالياً في الإصدار 0.2.1. النواة مستقرة، اختبارات الأداء قابلة للتكرار، والروابط تعمل. في الخطط القريبة:
- خلفية io_uring — نقل شبكي بدون نسخ على Linux 6.0+ (IORING_OP_SEND_ZC)
- DPDK / AF_XDP — تجاوز النواة للحالات التي تكون فيها كل ميكروثانية حاسمة
- Multi-Raft — تجزئة حسب الأدوات المالية/الاستراتيجيات
- أرشيف عمودي — تكامل مع Apache Arrow/Parquet للتحليلات
- دعم الصفحات الضخمة — صفحات ضخمة مسبقة التخصيص بحجم 2 ميغابايت/1 غيغابايت لتقليل إخفاقات TLB
جرّبه
- الموقع: zigbolt-landing.vercel.app
- التوثيق: zigbolt-landing.vercel.app/getting-started/introduction/
- الكود المصدري: github.com/suenot/zigbolt
- الرخصة: MIT
ابنِ من المصدر (zig build)، شغّل اختبارات الأداء (zig build bench)، واتصل عبر FFI من أي لغة. إذا كان لديك Zig 0.15.1 ودقيقتان — جرّب اختبار ping-pong وقارنه مع حلّك الحالي.
روابط:
- موقع ZigBolt: 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: لماذا بنينا نظام Aeron الخاص بنا بلغة Zig وحققنا 20 نانوثانية لكل رسالة},
year = {2026},
url = {https://marketmaker.cc/ar/blog/post/zigbolt-zig-messaging-hft},
version = {0.2.1},
description = {كيف ولماذا بنينا من الصفر نظام رسائل فائق السرعة للتداول عالي التردد بلغة Zig. بدون JVM، بدون GC، بدون مفاجآت.}
}
MarketMaker.cc Team
البحوث والاستراتيجيات الكمية