محسن میرزانیا
محسن میرزانیا
خواندن ۶ دقیقه·۵ سال پیش

برنامه‌های "cloud-native" - قسمت ششم Node-Failure

در این قسمت می‌خواهیم بررسی کنیم که برای مقابله با failure در لایه‌ی سرویس چه کارهایی باید انجام دهیم. یکی از اصول ۳ گانه‌ی سیستم‌های “Highly available” شناسایی خطا و توانایی کنترل کردن آن بدون ایجاد وقفه در عملکرد نرم‌افزار است. خطا می‌تواند بنا به دلایل سخت‌افزاری یا نرم‌افزاریِ داخل کد اپلیکیشن اتفاق بیافتد؛ ولی نکته‌ی مهم، واکنش مناسب در برابر آن است، به گونه‌ای که کمترین تاثیر منفی را روی تجربه‌ی کاربر بگذارد.

الگوی “Node Failure” به وظایف یک اپلیکیشن در برابر خطای نودی که روی آن در حال اجرا است می‌پردازد. این وظایف عبارتند از:

  • ایجاد آمادگی از قبل برای به حداقل رساندن معضلات هنگامی که یک نود دچار خطا می‌شود.
  • کنترل کردن خطا به صورت “graceful”.
  • ریکاوری و ادامه به کار بعد از خطا.

این الگو برای پیدا کردن راهکار در مقابل چالش‌هایی از قبیل موارد زیر مطرح شده است:

  • اپلیکیشن‌هایی که از الگوی “Queue-Centric” استفاده می‌کنند، باید پیام‌هایی که روی صف منتشر می‌شوند را حداقل یکبار مورد پردازش قرار دهند.
  • اپلیکیشن‌هایی که از الگوی “Auto-Scaling” استفاده می‌کنند، باید بتوانند به صورت graceful منابع را آزاد کنند.
  • اپلیکیشن‌ها باید به صورت graceful بتوانند خطا را مدیریت کنند.
  • اپلیکیشن‌ها باید بتوانند به صورت graceful خطاهای نرم‌افزاری را مدیریت کنند.



پیاده‌سازی

هدف از مفهوم “Node-Failure” بالا نگه داشتن اپلیکیشن هنگامی که نودها دچار خطا می‌شوند است. خطا می‌تواند دلایل متعددی داشته باشد، اما اپلیکیشن باید به تمام آن‌ها به یک چشم نگاه کند، و بتواند تمام آن‌ها را اصطلاحا به صورت graceful مدیریت کند.

قانون N + 1

اگر یک اپلیکیشن بخواهد خودش را برای مقابله با خطا آماده کند، باید مطمئن باشد که همیشه ظرفیت کافی در اختیار خواهد داشت.

اول باید این سوال پرسیده شود که چه تعداد نود برای نگهداری سیستم به صورت “Highly available” مورد نیاز است؟ قانون “N+1” می‌گوید اگر به N نود برای پشتیبانی از درخواست‌های همزمان کاربران نیاز دارید، حداقل “N+1” نود ایجاد کنید. در این حالت، از دست دادن یک نود تاثیری در روند اجرای اپلیکیشن نخواهد داشت. هم‌چنین باید همیشه توجه داشت که در معماری سیستم “Single point of failure” وجود نداشته باشد. بنابراین اگر در جایی به یک نود نیاز است (مثلا نود load balancer)، حتما باید دو نود در نظر گرفته شود. این موضوع می‌تواند حتا شامل سوئیچ rack در دیتاسنتر نیز می‌شود.

هنگامی که خطا رخ دهد، یک فاصله‌ی زمانی طول خواهد کشید تا سیستم مانیتورینگ متوجه این خطا بشود. در این بین، اپلیکیشن شما با ظرفیت کمتری به کار خودش ادامه خواهد داد تا یک نود جدید به صورت کامل آماده شود. به این ترتیب، برای مثال اگر نودها پشت یک load balancer باشند، تا زمانی که lb متوجه خطا شود، ترافیک برای آن نود ارسال خواهد شد؛ اما بعد از تشخیص آن نود از lb کنار گذاشته خواهد شد. ولی تا آماده شدن نود جدید و اضافه شدن آن به lb مدت زمانی طول خواهد کشید. این موضوع در lb پلتفورم‌های ابری مثل AWS، و هم در راهکارهای دیگر مثل کوبرنتیز یا lbهای معمولی مثل HAProxy رعایت می‌شود.

مدیریت Shutdown شدن نودها

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

در ریکاوری بعد از خطا دو مسئله وجود دارد: عدم متاثر شدن تجربه‌ی کاربری تا جای ممکن، و ادامه‌ دادن به کارِ در حال انجامی که دچار وقفه شده است. اگر نودی دچار خطا شود، پلتفورم ابری متوجه این مشکل خواهد شد و دیگر ترافیکی برای آن نود ارسال نخواهد کرد؛ و درخواست‌های جدید فقط برای نودهای آماده ارسال خواهند شد. بنابراین شما در اینجا کار چندانی انجام نخواهید داد، و فقط باید قانون “N+1” را اجرا کنید. اما برای اینکه کاملا در برابر خطاها مقاوم باشید، باید در نظر بگیرید که در هر لحظه ممکن است در پردازش یک درخواست خطا رخ دهد. بهترین راهکار مقابله در برابر این موضوع، قرار دادن منطق “retry” در سمت کلاینت است. در برنامه‌های موبایل، یا صفحه‌های وب اپلیکیشنی که به صورت “single-page” نوشته شده‌اند و آپدیت‌ها را از طریق فراخوانی‌های سرویس انجام می‌دهند، پیاده‌سازی این موضوع پیچیده نیست. اما در پیاده‌سازی‌های قدیمی‌تر که وب اپلیکیشن کل یک صفحه را هر بار نمایش می‌دهد، ممکن است کاربران خطا را مشاهده کنند. بنابراین این موضوع از کنترل شما خارج خواهد بود، و باید منتظر خود کاربران باشید تا عمل “retry” را انجام دهند.

خطاهای ناگهانی به جز تاثیر روی تجربه‌ی کاربری، می‌توانند پردازش در لایه‌ی سرویس را با وقفه مواجه کنند. راهکار این مشکل، همان‌طور که در قسمت مربوط به صف‌ها بررسی شد، به کار گیری مفهوم “idempotency” در پروسه‌ها خواهد بود؛ یعنی باید بتوانیم یک ورودی را بدون به وجود آمدن هیچ مشکلی چندین مرتبه پردازش کنیم. ریکاوری موفقیت‌آمیز نیازمند نودهای stateless و ذخیره‌سازیِ اطلاعات مهم روی یک فضای ذخیره‌سازی قابل اعتماد است. بنابراین، مهم‌ترین نکته عدم استفاده از فضای ذخیره‌سازی محلی نودهای compute برای نگهداری اطلاعات مهم است؛ به گونه‌ای که state اپلیکیشن در یک فضای قابل اعتمادِ جداگانه ذخیره شود. همان‌طور که قبلا گفته شد، یک تکنیک معمول در برنامه‌های “cloud-native” که از ریکاوری پروسه‌های دچار وقفه شده پشتیبانی می‌کند، الگوی “Queue-Centric” و استفاده از صف‌ها است، که علاوه‌ بر آن می‌تواند وضعیت تسک در حال اجرا را ذخیره نیز بکند.

یک مثال اولیه و عملی برای مدیریت خطا در نودهای لایه‌ی سرویس و نودهای توزیع کننده‌ی بار در این لینک وجود دارد.

cloud nativeload balancingnode failureidempotencyqueue
شاید از این پست‌ها خوشتان بیاید