تسریع عملکرد با یکپارچه‌سازی تدریجی Rust در کد موجود

تسریع عملکرد با یکپارچه‌سازی تدریجی Rust در کد موجود

تسریع عملکرد با یکپارچه‌سازی تدریجی Rust در کد موجود

در این ارائه، لیلی مرا توضیح می‌دهد که چگونه می‌توان از بازنویسی‌های نرم‌افزاری پرخطر جلوگیری کرد و با استفاده از بازسازی FFI (واسطه توابع خارجی C) به صورت گام به گام پیش رفت. او روشی را برای جایگزینی گلوگاه‌های پایتون با Rust، از طریق PyO3، ارائه می‌کند که منجر به افزایش چشمگیر سرعت عملکرد در سطح تابع، یکپارچه‌سازی آزمایشی بی‌نقص و صرفه‌جویی قابل توجهی در هزینه‌های زیرساخت بدون سربار میکروسرویس‌ها می‌شود.

لیلی مرا مهندس ارشد در Discord و مسئول پلتفرم اعلان‌ها است. او سیستم‌های توزیع‌شده‌ای را برای ارسال ده‌ها میلیارد اعلان به کاربران Discord روزانه توسعه می‌دهد. پیش از این، او مدیر مهندسی و مهندس نرم‌افزار در OneSignal واقع در سان ماتئو، کالیفرنیا بوده است. لیلی نویسنده کتاب «بازسازی به Rust» نیز می‌باشد.

ایریک متاج مدیر بازاریابی محصول، تحویل نرم‌افزار در Datadog و روهین چاندرا مدیر محصول، بهینه‌سازی CI/CD در Datadog ارائه دهندگان این سخنرانی هستند. همچنین مارتین رینولدز به‌عنوان مدیر ارشد فناوری میدانی (Field CTO) در Harness نیز حضور دارد.

لیلی مرا: امروز قصد دارم در مورد چگونگی تسریع یک قطعه نرم‌افزاری موجود با افزودن تدریجی کد Rust به آن صحبت کنم. برای کمی پیش‌زمینه، من لیلی مرا مهندس ارشد در Discord هستم و بیش از یک دهه است که از Rust استفاده می‌کنم (که فکر کردن به آن عجیب است!). البته نه کل این مدت به صورت حرفه‌ای، در ابتدا بیشتر کارهای جانبی انجام می‌دادم، اما از سال 2019 به طور حرفه‌ای از Rust استفاده کرده‌ام. من در QCon و بسیاری از کنفرانس‌های نرم‌افزاری دیگر درباره Rust صحبت کرده‌ام و کتاب «بازسازی به Rust» را نیز نوشته‌ام که همان موضوعی است که امروز در این سخنرانی مورد بحث قرار می‌دهیم.

به نظر می‌رسد تقریباً غیرممکن است، به‌عنوان یک مهندس نرم‌افزاری آگاه و علاقه‌مند (همانطور که شما هم هستید چون در یک کنفرانس نرم‌افزاری حضور دارید)، از این نکته غافل شده باشید که Rust در سال‌های اخیر بسیار مورد توجه قرار گرفته است. مطمئناً بارها شنیده‌اید که بسیار سریع است؛ به‌مراتب سریع‌تر از زبان‌های پویا (Dynamic Languages) که اکثر نرم‌افزارهای بزرگ و یکپارچه ما با آن‌ها نوشته شده‌اند، مانند پایتون، روبی یا Node. به طور غریزی، وقتی چیزی را سریع می‌بینیم، تمایل داریم بگوییم: «خب، بیایید همه چیز را از صفر دوباره شروع کنیم و همه را با Rust بازنویسی کنیم.» این ایده خیلی جذاب است! ما یک زبان جدید و خنک داریم که تمام اصطلاحات خوب (Idioms) را دارد و فوق‌العاده سریع هم هست. پس بیایید همه چیز را با Rust بازنویسی کنیم.

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

شاید هم فکر کنید که قرار نیست کل سیستم را بازنویسی کنیم، اما یک مونولیت بزرگ داریم و می‌خواهیم آن را به میکروسرویس‌ها تقسیم کنیم و کم‌کم سرعت آن را افزایش دهیم. من امروز قصد دارم در مورد چیزی صحبت کنم که حتی از میکروسرویس‌ها هم دقیق‌تر است. چیزی که عمیقاً در پشته قرار دارد: بازسازی FFI، جایی که ما یک قطعه عملکردی را در سطح تابع بازنویسی می‌کنیم و آن را به یک زبان برنامه‌نویسی سریعتر (در این مورد Rust) منتقل می‌کنیم. برای اتصال این دو چیز از C Foreign Function Interface استفاده خواهیم کرد. اساساً هر سیستم‌عامل مدرن و هر زبان برنامه‌نویس می‌داند چگونه یک تابع C را فراخوانی کند، زیرا حجم زیادی از کد C در حال حاضر وجود دارد که کارهای مهمی را انجام می‌دهد.

این به نوعی زبان مشترک (Lingua Franca) برای نرم‌افزار تبدیل شده است. ما از این موضوع برای اتصال کد پایتون و Rust به روشی با کارایی بالا استفاده خواهیم کرد. این یک تکنیک جدید نیست، قطعاً چیزی نیست که خودم ابداع کرده باشم. اگر تا به حال از چنین نرم‌افزاری استفاده کرده باشید، مطمئناً از این نوعbindings زبانی عبوری (Cross-Language bindings) استفاده کرده‌اید. NumPy و SciPy بر اساس کد Fortran هستند که با کد پایتونی که توسعه‌دهندگان برنامه می‌نویسند ارتباط دارند. TensorFlow به C++ نوشته شده است و binding هایی برای زبان‌های مختلف دارد. OpenSSL نیز به C نوشته شده است و binding ها در تقریباً هر زبان برنامه‌نویسی موجودی وجود دارد.

بیایید در مورد امکان سنجی صحبت کنیم: چه واقعیت‌هایی را باید در نظر گرفت اگر قصد انجام چنین پروژه‌ای را دارید؟ همیشه معامله‌ای در کار است. همه می‌دانند که هیچ راه‌حل جادویی (Silver bullet) وجود ندارد. برخی از معاملات احتمالی عبارتند از:

  • پیچیدگی بیشتر در استقرار: اگر کد پویایی را در پایتون یا روبی یا هر چیز دیگری مستقر می‌کنید، ممکن است استقرار شما به سادگی FTP کردن نرم‌افزار به یک سرور و بارگذاری مجدد یک سرویس systemd باشد. با معرفی کد بومی سفارشی، این موضوع پیچیده‌تر می‌شود.
  • پیچیدگی بیشتر در محیط‌های توسعه: باید کامپایلرهای Rust را روی ماشین‌های توسعه خود قرار دهید یا binary های native را به ماشین‌های توسعه ارسال کنید که با سیستم‌عامل و micro-architecture مطابقت داشته باشند، یا باید یک پیاده‌سازی قدیمی پویا و یک پیاده‌سازی جدید بومی Rust را به‌طور همزمان حفظ کنید.
  • احتمال افزودن اشکال: همیشه این احتمال وجود دارد که هنگام بازنویسی، اشکالی به سیستم اضافه کنید. با این حال، وقتی در مقیاس کوچکتر کار می‌کنید و محدودیت‌هایی دارید، احتمال وقوع آن کمتر است.

چه پروژه‌هایی کاندیدای مناسبی برای این نوع کار هستند؟ چه پروژه‌هایی کاندیدای نامناسبی هستند؟ به نظر من موضوع اصلی مجموعه‌ها (Aggregates) هستند – مجموعه‌ای از چیزهایی که برنامه شما در آن زمان را صرف می‌کند و چگونه می‌توان این مقدار را کاهش داد. می‌تواند عملیاتی باشد که گاهی اوقات اتفاق می‌افتند اما بسیار پرهزینه هستند، یا عملیاتی که نسبتاً ارزان هستند اما دائماً در حال تکرار هستند. به‌عنوان مثال، کد تأیید درخواست (Request verification code) که در جلوی هر یک از handlers API شما قرار دارد، ممکن است به تنهایی سنگین نباشد و مقدار زیادی CPU مصرف نکند، اما روی هر درخواست فراخوانی می‌شود و توسط تمام تیم‌هایتان استفاده می‌شود. اگر ببینید 10% زمان اجرای سرور شما صرف اجرای این منطق تأیید داخلی سفارشی می‌شود، چه می‌شد اگر این میزان 1% شود؟

Go برای مثال از لحاظ ابزارهای پشتیبانی کننده به دلیل محدودیت‌هایی که runtime آن بر نحوه واگذاری (Yielding) اجرا تحمیل می‌کند دچار مشکل است و باید بسیاری از آن را کنار بگذارد.

📌 توجه: این مطلب از منابع بین‌المللی ترجمه و بازنویسی شده است.