مسئله دو فرمانده

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

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

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

مقدمه

با توجه به مسئله دو فرمانده که مطرح شده نمی توان بطور دقیق متوجه شد آیا پیام به فرمانده دوم رسیده است یا خیر .اجازه دهید کمی آن را به فضای میکروسرویس و نرم افزاری نزدیک کنیم.

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

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

1.خطا  یا قطع بودن شبکه

2.مشکل در Message broker

3.Crash کردن سیستم قبل از ارسال پیام

در واقع نکته ای که وجود دارد این است که بعد از ثبت درخواست مشتری در میکروسرویس  ثبت سفارش  در زمانی که قصد ارسال پیام به صف داریم  به خطا می خوریم مثل اینکه شبکه  به مشکل خورده و ارتباط این میکروسرویس با Message Broker قطعه شده است یا مثلا message broker  در وضعیت stable قرار ندارد  و هیچ ACK از سمت broker دریافت نکنیم و یا این که در زمانی که قصد ارسال پیام داریم سیستم Crash می کند(برق رفتن،خطای سخت افزاری و..) و در نهایت  پیامی ارسال نمی شود.

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

قبل از اینکه به بررسی راه حل بپردازیم باید مواردی را مطرح و بررسی کنیم

Loose coupling

یکی از ویژگی مهم میکروسرویس ها Loose coupling است اجازه دهید قبلا از بررسی این موضوع در میکروسرویس به تعریف این ویژگی بپردازیم:

Loose coupling به این معنا که در طراحی سیستم :

-هر کامپونت دارای ارتباط بسیار ضعیفی با دیگر اجزا می باشد و بنابراین تغییر در هر کامپونتی تاثیری بر موجودیت یا عملکرد کامپونت دیگر نداشته باشد.

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

این اتفاق دارای مزایا و معایب خودش است یعنی با این اتفاق :

-براحتی می توان یک کامپونت را برداشت و کامپونت دیگر جایگزین کرد بودن تاثیر بر دیگر اجزا

-تست کردن ساده تر خواهد شد

-عیب یابی ساده می شود و..

و البته معایب هم دارد زمانی که سیستم رو بصورت اجرای مستقل از یکدیگر طراحی میکنم  بحث ارتباط بین کامپیونتها مطرح خواهد شد در حالت یکپارچه (monolithic)کل فرآیند با هم اجرا می شود(ثبت سفارش و صدور فاکتور در یک سیستم انجام می شود) اما در سیستمی که از کامپونت های که کاملا از هم مجزا هستند بخشی از کار در یک کامپوننت و بخش دیگرد در کامپوننت دیگر(ثبت سفارش در یک میکروسرویس و صدور فاکتور در میکروسرویس صدور فاکتور)و consistency دادها ممکنه است تحت تاثثیر قرار بگیرد(هیچگاه پیام به میکروسرویس صدور فاکتور نرسد) .اگر ما یک ارتش کامل داشته ایم و به دو بخش تقسیم نشده بود نیازی به هماهنگی بین فرمانده ها نبود و ارسال پیام و اطمینان از رسیدن پیام در کار نبود بنابراین همانطور که نگاه می کنید هر انتخاب مزایا و معایب خود را نیز خواهد داشت.و نکات دیگر…

استقلال دادها در میکروسرویس ها

    همانطور که گفته شدن Loose coupling یکی از ویژگی های مهم در معماری میکروسرویس می باشد بنابراین هر میکروسرویس باید بتواند به طور مستقل  دادهای خود رو دخیره و بازیابی و پردازش بکند .یعنی دادهای هر میکروسرویس مربوط به خود آن میکروسرویس است و دیگر میکروسرویس ها نباید هیچ دانش و آگاهی از لایه دادها میکروسرویس  دیگر داشته باشند در صورت نیاز به دادهای میکروسرویس دیگر باید سرویس های میکروسرویس  مورد نظر خود را فراخوانی کنند .

 سه طریق می توان  دادهای هر میکروسرویس را از دیگر میکروسرویس ها مستقل نمود  که به صورت مختصر عبارت است از:

-Private-tables-per-service

در این روش هر میکروسرویس بصورت طبیعی  دارای تعدادی  جدول می باشد(اگر مدل  دیتابیس رابطی باشد) که باید از دسترسی  و دید دیگرمیکروسرویس مخفی باشد یعنی دیگر میکروسرویس ها نباید دسترسی و اطلاعی از آنها داشته باشند   در این روش یک دیتابیس برای کل سیستم که جداول مربوط به تمام مایکروسرویس  ها در آنجا نگهداری  می شود در نظر گرفته شده است  برای استقلال دادهای هر میکروسرویس یک user در سطح دیتابیس  برای میکروسرویس A تعریف می کنیم که فقط دسترسی به جداول خود را خواهد داشت و کاربری که برای میکروسرویس B  تعریف کرده ایم کاملا مستقل از دسترسی میکروسرویس A می باشد و جداول A  رو نمی توانند ببنید.بنابراین هر میکروسرویس بصورت مستقل از دیگر میکروسرویس ها فقط به جداول خود دسترسی خواهد داشت.

-Schema-per-service

این روش هم مانند روش قبل است با این تفاوت در این روش برای هر میکروسرویس یک اسکیمای جداگانه در نظر میگریم که فقط آن میکروسرویس دسترسی به آن دارد.

-Database-server-per-service

در این روش کاملا یک دیتابیس جدا برای هر میکرو سرویس در نظر میگیرم یعنی هر  میکروسرویسی دارای دیتا بیس  مستقل مربوط به خود می باشد در این روش به دلیل اینکه هر میکروسرویس دارای دیتابیس مربوط به خودش است هر میکروسرویس می تواند  براساس نیاز هر نوع دیتابیس با هر تکنولوژی که نیازمند است انخاب کند یک میکروسرویس می تواند دیتابیس sql server انتخاب کند و یک میکروسرویس Mongo  , بر عکس روش قبل میکروسرویس ها مجبور نیستند از یک مدل استفاده کنند. و همچنین بر عکس روش هاس قبلی که یک دیتابیس مشترک وجود داشت و با خطا خوردن و بر روز مشکل در این دیتابیس کل سیستم دچار مشکل شود به عبارت دیگر single point of failure دیگر نخواهیم  نداشت در صورت مشکل دردیتابیس یک میکروسرویس فقط آن بخش از سیستم از مدار خارج خواهد شد و کل سیستم دچار مشکل نخواهد بود.

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

بنابراین ارتباط بین میکروسرویس ها ممکن  است باعث به وجود آمدن inconsistency دادها بشود.مانند  اینکه سفارشی ثبت کنیم اما فاکتوری برای آن صادر نشود که مطرح شد.

چگونه ارتباط بین میکروسرویس ها باعث به وجود آمدن inconsistency  خواهد شد؟

فرض کنید که میکروسرویس ثبت درخواست مشتری یک درخواست را از کلاینت دریافت می کند از طریق Rest service و پس از بررسی درخواست سفارش مشتری  را بر روی دیتابیس خود کامیت می کند سپس برای ثبت فاکتور با فراخوانی سرویسی از میکروسرویس فاکتور بصورت Rest عملیات ثبت فاکتور را انجام می دهد یعنی فرایند بصورت زیر می باشد:

بنظر همه چیز درست است در نگاه اول اما اگر دقیق تر نگاه کنیم  یک وابستگی در این سیستم شکل گرفته است که ماهیت استقلال میکرویس رو زیر سوال می برد میکرویس A به دلیل ماهیت Synchronus بودن  به بالا بودن میکروسرویس B نیاز دارد.اگر به هر دلیلی میکرویس B به دلیل مشکل یا تغییرات پایین باشد ثبت سفارش انجام می شود ولی ثبت فاکتور را نمی توان انجام داد.به این نوع ارتباط  temporal coupling می گویند که برای پردازش این درخواست هر دو Node باید در دسترس باشند به معنی که A برای تکمیل فرایند خود نیاز به B داره و البته اگر همین دو میکرسرویس رو داشته باشیم بنابراین یک وابستگی temporal coupling  شکل گرفته شده است.

برای حل این مسله با اضافه کردن message broker  بین میکروسرویس ثبت سفارش و صدور فاکتور این وابستگی را حذف می کنیم  با این تغییر میکروسرویس A یا همان ثبت سفارش نیازی به دانستن آدرس فیزیکی B  هم نخواهد داشت و وابستگی بین این دو سرویس بصورت مستقیم حذف خواهد شد بنابراین میکروسرویس A بر روی دیتابیس خود کامیت می کند سپس یک پیام بر روی broker قرار می دهد message broker وظیفه داره که پیام را به دست گیرنده برساند میکروسرویس   B یا صدور فاکتور اگر پیامی بر روی خط بود آن را برداشته و فاکتور آن سفارش را صادر می کند و در صورتی که میکروسرویس صدور فاکتور به هر دلیلی فعال نبود  پیام روی صف باقی خواهد ماند  تا B دوباره به چرخه برگردد.

به نظر مشکل  temporal coupling  بین میکروسرویس A و B  برطرف شده است  اما بهتر است دقیق تر نگاه کنیم در واقع ارتباط بین میکروسرویس A و message broker یک ارتباط مستقیم (synchronous communication)  شکل داده است یعنی اگر Message Broker در دسترس نباشد به هر دلیل که مطرح شد میکروسرویس A نمی تواند درست کار بکند و نیازمند به Message Broker خواهد بود و باز هم  یک وابستگی temporal coupling شکل گرفته است. یعنی تا زمانی که ما از Message Broker یک ACK مبنی بر دریافت  پیام دریافت نکنیم نمی توان گفت عملیات در A انجام شده است  و A نیازمند این ACK خواهد بود.

هر چند ابزار های message broker  بشدت قوی و مطمن هستن اما به هر حال downtime  وجود دارد بدون شک در هر سیستمی.و.ممکن است یک node دچار مشکل باشد و بر مدتی در وضعیت  unstable  قرار داشته باشد و حتی مجبور به reset شدن بشویم و زمانی طول خواهد کشید تا دوباره به مدار برگردیم.

بنابراین ما شبیه فرمانده اول شده ایم که پیام را ارسال می کنیم اما نمی توان مطمن بشویم که پیام ما حتما خواهد رسید.

می توان با ارسال پیام مجدد در صورت عدم دریافت  پاسخ شانس رسیدن پیام را افزایش دهیم (یعنی اینکه دوباره یک پیام ارسال کنیم بسمت فرمانده دوم)در شکل زیر نگاه کنید ایتدا یک ریکوست می دهیم در صورتی که به خطا برخورد کنیم با ارسال مجدد در خواست سعی میکنم که پیام را به دست میکروسرویس B برسانیم البته شکل زیر در حالت sync می باشد.

همانطور که در شکل بالا مشاهده می کنید request  پیام اول به خطا می خوره سپس پیام دوم با موفقیت به دست میکرو سرویس B می رسد و زمانی که میکرویس B در حال پاسخ دادن به A می باشد به هر دلیل به  خطا میخورد و پیام به دست میکروسرویس A نمی رسد ( ثبت فاکتور انجام می شود اما در زمان پاسخ به میکروسرویس A به خطا میخورد بنابراین یک فاکتور ثبت کردیم اما پیام انجام به میکروسرویس ثبت سفارش نمی رسد). سپس میکروسرویس  A  دوباره ریکوست رو ارسال می کند اینار پیام موفق میگیرد اما مشکلی که به وجود آمده دوبار پیام به میکروسرویس B رسیده است (فاکتور دوبار صادر می شود ).مشکل ارسال پیام مجدد را می توان در حالت Message broker نیز داشت و این تحدید پیام تکراری نیز وجود دارد هر چند که بعضی از message broker ها پیام تکراری را مدیریت می کندم مانند kafka.

به دنبال راه حل برای حل همزمانی ثبت سفارش و ارسال مطمن پیام

روش اول

یک روش می تواند بدین شکل باشد که ابتدا در خواست ثبت مشتری رو دریافت کنیم قبل از ثبت سفارش مشتری در میکروسرویس ثبت سفارش  پیام  صدور فاکتور را بر روی  message broker  قرار دهیم در صورت دریافت ACK  که پیام بر روی خطا قرار گرفته است   حالا با اطمینان از قرار گرفتن صدور فاکتور در صف اقدام ثبت سفارش کنیم.راه حل خوبی نیست چرا؟

به این دلیل اگر ما با موفقیت پیام صدور فاکتور را بر روی صف قرار دادیم و ACK مثبت هم دریافت کردیم  سپس اقدام به ثبت سفارش مشتری نمایم اگر به هر دلیل در هنگام ثبت سفارش مشتری به خطا خوردیم و تراکنش rollback شد و از سمت دیگر میکروسرویس صدور فاکتور  پیام را دریافت کرده و  فاکتوری برای سفارشی که ثبت نشده است صادر شده است. پس این روش راه حل مناسب و مطمنتی نیست.

روش دوم

در این روش سفارش را از کلاینت دریافت می کنیم  و یک transaction باز می کنیم و دادها را بر روی دیتابیس خود ثبت می کنیم  اما کامیت نمی کنیم سپس پیام را بر روی خطا قرار می دهیم اگر ACK مثبت از سمت Message Broker دریافت کردیم transaction را کامیت می کنیم در غیر اینصورت transacstion را rollback می کنیم.خوب به نظر موفق شدیم اما این روش در حالتی که حجم درخواست زیاد باشد و ما مجبور به باز نگه داشتن transaction هستیم تا پاسخی از سمت broker دریافت کنیم اگر Broker به هر دلیل کند باشد و دیر پاسخ دهد و حجم ریکوست زیاد باشد کلی transaction باز خواهیم داشت و ممکن است با کمبود منابع مواجع شویم سیستم Crash کند و نرم افزار ازدسترس خارج شود .

مروری دیگر بر مسئله :

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

Transactional outbox

این الگو  این  مسله را بصورت زیر حل می کند:

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

در مرحله دوم یک یک پردازش که در background همیشه در حال اجرا است به این جدول نگاه می کند که آیا پیامی  برای ارسال دارد اگر پیامی برای ارسال وجود داشته باشد پیام را برای میکروسرویس بعدی ارسال می کند و در صورت گرفتم ACK از سوی Broker  وضعیت پیام را به ارسال شده تغییر می دهد  و در غیر اینصورت که هیچ.اگر به هر دلیلی worker پایین باشد در راند بعدی که فعال می شود این پیام را دوباره ارسال خواهد کرد.

 در این روش بر عکس راه حل های که مطرح شد هم ثبت سفارش و هم پیامی که برای میکروسرویس صدور فاکتور ارسال خواهد در یک transaction  ثبت می کنیم حالا اگر به هر دلیلی سیستم crash کند پیام ثبت شده است و یک background job  پیام را ارسال خواهد کرد حتی اگر سیستم ریست شود بعد از اینکه سیستم به حالت نرمال برگردد پیامی در حافظه نبوده که جایی نگرانی باشد بلکه بر روی جدولی  در دیتابیس ذخیره شده است بنابراین جاب شروع به ارسال پیام های ارسال نشده خواهد نمود .در روش اول ابتدا پیام را ارسال می کردم و بعد ثبت سفارش میکردیم که توضیح داده شد که  ممکن است فاکتوری صادر شود که سفارش آن ثبت نشده است  اما در این روش این مشکل وجود ندارد چون هم پیام ارسالی و هم ثبت سفارش در یک transaction ثبت می شوند امکان چنین خطای وجود ندارد  و یک جاب وظیفه ارسال پیام و اطمینان از قرار گرفتن را بر روی صف تامین می کند پس مشکل روش اول حل خواهد شد.

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

سوالی که الان مطرح می شود این است که باز امکان ارسال پیام تکراری مطرح می شود در وضعتی که ما قصد آپدیت پیام ارسال شده را  ازوضعیت ارسال نشده به ارسال شده داشته باشیم سیستم به مشکل بخورد و crash کنیم حالا وضعیت پیام در حالت ارسال نشده باقی خواهد ماند و راند بعدی خواندن پیام های ارسال نشده این پیام دوباره ارسال خواهد شد کاملا درست رفع این مشکل باید در سطح BROKER و یا میکروسرویس که پیام را دریافت می کند بر طرف بشود.

.

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

اما این روش شامل معایبی نیز خواهد بود

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

البته شما می توانید از ابازرهای مثل Debezium  نیز استفاده کنید.توضیح در مورد این ابزار به زمانی دیگر اختصاص خواهیم داد.

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

Transactional Inbox

با الگوی outbox  متوجه شدم حداقل برای یک بار پیام صدور فاکتور را بروی   broker قرار خواهیم داد  و در صورتی که میکروسرویس صدور فاکتور بدون مشکل در حال کار باشد فاکتور مشتری صادر خواهد شد.

در نظر بگیرید که میکروسرویس ثبت درخواست مشتری باید از صدور و یا عدم صدور فاکتور از سوی میکروسرویس صدور فاکتورآگاه گردد به این معنی دیگر قرار گرفتن درخواست ثبت شده درصف کافی نیست و فرمانده اول باید از نتیجه پیام ارسالی خود به فرمانده دوم آگاه شود. (این موضوع مثلا در الگوی saga اهمیت دارد ).

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

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

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

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

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

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

حالا باید با فرض دیگری پیش برویم فرض کنید که بصورت sync  درخواست صدور فاکتور را برای میکروسرویس فاکتور ارسال می کنیم به دلایل مختلف اعم از پیچیده بودن  این پیام و یا کند بودن سرور و هر دلیلی دیگر که باعث  بالا بردن زمان پاسخ شود  میکروسرویس ثبت سفارش پس از مدتی به خطایی  timeout  خواهد خورد بنابراین در این حالت نیز میکروسرویس ثبت سفارش با فرض اینکه فاکتوری ثبت نشده است اقدام به ارسال مجدد پیام می نمایید باز هم فاکتور تکراری و ریکوست تکراری و تحدیدی دیگر در سیستم .

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

همانطور که در شکل زیر مشاهد می کنید ممکن به دلیل timeout شدن ما دو فاکتور برای سفارش ثبت شده صادر کرده ایم.

حل این مشکلات توسط الگوی INBOX

الگوی inbox خیلی شبیه outbox می باشد.بدین صورت ابتدا یه جدول مانند  Outbox ایجاد می کنیم برای دستور صدور فاکتورهای ارسالی از سوی میکروسرویس ثبت سفارش  بعد از اینکه یک پیام دریافت شد ما شروع به پردازش نخواهیم کرد ابتدا پیام را ذخیره خواهیم و ACK در تایید دریافت پیام ارسال خواهیم کرد.در قدم بعدی یک پردازش دیگر درخواست صدور فاکتور را از این جدول برداشته و شروع به صدور فاکتور برای سفارش وارد شده می نمایید پس از پایان کار وضعیت این درخواست را در جدول Inbox به انجام شده تغییر میدهیم (در یک transaction).

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

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

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

الگوی صندوق ورودی می تواند در مواردی که ترتیب پیام ها مهم است بسیار مفید باشد.

گاهی اوقات ترتیب پردازش پیام ها اهمیت خواهد داشت بنابراین با کمک این روش می توان ترتیب پردازش پیام ها رو مدیریت کرد البته هر چند که مثلا Kafka این مسله را مدیریت می کند اما بعصی از broker ها شاید توانایی  مدیریت این مسله را نداشته باشند و یا فرض کنید  که فرایند صدور فاکتورها باید بر اساس ترتیب شماره انجام شود یعنی اول فاکتور با شمار یک صادر شود و فاکتورهای بعدی به ترتیب صادر گردد . البته  باید  در نظر داشت  فاکتورها باید با شناسه ای قابلیت تشخیص ترتیب داشته باشند به این صورت می توان ترتیب پیامی های ارسالی را  متوجه شویم.بنابراین با این الگو می توانید ترتیب اجرا را رعایت کنید که در پیام های که دست ما نرسیده است کدامیک هستن مثلا 1و2و3و5 دست ما رسیده است و هنظوز 4 را دریافت نکرده ایم بنابراین می توان ترتیب را تشخیص داد و منتظر رسیدن دستور پرداخت 4 به عنوان پیامی که دست ما نرسیده است را تشخیص  داد.

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

دوستان خوبم امیدوارم لذت برده باشید و خوشحالم میشم که در این مورد با هم دانشمون رو به اشتراک بگذارید

https://Github.com/LotfiAli

LotfiAliDev@gmail.com

منبع:

https://softwaremill.com/microservices-101/

https://learn.microsoft.com/en-us/azure/architecture/best-practices/transactional-outbox-cosmos

https://microservices.io/patterns/data/database-per-service.html

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

https://analica.ir/byzantine-generals-problem/

دسته بندی شده در:

برچسب ها: