نقشهٔ بلادرنگ وابستگی سرویسها در نتفلیکس؛ گزارش یک نبرد با مقیاس
خلاصهٔ کاملتر
نویسندههای وبلاگ فنی نتفلیکس تو این پست سراغ بخش «چطور» ساخت نقشهٔ بلادرنگ وابستگی سرویسها رفتن. جملهٔ صادقانهای که اول متن اومده لحن کل مقاله رو مشخص میکنه: نسخهٔ اول تو محیط لوکال بینقص کار میکرد، ولی پروداکشن داستان دیگهای بود — مصرفکنندههای کافکا عقب افتادن، اینستنسها حافظه کم آوردن، بعضی نودها ۱۰۰ برابر بقیه ترافیک گرفتن و مکثهای garbage collection بیشتر از خود منطق کسبوکار CPU مصرف کرد.
تصمیم معماری اصلی، جریانی بودن بهجای دستهای بود. سیستمهای سنتی توپولوژی، داده رو ساعتی یا روزانه تجمیع میکنن؛ ولی به قول نویسندهها، ساعت سه صبح و وسط یه حادثه، نقشهٔ وابستگیِ یکساعتپیش دیگه observability نیست، باستانشناسیه. بهجاش دادهها بهصورت پیوسته از کافکای چندمنطقهای و متریکهای IPC خونده و از پایپلاینهای reactive رد میشن.
برای اینکه این حجم داده سیستم رو نترکونه، از backpressure استفاده شده: وقتی مرحلهٔ سوم نمیتونه بهاندازهٔ کافی سریع روی دیتابیس گراف بنویسه، به مرحلهٔ دوم علامت میده کندتر بشه، اون به مرحلهٔ اول، و در نهایت مصرفکنندهٔ کافکا مکث میکنه. داده تو کافکا منتظر میمونه تا ظرفیت برگرده. مزیتش نسبت به صف بیکران (که حافظه رو تموم میکنه) یا دور ریختن داده (که توپولوژی رو ناقص میکنه) روشنه، ولی هزینهش پیچیدگیه.
قلب سیستم یه پایپلاین تجمیع سهمرحلهایه. مسئله اینه که لاگهای جریان شبکه فقط تکتک هاپها رو نشون میدن، نه ارتباط منطقی سرویسها: «اپ A به لودبالانسر» و «لودبالانسر به اپ B» دو جریان جدا دیده میشن، در حالی که مهندس «A به B» رو لازم داره. مرحلهٔ اول تجمیع اولیه در پنجرههای ۵ دقیقهای میکنه، مرحلهٔ دوم جریانها رو بر اساس شناسهٔ واسطه بازتوزیع و یال مستقیم رو استخراج میکنه، و مرحلهٔ سوم تجمیع نهایی، غنیسازی و نوشتن روی دیتابیس گراف رو انجام میده — همون الگوی کلاسیک map-reduce.
چرا سه مرحله و نه دو تا؟ نسخهٔ دومرحلهای تو تست کار کرد و تو مقیاس واقعی شکست. حل واسطهها نیاز داره همهٔ جریانهای یه لودبالانسر روی یه اینستنس جمع بشن، و برای سرویسهای پرترافیک همین باعث میشد اون اینستنس داغ کنه — بدتر اینکه همون نود شلوغ بیشترین I/O غنیسازی رو هم داشت. جدا کردن حل واسطه (سنگین از نظر CPU) از غنیسازی و ذخیرهسازی (سنگین از نظر I/O) مشکل نودهای داغ رو هم حل کرد؛ درس کلیاش اینه که تصمیم معماری برای یه نیاز، گاهی مشکل دیگهای رو هم حل میکنه.
یکی از تصمیمهای خلافجریانشون کنار گذاشتن gRPC بود. برای ارتباط بین مراحل، سربار سریالسازی، مدیریت connection pool و فشار حافظهٔ gRPC بیشتر از خود منطق کسبوکار CPU میخورد. جایگزینش Server-Sent Events شد: پروتکل سبک مبتنی بر HTTP با سریالسازی حداقلی که بهطور طبیعی با backpressure جور درمیآد. درسی که ازش میگیرن اینه که «برای ارتباط بین سرویسها از gRPC استفاده کن» یه قاعدهٔ جهانشمول نیست — اندازه بگیر، فرض نکن.
برای توزیع بار هم از consistent hashing با کشف پویای اینستنسها استفاده میکنن: هر اینستنس فهرست نودهای سالم رو از رجیستری سرویس میگیره و مرتب نگه میداره، پس وقتی Auto Scaling Group بزرگ یا کوچیک میشه، تابع هش خودش بازتوزیع میکنه و نیازی به پروتکل هماهنگی جداگانه نیست. نکتهٔ هوشمندانه اینه که از زیرساخت موجود (رجیستریای که برای health check هست) دوباره استفاده کردن.
بخش حافظه و GC شاید جسورانهترین قسمت گزارشه. اجسام زیادی تا کاملشدن پنجرهٔ ۵ دقیقهای تو هیپ میموندن و سبک متداول Scala یعنی ساختارهای immutable باعث میشد هر بهروزرسانی یه شیء تازه بسازه — با میلیونها رکورد در ثانیه، فاجعه. تیم آگاهانه روی مسیر داغ سراغ ساختارهای mutable رفت (بقیهٔ جاها immutable موند) و تخصیص هیپ بیشتر از ۵۰٪ کم شد. جمعبندی خودشون: best practiceها نقطهٔ شروعن نه قانون مطلق، ولی انحراف ازشون باید عمدی و با اندازهگیری باشه.
نکات کلیدی:
- معماری جریانی بهجای دستهای، چون نقشهٔ وابستگی کهنه وسط حادثه بهدرد نمیخوره
- backpressure باعث میشه سیستم زیر فشار کند بشه، نه اینکه داده بریزه یا بترکه
- پایپلاین سهمرحلهای واسطههای شبکه رو حل میکنه و همزمان جلوی نودهای داغ رو میگیره
- جایگزینی gRPC با SSE مصرف منابع رو بهشکل چشمگیری کم کرد
- consistent hashing پویا با رجیستری سرویس، بازتوزیع خودکار هنگام اسکیل رو ممکن کرد
- استفاده از ساختارهای mutable روی مسیر داغ، تخصیص هیپ رو بیش از ۵۰٪ کاهش داد
- reactive stream قدرتمنده ولی دیباگ سخت و منحنی یادگیری تندی داره




