ساختن سیستمهای ارتجاعی: الگوی مجدد را در میکروسرویسها امتحان کنید

معرفی
هر برنامه کاربردی که با منابع دیگر از طریق شبکه ارتباط برقرار می کند، باید در برابر خرابی های گذرا مقاوم باشد.
این شکست ها گاهی اوقات خود اصلاح می شوند. به عنوان مثال، سرویسی که هزاران درخواست همزمان را پردازش میکند، میتواند الگوریتمی را برای رد موقت درخواستهای دیگر تا زمانی که بار آن کاهش یابد، پیادهسازی کند. برنامهای که میخواهد به این سرویس دسترسی پیدا کند ممکن است در ابتدا موفق نشود، اما اگر دوباره تلاش کند ممکن است موفق شود.
هنگام طراحی هر سیستمی، مقاوم سازی آن در برابر چنین خرابی ها ضروری است. در این مقاله، یکی از راه های دستیابی به آن را با استفاده از آن بررسی خواهیم کرد دوباره تلاش می کند.
در سازمان کنونی ما، ما از این مکانیسم در سراسر میکروسرویسهای خود استفاده میکنیم تا اطمینان حاصل کنیم که خرابیها را مدیریت میکنیم و در عین حال بهترین خدمات خود را به مشتریان خود ارائه میکنیم.
بیایید ابتدا تعریف کنیم که منظورمان از شکست چیست.
شکست چیست؟
هنگامی که سرویس های ما از طریق شبکه با یکدیگر در ارتباط هستند، خرابی ها می تواند به دلایل متعددی ایجاد شود. چند نمونه از انواع خرابی عبارتند از:
-
پاسخ آهسته / اصلاً پاسخی نیست
-
پاسخی با فرمت نادرست
-
پاسخی که حاوی داده های نادرست است
در برنامه ریزی برای شکست، باید به دنبال مدیریت هر یک از این خطاها باشیم.
دوباره امتحان کنید
سعی مجدد فرآیندی است برای تکرار خودکار یک درخواست در صورت تشخیص هر گونه شکست. این کمک می کند تا خطاهای کمتری به کاربران برگردانده شود و تجربه مصرف کننده در برنامه ما بهبود یابد.
تنها نکته ای که در مورد تلاش مجدد به میان می آید این است که درخواست های متعدد به یک منبع باید تأثیری مشابه ایجاد یک درخواست واحد داشته باشد، یعنی منبع باید باشد. ناتوان.
در API های REST،
GET
،HEAD
وOPTIONS
روشها معمولاً وضعیت منبع روی سرور را تغییر نمیدهند و از این رو اغلب قابل امتحان مجدد هستند.
چه زمانی باید درخواست را دوباره امتحان کنیم؟
در حالت ایدهآل، تنها زمانی باید یک درخواست ناموفق را دوباره امتحان کنیم که بدانیم در دفعه بعد امکان موفقیت آن وجود دارد، در غیر این صورت فقط منابع (CPU، حافظه و زمان) هدر میرود.
به عنوان مثال، زمانی که سرور در حین اتصال به میزبان DB دچار مشکل غیرمنتظره ای شد، ممکن است با کد وضعیت 503 (سرویس در دسترس نیست) با خطا مواجه شوید. اگر تماس دوم با سرویس بالادستی یک نمونه DB را دریافت کند که در دسترس است، ممکن است تلاش مجدد در اینجا کارساز باشد.
از سوی دیگر، تلاش مجدد برای خطاهایی با کد وضعیت 401 (غیر مجاز) یا 403 (ممنوع) ممکن است هرگز کارساز نباشد زیرا نیاز به تغییر خود درخواست دارد.
ایده کلی این است که اگر یک درخواست بالادستی با شکست مواجه شد، بلافاصله دوباره تلاش می کنیم و احتمالاً درخواست دوم موفق خواهد شد. با این حال، این همیشه به نفع ما نیست. پیاده سازی واقعی باید شامل تاخیر بین درخواست های بعدی باشد. در بخش های بعدی به آن خواهیم پرداخت.
مشکل با تکرارهای معمولی
در نظر بگیرید که چه اتفاقی میافتد وقتی 100000 درخواست همزمان دریافت میکنیم و همه میزبانهای سرویس بالادستی در آن نمونه از کار میافتند. اگر بخواهیم بلافاصله دوباره امتحان کنیم، این 100000 درخواست ناموفق بلافاصله در یک بازه ثابت دوباره امتحان میشوند. آنها همچنین با درخواست های جدید ناشی از افزایش ترافیک ترکیب می شوند و می توانند دوباره سرویس را از کار بیاندازند.
این چرخهای ایجاد میکند که تا زمانی که تمام تلاشهای مجدد تمام شود، تکرار میشود و همچنین در نهایت سرویس بالادستی را تحت تأثیر قرار میدهد و ممکن است سرویسی را که در حال حاضر تحت فشار است، کاهش دهد. این یک مشکل رایج علوم کامپیوتر است که به عنوان مشکل گله رعد نیز شناخته می شود.
برای مقابله با این موضوع، میتوانیم در تلاش مجدد یک درخواست ناموفق مقداری تاخیر ایجاد کنیم.
بکش کنار
زمان انتظار بین یک درخواست و تلاش مجدد بعدی آن را backoff می نامند.
با عقب نشینی، با افزایش تعداد درخواست های تلاش مجدد، مدت زمان بین درخواست ها به طور تصاعدی افزایش می یابد.
سناریویی که در بالا به آن پرداختیم، جایی است که داشتن یک عقبنشینی کمک میکند، زمان انتظار بین تلاشها را بر اساس تعداد شکستهای قبلی تغییر میدهد.
از شکل بالا، فرض کنید درخواست اولیه در 0 میلی ثانیه می رود و با یک کد وضعیت قابل امتحان مجدد با شکست مواجه می شود. با فرض اینکه زمان بازگشت 200 میلی ثانیه را تنظیم کرده ایم و زمان درخواست پاسخ را نادیده می گیریم، اولین تلاش مجدد در 200 میلی ثانیه (1*200 میلی ثانیه) انجام می شود. اگر دوباره با شکست مواجه شد، دومین تلاش مجدد در 400 میلیثانیه (200*2) انجام میشود. به طور مشابه، تلاش مجدد بعدی در 800 میلی ثانیه اتفاق می افتد تا زمانی که تمام تلاش های مجدد را تمام کنیم.
اگر هر یک از شکستهای درخواستی ناشی از بارگیری بیش از حد سرویس بالادستی باشد، این مکانیسم گسترش درخواستها و تلاشهای مجدد به ما شانس بیشتری برای دریافت پاسخ موفقیتآمیز میدهد.
عصبانیت
استراتژی Backoff به ما اجازه می دهد تا بار ارسال شده به سرویس های بالادستی را توزیع کنیم. با این حال، معلوم میشود که همیشه عاقلانهترین تصمیم نیست، زیرا همه تلاشهای مجدد همچنان همگام هستند که میتواند منجر به افزایش در سرویس شود.
جیتر فرآیند شکستن این هماهنگی با افزایش یا کاهش تاخیر عقبنشینی برای پخش بیشتر بار است.
به مثال قبلی خود برگردیم، فرض کنید سرویس بالادستی ما حداکثر باری را که میتواند انجام دهد ارائه میکند و چهار مشتری جدید درخواستهای خود را ارسال میکنند که با شکست مواجه میشوند زیرا سرور نمیتواند آن مقدار از درخواستهای همزمان را مدیریت کند. تنها با اجرای backoff، فرض کنید بعد از 200 میلی ثانیه، هر 4 درخواست ناموفق را دوباره امتحان کنیم. اکنون، این درخواستهای امتحان مجدد نیز به همین دلیل دوباره با شکست مواجه میشوند. برای جلوگیری از این امر، اجرای سعی مجدد باید تصادفی باشد.
پیاده سازی
با مرور این مستندات AWS که به عمق الگوریتم عقبنشینی نمایی میرود، مکانیسم امتحان مجدد خود را با استفاده از رهگیر Axios پیادهسازی کردهایم.
مثال در داخل یک برنامه Nodejs نوشته شده است، اما بدون توجه به اینکه از کدام چارچوب جاوا اسکریپت استفاده می کنید، فرآیند مشابه خواهد بود.
در این مورد، ما تنظیمات زیر را انتظار داریم:
مجموع تلاش های مجدد تعداد حداکثر تلاش مجددی که می خواهید قبل از بازگرداندن پاسخ ناموفق به مشتری.
کدهای وضعیت را دوباره امتحان کنید کدهای وضعیت HTTP که می خواهید دوباره امتحان کنید. به طور پیش فرض، ما آن را برای همه کدهای وضعیت >=500 روشن نگه داشته ایم.
بکش کنار این حداقل زمانی است که باید در هنگام ارسال درخواست مجدد بعدی منتظر بمانیم.
axios.interceptors.response.use(
async (error) => {
const statusCode = error.response.status
const currentRetryCount = error.response.config.currentRetryCount ?? 0
const totalRetry = error.response.config.retryCount ?? 0
const retryStatusCodes = error.response.config.retryStatusCodes ?? []
const backoff = error.response.config.backoff ?? 100
if(isRetryRequired({
statusCode,
retryStatusCodes,
currentRetryCount,
totalRetry})
){
error.config.currentRetryCount =
currentRetryCount === 0 ? 1 : currentRetryCount + 1;
// Create a new promise with exponential backoff
const backOffWithJitterTime = getTimeout(currentRetryCount,backoff);
const backoff = new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, backOffWithJitterTime);
});
// Return the promise in which recalls Axios to retry the request
await backoff;
return axios(error.config);
}
}
);
function isRetryRequired({
statusCode,
retryStatusCodes,
currentRetryCount,
totalRetry}
){
return (statusCode >= 500 || retryStatusCodes.includes(statusCode))
&& currentRetryCount < totalRetry;
}
function getTimeout(numRetries, backoff) {
const waitTime = Math.min(backoff * (2 ** numRetries));
// Multiply waitTime by a random number between 0 and 1.
return Math.random() * waitTime;
}
هنگام ایجاد یک درخواست Axios، باید مطمئن شوید که متغیرها را در تنظیمات درخواست اضافه کنید:
const axios= require('axios');
const sendRequest= async () => {
const requestConfig = {
method: 'post',
url: 'api.example.com',
headers: {
'authorization': 'xxx',
},
data: payload,
retryCount : 3,
retryStatusCodes: ['408', '429'],
backoff: 200,
timeout: 5000
};
const response = await axios(requestConfig);
return response.data;
}
هنگام پیکربندی مکانیسم تلاش مجدد، تنظیم کل تلاش های مجدد و حداکثر تاخیر با هم مهم است. هدف این است که این ارزشها را با در نظر گرفتن بدترین زمان پاسخگویی به مصرفکنندگانمان تنظیم کنیم.
به صورت تصویری این روش کار خواهد کرد:
برای برنامه های مبتنی بر جاوا، همین کار را می توان با استفاده از resilience4j انجام داد
نتیجه
در این پست، ما به یکی از مکانیسم های متعدد قابلیت اطمینان سرویس نگاه کردیم: تلاش مجدد. ما دیدیم که چگونه کار می کند، چگونه آن را پیکربندی کنیم و با برخی از مشکلات رایج در تلاش های مجدد با استفاده از backoff و jitter مقابله کنیم.
امیدوارم برای شما مفید بوده باشد. نظرات یا اصلاحات همیشه پذیرفته می شود.
لینک GitHub برای کد منبع بالا را می توانید در اینجا پیدا کنید