تصویر مقاله

در این مقاله، گام‌به‌گام با مراحل طراحی و پیاده‌سازی یک هندلر اختصاصی و کمینه‌گرای TCP در زبان Rust آشنا می‌شویم که با استفاده از تکنیک Kernel bypass در DPDK و برای یک پلتفرم بازارسازی سهام (Equities Market-Making) توسعه یافته است. کدهای ارائه‌شده کاملاً واقعی هستند، تصمیمات فنی با استدلال‌های دقیق اتخاذ شده‌اند و اعداد و ارقام کارایی نیز حاصل اندازه‌گیری‌های عملی در محیط واقعی هستند.

با این حال، یک نکته مهم: این مطلب یک مطالعه موردی از شرایط بسیار خاص و افراطی است، نه یک آموزش عمومی. توسعه یک پشته اختصاصی TCP (Custom TCP Stack) تقریباً در اکثر سناریوها روش مناسبی برای دستیابی به تأخیر کم (Low Latency) نیست؛ حتی در حوزه معاملات HFT. بنابراین پیش از ادامه مطالعه، این درخت تصمیم‌گیری را بررسی کنید:

از پشته استاندارد کرنل لینوکس (همراه با بهینه‌سازی و Tuning) استفاده کنید اگر بودجه تأخیر (Latency budget) شما بالاتر از ۵ میکروثانیه است. یک کرنل لینوکس که به‌درستی با پارامترهایی مانند SO_BUSY_POLL، TCP_NODELAY، IRQ affinity، isolcpus و nohz_full پیکربندی شده باشد، در محیط‌های Colocation به تأخیر رفت‌وبرگشت (RTT) بین ۵ تا ۸ میکروثانیه دست پیدا می‌کند. این رقم برای بخش اعظم استراتژی‌های معاملات الگوریتمی و کمی (Quantitative Trading) کاملاً کافی و پاسخگو است.

از کتابخانه‌های آماده Kernel-bypass برای TCP استفاده کنید — مانند Solarflare OpenOnload، Mellanox VMA یا یک پشته مبتنی بر DPDK مانند F-Stack — اگر به تأخیر ۱ تا ۳ میکروثانیه‌ای نیاز دارید و در عین حال می‌خواهید رابط برنامه‌نویسی BSD socket API را حفظ کنید. این ابزارها بیش از یک دهه در شرایط واقعی آزموده شده‌اند، سناریوهای نادری (Edge cases) را مدیریت می‌کنند که ما در این پروژه عمداً از آن‌ها صرف‌نظر کرده‌ایم و مهم‌تر از همه، هیچ هزینه نیروی انسانی برای نگهداری روی دوش تیم شما نمی‌گذارند. در حقیقت، بیشتر شرکت‌های HFT نیز از همین رویکرد استفاده می‌کنند.

تنها در صورتی به سراغ توسعه یک Userspace Stack اختصاصی بروید که همه شرایط زیر برقرار باشد:

  • حداقل ۳ مهندس تمام‌وقت در تیم داشته باشید که وظیفه اختصاصی آن‌ها توسعه و نگهداری پشته شبکه باشد.
  • قبلاً ابزارهایی مانند OpenOnload یا معادل آن را پیاده‌سازی کرده و سربار عملکردی (Overhead) آن‌ها را به‌دقت پروفایل و تحلیل کرده باشید.
  • مزیت رقابتی استراتژی شما دقیقاً بر مبنای تأخیر مبدأ تا مقصد روی کابل شبکه (Wire-to-wire latency) تعریف شده باشد (نه در سرعت محاسبات الگوریتم یا سخت‌افزار سرورهای Colocation).
  • حاضر باشید قطعی‌های گهگاهی ناشی از محدودیت‌های پروتکل را بپذیرید؛ مواردی که یک پشته کامل و استاندارد TCP به‌راحتی آن‌ها را مدیریت می‌کند.
  • الزامات قانونی و رگولاتوری یا سیاست‌های سازمانی مانع از استفاده شما از کدهای شبکه ارائه‌دهندگان ثالث (Third-party) شود.

اگر به تأخیر زیر ۱۰۰ نانوثانیه نیاز دارید، به سراغ شبکه‌سازی مبتنی بر FPGA بروید. در چنین سطحی، نرم‌افزار — حتی نرم‌افزارهای مبتنی بر Kernel-bypass — به گلوگاه سیستم تبدیل می‌شوند و عملاً کل این مقاله برای نیاز شما کاربردی نخواهد داشت.

با شفاف شدن این چارچوب و پیش‌نیازها: در ادامه شرح می‌دهیم که چگونه پشته اختصاصی خود را ساختیم، چه منطقی پشت انتخاب‌های ما بود و این تصمیم‌ها در چه جاهایی به ضررمان تمام شد.

چشم‌انداز تأخیر: ارقامی واقعی و شفاف

در روایت‌های رایج پیرامون مزایای Kernel-bypass، معمولاً یک پشته Userspace دست‌ساز و بهینه‌شده را با کرنل خام و دست‌نخورده لینوکس مقایسه می‌کنند و مدعی بهبود ۳۰ تا ۵۰ برابری می‌شوند. این ادعا گمراه‌کننده است. اعدادی که ما در شرایط سخت‌افزاری کاملاً یکسان (کارت شبکه Mellanox ConnectX-5، پردازنده Intel Xeon Gold 6254 و اتصال مستقیم Cross-connect به گیت‌وی صرافی) در سه پیکربندی مختلف ثبت کردیم، به شرح زیر است:

CSS

بهبود واقعی نسبت به یک کرنل بهینه‌سازی‌شده (Tuned)، تقریباً ۵ تا ۶ برابر در میانه (Median) و ۶ تا ۸ برابر در تأخیر دنباله (Tail latency) است، نه ۳۰ برابر! البته همین بهبود ۵ برابری زمانی که آلفای استراتژی شما در مقیاس میکروثانیه‌ای از بین می‌رود فوق‌العاده حیاتی است؛ اما این موضوع با این ادعای نادرست که «کرنل لینوکس غیرقابل استفاده است» تفاوت بنیادین دارد. عملکرد کرنل لینوکس مناسب است، اما ما به چیزی فراتر از «مناسب» نیاز داشتیم.

بهبود تأخیر دنباله (Tail Latency) به مراتب مهم‌تر از میانه است. مقدار p99.9 برابر با ۲۴ میکروثانیه در کرنل بهینه‌شده به این معناست که از هر هزار بسته، یکی از آن‌ها با تأخیر زمان‌بندی، خطای TLB miss در بافرهای غیر Hugepage یا وقفه‌ای که از سد isolcpus عبور کرده مواجه می‌شود. در سیستمی که ۵۰۰ هزار تیک دیتای بازار را در ثانیه پردازش می‌کند، این یعنی دیر رسیدن ۵۰۰ تیک در ثانیه. در مقابل، با پشته DPDK، مقدار p99.9 تنها ۲.۹ میکروثانیه است؛ توزیع داده‌ها بسیار فشرده و پایدار است، زیرا اساساً هیچ عاملی در مسیر پردازش وجود ندارد که نوسان یا لرزش (Jitter) ایجاد کند.

نمای کلی معماری سیستم

یک سیستم معاملاتی مبتنی بر Kernel-bypass، شیوه تعامل برنامه با شبکه را کاملاً متحول می‌کند. در این معماری، کارت شبکه (NIC) از درایور کرنل جدا شده و کنترل آن به فریم‌ورکی در سطح Userspace سپرده می‌شود که مستقیماً از طریق بافرهای حلقوی DMA (نگاشت‌شده در فضای آدرس پردازه) آن را مدیریت می‌کند.

VBNET

اجزای کلیدی پشته Kernel-bypass ما که در Rust پیاده‌سازی شده عبارتند از:

  1. درایور حالت پولینگ DPDK (یا DPDK Poll-Mode Driver - PMD) — کارت شبکه را در فضای Userspace از طریق اتصالات FFI بسته dpdk-sys هدایت کرده و صف‌های دریافت را در یک حلقه فشرده و بی‌وقفه، بدون کوچک‌ترین سربار ناشی از وقفه (Interrupt)، بررسی (Poll) می‌کند.
  2. ماشین وضعیت اختصاصی TCP (یا Custom TCP State Machine) — یک پیاده‌سازی مینیمال و بدون تخصیص حافظه (Allocation-free) از پروتکل TCP که فقط الگوهای اتصالی مدنظر ما (نشست‌های طولانی‌مدت FIX) را مدیریت می‌کند و با فدا کردن عاملیت پروتکل، سرعت را به حداکثر می‌رساند.
  3. خط لوله پردازش بدون کپی بسته‌ها (Zero-Copy Packet Pipeline) — بسته‌ها مستقیماً و درجا از ساختارهای DPDK mbufs تجزیه (Parse) می‌شوند و برنامه داده‌ها را مستقیماً از حافظه نگاشت‌شده به DMA می‌خواند.
  4. حلقه رویداد قفل‌شده روی هسته (Core-Pinned Event Loop) — یک ساختار Reactor تک‌نخی مبتنی بر Busy-polling که به یک هسته ایزوله‌شده پردازنده اختصاص و قفل (Pin) شده است.

پیش‌نیازهای سخت‌افزاری و سیستم‌عامل

پیش از نوشتن حتی یک خط کد در Rust، سیستم میزبان باید برای رفتاری کاملاً قطعی و بدون نوسان (Deterministic) پیکربندی شود. این اقدامات هم برای سیستم‌های مبتنی بر Kernel-bypass و هم برای کرنل‌های بهینه‌سازی‌شده ضروری هستند؛ بنابراین اگر هنوز این تنظیمات را اعمال نکرده‌اید، قبل از رفتن به سراغ DPDK کار را از این مرحله شروع کنید.

پیکربندی BIOS و Firmware

Python

بهینه‌سازی و تنظیمات کرنل لینوکس (Linux Kernel Tuning)

Terminal / Bash

پیکربندی صفحات بزرگ حافظه (Hugepage)

فریم‌ورک DPDK برای استخرهای حافظه خود به Hugepageها نیاز دارد. بهره‌گیری از صفحات ۱ گیگابایتی خطاهای TLB miss را در دسترسی به بافر بسته‌ها به‌کلی برطرف می‌کند:

Terminal / Bash

اتصال مستقیم کارت شبکه (NIC Binding)

YAML

ساختار پروژه و بایندینگ‌های DPDK

ما به جای استفاده از کتابخانه‌های لایه بالاتر، از بایندینگ‌های مستقیم و خام FFI برای ارتباط با DPDK بهره می‌بریم تا کنترل کاملی روی مدیریت حافظه و نحوه پردازش بسته‌ها داشته باشیم.

GraphQL

پیکربندی بیلد پروژه (Build Configuration)

Config / INI
Rust

تجرید بدون کپی بافر داده‌ها (Zero-Copy Mbuf Abstraction)

ساختار rte_mbuf در DPDK، بافر اصلی داده‌ها است؛ یک هدر متادیتا که به حافظه نگاشت‌شده Hugepage از طریق DMA (محل قرارگیری مستقیم بایت‌های خام توسط کارت شبکه) اشاره می‌کند. ساختار پوششی ما در Rust امکان دسترسی امن و بدون کپی (Zero-copy) را بدون ایجاد سربار تخصیص حافظه در مسیر بحرانی اجرا فراهم می‌سازد.

یک نکته فنی درباره ادعای امنیت حافظه: پیاده‌سازی Drop تضمین می‌کند که تمام mbufها در تمام مسیرهای اجرای کد به mempool مربوطه بازگردانده شوند. البته این به معنی بررسی قواعد DPDK توسط Borrow checker نیست — اساساً چنین کاری در مرز کدهای FFI امکان‌پذیر نیست. مزیت اصلی این است که جلوی باگ معروف زبان C گرفته می‌شود؛ جایی که برنامه‌نویس با یک return زودهنگام، دستور goto out_free را جا می‌اندازد. این یک مزیت محدود اما ارزشمند است. کدهای داخل بلاک‌های unsafe همچنان نیازمند بررسی دقیق هستند و مسئولیت آن‌ها بر عهده توسعه‌دهنده است. به بیان دیگر، Rust کتابخانه DPDK را ذاتاً امن نمی‌کند، بلکه احتمال خطا در استفاده از لایه Wrapper را به حداقل می‌رساند.

Rust

تجزیه بدون کپی بسته‌ها (Zero-Copy Packet Parsing)

با دسترسی به بایت‌های خام از طریق mbuf، هدرهای Ethernet، IP و TCP را با تبدیل درجا (In-place) اسلایس‌های بایتی به رفرنس‌های هدر تجزیه می‌کنیم. بدون هیچ‌گونه تخصیص حافظه یا کپی اضافی — هر عملیات تجزیه صرفاً به یک کست اشاره‌گر و یک بررسی محدوده (Bounds check) تبدیل می‌شود (حدود ۲ تا ۵ نانوثانیه برای هر سه لایه).

Rust

ماشین وضعیت TCP: کمینه‌گرا از پایه

یک پیاده‌سازی استاندارد و همه‌منظوره از TCP، ده‌ها حالت پیچیده و نادر را پوشش می‌دهد: باز شدن هم‌زمان نشست‌ها، داده‌های فوری، Silly window syndrome، الگوریتم Nagle، شروع آهسته (Slow start)، اجتناب از ازدحام (Congestion avoidance) و موارد دیگر. در نقطه مقابل، پشته ما دقیقاً سناریوهای موردنیاز را پیاده می‌کند: ارتباط‌های پایدار و از پیش برقرارشده با گیت‌وی‌های شناخته‌شده صرافی‌ها برای جابه‌جایی پیام‌های FIX روی بسترهای مطمئن دیتاسنتری.

در این بخش، کاملاً شفاف توضیح می‌دهیم چه بخش‌هایی را حذف کردیم، چرا آن‌ها را کنار گذاشتیم و این حذفیات در چه جاهایی گریبان‌گیر ما شدند.

Rust

قابلیت‌هایی که عمداً حذف کردیم — و جاهایی که متضرر شدیم

ویژگی | دلیل حذف | ریسک | تجربه عملی ما | الگوریتم Nagle | تمام پیام‌ها حساس به تأخیرند؛ هرگز نباید با هم ترکیب شوند | در عمل بدون ریسک | بدون مشکل | تأییدیه‌های با تأخیر (Delayed ACKs) | هنگام فلگ PSH بلافاصله تأیید می‌دهیم و در سایر حالات دسته‌ای عمل می‌کنیم | سربار جزئی پهنای باند | بدون مشکل | شروع آهسته (Slow start) | لینک اختصاصی است و مسیر مشترکی وجود ندارد | جهش‌های ترافیکی (Microburst) در ورودی سوئیچ | به بخش ازدحام رجوع شود | کنترل کامل ازدحام (Full congestion control) | سربار تأخیر بسیار بالا | از دست رفتن بسته منجر به بازیابی فقط از طریق ارسال مجدد می‌شود | طوفان‌های PFC — که منجر به افزودن واکنش به ECN شد | مرتب‌سازی بسته‌های نامنظم (Out-of-order reassembly) | کابل اختصاصی است و بسته‌ها نباید جابه‌جا شوند | خراب شدن خاموش داده‌ها در صورت نقض فرضیات | یک مورد حادثه در ۱۸ ماه | پیشگیری از پنجره احمقانه (Silly window avoidance) | پیام‌های FIX ساختار درخواست/پاسخ دارند | هیچ | بدون مشکل | بازیافت وضعیت TIME_WAIT | اتصالات پایدار و طولانی‌مدت هستند | تأخیر در اتصال مجدد | قابل قبول

حذف مکانیزم کنترل ازدحام (Congestion control) پرهزینه‌ترین اشتباه ما در این مسیر بود. در ماه چهارم حضور در محیط پروداکشن، یک باگ فریم‌ور روی سوئیچ ورودی صرافی رخ داد؛ هنگام نوسانات شدید بازار و با پر شدن بافر کوچک سوئیچ توسط ارسال رگباری داده‌ها (Burst)، سوئیچ اقدام به ارسال فریم‌های توقف Priority Flow Control (PFC) کرد. این توقف PFC به کارت شبکه ما منتقل شد و کارت شبکه نیز طبق استاندارد، کل ارسال‌ها را به مدت ۲۰۰ میلی‌ثانیه متوقف کرد — زمانی که در دنیای HFT معادل یک قرن است! در این بازه، به خاطر ناتوانی در هج کردن موقعیت‌های معاملاتی، زیان مالی قابل‌توجهی (P&L) متحمل شدیم.

راه‌حل این چالش، پیاده‌سازی سیستم کنترل نرخ سبک مبتنی بر ECN بود. این راهکار تنها حدود ۳ نانوثانیه سربار به ازای هر بسته ایجاد می‌کند (یک شرط روی ip.ecn() و یک شمارنده) و توانست مانع تکرار آن اتفاق شود. درسی که گرفتیم این بود: این فرض که «هیچ زیرساخت اشتراکی برای بروز ازدحام وجود ندارد» یک توهم است؛ چراکه حتی یک اتصال مستقیم Cross-connect نیز در نهایت از تراشه‌های سوئیچ با بافرهای محدود عبور می‌کند.

پارسر پروتکل FIX: منشأ اصلی تأخیر کجاست؟

لایه TCP جریان بایت‌ها (Byte streams) را تحویل می‌دهد، اما در مرحله بعد این لایه کاربرد (Application Layer) است که باید آن بایت‌ها را به پیام‌های معاملاتی معنادار و کاربردی تبدیل کند. در پروتکل FIX — که قالب داده‌ای رایج در بورس‌های بین‌المللی است — پردازش و تجزیه پیام‌ها اغلب بسیار سنگین‌تر از خود پردازش TCP تمام می‌شود.

پروتکل FIX یک ساختار متنی ASCII مبتنی بر کلید-مقدار (Tag-value) است. یک نمونه از پیام گزارش انجام معامله (Execution Report) به صورت زیر است:

Config / INI

(که در آن | نشان‌دهندهٔ جداکنندهٔ SOH یا همان 0x01 است.)

طول پیام‌ها متغیر است، ترتیب فیلدها درون گروه‌ها تضمینی نیست و گروه‌های تکرارشونده هم ساختاری تو در تو ایجاد می‌کنند. این ساختار مسلماً «فریم‌های با اندازهٔ ثابت» نیست و پارس کردن بهینهٔ آن بدون تخصیص حافظه (Allocation) نیازمند طراحی بسیار دقیقی است.

Rust

عملکرد و کارایی پارس کردن FIX

اندازه‌گیری‌شده در مسیر بحرانی (Hot path) با استفاده از criterion:

SQL

برای مقایسه، پردازش هدر TCP برای همان بسته حدود ۱۵ نانوثانیه زمان می‌برد؛ در حالی که لایهٔ پارس کردن FIX بین ۴ تا ۷ برابر سنگین‌تر از TCP تمام می‌شود. دقیقاً به همین دلیل، نادیده گرفتن این لایه در مقالهٔ اصلی گمراه‌کننده بود. وقتی ۸۲ نانوثانیه صرف پارس کردن FIX می‌شود، بهینه‌سازی TCP از ۱۵ به ۱۰ نانوثانیه اولویت‌بندی اشتباهی است. در عمل، ما زمان مهندسی بسیار بیشتری را صرف بهینه‌سازی پارسر FIX کردیم (مثل اسکن SIMD برای جداکننده‌های SOH، پارس کردن بدون انشعاب یا Branchless تگ‌ها و محاسبات اعشاری با ممیز ثابت یا Fixed-point) تا ماشین حالت TCP.

ارسال بسته‌ها (Packet Transmission)

هنگامی که نیاز به ارسال یک سگمنت TCP داریم، یک mbuf از mempool تخصیص می‌دهیم و هدرها را مستقیماً درون حافظهٔ DMA می‌نویسیم. قابلیت Hardware checksum offload نیز حدود ۱۰۰ نانوثانیه از سربار پردازش هر بسته را حذف می‌کند.

Rust

حلقهٔ رویداد (The Event Loop)

حلقهٔ اصلی Poll روی یک هستهٔ مجزای CPU اجرا می‌شود؛ هسته‌ای که هرگز به خواب نمی‌رود، کنترل را واگذار نمی‌کند و هیچ فراخوان سیستمی (System call) انجام نمی‌دهد. برای سنجش زمان هم به‌جای clock_gettime (حدود ۵۰ نانوثانیه)، از دستورالعمل rdtsc (حدود ۱ نانوثانیه) استفاده می‌شود.

Rust

مقیاس‌پذیری: چالش وضعیت مشترک (Shared State)

یک حلقهٔ رویداد تک‌هسته‌ای می‌تواند یک یا چند اتصال به صرافی را مدیریت کند، اما بازارسازی واقعی روی چندین دارایی نیازمند ده‌ها نشست (Session) هم‌زمان است. از همه مهم‌تر، این نشست‌ها به یک وضعیت مشترک نیاز دارند: محدودیت‌های مدیریت ریسک باید روی تمام موقعیت‌ها تجمیع شوند و تصمیمات معاملاتی روی ابزار مالی A به قیمت‌های ابزار B وابسته است.

در نسخهٔ قبلی مقاله این‌طور القا شده بود که RSS (Receive Side Scaling) با توزیع اتصالات بین هسته‌ها این مشکل را حل می‌کند. این حرف برای افزایش توان پردازشی (Throughput) درست است، اما برای مدیریت وضعیت مشترکی که به تأخیر حساس است پاسخگو نیست. نحوهٔ مدیریت واقعی آن به این شکل است:

Architecture: هسته‌های مستقل ورودی/خروجی شبکه (shared-nothing network I/O) که داده‌ها را به یک هستهٔ اختصاصی استراتژی تغذیه می‌کنند.

CSS

هسته‌های شبکه در واقع موتورهای صرفاً I/O هستند: آن‌ها TCP را اجرا می‌کنند، پیام‌های FIX را پارس می‌نمایند و پیام‌های دیکودشده (نه بایت‌های خام) را به صف‌های حلقوی SPSC اختصاصی هر هسته می‌فرستند. در مقابل، هستهٔ استراتژی مالک تمام وضعیت عمومی (Global state) سیستم است؛ از موقعیت‌ها و حد ریسک گرفته تا دفاتر سفارشات (Order books) و سیگنال‌های بین‌نمادی. این هسته تمام صف‌های ورودی را در یک حلقهٔ Poll واحد می‌خواند و به محض اینکه تصمیمی برای معامله بگیرد، پیام پیش‌قالب‌بندی‌شدهٔ سفارش را به حلقهٔ خروجی همان هستهٔ شبکه مربوطه ارسال می‌کند.

این شیوه به این معنی است که ارتباط بین هسته‌ها در هر مرحله (Push + Pop در حلقهٔ SPSC) حدود ۱۰ تا ۱۵ نانوثانیه تأخیر اضافه می‌کند. برای تیک دادهٔ بازاری که به هستهٔ شبکهٔ ۰ می‌رسد و باعث ثبت سفارشی در اتصال تحت مدیریت هستهٔ شبکهٔ ۱ می‌شود، مسیر به این شکل خواهد بود: هستهٔ شبکهٔ ۰ ← حلقه ← هستهٔ استراتژی ← حلقه ← هستهٔ شبکهٔ ۱. این یعنی حدود ۲۵ نانوثانیه سربار بین‌هسته‌ای علاوه بر کل پردازش انتها به انتها (Wire-to-wire) اضافه می‌شود.

پیاده‌سازی حلقهٔ SPSC:

Rust

در معماری x86_64، عملیات ذخیره‌سازی Release به یک دستور سادهٔ MOV کامپایل می‌شود؛ زیرا مدل حافظهٔ قوی (Strong memory model) قابلیت‌های معنایی Release را بدون هیچ هزینهٔ اضافی در اختیارمان می‌گذارد.

راهکارهای جایگزین: گزینه‌هایی که احتمالاً باید به‌جای این روش استفاده کنید

قبل از ساخت چنین سیستمی، ما این گزینه‌های جایگزین را ارزیابی کردیم. در ادامه مقایسه‌ای صادقانه از آن‌ها آمده است:

راهکار / میانه RTT / صدک p99.9 / سوکت API / هزینه نگهداری Solarflare OpenOnload ۲.۵ میکروثانیه ۵ میکروثانیه Full BSD پشتیبانی شرکتی کارت شبکه + لایسنس Mellanox VMA ۲.۸ میکروثانیه ۶ میکروثانیه Full BSD پشتیبانی شرکتی رایگان به همراه ConnectX AF_XDP ۳.۵ میکروثانیه ۸ میکروثانیه سفارشی (XDP) جامعه کاربری رایگان F-Stack (DPDK + FreeBSD TCP) ۱.۸ میکروثانیه ۳.۵ میکروثانیه تقریباً Full BSD جامعه کاربری رایگان Custom DPDK (this article) ۱.۱ میکروثانیه ۲.۹ میکروثانیه ندارد خودتان حقوق تیم مهندسی

OpenOnload نقطه‌ای است که اکثر شرکت‌ها باید از آن شروع کنند. این ابزار فراخوان‌های استاندارد سوکت را شنود کرده و آن‌ها را از طریق پشتهٔ TCP فضای کاربری Solarflare هدایت می‌کند. کدهای فعلی‌تان بدون هیچ تغییری کار خواهند کرد و هم‌زمان کاهش تأخیر ۲ تا ۳ برابری را تجربه می‌کنید. بهای آن هم وابستگی به یک شرکت خاص (تنها کارت‌های شبکهٔ Solarflare/Xilinx) و مبهم بودن ساختار داخلی است که امکان تنظیم و بهینه‌سازی دستی را از شما می‌گیرد.

Mellanox VMA معادل همین قابلیت برای کارت‌های شبکه Mellanox/NVIDIA ConnectX است؛ با رویکرد و عملکردی مشابه که به همراه سخت‌افزار به صورت رایگان ارائه می‌شود. گرچه شاید به اندازهٔ OpenOnload جاافتاده نباشد، اما توسعهٔ بسیار فعالی دارد.

AF_XDP به عنوان یک گزینهٔ میانه جذاب است: این فریم‌ورک درون‌کرنلی، دسترسی به بسته‌های خام را بدون خروج کامل از فضای هسته (Kernel) فراهم می‌کند. هرچند تأخیر آن از DPDK بیشتر است، اما امکان پایش با tcpdump همچنان حفظ می‌شود و نیازی به مدیریت دستی ARP یا ICMP نخواهید داشت.

F-Stack پشتهٔ بالغ و پایدار TCP در FreeBSD را روی DPDK پیوند می‌زند. با این روش، هم از یک پیاده‌سازی واقعی و کامل TCP بهره‌مند می‌شوید و هم کارایی Kernel-bypass را به دست می‌آورید. اگر کارایی DPDK را می‌خواهید اما مایل نیستید صفر تا صد یک ماشین حالت TCP را بنویسید، این گزینه احتمالاً بهترین انتخاب است.

ما رویکرد پیاده‌سازی سفارشی را انتخاب کردیم، چرا که:

  1. پشتهٔ TCP در FreeBSD متعلق به F-Stack شامل قابلیت‌هایی بود که نیازی به آن‌ها نداشتیم و سربار آن‌ها، هرچند اندک، در ترافیک ۵۰۰ هزار بسته در ثانیه انباشته می‌شد.
  2. برای برآورده کردن نیازمندی‌های خاص صرافی‌ها، به کنترل بسیار دقیقی روی زمان‌بندی ACK نیاز داشتیم.
  3. الزامات انطباق و قوانین نظارتی (Compliance)، داشتن مالکیت کامل روی سورس‌کد پشتهٔ شبکه را ضروری می‌کردند.

مورد سوم عامل تعیین‌کننده بود. اگر تیم انطباق شما چنین الزامی ندارد، حتماً از OpenOnload یا F-Stack استفاده کنید.

بودجهٔ تأخیر (Latency Budget): زمان واقعاً کجا صرف می‌شود؟

پس از پیاده‌سازی و ارزیابی عملکرد (Profiling) کل خط لوله، تفکیک دقیق زمان‌ها به این صورت است:

CSS

بخش عمدهٔ زمان به تصمیم‌گیری استراتژی اختصاص دارد که ۶۵ درصد از کل تأخیر خط به خط (Wire-to-wire) را شامل می‌شود. کل پشتهٔ شبکه (شامل TCP، FIX و TX) تنها حدود ۳۰ درصد را تشکیل می‌دهد. این همان حقیقت تلخ دربارهٔ پشته‌های سفارشی TCP است: حتی اگر زمان پردازش TCP را به صفر نانوثانیه برسانید، کل تأخیر شما فقط از ۶۸۷ به ۶۶۹ نانوثانیه کاهش می‌یابد. بازدهی نهایی (Marginal return) بهینه‌سازی TCP در مقایسه با بهینه‌سازی استراتژی یا پارس کردن FIX واقعاً ناچیز است.

مهم‌ترین مزیت TCP سفارشی، کاهش میانهٔ تأخیر نیست، بلکه پیش‌بینی‌پذیری در تأخیر دُم (Tail latency determinism) است. نسبت صدک p99.9 به p50 در پشتهٔ سفارشی ما ۲.۷ برابر است؛ در حالی که برای یک هستهٔ بهینه‌سازی‌شده این رقم ۳ تا ۴ برابر و برای هستهٔ پیش‌فرض ۷ تا ۸ برابر است. در معاملات فرکانس بالا (HFT)، فشردگی و مهار تأخیرهای دُم بسیار حیاتی‌تر از سرعت میانه است.

درس‌های عملی از محیط پروداکشن

پس از ۱۸ ماه اجرای این سیستم در محیط عملیاتی:

آنچه جواب داد: مدل مالکیت (Ownership) برای Mbuf جلوی تمام باگ‌های نشت بافر (Buffer leak) را که قبلاً در کدهای C دیده بودیم گرفت. صف‌های حلقوی SPSC حتی یک مورد حادثه هم به وجود نیاوردند. کنترل نرخ بر پایهٔ ECN (که بعد از طوفان PFC اضافه شد) جلوی دو حادثهٔ احتمالی بعدی را گرفت. همچنین استفاده از تنظیمات panic = "abort" در Rust باعث شد در صورت بروز خطا، سیستم بلافاصله متوقف شود (Fail fast) تا جلوی خراب شدن وضعیت (State) گرفته شود.

چالش‌ها و سختی‌ها: دیباگ کردن این سیستم طاقت‌فرساست. ابزاری مثل tcpdump در دسترس نیست چون هستهٔ لینوکس اصلاً بسته‌های ما را نمی‌بیند. برای تحلیل، مجبور شدیم ابزار اختصاصی ضبط بسته‌ای بسازیم که بخشی از ترافیک را روی یک کارت شبکهٔ جداگانه Mirror کند. بازیابی شمارهٔ توالی (Sequence number) در FIX پس از کرش استراتژی نیز نیازمند هماهنگی ظریفی بین لایهٔ TCP (که اتصال را باز نگه می‌داشت) و لایهٔ نشست FIX (که باید توالی‌ها را دوباره مذاکره می‌کرد) بود؛ موضوعی که حل آن ماه‌ها بیشتر از تخمین اولیه‌مان زمان برد.

آنچه اگر برمی‌گشتیم تغییر می‌دادیم: کار را با F-Stack شروع می‌کردیم و تنها بخش‌هایی (مثل زمان‌بندی ACK یا پارس کردن FIX) را بازنویسی می‌کردیم که بنچمارک‌ها سودمندی‌شان را اثبات می‌کردند. ماشین حالت سفارشی TCP، به نسبت ساعت‌های مهندسی صرف‌شده، کم‌ارزش‌ترین بخش این سیستم بود.

نتیجه‌گیری

پیاده‌سازی یک مدیریت‌کنندهٔ سفارشی TCP در Rust با کمک DPDK به تأخیر انتها به انتهای زیر ۲ میکروثانیه همراه با توزیع دُم بسیار فشرده دست می‌یابد؛ یعنی حدود ۵ برابر بهتر از یک پشتهٔ هسته که به خوبی بهینه‌سازی شده باشد. مدل مالکیت Rust در مرز FFI برای DPDK ارزش واقعی خود را نشان می‌دهد؛ البته این مدل کدهای unsafe را امن نمی‌کند، بلکه کار با رپرهای امن (Safe wrappers) را به شکلی درمی‌آورد که احتمال استفادهٔ نادرست از آن‌ها به حداقل برسد.

با این حال، این یک اقدام بسیار افراطی است که فقط برای نیازمندی‌های فوق‌العاده خاص توجیه‌پذیر خواهد بود. هزینهٔ مهندسی آن با نفر-سال سنجیده می‌شود، بار عملیاتی آن دائمی است و بهبود تأخیر نهایی آن در مقایسه با راهکارهای آمادهٔ عبور از هسته (مانند OpenOnload، VMA و F-Stack) ناچیز است. در نتیجه، قوی‌ترین توجیه برای ساخت یک پشتهٔ سفارشی کارایی نیست، بلکه دستیابی به کنترل کامل است: کنترل بر زمان‌بندی ACK، کنترل بر محدودسازی نرخ و تسلط بر تک‌تک بایت‌هایی که وارد شبکه می‌شوند.

اگر در حال بررسی این حوزه هستید، کار را با بهینه‌سازی کرنل شروع کنید. سپس به سراغ OpenOnload یا VMA بروید و در مرحلهٔ بعد F-Stack را امتحان کنید. تنها در صورتی که بنچمارک‌ها نشان دادند پشتهٔ عمومی TCP گلوگاه اصلی شماست و تیمی متخصص برای نگهداری آن در اختیار دارید، به سراغ راهکارهای مطرح‌شده در این مقاله بروید.

حقیقت ناگفتهٔ شبکه‌های HFT این است: شرکت‌هایی که سریع‌ترین سیستم‌ها را دارند، این برتری را با خرید سخت‌افزارهای قوی‌تر و استقرار سرورها در نزدیک‌ترین فاصلهٔ فیزیکی به صرافی (Colocation) به دست آورده‌اند، نه با نوشتن ماشین‌های حالت بهتر برای TCP. بهینه‌سازی نرم‌افزاری تنها زمانی معنا پیدا می‌کند که تمام آن گزینه‌ها را تا نهایت ممکن به کار گرفته باشید.

می‌توانید به پیاده‌سازی کامل این پروژه در مخزن گیت‌هاب از اینجا دسترسی داشته باشید.

دربارهٔ نویسنده

Luis Soares مهندس نرم‌افزار و متخصص در زمینه‌های Rust، بلاک‌چین، سیستم‌های توزیع‌شده، برنامه‌نویسی سطح پایین و توسعهٔ بک‌اند است.

ارتباط با نویسنده: LinkedIn | X

کتاب‌های من دربارهٔ Rust در آمازون: