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

برنامه‌های "cloud-native" - قسمت سوم Queues

در قسمت قبل در مورد مدیریت نشست‌ها در برنامه‌های cloud-native صبحت کردیم. در قسمت سوم در مورد استفاده از صف‌ها یا Queues برای برقراری ارتباط ناهمگام یا Async بین سرویس‌های مختلف صحبت خواهیم کرد.

استفاده از صف‌ها

مهم‌ترین الگو در “loose coupling” تحویل غیر همزمان یا asynchronous یا اختصارا async درخواست‌هایی است که از سمت کاربر (لایه‌ی وب) برای لایه‌ی سرویس back-end ارسال می‌شود و نیاز به پردازش دارد. “loose coupling” طبق تعریف ویکی‌پدیا یعنی سیستمی که هر یک از اجزای آن از هیچ یا اندکی آگاهی نسبت به مشخصات سایر مؤلفه‌های مجزای آن برخوردار هستند. برای مطالعه‌ی بیشت به لینک زیر مراجعه کنید:

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

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

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

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

نکته‌ی مهم این است که این الگو در پاسخ به درخواست صفحه‌های read-only استفاده نمی‌شود، و برای به روز رسانی باید از آن استفاده کرد.

چه زمانی از الگوی Queue-Centric استفاده می‌شود؟

اپلیکیشن به لایه‌های مختلفی تقسیم (decoupled) شده است، اما لایه‌ها نیاز دارند که با یکدیگر در ارتباط باشند.

اپلیکیشن باید تضمین کند که پیام‌ها حداقل یکبار مورد پردازش قرار می‌گیرند (at-least-once processing).

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

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

چگونگی استفاده از صف‌ها

گفتیم که الگوی مبتنی بر صف در وب اپلیکیشن‌ها مورد استفاده قرار می‌گیرد تا ارتباط بین لایه‌ی وب (که رابط کاربری را پیاده‌سازی می‌کند – front)، و لایه‌ی سرویس (که “business-logic” را پیاده‌سازی می‌کند – back-end) اصطلاحا “decouple” شود. برای اطلاعات بیشتر در مورد معماری چند لایه می‌توانید به لینک زیر مراجعه کنید:

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

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

راه‌حل این مشکل، استفاده از ارتباط‌های async است. در این حالت، لایه‌ی وب commandای را برای لایه‌ی سرویس ارسال می‌کند، که command درخواست برای انجام کاری است. برای مثال، ایجاد یک کاربر جدید، اضافه کردن یک عکس،‌به روز رسانی status در یک رسانه‌ی اجتماعی، رزرو کردن اتاق هتل و ...

همان‌طور که قبلا هم اشاره شد، این commandها با استفاده از یک صف برای لایه‌ی سرویس ارسال می‌شوند. صف هم که یک ساختار داده‌ای ساده با دو عمل اساسی است: “add” و “remove”. ترتیب برداشتن‌ پیام‌ها از صف به صورت “FIFO” است. به عمل اضافه کردن پیام به صف “enqueueing” و به عمل برداشتن پیام از صف “dequeueing” گفته می‌شود.

در این حالت فرستنده و گیرنده اصطلاحا “loosely coupled” هستند. فرستنده لزوما رابط کاربری لایه‌ی وب نیست، و برای مثال می‌تواند یک موبایل اپلیکیشن باشد که از طریق REST API درخواست‌هایش را ارسال می‌کند.

مدل برنامه‌نویسی سمت گیرنده

مدل کلی به شکل زیر خواهد بود:

  • پیام بعدی را از روی صف بردار – dequeue.
  • پیام را پردازش کن.
  • پیام را از روی صف حذف کن – delete.

در این پیاده‌سازی ابتدا پیام “dequeue” می‌شود، و سپس “delete” می‌شود. این فرآیند تضمین می‌کند که حداقل یکبار پیام پردازش می‌شود. مثلا اگر failureای در سطح سخت‌افزار در هنگام پردازش پیام رخ دهد، پیام باز هم در صف وجود دارد و توسط نود دیگری پردازش خواهد شد. اما چگونه می‌توان از پردازش دوباره‌ی پیامی که “dequeue” شده است جلوگیری کرد؟ در واقع هنگامی که پیام “dequeue” می‌شود از صف حذف نمی‌شود، و برای مدت مشخصی به صورت پنهان در می‌آید. به این مدت زمان “invisibility window” گفته می‌شود. بنابراین هنگامی که یک پیام در “invisibility window” باشد، نمی‌توان آن را دوباره “dequeue” کرد.

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

حالت دیگری که ممکن است دو پیام همزمان مورد پردازش قرار بگیرند در رابطه با “Eventual Consistency” است که در قسمت‌های آینده مورد بررسی قرار خواهد گرفت.

مفهوم Idempotency در صف‌ها

عمل “idempotent” به عملی گفته می‌شود که بتواند تکرار شود، به طوری که هر چند بار اجرایِ موفقِ آن با یکبار اجرای موفق قابل تمایز نباشد. برای مثال verbهای پروتکل HTTP مانند PUT، GET، و DELETE همگی “idempotent” هستند. فرقی نمی‌کند که یک resource را یکبار یا صد بار با HTTP DELETE حذف کنیم؛ نتیجه یکسان است: آن resource از بین رفته است.

بعضی از عملیات‌ها به صورت ذاتی “idempotent” هستند، برای مثال همین HTTP DELETE. اما بعضی از عملیات‌های دیگر، برای مثال انتقال پول از یک حساب به حساب دیگر، مسلما به صورت ذاتی “idempotent” نیستند، ولی می‌توان کاری کرد دارای این ویژگی شوند.

سرویس‌های ابری صف، تعداد باری که یک پیام “dequeue” می‌شود را نگهداری می‌کنند. هر زمانی که یک پیام “dequeue” شود، سرویسِ صف این مقدار را همراه با پیام ارائه می‌کند. به این مقدار “dequeue count” گفته می‌شود. اولین باری که پیام “dequeue” شود این مقدار ۱ خواهد شد. اپلیکیشن می‌تواند با بررسی کردن این مقدار متوجه شود که آیا اولین باری است که پیام مورد پردازش قرار می‌گیرد یا نه.

کنترل Poison messages

برخی از پیام‌ها را به دلیل محتوایشان نمی‌توان پردازش کرد. به این پیام‌های “Poison” گفته می‌شود. فرض کنید یک پیام حاوی commandای برای ایجاد یک کاربر جدید بر اساس آدرس ایمیل باشد. اگر مشخص شود که آدرس ایمیل قبلا ثبت شده است، اپلیکیشن شما باز هم قادر خواهد بود که پیام را به صورت موفق پردازش کند، هرچند کاربر جدیدی در سیستم ثبت نخواهد شد. در این حالت، این یک پیام Poison نیست.

اما اگر آدرس ایمیل شامل یک استرینگ 10,000 کاراکتری باشد، و این مورد در کد برنامه پیش‌بینی نشده باشد، ممکن است منجر به crash کردن برنامه‌ی شما شود. در این حالت، این یک پیام Poison است.

بنابراین اگر برنامه‌ی شما در حین پردازش یک پیام Poison دچار crash شود، در نهایت مدت “invisibility window” خواهد گذشت، و پیام دوباره در صف برای پردازش بعدی ظاهر خواهد شد.

همانطور که گفته شد، هنگامی که یک پیام “dequeue” شود، سرویس ابری صف یک شمارشگر “dequeue count” ارائه می‌کند، که توسط آن می‌توان متوجه شد که آیا این اولین تلاش برای پردازش پیام است یا نه. بنابراین در کد برنامه باید منطقی برای تشخیص پیام Poison بر اساس این شمارشگر وجود داشته باشد. به این ترتیب که اگر یک پیام برای بار Nام در صف ظاهر شد، به عنوان پیام Poison شناخته شود. انتخاب مقدار N یک تصمیم بیزنسی است، زیرا باید بین هزینه‌‌ای که برای پردازش پیام‌های Poison هدر می‌رود، و ریسک اینکه یک پیام معتبر از دست برود، تعادل به وجود آورد.

چالش بعدی در رابطه با نحوه‌ی برخورد با پیامی است که به عنوان یک Poison message تشخیص داده شده است. راهکار اول می‌تواند این باشد که یک فرد این پیام‌ها را مورد بررسی بیشتر قرار دهد، تا روش‌های کنترل آن بهبود پیدا کند. روش دوم استفاده از مفهوم “dead letter queue” است، که جایی است که پیام‌هایی که به صورت معمول نمی‌توان آن‌ها را پردازش کرد در آن قرار می‌گیرند. روش دیگر این است که اگر پیام‌ها ارزش کمی داشته باشند، کلا آن‌ها را حذف کنیم. اما نکته‌ی اصلی در بین تمام این روش‌ها این است که به محض اینکه یک Poison message شناسایی شد، آن را از صف حذف کنیم.

cloudqueueidempotencyasynchronousredis
شاید از این پست‌ها خوشتان بیاید