سرور FUSE با OCaml برای استوریج داخلی Jane Street
خلاصهٔ کاملتر
Jane Street برای پردازش دادههای حجیم قبلاً از استوریجهای آمادهی شرکتهایی مثل Dell Isilon و VAST استفاده میکرد، ولی با بزرگتر شدن دیتاستها به سقف مقیاسپذیری این محصولات خورد. نتیجه شد Depot، یه object store داخلی (چیزی شبیه S3 که برای نیازهای خودشون بهینه شده) که با OCaml نوشته شده. مشکل اینجاست که خیلی از ابزارهای تحقیقاتیشون فایلها رو از طریق رابط معمولی POSIX میخونن و بازنویسی بعضیاشون یعنی تغییرات عمیق تو معماری.
راهحل یه سرور FUSE بود. FUSE (یعنی Filesystem in Userspace، پروتکلی که کرنل لینوکس باهاش درخواستهای فایلسیستم رو به یه برنامهی معمولی میفرسته و جواب رو برمیگردونه) اجازه میده Depot مثل یه فایلسیستم دیده بشه. چون کاربرد اصلی تحقیقاتیه، سرور باید سریع باشه ولی میتونه فقطخواندنی باشه. این پروژه رو Kian Kasad تو کارآموزی تابستون ۲۰۲۶ انجام داد.
معمولاً سرورهای FUSE از libfuse استفاده میکنن، ولی این کتابخونهی C با IO مسدودکننده کار میکنه و با runtime Async تو OCaml جور درنمیاد؛ FFI هم دردسر و ریسک کارایی داره. پس تیم پروتکل رو خودش پیاده کرد. چون مستندات FUSE خیلی کمه، Kian کد کرنل رو برای هر نوع پیام خوند. برای تست هم یه پیادهسازی مبتنی بر libfuse رو تغییر داد تا پیامهای واقعی رو ضبط کنه و از اونا بهعنوان دادهی تست serializerها استفاده کرد.
یه درس سخت این بود که /dev/fuse توقع داره هر read() یا write() دقیقاً یه پیام کامل باشه، و این جایی مستند نشده. کتابخونههای استاندارد فرض میکنن نوشتن ممکنه نصفه انجام بشه و خودشون دوباره تلاش میکنن، که اینجا مشکلسازه. مثلاً اگه به Bigstring_unix.write مقدار ~min_len بدی، ممکنه چند بار write() صدا بزنه. واسه همین کد نوشتن پیام اینجوری شد:
Fd.syscall_in_thread t ~name:"fuse-write" (fun fd ->
(* Even though we require the full message to be written, we don't set [~min_len],
because that causes [Bigstring_unix.write] to issue multiple writes. *)
Bigstring_unix.write fd buf ~pos ~len:msg_len)سمت Depot هم باید معنای یه object store با آبجکتهای تغییرناپذیر به قوانین POSIX ترجمه میشد. مثلاً Depot اسمهایی تا ۷۰۰۰ کاراکتر قبول میکنه ولی FUSE فقط تا ۱۰۲۴. پس اسمهای بلند موقع لیست کردن کوتاه میشن و موقع استفاده به اسم اصلی برمیگردن. «bundle»ها (چیزی شبیه پوشه) هم میتونن از چند مسیر دیده بشن، پس inodeها بر اساس مسیر دسترسی ساخته شدن تا هر مسیر یه شیء جدا به نظر بیاد.
نسخهی اول فقط حدود ۳۰ MiB/s سرعت داشت. اول معلوم شد خوندن موازی از یه فایل بیخطره و همین سرعت رو دو برابر کرد، چون readahead کرنل درست کار کرد. ولی تأثیر اصلی از حذف کپی بود: دادهها به شکل string نگه داشته میشدن و هر خوندن دو بار کپی میشد، طوری که ۹۲٪ کل allocationها از همین بود. با پاس دادن مستقیم بافرها، سرعت به ۱.۳ GiB/s رسید.
نکات کلیدی:
- Depot اسم تا ۷۰۰۰ کاراکتر قبول میکنه، FUSE تا ۱۰۲۴ و خیلی از ابزارهای یونیکس تا ۲۵۶
- سرعت خوندن فایلهای بزرگ از حدود ۳۰ MiB/s به ۱.۳ GiB/s رسید
- خوندن یه فایل ۴ گیگابایتی قبلاً حدود ۸ گیگابایت allocation داشت
- به جای libfuse، پروتکل FUSE مستقیم با OCaml و سازگار با Async پیاده شده
- به گفتهی تیم، گلوگاه بعدی احتمالاً پنجرهی کوچیک readahead لینوکسه




