نرم‌افزارهایی که روی ابر زندگی می‌کنند!

در قسمت قبل دیدیم گوگل چطور Borg را به دنیا آورد و در این مطلب به موضوع زیر می‌پردازیم:

نرم‌افزارهایی که روی ابر زندگی می‌کنند!

پس از هر انقلاب پردازشی روش‌های مهندسی نرم‌افزار نیز تغییر می‌کنند.

از این رو نویسندگان در بخش «نوشتن نرم‌افزار برای محاسبات مدیریت شده»(Writing software for managed compute) به تلاش توسعه دهندگان گوگل برای تطبیق بین لایه نرم‌افزاری و پردازشی پرداخته‌اند.


معماری با توجه به شکست

صرفا این نکته که ارائه دهنده خدمات ابری پایداری را تضمین کند کافی نیست و اگر واقعا نرم‌افزار از ابتدا برای استقرار روی یک خدمت ابری طراحی شده، که به اصطلاح ابرزی(cloud-native) خوانده می‌شود، باید به گونه‌ای نوشته و مستقر شده باشد که با از دسترس خارج شدن یک نمونه(instance) از نرم‌افزار کل خدمت نهایی از دسترس خارج نشود.

در اینجا مثالی از پردازش اسناد توسط Borg زده می‌شود:

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

اگر پردازش هر سند یک ثانیه زمان ببرد دست کم ۱۲ روز طول می‌کشد تا یک ماشین بتواند تمامی ۱ میلیون سند را پردازش کند.

بنابراین کار را بین ۲۰۰ ماشین توزیع می‌کنیم که به زمان قابل قبول ۱۰۰ دقیقه برسیم.

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

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

وقتی که ماشین شما یک «حیوان خانگی» باشد در صورت بروز خطا باید یک مهندس درگیر عیب‌یابی و رفع ایراد بشود ولی زمانی که عضوی از یک گله دام باشد کافی است آن را حذف کرده و نسخه دیگری را راه اندازی کنیم.

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

برای حل این مشکل لازم بود تغییر دیگری نیز در معماری پردازشی اعمال شود:

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

هر وقت یکی از سرورهای کارگر پردازش یک قطعه را تمام کرد نتایج را گزارش می‌دهد و به سراغ قطعه بعد می‌رود.

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

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

مشابه این قضیه، وقتی برنامه‌ریز Borg تصمیم به جابجایی یک دربرگیرنده می‌کند برای پرهیز از نمایش خطا به کاربر، پیشاپیش به آن هشدار می‌دهد تا از پذیرش درخواست‌های جدید جلوگیری کند و همچنین فرصت خاتمه ارتباطات قبلی را داشته باشد و این نیازمند آن است که توزیع کننده بار(Load balancer) نیز پاسخ «نمی‌توانم درخواست جدیدی را بپذیرم» بفهمد و ترافیک را به Replica دیگری منتقل کند.

نوع کارهای Batch در برابر Serving

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

این نوع از کارها نقطه مقابل Serving هستند که به شکل بی‌پایانی ادامه دارند و منتظر درخواست‌های جدید برای پاسخگویی‌اند( مثل برنامه پاسخگویی به جستجوهای کاربران گوگل ).

این دو نوع کار، خصوصیات کاملا متفاوتی دارند از جمله:

  • کارهای Batch روی توان عملیاتی(تعداد درخواست‌های قابل پاسخگویی) تمرکز دارند حال اینکه در نوع Serving مدت زمان پاسخگویی به هر درخواست مهم است.
  • عمر Batch کوتاه است( به چند دقیقه یا نهایتا چند ساعت می‌رسد) ولی در Serving تا زمانی که نسخه جدیدی برای انتشار نداشته باشیم و یا راه اندازی مجدد نشود، فرایند پاسخگویی ادامه دارد.
  • اغلب به دلیل طولانی مدت بودن کارهای Serving، راه اندازی آن‌ها نیز زمان‌بر است.

کارهای نوع Batch به خودی خود گزینه‌های ایده آلی برای مواجهه با خطا هستند و کافی است داده‌ها به قطعات درستی تقسیم و بعد تخصیص داده شوند( چارچوب(Framework) رایج این کار در گوگل MapReduce بود که بعد با Flume جایگزین شد).

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

مثلا نمونه رایج آن سرورهایی هستند که اغلب از آن با عنوان «رهبر»(Leader) یاد می‌کنیم.

این سرورها که به نوعی وظیفه حفظ حالت برنامه را به عهده دارند( چه درون حافظه یا روی دیسک) را نمی‌توان در صورت از کار افتادن صرفا دوباره راه اندازی کرد چرا که وضعیت سیستم از دست رفته است.

یا اگر چنین سروری با اسم میزبانی‌اش(hostname) در شبکه شناخته شده باشد و از دسترس خارج شود، دیگر بخش‌های سرویس نیز دچار ایراد می‌شوند.

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

مدیریت حالت(Managing State)

همانطور که دیدیم حالت(State) یک عامل دردسر ساز است.

مثلا وقتی قصد ما «احشام» در نظر گرفتن کارها است؟ ، باید به داده‌های ذخیره شده در حافظه یا دیسک هم به این شکل نگاه کنیم.

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

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

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

مثلا باید دید این «سامانه‌ی خارج از ماشین، قابلی دسترسی از دیگر نقاط و همچنین ماندگار» چطور پیاده سازی شده و آیا رویکرد «احشام، نه حیوانات خانگی» در این مورد هم رعایت شده؟

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

در اینجا نویسنده به یکی از دروس کلیدی که گوگل حین زمان تکمیل این معماری گرفته اشاره کرده و آن چیزی نیست جز:

«برای حل کردن چالش‌های مربوط به تاخیر(latency) از Cache و از برنامه اصلی جهت پاسخگویی به بار کلی استفاده کنید».

این بدین معنا است که برنامه شما بدون استفاده از Cache نباید در اثر بار ساقط شود بلکه صرفا باید انتظار کمی تاخیر بیشتر را داشت.

اتصال به یک خدمت

این بخش یکی از مهمترین مباحث رایانش ابری یعنی کشف خدمت (Service Discovery) را بررسی می‌کند.

چطور چنین مفهومی زاده شد؟

همانطور که قبلا دیدیم اگر برنامه شما از طریق نام میزبان(hostname) و به صورت Hardcoded یا حتی به عنوان یک پارامتر تنظیمی حین راه اندازی آدرس دهی می‌شود، دیگر نمی‌توان برنامه را پیرو رویکرد «احشام» دانست چرا که با هر تغییر باید به شکل دستی تنظیمات را از نو اعمال کرد.

راه حل چیست؟

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

مثلا برنامه‌ریز پس از هر بار راه اندازی مجدد یک ماشین در محلی دیگر آدرس آن را جایی ذخیره می‌کند تا برنامه‌های دیگر برای دسترسی به آن از این طریق اقدام کنند و این همان توضیح کلی مفهوم کشف خدمت(Service Discovery) است.

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

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

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

مورد غافلگیر کننده دیگر، اختلال در ارتباط بین برنامه‌ریز و یکی از ماشین‌ها است.

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

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

برای حل این مشکل باید ببینیم سیستم «کشف سرویس» به کدام نمونه ارجاع داده.

کدهایی که یکبار اجرا می‌شوندـ(One-Off Code)

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

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

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