ارتباط HorizontalPodAutoscaler و تعیین تعداد پادها به صورت دستی در کوبرنتیس

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

یکی از روش‌های افزایش دسترسی‌پذیری٬ تغییر مقایس به صورت افقی (Horizontal Scaling) می‌باشد. به طور خلاصه تغییر مقیاس به صورت افقی یعنی در صورت افزایش بار بر روی سیستم تعداد بیشتری از برنامه را برای پاسخ به آن بار آماده کنیم. همچنین در صورت کاهش بار بر روی سیستم عمل برعکس را انجام دهیم. در کوبرنتیس برای دستیابی به این مفهوم می‌توانیم از HorizontalPodAutoscaler استفاده کنیم. با استفاده از HPA می‌توانیم یک متریک مشخص را انتخاب کنیم (مثلا میزان استفاده از پردازنده) و اجازه دهیم با تغییر این متریک تعداد پادهای اجراکننده برنامه نیز به صورت خودکار تغییر کند. همچنین در کوبرنتیس ما می‌توانیم در فایل‌های کانفیگ حاوی manifest خود تعداد مد نظر پادها را مشخص کنیم. در این جا ممکن است برایتان سوال شود که تعداد پادی که HPA تعیین کرده و تعداد پادی که در فایل کانفیگ مشخص می‌کنیم چگونه با هم مرتبط هستند. در حقیقت بی‌توجهی به این سوال ممکن است منجر به عملکرد‌های ناخواسته و بسیار خطرناکی شود. در این نوشته سعی می‌کنیم این موضوع مهم را که اشتباه در آن بسیار متداول است در چند سناریو بررسی کنیم.

سناریو اول

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

این فایل را در deployment.yaml ذخیره کرده و با این دستور اعمال می‌کنیم:

kubectl apply -f deployment.yaml

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

kubectl autoscale deployment busybox --cpu-percent=70 --min=3 --max=20

با استفاده از این دستور یک HPA می‌سازیم که با توجه به میزان استفاده از پردازنده تعداد پادها را بین ۳ تا ۲۰ عدد عوض می‌کند. تا این جا همه چیز مرتب است و HPA تعداد پادها را کنترل می‌کند. حالا فرض کنید در یک بعد از ظهر که تعداد پادها با توجه لود ۸ عدد شده تصمیم می‌گیریم ورژن جدید اپلیکیشن را دیپلوی کنیم. (در این متن ورژن جدید را در کامندی که echo می‌شود شبیه سازی کردیم). با توجه به استراتژی پیشفرض دیپلوی انتظار داریم که یک rolling update روی پادها انجام شود. یعنی با توجه به مقادیر maxUnavailable و maxSurge یک ReplicaSet جدید ساخته شده و پادهای جدید در آن ساخته شوند و همزمان پادهای ReplicaSet قدیمی متناظر با آن کم شوند. با این استراتژی دیپلوی ما یک دیپلوی بدون down time خواهد بود و اختلالی از سمت کاربران دیده نمی‌شود.

تغییرات را با این دستور اعمال می‌کنیم. توجه داشته باشید که تغییر در spec.template باعث شروع یک rollout جدید می‌شود:

kubectl apply -f deployment.yaml

بعد از اجرای این دستور به جای آپدیت پله به پله‌ای که انتظار داشتیم به یک باره ۶ پاد از ۸ پادی که مشغول پردازش بار موجود بودند terminate شده و سپس ReplicaSet جدید با یک پاد ساخته شده و rolling updateی که انتظار داشتیم روی ۸ پاد انجام شود روی همان ۲ پاد انجام می‌شود. در نهایت بعد از مدتی دوباره HPA تعداد پادهای ReplicaSet جدید را با توجه به لود به ۸ می‌رساند. اتفاقی که افتاد باعث شد تا در مدتی٬ تمام باری که ۸ پاد تحمل می‌کردند به یک باره به ۲ پاد برسد. احتمال این که این ۲ پاد نتوانند این بار را تحمل کنتد و خود نیز از حالت running در بیایند کم نیست و این به معنی یک downtime کامل است!

برای این که مشکل پیش آمده را درک کنیم لازم است تا ابتدا درک درستی از نحوه اعمال کانفیگ‌ها (در این جا با دستور kubectl apply) داشته باشیم. در این روش ما یک فایل حاوی کانفیگ مد نظرمان را تعریف کرده و آن را با دستور kubectl apply اعمال می‌کنیم. این دستور تغییرات مد نظر ما را روی کانفیگ فعلی (live configuration) اعمال می‌کند. به همین دلیل به این روش declarative نیز می‌گویند. بعد از زدن دستور kubectl apply یک درخواست patch به سمت سرور فرستاده شده و در آن فیلدهایی که باید تغییر یابند مشخص می‌شوند. در این سناریو در فایلی که برای ورژن جدید مشخص کردیم مقدار فیلد spec.replicas را که HPA به ۸ تغییر داده بود به ۲ تغییر دادیم و در این صورت اتفاقی که پیش آمد امری بدیهی بود.

سناریو دوم

برای حل این مشکل لازم است تا مقدار spec.replicas را که کنترل آن را به HPA دادیم مشخص نکنیم. برای همین تصمیم می‌گیریم تا در دیپلوی بعد این فیلد را کانفیگ حذف کنیم. در این صورت انتظار داریم که rolling update به صورت معقول انجام شود. فایل بعدی به این شکل خواهد بود:

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

برای این که دلیل این اتفاق را بفهمیم به نحوه عمل کردن دستور kubectl apply برمی‌گردیم. همان طور که قبلا اشاره کردیم دستور apply تغییرات مورد نیاز را از فایلی که به آن دادیم محاسبه و در تغییرات موجود بر روی سرور اعمال می‌کند. نکته مهم در این عمل نحوه محاسبه تغییرات توسط apply است. با هربار اعمال دستور apply کانفیگ اعمال شده در یک annotation به نام kubectl.kubernetes.io/last-applied-configuration ذخیره می‌شود. وقتی می‌خواهیم یک سری تنظیمات جدید را با apply اعمال کنیم محتویات فایل با محتویات kubectl.kubernetes.io/last-applied-configuration مقایسه می‌شوند و هر فیلدی که در کانفیگ جدید حذف شده باشد از کانفیگ موجود در سرور حذف می‌شود. به این علت که در این آپدیت سه مقدار کانفیگ فعلی٬ کانفیگ جدید موجود در فایل و کانفیگ موجود در kubectl.kubernetes.io/last-applied-configuration مورد مقایسه قرار می‌گیرند به این آپدیت یک ادغام سه‌گانه نیز می‌گویند. در سناریو قبل اتفاقی که افتاد این بود که فیلد spec.replicas در kubectl.kubernetes.io/last-applied-configuration موجود بود و در فایل جدید کانفیگ ما این فایل را حذف کرده بودیم. دستور apply با مقایسه این دو نتیجه گرفت که فیلد spec.replicas که مقدار آن را آخرین باز HPA تنظیم کرده بود باید پاک شود و در نتیجه مقدار پیش فرض آن یعنی ۱ اعمال شد.

سناریو سوم

با این حساب اگر دوباره همین تنظیمات را در دیپلوی بعدی اعمال کنیم باید شاهد نتیجه مد نظرمان باشیم:

بعد از زدن دستور apply ‌می‌بینیم که دیگر مشکل قبلی پیش نیامد و با توجه به استراتژی آپدیت تعداد پادها در طول rolling update متعادل ماند. دلیل این اتفاق این است که در بار آخر در مقدار موجود در kubectl.kubernetes.io/last-applied-configuration فیلد spec.replicas وجود نداشت (در سناریو قبل حذفش کرده بودیم) برای همین در مقدار فعلی spec.replicas که توسط HPA پر شده بود تغییری ایجاد نشد. می‌توانیم از این به بعد مشکل را حل شده در نظر بگیریم چرا که با توجه حذف spec.replicas از فایل‌ کانفیگی که در دیپلوی‌ها اعمال می‌شود دیگر مشکل تعارض تعداد replicaی مشخص شده در فایل و HPA را نداریم. همچنین با توجه به این که فیلد spec.replicas در kubectl.kubernetes.io/last-applied-configuration نیز حذف شده دیگر تفاوت آن با فایل کانفیگ اعمالی موجب تک پاد شدن اپلیکیشن نمی‌شود. اما این حل شدن به قیمت یک downtime احتمالی در سناریو دوم حل شد! می‌توانیم بدون این هزینه اضافه نیز این مشکل را حل کنیم. برای همین فرض کنید به قبل از اجرای سناریو دوم برگشتیم و می‌خواهیم بدون پیش آمدن مشکل فیلد spec.replicas را حذف و کنترل آن را به HPA واگذار کنیم.

سناریو چهارم

حالا که نحوه عملکرد apply در حذف فیلدها را می‌دانیم می‌توانیم این مشکل را به راحتی حل کنیم. در سناریو دوم علت به وجود آمدن مشکل مغایرت بین مقدار kubectl.kubernetes.io/last-applied-configuration و فایل اعمالی در مقدار spec.replicas بود که منجر به حذف spec.replicas و استفاده از مقدار پیش فرض آن شد. برای جلوگیری از این اتفاق کافی است تا مقدار spec.replicas را علاوه بر از فایل کانفیگ از kubectl.kubernetes.io/last-applied-configuration نیز حذف کنیم.

برای این کار ساده‌ترین راه استفاده از کامند edit-last-applied در apply است.

kubectl apply edit-last-applied deployment busybox

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

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

در حقیقت ما می‌توانستیم با دستور diff نیز از تغییر پیش آمده در سناریو دوم قبل از اعمال مطلع شویم. فایل سناریو دوم را در نظر بگیرید:

اگر بدون تغییر در kubectl.kubernetes.io/last-applied-configuration دستور diff رو روی این فایل انجام دهیم نتیجه به این شکل خواهد بود:

kubectl diff -f deployment.yaml


...
-  replicas: 8
+  replicas: 1
...
-     - echo v2 && sleep 3600
+     - echo v3 && sleep 3600
...


اما در صورتی که مانند سناریو چهارم عمل کنیم نتیجه diff به این صورت خواهد بود:

...
-     - echo v2 && sleep 3600
+     - echo v3 && sleep 3600
...

در نهایت توانستیم به راحتی تعیین تعداد پادها را بدون هیچ مشکل اضافه به HPA واگذار کنیم.

همان طور که در ابتدای متن نیز اشاره کردم٬ اسنپ با دارا بودن یک کلود بزرگ خصوصی بر پایه کوبرنتیس و با وجود چند ده میکروسرویس که به میلیون‌ها درخواست سفر در طول روز پاسخ می‌دهند یک فضای جذاب برای مواجه شدن با چالش‌های به روز فنی است. اگر دوست داشتید که به این تیم با‌انگیزه ملحق شوید روزمه‌‌تان را به engineering@snapp.cab ارسال کنید.

مراجع و مطالعات بیشتر:

https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale

https://github.com/kubernetes/kubernetes/issues/25238

تولید عکس کدها با سرویس ray.so انجام شده است.