پرش به محتوای اصلی

اصول برنامه های کاربردی سریع توکیو

هکرنیوز۱۴۰۵ شهریور ۲۳, دوشنبه، ساعت ۱۸:۵۷حدود 10 دقیقه مطالعه

آدرس مقاله: https://dial9-rs.github.io/blog/principles-for-fast-tokio-applications/ آدرس نظرات: https://news.ycombinator.com/item?id=49698607 امتیاز: 238 # نظرات: 62

من در راه بازگشت از RustConf هستم. در Unconf، ما یک بحث سازنده در مورد اشکال زدایی و محک زدن برنامه های کاربردی async داشتیم. بینش های جالب بسیاری به اشتراک گذاشته شد. من سعی می کنم برخی از آنها را به همراه برخی از تجربیات خودم در اینجا برشمارم. این اولین پیش نویس چیزی است که امیدوارم بتواند به یک سند زنده از بهترین شیوه ها تبدیل شود. با خیال راحت مشکلی را مطرح کنید یا یک PR باز کنید. من امیدوارم که در روزهای آینده یک برنامه نمونه نیز اضافه کنم که این مشکلات را همراه با نحوه ظاهر شدن ردیابی dial9 نشان دهد.

قوانین سخت و سریع کمی برای نوشتن کد وجود دارد که در زمان های اجرا توکیو عملکرد خوبی دارد. پاسخ به بسیاری از سوالات این است که "بستگی دارد". عملکرد یک حجم کاری بستگی به این دارد که چه چیز دیگری در زمان اجرا در آن لحظه اجرا می شود. به همین دلیل است که بسیاری از مشکلات فقط در تولید ظاهر می شوند! نوشتن برنامه‌های غیر همگام که عملکرد خوبی دارند، تعادلی بین عدالت و دسته‌بندی، مجادله و انزوا است.

این پست برخی از اصول کلی را بیان می کند و در مواردی که من می توانم موارد استثنا را پوشش می دهد. این امر مستلزم آشنایی اولیه با زمان اجرای کار-دزدی توکیو است. یک خلاصه سطح بالا در پیوست گنجانده شده است.

اگر شروع به جستجوی پرچم های قرمز در یک برنامه توکیو کنید، آنها را پیدا خواهید کرد. تقریبا همه برنامه‌های واقعی که من دیده‌ام، نظرسنجی‌هایی دارند (زمان بین نقاط انتظار زمانی که کد به زمان اجرا برمی‌گردد) بسیار طولانی‌تر از ۱۰ تا ۱۰۰ میکروثانیه‌ای که آلیس رایل در پست عالی‌اش توصیه می‌کند مسدود کردن چیست؟ . این مشکلات ممکن است بر معیارهای برنامه یا رفتاری که واقعا به آن اهمیت می دهید تأثیر بگذارد یا نه (نگاه کنید به: نظرسنجی های طولانی گاهی اوقات خوب هستند). مهم است که نسبت به معیار واقعی که در تلاش برای بهبود آن هستید، به عقب عمل کنید.

به عنوان مثال، یک برنامه می تواند نظرسنجی های طولانی داشته باشد که کاملا خوش خیم هستند. "اصلاح" آنها به طور قابل اندازه گیری بر معیارهای مواجهه با کاربر تأثیر نمی گذارد.

در اکثریت قریب به اتفاق مشکلاتی که من با آن روبرو شده ام، مشکل در خود کد برنامه بود، اغلب در تعامل بین چندین مؤلفه یک سیستم توزیع شده (و نه در واقع در توکیو). dial9 دید زیادی به توکیو داده است. حداقل هر چند وقت یک بار که مشکل توکیو پیدا می کند، در واقع به وضوح فقدان آن را نشان می دهد (که به مردم اعتماد به نفس می دهد تا در جای دیگر جستجو کنند). البته گاهی اوقات مشکل توکیو است.

از نظر معیارهای توکیو، مفیدترین هیستوگرام تاخیر زمانی است که اخیرا اضافه شده است. تأخیر زمان‌بندی، مدت زمانی است که بین آماده شدن کار شما برای اجرا (به عنوان مثال، به دلیل داشتن داده‌های سوکت) و توکیو در واقع نظرسنجی آینده است. اگرچه این به شما نمی‌گوید علت چیست، تاخیر زمان‌بندی رایج‌ترین علامت تعامل ضعیف بین توکیو و کد شما است.

تأخیر کم در بسیاری از درخواست ها مستلزم رعایت انصاف بین اتصالات است.

Redis (یا هر برنامه‌ای که از خط لوله درخواست پشتیبانی می‌کند) را در نظر بگیرید. یک پیاده سازی ساده، داده ها را مستقیما از اتصال می خواند در حالی که داده های بیشتری در دسترس است. هنگامی که درخواست ها خط لوله می شوند، کل درخواست خط لوله (یا بیشتر آن) به یک بافر در حافظه ختم می شود. وقتی فریم هایی از آن را می خوانید، هر کدام Poll::Ready خواهند بود (بدون بازگشت به شبکه). این باعث ایجاد نظرسنجی طولانی و بی عدالتی بین مشتریان می شود.

تأثیر روی توان عملیاتی معمولا کمتر است: همان تعداد درخواست پردازش می شود. با این حال، تأخیر به‌طور چشمگیری تغییر می‌کند، زیرا کل یک خط لوله می‌تواند پشت سر دیگری منتظر بماند. تسلیم صریح پس از هر درخواست می تواند تاخیر را تقریبا 10× در این مثال کاهش دهد. تنها پس از چندین بار خواندن آماده فورا متوالی، می توانید حتی بهتر عمل کنید.

تسلیم شدن پس از چهار خواندن متوالی آماده فورا، درخواست‌های خط لوله را بسیار منصفانه‌تر می‌کند بدون اینکه به طور کامل از دسته‌بندی صرف نظر کنید.

انصاف رایگان نیست. هرچه کار مفیدتری در هر رویداد زمان اجرا انجام دهید - تغییر وظایف، نظرسنجی، جابجایی بین کارگران یا تغییر رشته ها - برنامه شما می تواند کارآمدتر باشد.

شاید بهترین مثال tokio::fs باشد. من گاهی تا آنجا پیش می روم که می گویم "tokio::fs مضر تلقی می شود." بدون io_uring، توکیو هر عملیات سیستم فایل را در استخر مسدود کننده اجرا می کند. هر تماس با spawn_blocking نیز هزینه دارد و هر زمان اجرا دارای یک استخر مسدود کننده مشترک است.

اگر می‌دانید که یک سری عملیات سیستم فایل را انجام خواهید داد - یا هر کار مسدود کردنی - آنها را در بزرگترین بخش مسدود کردن معقول قرار دهید. در برخی موارد، یک رشته سیستم عامل اختصاصی مناسب تر است.

این اصل در هر جایی که با توکیو تعامل داشته باشید اعمال می شود. اگر می دانید که کار را به صف جهانی ارسال خواهید کرد، بچینگ می تواند این هماهنگی را نیز مستهلک کند.

حتی چیزهایی به سرعت تخم ریزی یک کار رایگان نیستند! ایجاد یک کار ارزان است، اما اگر 100 یا 1000 کار انجام دهید، هر کدام نشان دهنده کاری است که زمان اجرا باید جداگانه با آن مقابله کند. هر کدام شانس بیشتری برای تحت تأثیر قرار گرفتن با تأخیر زمان‌بندی، نظرسنجی‌های فردی بیشتری که زمان اجرا نیاز دارد، و به طور کلی هزینه‌های اضافی بیشتری ایجاد می‌کند. هنگامی که یک کار را تخم ریزی می کنید، در نظر بگیرید که واقعا چقدر کار را برنامه ریزی می کنید: تخم ریزی یک واحد کار 10 میکروثانیه ای روی کار خودش احتمالا ضد کمک است. ابزارهایی مانند dial9 یا tokio-metrics می توانند به شما در ردیابی چرخه عمر وظایف کمک کنند.

زمان‌بندی زمان اجرا توکیو روی کارگران کار می‌کند: رشته‌های اختصاصی که وظایف آماده را نظرسنجی می‌کنند. کارگران در سراسر هسته ها مقیاس می شوند، اما برخی از منابع زمان اجرا هنوز به هماهنگی مشترک نیاز دارند.

استخر مسدود کننده در حال حاضر یک منبع جهانی است. در نرخ های به اندازه کافی بالا، فشار دادن کار به صف مسدود کردن به یک گلوگاه تبدیل می شود و spawn_blocking می تواند در فلمگراف ها قابل مشاهده باشد. من اثرات منفی عملکرد را در تقریبا 50000 مسدود کردن وظایف در ثانیه در یک میزبان 32 هسته ای دیده ام. مسافت پیموده شده شما متفاوت خواهد بود spawn_blocking یک راه حل جادویی برای هر قطعه مسدود یا کد سنگین CPU نیست. به طور خلاصه، کار محدود، ممکن است سریع‌تر باشد که به کارگران توکیو و کار دزدی اجازه رسیدگی به آن را بدهیم، اما، مثل همیشه، «بستگی دارد».

توکیو همچنین دارای یک صف کار جهانی است. هنگامی که صف‌های کارگر محلی سرریز می‌شوند، که معمولا نادر است، یا زمانی که کار از خارج از یک کارگر زمان اجرا برنامه‌ریزی می‌شود، که می‌تواند در برخی از برنامه‌ها رایج باشد، وظایف به آنجا می‌رسند. یک مثال کانالی است که فرستنده آن روی یک رشته غیر توکیو اجرا می شود.

یکی از ساده‌ترین راه‌ها برای متوقف کردن کل زمان اجرا، مسدود کردن یک کارگر در یک mutex متضاد است.

مواردی مانند رجیستری متریک که در پشت قفل mutex یا read-write ذخیره شده است، به ویژه در معرض این مشکل هستند. اگر یک فلاش قفل را در حین انجام کارهای گران قیمت نگه دارد، هر کارگر توکیو ممکن است در نهایت کاری را برنامه ریزی کند که سعی کند یک متریک را ثبت کند و روی همان قفل مسدود شود. دزدی غیر ممکن می شود زیرا هر کارگری گیر کرده است!

بخش‌های مهم در برنامه‌های غیرهمگام را بسیار کوتاه نگه دارید (مثلا یک به‌روزرسانی هشمپ). RWLock تقریبا هرگز بدوی مناسبی برای استفاده نیستند، زیرا هنوز در مورد اتم ها، حتی برای مسیر خواندن، اختلاف ایجاد می کنند. هنگام شستشو، انجام I/O یا منتظر آینده دیگری، قفل را نگه ندارید. یک ترفند رایج این است که mutex را قفل کنید، سپس داده ها را شبیه سازی کنید. اگر بتوانید یک خواندن قدیمی را تحمل کنید (و نیازی به بازنویسی داده ها ندارید)، این می تواند زمان صرف شده برای نگه داشتن mutex را محدود کند.

tokio::sync::Mutex یک موضوع را با مشکل دیگر مبادله می‌کند: قفل کردن موتکس‌های توکیو بسیار گران‌تر است، در معرض مشکلات ظریفی مانند FutureLock هستند و واقعا تنها زمانی مناسب هستند که بخش بحرانی چندین میلی‌ثانیه طول بکشد.

به جای Mutexes، سایر اصول اولیه همگام سازی مانند کانال ها را در نظر بگیرید. یک کار می تواند داده های مشترک را داشته باشد و پیام هایی که از کانال می آیند نشان دهنده جهش هستند. اینها اغلب به معماری مجدد برنامه شما متکی هستند اما می توانند عملکرد را ساده و بهبود بخشند. برای راهنمای دقیق استفاده از کانال‌ها برای اجرای الگوی بازیگر، به Actors With Tokio، همچنین توسط آلیس رایل مراجعه کنید. حتی زمانی که از یک کانال استفاده می‌کنید، برای حداکثر کارایی باید دسترسی به کانال را استهلاک یا دسته‌ای در نظر بگیرید. خود کانال به منبع جهانی تبدیل می شود.

یک mutex مسدودکننده مدعی هر چهار کارگر زمان اجرا را به طور همزمان متوقف می کند.

توکیو خوشبختانه می تواند وظایف بسیار بیشتری نسبت به بقیه سیستم شما ایجاد کند. باز کردن تصادفی 3000 اتصال همزمان به S3 به دلیل حجم کاری که تعداد نامحدودی از کارها را ایجاد می کند بسیار رایج است.

پاسخ خسته کننده است: همزمانی را محدود کنید. الگوریتم های تطبیقی ​​فانتزی گاهی اوقات مناسب هستند، اما یک Semaphore اغلب کافی است.

طراحی توکیو به کارگرانی که به سرعت بیدار می شوند متکی است. با این حال، اگر سیستم عامل بسیار بارگذاری شده باشد، ممکن است 10 تا 20 میلی ثانیه یا بیشتر طول بکشد تا زمانی که هسته یک کارگر را پس از تلاش توکیو برای بیدار کردن آن برنامه ریزی کند. اگر تاخیر P99 را در میلی ثانیه تک رقمی اندازه گیری کنید، این یک فاجعه است. من این را در طول مهاجرت‌های تدریجی از جاوا به Rust در آمازون مشاهده کرده‌ام، جایی که هر دو فرآیند روی یک میزبان اجرا می‌شدند و فرآیند Rust به تدریج کار بیشتری را به عهده گرفت.

هرچه فرآیند جاوا کار کمتری انجام دهد، روند Rust سریع‌تر می‌شود، حتی اگر کار بیشتری انجام دهد. این اثر زمانی قوی تر می شود که سایر برنامه ها از تعداد زیادی نخ استفاده می کنند.

ابتدایی ترین راه حل استفاده از cgroup ها یا API های مرتبط برای پین کردن کارگران توکیو و سایر کدها برای جداسازی هسته های CPU است.

همین موضوع می تواند از سایر رشته های Rust نیز ناشی شود. رشته‌های پس‌زمینه مانند مواردی که توسط tracing_appender استفاده می‌شوند، گاهی اوقات می‌توانند بیش از 100 میلی‌ثانیه کار را بدون خروج از CPU انجام دهند. اگر توکیو تلاش کند کارگری را در این مدت بیدار کند، آن کارگر ممکن است تا زمانی که هسته از رشته دیگر جلوگیری کند به تعویق بیفتد.

اگر می بینید که این اتفاق می افتد، راه حل یکسان است: کار پس زمینه غیر بحرانی را به هسته خودش پین کنید و کارگران توکیو را به هسته های دیگر منتقل کنید. شما به ندرت به هر هسته برای توکیو نیاز دارید، و رزرو هسته ها برای کارهای دیگر باعث بهبود تاخیر می شود.

الگوهای این بخش عموما کار درستی نیستند، اما گاهی اوقات دقیقا همان چیزی هستند که یک حجم کاری به آن نیاز دارد.

در یک برنامه ناهمگام ایده‌آل، همه کارها در فواصل زمانی کوچک و با بازدهی مکرر به توکیو انجام می‌شود. دنیای واقعی همیشه اینطور کار نمی کند و انفجارهای کوچک لزوما سریعترین راه برای اجرای نرم افزار نیستند. کار دسته بندی می تواند کارآمدتر باشد.

در عمل، نظرسنجی های طولانی همیشه مشکل ساز نیستند. تحت بار سبک، دزدی کار توکیو می تواند جبران کند زمانی که یک کارگر بیش از حد معمول مشغول باشد. که تحت دو شرط شروع به خراب شدن می کند:

در هر دو مورد، کار دزدی بیشتر طول می کشد. اگر کار با سرعت کافی به سرقت نرود، تعمیر و نگهداری در زمان اجرا اصلی - مانند رانندگی I/O - ممکن است به اندازه کافی برای حفظ تاخیر کم اتفاق نیفتد.

نکته مهم! اگر از مواردی مانند tokio::join استفاده می کنید، این توصیه اعمال نمی شود! و توکیو::انتخاب کنید! که از همزمانی درون کار استفاده می کنند. در یک کار واحد، دزدی کار وجود ندارد. اگر اجرا کننده را مسدود کنید، هیچ چیز دیگری که روی آن کار اجرا می شود نمی تواند پیشرفت کند. این گاهی اوقات به صورت وقفه های غیرمنتظره و به طور کلی تاخیر بد ظاهر می شود.

قوی ترین انزوا از تخصیص کار به زمان های اجرا جداگانه و سنجاق آن زمان های اجرا به هسته های اختصاصی ناشی می شود. بسیاری از سرویس های شبکه هم کار حساس به تأخیر دارند و هم کارهای پس زمینه با اولویت پایین. قرار دادن آنها در زمان های اجرا جداگانه، یک مرز زمان بندی بین این دو ایجاد می کند.

همچنین می توانید هنگام شروع رشته های زمان اجرا، زیبایی سطح سیستم عامل را تنظیم کنید. به مثال چندباره dial9 و قلاب on_thread_start توکیو مراجعه کنید.

در TokioConf تصور کلی از بیشتر صحبت‌ها این است که مردم در نهایت به راه‌حلی با حداقل دو زمان اجرا رفتند.

این یک تاکتیک بسیار پیشرفته برای تعقیب تاخیر اندازه گیری شده در میکروثانیه است. من توصیه نمی کنم ابتدا به این موضوع برسید، اما قطعا می تواند کارساز باشد.

هر بار که به زمان‌بندی توکیو تسلیم می‌شوید - یا توکیو نخ کارگری را پارک می‌کند و آن را به سیستم عامل تحویل می‌دهد - فرصتی ایجاد می‌کنید که آن کار پس از بیدار شدن دوباره به تأخیر بیفتد.

برای کارهای بسیار حساس به تأخیر، یک گزینه این است که عمدا برای یک دوره از پیش تعیین شده کوتاه، شاید 50 میکروثانیه، به جای تسلیم شدن در حین انتظار برای کار مفید بعدی، بچرخید. این یک هسته را مصرف می کند و می تواند به بارهای کاری مجاور آسیب برساند، بنابراین احتمالا برای اکثر برنامه ها اشتباه است. با این حال، تحت شرایط به دقت کنترل شده، می تواند معامله مناسبی باشد.

Tokio 1.52.0 برای مدت کوتاهی یک صف مسدود کننده خرد شده ارسال کرد، اما 1.52.1 آن را پس از یک رگرسیون که می‌توانست باعث قطع شدن spawn_blocking شود، برگرداند. Tokio PR #8337 بعدا صف تکه تکه شده را به عنوان یک ویژگی ناپایدار که به طور پیش فرض غیرفعال است، دوباره فرود آورد.

خواندن متن کامل در هکرنیوزبه زبان اصلی، در سایت ناشر باز می‌شود
متن اصلی (انگلیسی)

Principles for Fast Tokio Applications

Article URL: https://dial9-rs.github.io/blog/principles-for-fast-tokio-applications/ Comments URL: https://news.ycombinator.com/item?id=49698607 Points: 238 # Comments: 62

همه‌ی اخبار فناوری