روزبه شریف‌نسب
روزبه شریف‌نسب
خواندن ۳۷ دقیقه·۲ سال پیش

بررسی معماری چند نرم‌افزار موفق: توییتر، ماشین مجازی جاوا، Nginx و توییچ

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

در این لیست هم نرم‌افزار‌های تحت وب با میلیون‌ها کاربر روزانه وجود دارد هم نرم‌افزاری که ۳ میلیارد دستگاه آن را اجرا می‌کنند. (بله ماشین مجازی جاوا)


توییتر

توییتر یکی از بزرگ‌ترین شبکه‌های اجتماعی در دنیاست که حدود ۴۰۰ ملیون کاربر دارد و از این بین، ۲۰۰ ملیون از آن‌ها کاربر ماهانه هستند، چیزی حدود ۳ برابر جمعیت ایران! این کاربران نیز در سراسر دنیا پخش شده‌اند و جدا از ایالات متحده، کاربران زیادی نیز ژاپن، هند، انگلستان، برزیل و ... دارد.

به عنوان یک شبکه اجتماعی مطرح در جهان، امنیت و پایداری زیادی هم دارد. یک جوک اینترنتی هست که می‌گوید «وقتی هر برنامه‌ای کار نمی‌کند در توییتر به هم می‌گوییم اما وقتی توییتر از کار می‌افتد کجا باید برویم؟».

توییتر به شکل سنتی قابلیت ارسال پیام متنی با محدودیت ۱۴۰ کارکتر را داشت، اما به مرور این محدودیت در حال کمتر شدن است، برای مثال در سال ۲۰۱۷ این محدودیت دوبرابر شد و به ۲۸۰ کارکتر رسید. امروز که در حال نوشتن این مطلب هستم نیز خبرهایی از توییت‌های طولانی‌تر ۴۰۰۰ کارکتری به گوش می‌رسد. البته این قابلیت هنوز در همه‌ی کشورها فعال نیست. از قابلیت‌های دیگر توییتر می‌توان به ارسال تعدادی تصویر و ویدیوهای کوتاه و نظرسنجی و امکان دریافت و ارسال پیام خصوصی اشاره کرد.

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

معماری کلی (بر اساس حدس‌ها)

پس از بررسی‌های مختلفی که روی معماری توییتر انجام شد، به یک تصویر کلی قابل استناد نرسیدیم، چرا که در مطلب‌های متفاوت و در گوشه‌‌گوشه‌ی اینترنت، مطالبی جزئی در مورد «نحوه حل یک مشکل» یا «نحوه‌ی کار یک قسمت» به چشم می‌خورد اما همه‌ی اینها کنار هم چه شکلی می‌شوند؟ دو تصویر رسمی مربوط به سال‌های ۲۰۱۲ و ۲۰۲۲ منتشر شده‌اند که ادعا می‌کنند «تصویر کلی توییتر» هستند اما هیچ کدام تصویر کاملی ارائه نمی‌دهند.

تصویری که ادعا می‌کند معماری توییتر در سال ۲۰۱۲ است اما بیشتر به روال «ارسال توییت» میپردازد


تصویری که ادعا می‌شود مربوط به معماری توییتر در سال ۲۰۲۲ است:

این تصویر نیز بیشتر به روند ساخت تایملاین اشاره دارد و همچنان تصویر کلی‌ای به ما نمی‌دهد.

بنابراین و بنا به این محدودیت‌ها، به جای استناد به موارد منتشرشده‌ی رسمی، خودمان حدس می‌زنیم. برای کمک گرفتن هم از یک مطلب استفاده شد. اینگونه مطالب معمولا با دید «سوالات مصاحبه‌ برای system design» پاسخ داده می‌شوند که از نیاز ما هم چندان دور نیست.

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

علاوه بر نیازمندی‌های کارکردی که در بخش مقدمه ذکر شد، برخی نیازمندی‌های غیرکارکردی نیز برای این قسمت مطرح شده‌اند:

  • دسترسی‌پذیری بالای سرورها
  • پرفورمنس مناسب: حداکثر در نیم ثانیه تایم‌لاین تولید شود.
  • امکان اسکیل سیستم متناسب با تعداد کاربران

در این طراحی (که قطعا تنها طراحی ممکن هم نیست)، معماری اصلی توییتر به چند سرویس شکسته شده است. این سرویس‌ها عبارت‌اند از:

  • سرویس ارسال توییت
  • سرویس ارسال توییت به سرچ و تایم‌لاین (فن اوت)
  • سرویس تایملاین مستقیم کاربر
  • سرویس تایم‌لاین الگوریتمی
  • سرویس گراف روابط (دنبال کننده و دنبال شونده)
  • سرویس جست‌و‌جو


چیزی که در تصویر بالا می‌بینیم برگرفته از وبسایت interviewnoodle است که منبع این حدس از ساختار توییتر است. در اینجا سرویس‌های اصلی مشخص‌شده اند و یک دیتابیس و کش یکپارچه نیز متصور شده است. البته در عمل باید این دیتابیس جزئیات بیشتری مثل sharding داشته باشد تا بتواند از پس چنین حجم کاربرانی بر بیاید. سرویس fanout که در اینجا اشاره شده نیز قرار است همانطور که گفته شد. همه توییت‌های ارسالی را به سرویس‌های تایم‌لاین و سرچ ارسال کند.

در نهایت در این تصویر می‌بینیم که کاربر (یا کلاینت کاربر) به شکل مستقیم با سرویس‌های مختلف در ارتباط است که شاید در عمل میسر نباشد و به جای ارتباط مستقیم، استفاده از از یک API gateway مناسب تر باشد، چرا که مزایایی مثل ورژن زدن برای api و امکان دیپلوی بدون داون تایم را به ما می‌دهد.


معماری بر اساس شواهد

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

معماری کلی

در صحبت‌های مقامات رسمی (مثلا ایلان ماسک رئیس جدید توییتر) صحبت از میکروسرویس می‌شود و حتی در مرحله‌ای چندین میکروسرویس خاموش شدند، بنابراین می‌توان با قطعیت گفت که توییتر از میکروسرویس استفاده می‌کند. چند میکروسرویس که می‌دانیم وجود دارند عبارت‌اند از «تایید هویت دومرحله‌ای» و «ساخت ترکیب تایم‌لاین الگوریتمی» و «اضافه کننده‌ی تبلیغات به تایم لاین» (بر اساس معماری منتشرشده‌ی توییتر در سال ۲۰۲۲)

دیپلوی

بر اساس شواهدی مثل تغییرات سریع کد حتی با کمبود برنامه‌نویسان می‌توان گفت که قطعا یک سیستم CI/CD مناسب وجود دارد که عملیات deploy را نیز به شکل مناسب انجام می‌دهد. اساس این صحبت آن است که در ماه‌های اخیر با کاهش تعداد برنامه‌نویسان، همچنان امکانات جدیدی به برنامه اضافه می‌شود و دیپلوی می‌شوند و بدون مشاهده‌ی مشکل خاصی از سمت کاربر این اتفاقات می‌افتد.

همچنین نکته‌ی دیگری که در زمینه‌ی استقرار وجود دارد این است که در گذشته تا کنون، همواره به شکل گسترده و طولانی از تست‌های استقرار جزئی استفاده می‌کرده است. برای مثال در روزهای اخیر امکان توییت ۴۰۰۰ کلمه‌ای به توییتر برخی کاربران در ایالات متحده اضافه شده که می‌توان گفت از نوع تست قناری است و خود تیم توییتر قصد دارد امکان‌سنجی فنی و عیب‌یابی کند. در گذشته نیز امکاناتی مثل ادیت پیام، ویدیو‌های طولانی‌تر و اسپیس و مخصوصا امکان فلیت به شکل محدود برای برخی کاربران عرضه شده بودند تا واکنش کاربران را بسنجند. (a/b testing) حتی گاها این روال به سال‌ها می‌رسید و برخی قابلیت‌ها نیز واقعا حذف شدند، مثلا همین فلیت مدت زیادی در حال تست بود و پس از مدت طولانی‌ای نیز با اینکه هیچگونه مشکلی از نظر فنی و حتی تجربه کاربری نداشت، حذف شد. یکی از انتقادات ایلان ماسک نیز به همین روند کند عرضه قابلیت‌های جدید بود.

زبان‌های مورد استفاده

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

نکته جالبی که وجود دارد این است که بین اسکالا و جاوا، کارمندانی که قبلا روبی کار کرده‌اند ترجیح می‌دهند اسکالا را انتخاب کنند و کارمندانی که قبلا با سی و سی‌پلاس‌پلاس کار کرده‌اند ترجیحاشان خود جاوا است.

همچنین لازم به ذکر است که استفاده از زبان‌های استاتیک‌تایپ برای توییتر مزیت داشته و گفته می‌شود بهره‌وری برنامه‌نویسان را افزایش داده است.


لاگ زدن

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

در گذشته از سرویس LogLens که محصول خود توییتر است استفاده می‌شد،‌ تلاش شده بود این سیستم تا حد امکان ساده باشد ولی بعد از مدتی این سادگی به قیمت امکانات کم تمام شد و باعث استفاده از سیستم دیگری شد.

معماری سیستم سنتی مانند تصویر زیر است:

روند کار به این شکل بود که لاگ‌ها در هر کانتینر در سیستم Scribe نوشته می‌شدند، سپس وارد کافکا شده و بعد توسط اندیس‌گذار‌های LogLens بررسی و اندیس‌گذاری می‌شدند. در نهایت هم برای ذخیره‌سازی از یک HDFS استفاده می‌شد. اما مشکلی که وجود داشت این بود که ظرفیت پایین این سیستم، باعث ایجاد گلوگاهی برای ورود لاگ‌ها می‌شد و به ناچار با تنظیم rate limiter‌هایی، ۹۰ درصد لاگ ها دور ریخته می‌شد!

در معماری جدید با کمک Splunk، این مشکل مرتفع شد. با امکانات بیشتر و ظرفیتی که وجود داشت نه تنها می‌شد لاگ‌ها دور ریخته نشوند، بلکه می‌شد لاگ‌های امکانات سخت‌افزاری مثل اجزای شبکه نیز بررسی و مدیریت شود. اینبار به جای Scribe، یک Forwarder در سرورها نصب شد که لاگ‌ها را به جای مناسبی فوروارد کند. در ادامه نیز مانند تصویر زیر، لاگ‌ها به کمک Splunk جمع‌آوری و اندیس‌گذاری می‌شوند.

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

نگاشت به منابع سخت‌افزاری

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

همانطور که در تصویر مشخص است، دیتابیس بیشترین تعداد سرورها را دارد و در رتبه‌های بعدی ذخیره‌سازی کلید-مقداری و هادوپ هستند.

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

  • راه میانی و تغییر موقت در کار نیست، بلکه هر کدام یک بدهی فنی هستند.
  • با کمک متریک‌ها، تصمیمات طراحی مناسبی بگیرید. همچنین مطمئن شوید که ذی‌النفعان دیگر مخصوصا در محیط‌های ابری نیز آن‌ها را خوب درک کنند.

خرید توسط ایلان ماسک

از زمانی که اقدام به جمع‌آوری اطلاعات و شروع این پروژه کردیم، متاسفانه توییتر با چالش‌هایی همراه شد که البته به درک بیشتر ما از معماری آن کمک کرد. شروع همه‌ی اتفاقات این بود که توییتر پس از مذاکراتی توسط یکی از ثروتمند‌ترین افراد دنیا خریداری شد. پس از خرید به سرعت سیاست‌های مختلف شرکت تغییر کرد. از امکان دورکاری که پیش‌تر برای همه کارکنان به رسمیت شناخته شده بود تا هزینه‌ی سرور‌ها و چابکی ارائه تغییرات.

چالش تعیین قیمت: قیمت‌گذاری روی یک نرم‌افزار با زیرساخت بزرگ، تعداد زیاد کاربران و محتوا و کد منبع بزرگ، قطعا نیاز به کار تخصصی دارد. (Due diligence). از طرفی با تحقیق روی زمینه‌ای که خریدار بیشترین چانه‌زنی را می‌کند شاید بتوان به هدف خرید نرم‌افزار پی برد. در این مثال، در بحبوحه‌ی مطرح شدن خرید شایعات خرید توییتر توسط آقای ماسک، خبرهایی روی چانه‌زنی‌ها و قیمت‌های مختلف هم مطرح شد. در نهایت ماسک به دلیل آمارهایی که مبنی بر وجود اکانت‌هایی که (به شکل غیرمجاز) توسط ربات‌ها کنترل می‌شوند توانست قیمت را پایین بیاورد و صحبتی از کدبیس نشد.

خاموش کردن سرورها: شاید عنوان این قسمت خنده‌دار به نظر بیاید ولی واقعا این اتفاق افتاد و تجربه کاربری را نیز تحت تاثیر قرار داد. علت این ماجرا این بود که ایلان ماسک به عنوان یک مهندس(!) و بدون گرفتن مشاوره از مهندسان ارشد این شرکت، تشخیص داد که میکروسرویس‌های اضافی زیادی در حال اجرا روی سرورها هستند و تصمیم گرفت که بسیاری از آن‌ها را به همراه سرور‌هایشان خاموش کند. نتیجه البته قابل توجه بود. برخلاف انتظار بسیاری از کاربران و صاحب‌نظران، توییتر با وجود دشواری‌ها همچنان پایداری نسبی خودش را حفظ کرد. به شکل دقیق‌تر میکروسرویس‌هایی که هنوز روشن بودند به حالت degraded mode رفتند ولی همچنان سیستم در دسترس بود و «اکثر خدمات خود را ارائه می‌داد». برای مثال در مدت زمانی مشخص لاگین دو-مرحله‌ای کار نمی‌کرد ولی به شکل مناسب اخطار به کاربر داده می‌شد و کاربرانی که لاگین بودند هم می‌توانستند به شکل مناسب از سیستم استفاده کنند. یا به دلیل بالا‌ رفتن لود روی سرور‌ها برخی قابلیت‌ها از کار افتادند برای مثال «نمایشگر تعداد ناتیفیکیشن» ولی همچنان سرویس‌های اصلی پایدار بودند. این «در دسترس بودن بالا» و تطبیق‌پذیری بسیاری را شگفت‌زده کرد.

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

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

منابع

نگاشت نرم‌افزار به سخت‌افزار

https://blog.twitter.com/engineering/en_us/topics/infrastructure/2017/the-infrastructure-behind-twitter-scale

لاگ‌ها در توییتر

https://blog.twitter.com/engineering/en_us/topics/infrastructure/2021/logging-at-twitter-updated

زبان‌های مورد استفاده در توییتر

https://readwrite.com/twitter-java-scala/

یک حدس از طراحی سیستمی مشابه توییتر

https://interviewnoodle.com/twitter-system-architecture-8dafce16aec4

مشکلات شرکت انسولین سازی با تیک آبی:

https://www.republicworld.com/business-news/international-business/us-pharma-giant-eli-lilly-sheds-billions-because-of-impostors-with-fake-twitter-blue-tick-articleshow.html

گزارش‌هایی از افت تجربه کاربری:

https://twitter.com/adambroach/status/1591120030887878656

خرید توییتر توسط ایلان ماسک:

https://en.wikipedia.org/wiki/Acquisition_of_Twitter_by_Elon_Musk

https://edition.cnn.com/2022/04/25/tech/elon-musk-twitter-sale-agreement/index.html

آمارهای توییتر:

https://backlinko.com/twitter-users#twitter-users

افزایش تعداد کارکترهای توییت

https://www.indy100.com/science-tech/twitter-blue-tweet-character-limit

ماشین مجازی جاوا (JVM)

معرفی JVM

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


معماری JVM

اگر درشت‌دانه به معماری JVM نگاه کنیم از سه بخش اصلی class loader ، runtime data area و execution engine تشکیل شده است که در ادامه به بررسی هر بخش می‌پردازیم. نمای کلی این معماری در تصویر زیر قابل مشاهده است:

معرفی بارگذار کلاس (class loader) در JVM

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

الف ) بارگذاری یا loading : در این بخش بایت کد کلاس‌ها و اینترفیس‌ها گرفته می‌شود و کلاس یا اینترفیس اصلی از روی آن بازسازی می‌شود. متد classLoader.loadClass از توابع اصلی این بخش است که یک کلاس را در حافظه اصلی لود میکند و یکی از 2 اکسپشن NoClassDefFoundError و یا ClassNotFoundException در صورت بروز خطا هنگام لود شدن کلاس‌ها یا فرزندانشان در حافظه پرتاب میشوند. در جاوا سه نوع بارگذار داخلی داریم که عبارتند از :

  • Bootstrap class loader
  • Extension class loader
  • Application class loader

در بخش اول که ریشه‌ی بارگذاران کلاس است، پکیج‌های استاندارد جاوا مثل util، lang، io و ... بارگیری می‌شوند .(موجود در پوشه‌ی JAVA_HOME/jre/lib) سپس extension class loader وارد کار می‌شود که از bootstrap class loader ارث بری میکند و خود سوپر کلاس applocation class loader است. در این بخش افزونه‌های کتابخانه‌های استاندارد زبان جاوا بارگیری می‌شوند. (موجود در پوشه‌ی JAVA_HOME/jre/lib/ext) و در اخر application class loader فایل‌های موجود در پوشه‌ی اپلیکیشن یا classpath را بارگذاری می‌کند.

ب )پیوند یا linking : برای اینکه مروری کنیم در کجا قرار داریم، ابتدا گفتیم JVM در معماری خود 3 بخش اصلی داشت که به سراغ بخش اول یعنی بارگذار کلاس رفتیم، سپس دیدیم در بارگذار کلاس 3مرحله اصلی داریم که عبارتند از بارگذاری، پیوند و مقدار دهی اولیه. همانطور که از تصویر پیداست در این بخش نیز 3 مرحله اصلی خواهیم داشت.

    • تایید یا Verify     • آماده سازی یا Prepare     • حل وفصل یا resolve
• تایید یا Verify • آماده سازی یا Prepare • حل وفصل یا resolve
  • تایید یا Verify
  • آماده سازی یا Prepare
  • حل وفصل یا resolve

در این بخش ابتدا ساختار فایل .class طبق مجموعه قوانین بررسی می‌شود و اگر تایید شد به مرحله بعدی می‌رود در غیر این صورت یک اکسپشن VerifyException پرتاب می‌شود. مثلا تطابق ورژن‌ها یکی از مواردی است که بایستی تایید شود. برای مثال اگر از یک ساختار نحوی که در جاوای 11 ارائه شده استفاده شده باشد ولی جاوای سیستم ما 8 باشد در مرحله‌ی تایید با این اکسپشن مواجه خواهیم شد. سپس درمرحله‌ی آماده سازی JVM برای فیلدهای استاتیک یک کلاس یا اینترفیس حافظه اختصاص می‌دهد و آنها را با مقادیر پیش‌فرض مقداردهی اولیه می کند (متغییر استاتیک در جاوا با کلیدواژه‌ی static مشخص می‌شود.). در نهایت در اخرین مرحله‌ی پیوند یعنی وضوح یا Resolution، مراجع نمادین با مراجع مستقیم موجود در مخزن زمان اجرا جایگزین می‌شوند. برای مثال اگر در کد شما 2 کلاس وجود داشته باشد که در یکی ارجاعی از دیگری وجود دارد در این مرحله ارجاعات با مقدار حقیقیشان جایگزین می‌شوند. برای همین به آن وضوح میگویند. در اینجا پیوند یا linking تمام می‌شود.

ج) مقدار دهی اولیه یا initialization : در نهایت در آخرین بخش بارگذار کلاس، بخش مقدار دهی اولیه یا Initialization به چشم میخورد که شامل اجرای متد مقداردهی اولیه کلاس یا اینترفیس است که اگر با زبان جاوا آشنایی داشته باشید حالت های مختلفی دارد مثلا سازنده کلاس ( constructor) و یا بلوک های استاتیک اجرا می‌شوند و مقادیر متغیرهای استاتیک هم به آنها داده می‌شود. احتمالا به این فکر میکنید که مقداردهی متغیرهای استاتیک که انجام شده بود! اینجا من هم برای بار اول کمی گیج شدم و وقتی بیشتر جست و جو کردم متوجه تفاوت مهم شدم. فرض کنید یک متغیر استاتیک به شکل زیر در برنامه دارید:

static boolean visited = true;

ابتدا در مرحله‌ی پیوند بخش آماده سازی(preparation) به این متغیر حافظه اختصاص داده می‌شود و مقدار پیشفرض به آن داده میشود. یعنی در یک جای حافظه به متغیر visited مقدار false داده شده که پیشفرض برای boolean است. حال در مرحله‌ی اخر یعنی مقداردهی، مقدار true به این متغیر داده می‌شود.

به این ترتیب به پایان بخش class loader میرسیم.


معرفی بخش منطقه‌ی داده‌های زمان اجرا یا Runtime Data Area

در این بخش پنج مولفه‌ی اصلی داریم که در تصویر زیر قابل مشاهده اند و در ادامه به شرح انها می‌پردازیم:

بهتر است این بخش را با یک مثال پیش ببریم. تکه کد زیر را در نظر بگیرید:


الف ) بخش Method Area یا ناحیه متد، هنگام راه اندازی ماشین مجازی ایجاد می شود و تنها یک ناحیه متد برای هر JVM وجود دارد. داده های سطح کلاس در این بخش ذخیره می‌شوند. برای مثال فیلدها.

در مرحله‌ی اول از کلاس دانشجو 2 فیلد اسم و شناسه در Method Area ذخیره می‌شوند.

ب) ناحیه‌ی هیپ (Heap) مخصوص ذخیره سازی تمام اشیا و نمونه‌های مربوط به آنهاست. همان طور که میدانید با کلید واژه ی new در جاوا یک نمونه ایجاد می‌شود.

Student s = new Student();

در اینجا نمونه ی s ساخته شده در هیپ ذخیره می‌شود. مانند ناحیه متد، هیپ هم هنگام راه اندازی ساخته میشود و فقط یک ناحیه هیپ برای JVM وجود دارد.

ج) متناظر با هر thread ساخته شده در JVM یک ناحیه پشته یا stack area ایجاد می شود که مختص زمان اجراست و تمامی متغیرهای محلی، فراخوانی توابع و نتایجشان در این بخش ذخیره می‌شود. احتمالا هنگام کد زدن با اکسپشن StackOverflowError مواجه شده باشید که وقتی پرتاب می‌شود که این پشته پر شده باشد و فضایی برای ذخیره مابقی اطلاعات باقی مانده نداشته باشید. در واقع پشته‌ی بزرگتری نیاز باشد.

نکته: برای هر فراخوانی متد یک stack frame روی پشته گفته شده تشکیل می‌شود که به 3 بخش اصلی تقسیم می‌شود: متغییرهای محلی، operand stack و frame data

متغیرهای محلی همانطور که احتمالا آشنا هستید داخل بدنه‌ی تابع به وجود می‌ایند و عمرشان پس از پایان تابع به پایان می‌رسد. در هر فریم پشته، یک پشته با قانون LIFO داریم که به انجام عملیات کمک می‌کند. در نهایت بخش فریم داده، تمام نمادهای مربوط به متد و اطلاعات بلوک catch را در صورت پرتاب شدن اکسپشن ذخیره می‌کند.

د) شمارنده برنامه یا program counter که به اختصار pc گفته می‌شود. همانطور که میدانید جاوا از برنامه نویسی multi-thread هم پشتیبانی می‌کند. هر thread دارای یک PC Register مخصوص خودش را دارد و ادرس خطی از برنامه که درحال اجراست را نگه داری میکند و بعد از اتمام آن دستور ادرس دستور بعدی را نشان میدهد.

هـ) در نهایت در آخرین کامپوننت یعنی Native Method Stack از متدهای بومی پشتیبانی می‌شود که به زبان‌هایی غیر از جاوا نوشته شده اند(مثلا خانواده‌ی c)

تا اینجا 2 بخش از 3 بخش اصلی معماری JVM یعنی بارگذار کلاس و ناحیه‌ی داده‌های زمان اجرا را بررسی کردیم. در نهایت به بخش آخر یعنی Execution Engine می‌رسیم.

معرفی موتور اجرا یا execution engine

بعد از اتمام مراحل قبلی، نوبت به اجرای برنامه می‌رسد. موتور اجرا این کار را با اجرای کدهای هر کلاس انجام می‌دهد. در این بخش از معماری 3 کامپوننت اصلی داریم که در تصویر زیر قابل مشاهده اند:

الف ) مفسر یا interpreter : در این بخش مفسر دستورات را خط به خط میخواند و اجرا می‌کند. (بعضی از زبان ها کامپایلر دارند برخی مفسر جاوا به گونه ای طراحی شده که ترکیبی از هردو را برای اجرا دارد تا شعار write once run anywhere اتفاق بیفتد.) 2 ایراد اصلی در این بخش وجود دارد که یکی سرعت پایین مفسر به علت خط به خط خواندن و اجرا کردن است و ایراد دوم این است که اگر یک متد چند بار فراخوانی شده باشد هربار باید تفسیر شود و مجددا باعث اتلاف زمان و انرژی می‌شود.

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

ج) در اخرین کامپوننت از این بخش زباله روب یا garbage collector را داریم که مسئولیت پاک سازی پشته از اشیاء مرده را دارد و کمک میکند جاوا کارامد شود. علاوه بر بهبود سرعت و سبک کردن کمک میکند تا فضای جدید برای اشیاء جدید ایجاد شود. دو مرحله‌ی اصلی در این بخش وجود دارند:

  • علامت گذاری : شناسایی اشیاء استفاده نشده در حافظه
  • جارو زدن: اشیائی که مرحله قبل شناسایی شد در این مرحله حذف می‌شوند.

در بخش نکات تکمیلی در باره‌ی مدل زباله روب بیشتر توضیح داده‌شده است.

تا اینجا 3 بخش اصلی معماری JVM را با یکدیگر مرور کردیم و کامپوننت های اصلی آنهارا یک به یک بررسی کردیم.

در نهایت اگر به تصویر اول یعنی نمای درشتگانه‌ی JVM برگردیم 2 کامپوننت دیگر یعنی JNI و کتابخانه‌های متدهای بومی را میبینیم.

همانطور که قبل تر اشاره شد، گاهی لازم است از زبان‌هایی غیر از جاوا در برنامه استفاده شود. مثلا هنگامی که نیاز است با سخت افزار تبادل صورت بگیرد. در اینجا JNI به صورت یک پل ارتباطی بین جاوا و سایر زبان‌ها عمل می‌کند و کتابخانه‌های متد های بومی (Native Method Library) که به صورت فایل‌های .dllیا .so هستند را هم بارگیری می‌کند.


نکات تکمیلی

زباله روب

قبل تر گفتیم فرایند زباله‌روب از دو مرحله اصلی علامت گذاری و حذف تشکیل میشود. اگر بخواهیم دقیق تر این مسئله را بررسی کنیم در واقع فرایند زباله روب از 3 بخش اصلی تشکیل میشود:

  • علامت‌گذاری: شناسایی اشیایی که در حال حاضر در حال استفاده هستند و در حال استفاده نیستند
  • حذف عادی: حذف اشیاء استفاده نشده و بازیابی فضای آزاد
  • حذف با فشرده سازی: انتقال تمام اشیاء باقی مانده به یک فضای بازمانده (برای افزایش عملکرد تخصیص حافظه به اشیاء جدیدتر)

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

برای حل این مشکلات، اشیاء جدید در فضاهای نسل بندی شده جداگانه Heap ذخیره می‌شوند به گونه ای که هر فضا نشان دهنده طول عمر اشیاء ذخیره شده در آن باشد. سپس جمع‌آوری زباله در 2 فاز اصلی به نام‌های Minor GC و Major GC انجام می‌شود و اشیا قبل از حذف کامل، اسکن شده و در بین فضاهای نسلی جابه‌جا می‌شوند.

در واقع اگر به تصویر دقت کنیم متوجه می‌شویم که هیپ به دو قسمت حافظه جوان و حافظه قدیمی تقسیم شده است. (مقدار حافظه هیپ قابل تغییر است. با کمک دستورات Xmx و Xms برای مقدار دهی اولیه و تعیین مقدار ماکسیمم)

بررسی تفاوت پشته و هیپ در حافظه جاوا

ابتدا به تصویر زیر توجه کنید:

به طور خلاصه، همانطور که بالاتر جداگانه گفته شد حافظه پشته جاوا برای اجرای یک thread استفاده می‌شود و حاوی مقادیر خاص متد و ارجاع به اشیاء دیگر در Heap است.(که مفصل بحث شد)

همچنین یک مثال بسیار خوب از منبع آخر پیدا کردیم که بررسی آن کمک میکند فرق این 2 را بهتر درک کنیم. ابتدا این تکه کد را در نظر بگیرید.

در تصویر زیر فریم‌های بحث شده در بخش پشته به خوبی مشخص شده اند و ارجاعات به هیپ هم با فلش نشان داده شده است.

منابع :

https://dzone.com/articles/jvm-architecture-explained

https://dzone.com/articles/a-detailed-breakdown-of-the-jvm

https://medium.com/interviewnoodle/jvm-architecture-71fd37e7826e

https://www.freecodecamp.org/news/jvm-tutorial-java-virtual-machine-architecture-explained-for-beginners/

https://medium.com/platform-engineer/understanding-jvm-architecture-22c0ddf09722

https://www.guru99.com/java-virtual-machine-jvm.html

https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973

https://medium.com/platform-engineer/understanding-java-garbage-collection-54fc9230659a


انجین‌ایکس (Nginx)

ابزار Nginx یکی از پرکاربردترین ابزارها در دنیای اینترنت و وب است. این برنامه‌ یک برنامه‌ی سیستمی است که روی وب سرور‌ها نصب می‌شود و عملیات خدمت‌رسانی به کاربران را انجام می‌دهد. این خدمت رسانی شامل ارائه محتوای وبسایت به کاربران، کش کردن برای دسترسی سریع‌تر، لود بالانس کردن و کنترل پهنای باند می‌شود. دور از ذهن نیست که چنین برنامه‌ای برای هندل کردن تعداد زیادی کاربر نیاز به روند مناسب استفاده از منابع دارد چرا که با افزایش کاربران سیستم، منابع مصرفی از جمله پردازنده و مموری زیادی مصرف می‌شود. رقیب اصلی انجین‌ایکس که اتفاقا پرکاربردترین وب سرور دنیای امروز است، Apache نام دارم. تمرکز معماری Nginx بر حل کردن مشکلاتی است که در نتیجه‌ی تصمیمات طراحی و معماری آپاچی به وجود آمده است. آپاچی برای تعداد اتصال‌های زیاد، مصرف منابع بسیار زیادی دارد (رشد مصرف منابع به نسبت تعداد کاربران تقریبا خطی است). این معماری اگرچه به توسعه‌پذیری محصول کمک کرده و باعث شده قابلیت‌های متفاوتی پیاده‌سازی شوند، اما به ازای هر اتصال جدید یک بار از پروسس فورک می‌شود که به مصرف خیلی زیاد منابع برای تعداد اتصالات بالا می‌انجامد.

معماری پیشنهادی

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

با تجربه‌ی به دست آمده از مزایا و معایب آپاچی، تیم توسعه‌ی Nginx تصمیم گرفتند از رویکرد «رویداد-محور» استفاده کنند. در این روش به جای ساخت تعداد زیادی ترد، تلاش زیادی می‌شود تا ترد/پروسس‌های ساخته شده مدیریت شوند. در واقع یک‌سری المان کارگر (worker) داریم که تعدادشان بسیار کم‌تر از تعداد کاربران است و تلاش می‌کنند بیشترین تعداد کاربری که می‌توانند را در آن واحد سرویس‌دهی کنند. این ورکر‌ها پروسس‌های تک‌نخی هستند که به شکل پیش‌فرض تعداد کمی مثلا یکی از آن‌ها داریم اما با افزایش لود کاربران می‌توانند کمی بیشتر هم شوند مثلا ۳ یا ۵ تا. دقت داریم که این تعداد کم ترد باعث می‌شود عملکرد نهایی پردازنده به بیشترین حالت خود برسد. به عنوان یک نمونه می‌توان گفت هر پروسس Worker می‌تواند تا هزاران اتصال را با همان یک نخ خود سرویس‌دهی کند.

همانطور که گفته شد پروسه‌های Worker مسئول پاسخ به درخواست‌های کاربران هستند اما هماهنگی و مدیریت آن‌ها میسر نمی‌شود، مگر با هماهنگی یک مسئول هماهنگ‌کننده‌ی مرکزی (Master).

در این قسمت خوب است به مکانیسم‌های Cache هم اشاره کنیم که این امکان را می‌دهند که صفحات به جای بارگذاری از روی حافظه ی جانبی با سرعت کم و تاخیر زیاد از حافظه‌ی موقت سریع‌تری بارگذاری شوند و زمان تاخیر نهایی بسیار کمتر شود. پر شدن کش با اولین درخواست به هر صفحه انجام می‌شود.


جزئیات یک ورکر

تا اینجا دانستیم که یک ورکر باید تعداد زیادی کاربر را با فقط یک نخ پردازشی مدیریت کند. برای این منظور یک حلقه ی اصلی وجود دارد (run-loop) که بر اساس الگوی «حل و فصل تسک‌ها به روش ناهمگام» و به کمک callback‌ها عملیاتی که برای هر کاربر باید انجام شود را وقتی نوبت به ان رسید انجام می‌دهد و سپس به سراغ کاربر بعدی می رود.

این لیست کاربرانی که باید توسط یک ورکر مدیریت شوند از قبل توسط مستر به ورکر ارجاع شده و در صورت نیاز هم بروزرسانی می‌شود. مزیتی که وجود دارد این است که این بروزرسانی کم‌هزینه است و نیازی به سیستم کال‌های زیاد مثلا برای ساخت و یا حذف یک ترد وجود ندارد و فقط همان عملیات شبکه که الزامی است را انجام می‌دهد.

جزئیات مستر

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

جزئیات کش

برای نیل به پرفورمنس مناسب استفاده از کش همواره یکی از تکنیک‌های محبوب است. انجین‌ایکس نیز به خوبی این نیاز را درک کرده و به شکل مناسب کش را پیاده‌سازی کرده است. برای کش از دو پروسس متفاوت استفاده می‌شود. اول cache loader که به اندیس‌گذاری فایل‌های مختلفی که امکان کش دارند در مموری می‌پردازد و به این شکل در مموری یک دیتابیس از متادیتای فایل‌های کش شده خواهیم داشت. پروسس دوم cache manager است که به زمان انقضای یک داده در کش توجه دارد و همواره در حال اجرا می‌ماند.

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

عملیات پر کردن کش به این شکل است که انجین‌ایکس داده‌ها را از سرور منبع اطلاعات می‌خواند و در یک فایل موقتی ذخیره می‌کند. بعد از اتمام دریافت فایل آن را به پوشه ی مخصوص کش منتقل می‌کند و سپس در صورت نیاز بر اساس URL موجود به محتوای کش به جای درخواست مستقیم به سرور دسترسی پیدا می‌کند.

زبان پیاده‌سازی

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

یکی از مشکلات Nginx قابلیت اجرا روی سیستم‌عامل‌های مختلف است. این ابزار با صرف زمان زیاد قادر شده روی اکثر سیستم‌‌عامل‌های دنیا سرویس دهد ولی بروی برخی سیستم‌عامل‌ها مثل ویندوز با تلاش زیاد و با غیرفعال شدن برخی ویژگی‌ها امکان کار پیدا کرده و استفاده از آن اصلا توصیه نمی‌شود و بیشتر جنبه‌ی «اثبات عمل» دارد. ریشه‌ی این امر در امکانات ناکافی هسته‌ی ویندوز در پشتیبانی از امکانات مورد نیاز در Nginx است. ممکن است فکر کنیم که در صورتی که از زبان‌های سطح بالاتری مانند جاوا استفاده می‌شود این مشکل وجود نداشت ولی اتفاقا برعکس. در این زبان‌ها امکان بهینه‌سازی و استفاده از قابلیت های خاص سیستم‌عامل اصلا به وجود نمی‌آمد و در همه‌ی سیستم‌عامل‌ها کارایی پایینی را تجربه می‌کردیم.

منابع

https://www.aosabook.org/en/nginx.html

https://medium.com/@premsuryamj/nginx-architecture-9f97cf7887e2

https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/


توییچ

معرفی توییچ (twitch)

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

معماری اولیه:

توییچ ابتدا با معماری یکپارچه یا به عبارتی monolith با استفاده از Ruby on Rails ساخته شد. از آنجایی که یک استات‌آپ تازه کار بود محدودیتی ایجاد نمی‌کرد و کمک کرد تا به سرعت محصول ارائه شود. کم کم با رشد سازمان برای ایجاد هماهنگی بیشتر و انعطاف پذیری و مهم تر از همه مقیاس پذیری لازم بود تا کدها به بخش‌های کوچک تر تغییر کنند. همچنین با افزایش کاربران بخش‌های مختلف سیستم به مرور در حال رسیدن به گلوگاه‌های عملکردی کردند. (bottleneck) برای مثال پایگاه داده، APIها و بخش جست و جو و به خصوص بخش چت دچار کندی شدند. یکی از مهم ترین بخش‌های توییچ هم چت بود و حتما لازم بود یک ارتباط در زمان واقعی بین صاحب ویدیو و بازدید کنندگان اتفاق بیفتد. کاربران توییچ می‌دانند که روح توییچ، چت آن است و باید با تاخیر بسیار ناچیز عمل کند.

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

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

ورود زبان Go به توییچ

خلاصه در آن زمان کم کم توییچ تلاش کرد به سمت میکروسرویس‌ها برود تا گلوگاه‌های اینچنین که گفته شد را از بین ببرد. آزمایش و تست‌های زیادی در ان زمان انجام شد. حتی روی استک های مختلف تحقیق صورت گرفت(به خصوص برای سرویس چت). در نهایت حدود سال 2012 تعدادی از مهندسان توییچ در یک جلسه به نام GoSF شرکت کردند و بررسی کردند که آیا استفاده از Go میتواند به محصولشان کمک کند یا خیر. (البته گفته میشه در آن زمان خود Go هم محبوبیت خوبی پیدا کرده بود). خلاصه مدیرعامل توییچ هم از این انتخاب حمایت کرد و تصمیم گرفتند تا ازمایش‌هایی انجام شود که بررسی کنند Go تا چه حدی می‌تواند ترافیک‌های دنیای واقعی را مدیریت کند. در این آزمایش علاوه بر تایید ایمن بودن Go یکی از مشکلات سرویس چت توییچ هم شناسایی شد!

اولین میکروسرویس توییچ با Go

اولین قطعه کد Go که روی سرورهای توییچ قرارگرفت بخش کوچک ولی مهمی از سرویس چت بود که مرتبط با تبادل پیام است. به گفته خودشان با حدود فقط 100 خط کد توانستند هر پیامی را با یک thread مدیریت کنند. بعد از اینکه Go در این مرحله خودش را به طور کامل اثبات کرد و تیم سرور pubsub پس از این مرحله به سرعت شروع به افزودن Go به توییچ کرد. به سرعت میکروسرویس‌های بعدی Go هم اضافه میشد. سرویس دوم Jax نام گرفت که طوری طراحی شده بود تا پخش زنده برتر را برای هر دسته فهرست کند. چالش ایندکس کردن داده‌های زنده در زمان واقعی گریبان‌گیر بود که برای حل آن راه‌های مختلفی ارائه شد مثلا PostgreSQL، شاخص‌های درون حافظه و در نهایت الاستیک سرچ.

مهاجرتی عظیم!

در حدود سال 2015 پس از اینکه Goخودش را به درستی اثبات کرد یک کمپین در توییچ صورت گرفت که تیم‌های مختلف کدهای خود را به سمت میکروسرویس‌های Go ببرند و اسم رمز خود را Wexit گذاشتند.

به جز برخی میکروسرویس‌هایی که گفتیم اکثر سیستم همچنان داخل بستر یکپارچه‌ی Rails بود و این موضوع خود چالش‌های زیادی داشت. که به گفته‌ی مهندسان توییچ عبارت بود از:

  • استفاده از یک خط‌لوله‌ی واحد برای دیپلوی همه‌ی کدها
  • استفاده از صفحات گسترده (spreadsheets) برای هماهنگی‌های محیط مرحله بندی (staging environments)
  • صبر کردن برای سرعت پایین بیلدها
  • تلاش برای ردیابی اشتباهات و لینک کردن آنها به صاحبشان
  • مسیریابی کند، به علت وجود نقاط پایانی زیاد در API (یا همان end pointها)
  • کوئری‌های مداوم و کم سرعت دیتابیس
  • تضادهای زمان ادغام بسیار زیاد و دست و پا گیر

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

یکی دیگر از مهم‌ترین تلاش‌ها که به گفته مهندسان شاید مهم‌ترین گام برداشته شده باشد افزودن یک لایه‌ی اضافه‌ در سمت بک‌اند برای API، یک NGINX پروکسی معکوس بود و برای انتقال درصدی از ترافیک به API جدیدی که با Go نوشته شده بود.

بعد از این گام چند فرایند مهم در شرکت تعریف شد:

  • میکروسرویس‌های جدیدی تولید شوند.
  • نقاط پایانی قدیمی در لبه‌های جدید تکرار شوند.
  • درصدی از ترافیک ها برای تست به این بخش انتقال داده شود.
  • بعد از تایید معیارها و تست‌ها 100% ترافیک به این بخش برود.

کمپین گفته شده مهاجرت بسیار بزرگ و عظیمی بود و تقریبا 2 سال از 2016 تا 2018 مهندسان را درگیر کرد.

زیرساخت پخش زنده ویدیو در Twitch

این بخش از منبع چهارم گرفته شده و از گفته‌های مهندس توییچ نیست.

همانطور که گفته شد هدف توییچ ایجاد بستری برای پخش زنده با قابلیت مقیاس پذیری و دردسترس بودن بالا بود. راه حلی که توییچ ارائه کرد به عنوان یک سرویس برای جهانیان از طریق AWS IVS فراهم است که سازمان‌هایی که از آن استفاده می‌کنند را قادر می‌کند تا خدمات استریمی خود را با سایر ادغام کنند.

نکات کلیدی زیرساخت توییچ که باید در نظر گرفته شود:

  • صدها هزار پخش کننده همزمان روی پلتفورم توییچ ایجاد می‌شود.
  • هر پخش زنده دارای 5 سطح کیفیت ویدیو است.
  • هر استریم یک بافر چندثانیه ای برای دانلود ویدیو قبل از موعد دارد که پخش به خوبی اجرا شود.
  • طی سال‌ها تلاش تاخیر از 15 ثانیه به 3 ثانیه رسیده است. البته در شرایط خوب شبکه و اینترنت در کشورهایی مثل کره جنوبی این تاخیر 1.5ثانیه است.

هنگامی که یک پخش کننده جریان خود را شروع می‌کند ویدیو وی توسط سرورهای توییچ رمز میشود. سپس به فرمت‌های مختلف تبدیل می‌شود تا در دستگاه‌های بیننده ها تحت شرایط مختلف پخش شود. از آنجایی که این موضوع گران بود ابتدا توییچ فقط 2 تا 3 % از کانال‌ها را رمز کرد. بعدها با کمک رمزگذاری مبتنی بر سخت افزار این هزینه‌ها را مقدار قابل توجهی کاهش داد.

برای اطمینان از پخش روان استریم، از شراکت چند ISP محلی استفاده می‌شود و چندین PoP حضور دارد که با کمک یک شبکه‌ی backbone تغذیه می‌شود. بینندگان از سراسر دنیا ویدیوهارا از PoP ها میگیرند.

در ابتدا، Twitch با یک مرکز داده (data center) واحد شروع به کار کرد که در آن جریان های ویدیویی زنده را پردازش می‌کرد. PoP ها HAProxy را اجرا کردند و جریان ها را به مرکز داده مبدا هدایت کردند. همانطور که گفتیم وقتی پلتفرم مورد توجه قرار گرفت و تعداد مراکز داده افزایش یافت، چندین چالش را با رویکرد HAProxy به همراه داشت. PoP ها به دلیل پیکربندی HAProxy، به صورت ایستا جریان های ویدئویی زنده را تنها به یکی از مراکز داده مبدا ارسال می کردند که منجر به استفاده ناکارآمد از منابع زیرساختی می‌شد.

برای مقابله با این چالش‌ها، HAProxy کنار گذاشته شد و Intelligest توسعه یافت که یک سیستم مسیریابی ورودی برای توزیع هوشمند ترافیک ویدیوی زنده از PoPs به مبدا است.

معماری Intelligest از دو جزء تشکیل شده است:

  • اول Intelligest Media Proxy در حال اجرا در هر PoP
  • دوم Intelligest Routing Service (IRS) که در AWS اجرا می‌شود

پروکسی رسانه، با کمک IRS، مرکز داده مبدا مناسب را برای ارسال ترافیک تعیین می کند و بر چالش‌های پیش روی HAProxy غلبه می کند. البته IRS دو سرویس فرعی دیگر نیز دارد، خازن و چاه!

خازن بر منابع محاسباتی موجود در هر مرکز داده مبدا و چاه نیز بر پهنای باند شبکه backbone نظارت دارد. با کمک اینها، IRS می تواند ظرفیت زیرساخت را در زمان واقعی تعیین کند. این باعث شده است که Twitch در زیرساخت های خود دسترسی بالایی (HA) داشته باشد.

نگاهی سریع بر تکامل فرانت‌اند

ابتدا بخش فرانت‌اند جدایی و استقلال مناسبی از سمت بک‌اند نداشته است و بخشی از یک برنامه استاندارد Rails بود که HTML را در سمت سرور رندر میکرد و برای تعاملات هم از jQuery استفاده می‌کرده است. با گذشت زمان تبدیل به یک برنامه Ember.js تک صفحه‌ای شد. با این روش فرانت‌اند به طور کامل و واضح از API سمت بک‌اند جدا شد. حدود سال‌های 2017 تا 2019 هم به زبان React js باز نویسی شد.

توییچ امروز

امروزه در توییچ اکثر تیم‌ها دارای چندین سرویس هستند و زبان انتخابی Go است. البته در سایر پلتفورم ها از زبان‌هایی مثل جاوا، کاتلین، پایتون، c++ و تایپ اسکریپت پشتیبانی میکند.

هر سرویس هم در یک جساب جداگانه AWS ایزوله شده این الگو "حباب‌های کوچک" نام دارد. امروزه تیم های مختلف به طور مستقل عمل میکنند و زیرساخت‌های خود را مدیریت می‌کنند.

منابع :

https://blog.twitch.tv/en/2022/03/30/breaking-the-monolith-at-twitch/

https://blog.twitch.tv/en/2022/04/12/breaking-the-monolith-at-twitch-part-2/

https://blog.twitch.tv/en/2015/12/18/twitch-engineering-an-introduction-and-overview-a23917b71a25/

https://scaleyourapp.com/live-video-streaming-infrastructure-at-twitch/

https://blog.twitch.tv/en/2021/09/07/guiding-a-monolith-with-a-gentle-touch-the-power-of-pairing-codeowners-and-lint-rules/


بررسی و مقایسه‌ی تاخیر لود سایت‌های توییتر و توییچ

در نهایت برایمان جالب بود تا با کمک ابزاری مانند Jmeter بتوانیم لود تست روی سرویس‌های گفته شده انجام دهیم. پس از جست و جو ابزار آنلاینی مشابه آن پیدا کردیم که Pingdom نام دارد. البته شایان ذکر است که این سایت ساده، زمان لود شدن صفحه‌ی اصلی سایت را اندازه‌گیری می‌کند و خبری از تست APIهای مختلف نخواهد بود.

ابتدا روی توییچ از اروپا و کشور آلمان لود تست انجام دادیم:

یکی از ویژگی‌های این ابزار این است که پیشنهاداتی برای بهبود سایت ارائه می‌دهد!

برای توییتر هم همین روند را پیش بردیم:

بخش جالبی که وجود داشت این است که با این که زمان لود توییتر کمتر بود امتیاز پرفورمنسش از توییچ کمتر شد که البته با توجه به پیشنهادات میتوان کمی این موضوع را درک کرد:

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

پس از آن یک ابزار دیگر هم به نام gtmetrix استفاده کردیم که نتایج نسبتا مشابهی داشت. البته با توجه به ساختار شبکه و سرعت اینترنت ممکن است نتایج این تست ها متفاوت باشد. برای مثال این بار از سمت ونکور در کانادا تست توییتر انجام شد:

از مزایای این ابزار قابلیت مقایسه بود برای استفاده توییچ و توییتر را باهم مقایسه کردیم :

البته در این ابزار دوم امتیازات توییچ با ابزار قبلی بسیار متفاوت بود! برای همین تصمیم گرفتیم از سمت شهرها و کشورهای دیگر مثل توکیو در ژاپن هم روی توییچ با ابزار اول تست انجام دهیم:

از لحاظ تاخیر تقریبا شبیه بودند ولی به نظر میرسد فرمول محاسبه امتیاز این دو ابزار متفاوت باشد. نگاهی هم به وضعیت ریسپانس ها بیندازیم:

و در اخر نتیجه تست از سانفرانسیکو در آمریکا:


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

#معماری_نرم_افزار_بهشتی

برنامه نویسینرم افزارمعماری_نرم_افزار_بهشتیمعماری نرم افزارماشین مجازی
همینجا بگم که روزبه شریف نسب درسته و نه شریف نصب یا شریفی نسب یا هرچیز غلط دیگه..
شاید از این پست‌ها خوشتان بیاید