برنامه نویسی

صندوق خروجی تراکنش: از ایده تا منبع باز

Summarize this content to 400 words in Persian Lang
سلام! میشا مرکوشین اینجا، سرپرست تیم تیم روبی پلتفرم در SberMarket Tech. ما خدمه پشت کتابخانه های داخلی و بهبود معماری میکروسرویس برای همه چیز روبی هستیم. این مقاله به الگوی صندوق خروجی تراکنش و ابزاری می پردازد که ما ساختیم و به طور مکرر در داخل آن را توسعه دادیم که اخیراً در جهان منتشر کرده ایم. این چالش حصول اطمینان از تحویل پیام قابل اطمینان و منسجم از برنامه شما را برطرف می کند و تضمین می کند که پیام ها تنها پس از تکمیل موفقیت آمیز تراکنش پایگاه داده ارسال می شوند.

تلاش برای تحویل قابل اعتماد

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

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

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

الگوی صندوق خروجی را وارد کنید

الگوی صندوق خروجی تراکنش یک الگوی معماری است که در سیستم های توزیع شده برای اطمینان از تحویل مطمئن پیام استفاده می شود. این کار با ماندگاری پیام‌ها به یک فروشگاه داده (معمولاً یک جدول “صندوق خروجی” در پایگاه داده) قبل از اینکه در نهایت به یک کارگزار پیام تحویل داده شود، کار می‌کند. این رویکرد یکپارچگی داده ها را تضمین می کند – یا همه چیز متعهد است، یا در صورت بروز خطا، کل عملیات به عقب برمی گردد.در اینجا نحوه کار آن آمده است:

ایجاد سفارش و ماندگاری: هنگامی که برنامه درخواست ایجاد سفارش را دریافت می کند، یک تراکنش پایگاه داده را باز می کند. جزئیات سفارش ذخیره می شوند و در همان تراکنش، پیامی برای سرویس دیگر تولید می شود و در جدول اختصاصی “صندوق خروجی” در پایگاه داده باقی می ماند. سپس معامله انجام می شود.

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

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

جدول صندوق خروجی معمولاً شامل فیلدهای زیر است:

کلید اصلی

ظرفیت ترابری (بدنه پیام)

وضعیت ها برای ردیابی وضعیت فعلی پیام

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

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

در اینجا یک نمونه کد ساده آورده شده است:

Order.transaction do
order.complete!
OrderOutboxItem.create(order)
end

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

زمان استفاده از الگوی صندوق خروجی

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

مثال 1: ردیابی موقعیت مکانی در زمان واقعی (نیاز به قابلیت اطمینان پایین)

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

مثال 2: قرار دادن سفارش تحویل غذا (الزامات با قابلیت اطمینان بالا)

مشتری برای تحویل پیتزا سفارش می دهد. رستوران برای شروع آماده سازی غذا باید سریعا سفارش را دریافت کند. در اینجا، پیام رسانی به موقع و قابل اعتماد بسیار مهم است:

تاخیر در دریافت سفارش: منجر به مشتری ناراضی می شود که پیتزای خود را دیر دریافت می کند.

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

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

سفر خروجی ما

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

پخش جریانی تنظیمات فروشگاه

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

یک فروشگاه می تواند باز یا بسته باشد.
یک فروشگاه ممکن است ساعات کاری خاصی داشته باشد.

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

اولین حمله ما

برای آزمایش مفهوم خود، ما مسیر کمترین مقاومت را انتخاب کردیم: یک کار Rake که توسط یک کار Cron آغاز می شود. ایده ساده بود: در هر دقیقه، این کار داده‌های مربوط به سفارش‌های اخیراً ارسال شده و هر سفارشی را که قبلاً ارسال نشده بود (مثلاً به دلیل خطاهای شبکه) جمع‌آوری می‌کرد و آنها را به صف کارگزار پیام تحویل می‌داد.

task :publish_store do
StoreOutboxItem.pending.find_each(&:publish!)
end

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حالی که برای اثبات مفهوم کاربردی است، این رویکرد دارای اشکالاتی بود:

زمان های آهسته راه اندازی برنامه: پردازش حجم زیادی از داده ها در هنگام راه اندازی برنامه منجر به افزایش زمان بوت شد.

عدم مقیاس پذیری: ماهیت تک رشته ای این راه حل، توانایی آن را برای مدیریت کارآمد حجم داده های رو به رشد محدود می کند.

گامی به سوی کارایی

راه اندازی یک فرآیند Ruby جدید برای هر کار ناکارآمد بود. Schked را وارد کنید، یک زمان‌بندی کار که به ما امکان می‌دهد یک کار تکراری را برای ارسال پیام به کارگزار در هر دقیقه برنامه‌ریزی کنیم. سپس سیدیق به این وظایف رسیدگی می کرد.

every “1m” do
StoreOutboxItemsPublishJob.enqueue
end

class StoreOutboxItemsPublishJob < ApplicationJob
def perform
StoreOutboxItem.pending.find_each(&:publish!)
end
end

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

برای کاهش این موضوع، Sidekiq را در حالت تک رشته ای اجرا کردیم و از اجرای متوالی وظایف اطمینان حاصل کردیم. این موضوع مشکل همزمانی را حل کرد اما توانایی ما را برای استفاده از پتانسیل کامل سیدیک برای پردازش موازی محدود کرد.

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

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

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

every “1m” do
PARTITIONS_COUNT.times do |partition|
StoreOutboxItemsPublishJob.enqueue(partition)
end
end

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

همپوشانی شغل

همانطور که بحث شد، مکانیسم فعلی رسیدگی به وظایف ما از تجمع و همپوشانی شغلی بالقوه رنج می برد. این می تواند سیستم را تحت الشعاع قرار دهد و از پردازش همه رویدادها در زمان تعیین شده جلوگیری کند. برای اطمینان از پردازش کار روان‌تر و مطمئن‌تر، می‌توانیم از موارد محبوب استفاده کنیم sidekiq-uniq-jobs گوهر اگر کار قبلی با همان شناسه منحصر به فرد همچنان در حال اجرا باشد، این جواهر از شروع کار جدید جلوگیری می کند.

class StoreOutboxItemsPublishJob < ApplicationJob
sidekiq_options lock: :until_executed

def perform(partition)
StoreOutboxItem.publish!
end
end

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این رویکرد چندین مزیت را ارائه می دهد:

اتکا به ابزارهای استاندارد: پشتیبانی و استقرار را ساده می کند.

قابلیت مشاهده خوب: معیارهای داخلی بینش ارزشمندی را ارائه می دهند.

مقیاس پذیری: افزایش موازی سازی به آسانی افزودن پارتیشن های بیشتر به موضوع کافکا و افزایش همزمانی صف سیدیک است.

با این حال، علیرغم این مزایا، سیستم همچنان پیچیده است، با چندین نقطه بالقوه شکست.همه چیز نسبتاً آرام پیش رفت تا اینکه یک روز … شکست. یک توسعه دهنده با تجربه Ruby به سرعت ضعیف ترین پیوند را شناسایی می کند. sidekiq-uniq-jobs ما را شکست داد ما با چالش جدیدی روبرو شدیم: کاهش نقاط شکست برای هموار کردن راه برای مقابله با چالش‌های در مقیاس بزرگتر.

انتقال پردازش سفارش از HTTP Async به Outbox

سیستم ما از دو جزء اصلی تشکیل شده است: یک فروشگاه و یک سیستم پشتی، که به طور فعال اطلاعات سفارش را با سرعتی در حدود 100 پیام در ثانیه رد و بدل می کند.

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

با درک اهمیت تبادل اطلاعات سفارش به موقع، تیم پلتفرم روبی تصمیم گرفت مکانیسم هماهنگ سازی سفارش را به کافکا منتقل کند. مشخص شد که راه‌اندازی Schked/Sidekiq/Uniq-jobs موجود ما نمی‌تواند قابلیت اطمینان و عملکرد مورد نیاز را ارائه دهد.

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

نقاط شکست را به یک کاهش دهید.

دیمون های مستقل، قابل تکرار و قابل تعویض را فعال کنید.

این تضمین می‌کند که شکست یک دیمون روی بقیه تأثیری نخواهد داشت.

مفهوم دیمون جدید ساده و مؤثر بود:

فرآیند اختصاصی: یک فرآیند جداگانه، پیام‌ها را برای تمام پارتیشن‌های کافکا به صورت چند رشته‌ای مدیریت می‌کند.

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

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

مهاجرت ایمن به کافکا

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

در اینجا نحوه نزدیک شدن ما به مهاجرت آمده است:

ادغام صندوق خروجی: ما تنظیمات جزئی را در پیاده سازی Sidekiq+HTTP موجود خود انجام دادیم تا با الگوی صندوق خروجی ادغام شود.

ارسال سفارش موازی: ما شروع به تکرار سفارشات از طریق کافکا کردیم.

مقایسه عملکرد و برش: با نظارت و مقایسه معیارهای عملکرد (به عنوان مثال، سرعت پردازش سفارش)، برتری راه حل جدید مبتنی بر کافکا را تأیید کردیم. پس از اطمینان، سیستم همگام سازی قدیمی را با خیال راحت از کار انداختیم و تنها مکانیسم حمل و نقل جدید کافکا را باقی گذاشتیم.

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

قابلیت مشاهده و ردیابی

اجرای قبلی صندوق خروجی مبتنی بر Sidekiq ما از معیارهای داخلی ارائه شده توسط ابزارهایی مانند بهره مند شد yabeda-schked و yabeda-sidekiq. این ابزارها بینش های ارزشمندی را در مورد سلامت و عملکرد سیستم با کمترین تلاش ارائه کردند. با این حال، توسعه یک دیمون صندوق ارسال سفارشی از ابتدا به این معنی بود که ما مجبور بودیم تمام معیارهای مشاهده پذیری، از جمله معیارها و ردیابی توزیع شده را خودمان پیاده سازی کنیم.

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

در طول توسعه و راه اندازی دیمون جدید، ما روی جنبه های کلیدی زیر تمرکز کردیم:

معیارهای عملکرد: ما معیارهایی را برای ردیابی تعداد پیام‌های پردازش شده، زمان پردازش، خطاها و سایر شاخص‌های حیاتی برای ارزیابی کارایی سیستم پیاده‌سازی کردیم.

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

در حالی که دیمون جدید ما پایداری و مقیاس‌پذیری افزایش‌یافته‌ای را ارائه می‌دهد، همچنین به تلاش بیشتری برای اطمینان از همان سطح مشاهده‌پذیری نیاز دارد – یک جنبه حیاتی برای حفظ یک سرویس با کیفیت بالا.

الگوی صندوق ورودی

بیایید دیدگاه خود را به مصرف کننده پیام تغییر دهیم، که با چالش های مشابهی مواجه است. برای مثال، کافکا ذاتاً معنای دقیق تحویل پیام را تضمین نمی کند. برای پرداختن به این موضوع، مفهوم “صندوق خروجی تراکنش” را به “صندوق ورودی تراکنش” تعمیم می دهیم، که مکانیسم را در سمت مصرف کننده منعکس می کند.

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

ماندگاری پیام: پس از ورود، پیام ها در یک جدول اختصاصی “صندوق ورودی” در پایگاه داده ذخیره می شوند. هنگام استفاده از کافکا، این کار توسط یک «inbox-kafka-consumer» مدیریت می‌شود که تعدیل‌های پیام پردازش شده را در یک تراکنش انجام می‌دهد.

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

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

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

توجه به این نکته مهم است که این رویکرد به ویژه زمانی ارزشمند است که تضمین های قوی برای پردازش صحیح پیام توسط مصرف کننده مورد نیاز باشد.

مقیاس بندی

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

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

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

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

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

پارتیشن های مجازی

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

برای پرداختن به این موضوع، مفهوم «سطل» را معرفی کردیم. به جای استفاده از اعداد پارتیشن، اکنون از اعداد سطلی هنگام ایجاد رکوردهای صندوق خروجی/صندوق ورودی استفاده می کنیم. تعداد سطل ها محدود است و به طور مشابه با شماره های پارتیشن محاسبه می شود – به عنوان باقیمانده تقسیم کلید رویداد. با این حال، چند سطل را می توان به یک پارتیشن کافکا نگاشت کرد.

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

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

دیمون صندوق ورودی/صندوق ورودی: این کنترل کامل بر پردازش پیام را فراهم می کند و بهینه سازی های متناسب را امکان پذیر می کند.

مقیاس بندی صندوق خروجی مبتنی بر پارتیشن کافکا: ما می توانیم با تغییر تعداد پارتیشن ها، توان عملیاتی سیستم را به صورت پویا تنظیم کنیم.

پیاده سازی الگوی صندوق ورودی قوی: این دقیقاً یک بار پردازش پیام را تضمین می کند که برای فرآیندهای تجاری حساس به داده بسیار مهم است.

مقیاس بندی مستقل با پارتیشن های مجازی (سطل): این امکان مقیاس‌پذیری سیستم را بدون تغییر پیکربندی‌های کافکا یا راه‌اندازی مجدد تولیدکنندگان فراهم می‌کند.

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

پذیرش گسترده صندوق خروجی

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

اجرای موفقیت آمیز: در چندین سرویس روبی مستقر شده است.

پایداری اثبات شده: عملکرد قابل اعتماد را در محیط های تولید نشان داد.

قابلیت مشاهده و مقیاس پذیری: قابلیت های نظارت و مقیاس بندی قوی ارائه شده است.

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

تعداد پارتیشن های صندوق خروجی
تعداد سطل.
تعداد فرآیندهای دیمون
تعداد نخ ها در هر فرآیند.

این پیکربندی برای کاربران بسیار پیچیده بود و آنها را مجبور کرد تا جزئیات پیاده سازی فنی را بررسی کنند. کاربران اغلب سیستم را به اشتباه پیکربندی می‌کنند، پارتیشن‌های بسیار کمی یا پردازش‌های شبح زیادی را تنظیم می‌کنند که منجر به کاهش عملکرد می‌شود.

برای پذیرش گسترده‌تر، باید پیکربندی را ساده‌سازی کنیم و فرآیند مقیاس‌بندی را بصری‌تر و کاربرپسندتر کنیم.

معماری جدید

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

در ابتدا، واحد مقیاس ما برای منطق تجاری، خود نخ بود که هم نظرسنجی و هم پردازش را مدیریت می کرد. این رویکرد منجر به یک نتیجه نامطلوب شد: افزایش مقیاس، شدت هر دو عملیات را حتی در مواقع غیرضروری افزایش داد. به عنوان مثال، اگر اجرای منطق تجاری به یک گلوگاه تبدیل شود، افزایش تعداد رشته‌ها نیز فرکانس نظرسنجی پایگاه داده را تقویت می‌کند، حتی اگر گلوگاه مربوط به بازیابی پیام نباشد. این منجر به “مقیاس سازی بیش از حد” شد.

افزایش تعداد رشته ها به طور متناسب باعث افزایش پرس و جوهای پایگاه داده می شود. ما دائماً از پایگاه داده برای پیام‌های جدید نظرسنجی می‌کنیم، بدون اینکه بر سرعت پردازش تأثیر بگذاریم، فرکانس نظرسنجی را کاهش نمی‌دهیم.معماری جدید ما این مسائل را با جداسازی دیمون به دو حوض رشته حل می کند: الف استخر رای گیری و الف استخر پردازش.

تفرقه بینداز و حکومت کن

معماری جدید ما مسئولیت های نظرسنجی و پردازش را با استفاده از دو مخزن رشته مجزا و استفاده از Redis به عنوان صف پیام واسطه جدا می کند.

استخر رای گیری:

Thread ها در این Pool وظیفه واکشی پیام های جدید از پایگاه داده برای هر پارتیشن را بر عهده دارند.
پس از کشف پیام‌های جدید، با استفاده از آن در Redis قرار می‌گیرند LPUSH فرمان
برای حفظ ثبات داده ها و نظم پردازش، یک قفل در پارتیشن در طول نظرسنجی به دست می آید.

استخر پردازش:

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

اکنون هر دیمون از دو thread pool تشکیل شده است: یک polling pool و یک processing pool. به‌طور پیش‌فرض، نسبت رشته‌های پردازشی به رشته‌های نظرسنجی 2:1 را حفظ می‌کنیم، اما این نسبت را می‌توان در صورت نیاز تنظیم کرد.

مزایای معماری جدید

معماری اصلاح شده ما چندین مزیت کلیدی را ارائه می دهد:

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

عملکرد پیشرفته: جداسازی نظرسنجی و پردازش، استفاده کارآمدتر از منابع را ممکن می‌سازد. رشته‌های نظرسنجی در طول پردازش پیام مسدود نمی‌شوند و رشته‌های پردازشی در انتظار داده‌های جدید بیکار نمی‌مانند.

پیکربندی انعطاف پذیر: نسبت نظرسنجی به رشته های پردازشی را می توان به راحتی بر اساس ویژگی های بار کاری و الزامات عملکرد تنظیم کرد.

مقیاس خودکار با Kubernetes HPA: استفاده از Redis به عنوان بافر بین استخرهای نظرسنجی و پردازش، مقیاس خودکار (HPA) بدون درز را در Kubernetes تسهیل می‌کند. اندازه صف Redis به دقت بار استخر پردازش را منعکس می کند. اگر صف رشد کند که نشان‌دهنده تنگناهای پردازش است، HPA می‌تواند به‌طور خودکار نسخه‌های شبح را افزایش دهد. برعکس، اگر صف خالی باشد، HPA می تواند کپی ها را برای بهینه سازی مصرف منابع کاهش دهد.

این معماری جدید سیستم پردازش پیام انعطاف پذیرتر، مقیاس پذیرتر و به راحتی قابل تنظیم را ارائه می دهد.

سفر تکاملی

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

غذای کلیدی؟ روبی تقریباً از عهده هر کاری بر می آید. بسیاری از پروژه های منبع باز به عنوان راه حل هایی برای مشکلات تجاری خاص شروع می شوند و به ابزارهای همه کاره و آزمایش شده در نبرد تبدیل می شوند که در سیستم های تولیدی متعدد استفاده می شوند.

این مقاله سه مرحله از توسعه ابزار Inbox/Outbox ما را بررسی می‌کند که توسط تیم Ruby Platform برای رسیدگی به چالش‌های مدیریت پیام ایجاد شده است. هر مرحله بر افزایش قابلیت اطمینان، مقیاس پذیری و کاربر پسند بودن تمرکز داشت.

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

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

تصویر جلد صندوق خروجی

تلاش برای تحویل قابل اعتماد

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

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

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

الگوی صندوق خروجی را وارد کنید

الگوی صندوق خروجی تراکنش یک الگوی معماری است که در سیستم های توزیع شده برای اطمینان از تحویل مطمئن پیام استفاده می شود. این کار با ماندگاری پیام‌ها به یک فروشگاه داده (معمولاً یک جدول “صندوق خروجی” در پایگاه داده) قبل از اینکه در نهایت به یک کارگزار پیام تحویل داده شود، کار می‌کند. این رویکرد یکپارچگی داده ها را تضمین می کند – یا همه چیز متعهد است، یا در صورت بروز خطا، کل عملیات به عقب برمی گردد.
در اینجا نحوه کار آن آمده است:

  1. ایجاد سفارش و ماندگاری: هنگامی که برنامه درخواست ایجاد سفارش را دریافت می کند، یک تراکنش پایگاه داده را باز می کند. جزئیات سفارش ذخیره می شوند و در همان تراکنش، پیامی برای سرویس دیگر تولید می شود و در جدول اختصاصی “صندوق خروجی” در پایگاه داده باقی می ماند. سپس معامله انجام می شود.
  2. نظرسنجی برای پیام های جدید: یک فرآیند جداگانه به صورت دوره ای جدول صندوق خروجی را برای ورودی های جدید نظرسنجی می کند.
  3. پردازش و تحویل پیام: این فرآیند پیام‌ها را از جدول خروجی دریافت می‌کند و آنها را مدیریت می‌کند. در مورد ما به معنای ارسال پیام ها به کارگزار پیام است.

جدول صندوق خروجی معمولاً شامل فیلدهای زیر است:

  • کلید اصلی
  • ظرفیت ترابری (بدنه پیام)
  • وضعیت ها برای ردیابی وضعیت فعلی پیام

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

الگوی صندوق خروجی تراکنش

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

در اینجا یک نمونه کد ساده آورده شده است:

Order.transaction do
  order.complete!
  OrderOutboxItem.create(order)
end
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

زمان استفاده از الگوی صندوق خروجی

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

مثال 1: ردیابی موقعیت مکانی در زمان واقعی (نیاز به قابلیت اطمینان پایین)

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

مثال 2: قرار دادن سفارش تحویل غذا (الزامات با قابلیت اطمینان بالا)

مشتری برای تحویل پیتزا سفارش می دهد. رستوران برای شروع آماده سازی غذا باید سریعا سفارش را دریافت کند. در اینجا، پیام رسانی به موقع و قابل اعتماد بسیار مهم است:

  • تاخیر در دریافت سفارش: منجر به مشتری ناراضی می شود که پیتزای خود را دیر دریافت می کند.
  • پیام سفارش گم شده: نتیجه این است که مشتری هرگز سفارش خود را دریافت نمی کند و برای شرکت هزینه بر درآمد و اعتماد مشتری دارد.

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

سفر خروجی ما

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

پخش جریانی تنظیمات فروشگاه

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

  • یک فروشگاه می تواند باز یا بسته باشد.
  • یک فروشگاه ممکن است ساعات کاری خاصی داشته باشد.

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

اولین حمله ما

برای آزمایش مفهوم خود، ما مسیر کمترین مقاومت را انتخاب کردیم: یک کار Rake که توسط یک کار Cron آغاز می شود. ایده ساده بود: در هر دقیقه، این کار داده‌های مربوط به سفارش‌های اخیراً ارسال شده و هر سفارشی را که قبلاً ارسال نشده بود (مثلاً به دلیل خطاهای شبکه) جمع‌آوری می‌کرد و آنها را به صف کارگزار پیام تحویل می‌داد.

task :publish_store do
  StoreOutboxItem.pending.find_each(&:publish!)
end
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حالی که برای اثبات مفهوم کاربردی است، این رویکرد دارای اشکالاتی بود:

  • زمان های آهسته راه اندازی برنامه: پردازش حجم زیادی از داده ها در هنگام راه اندازی برنامه منجر به افزایش زمان بوت شد.
  • عدم مقیاس پذیری: ماهیت تک رشته ای این راه حل، توانایی آن را برای مدیریت کارآمد حجم داده های رو به رشد محدود می کند.

گامی به سوی کارایی

راه اندازی یک فرآیند Ruby جدید برای هر کار ناکارآمد بود. Schked را وارد کنید، یک زمان‌بندی کار که به ما امکان می‌دهد یک کار تکراری را برای ارسال پیام به کارگزار در هر دقیقه برنامه‌ریزی کنیم. سپس سیدیق به این وظایف رسیدگی می کرد.

every "1m" do
  StoreOutboxItemsPublishJob.enqueue
end

class StoreOutboxItemsPublishJob < ApplicationJob
  def perform
    StoreOutboxItem.pending.find_each(&:publish!)
  end
end
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

برای کاهش این موضوع، Sidekiq را در حالت تک رشته ای اجرا کردیم و از اجرای متوالی وظایف اطمینان حاصل کردیم. این موضوع مشکل همزمانی را حل کرد اما توانایی ما را برای استفاده از پتانسیل کامل سیدیک برای پردازش موازی محدود کرد.

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

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

پارتیشن های صندوق خروجی

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

every "1m" do
  PARTITIONS_COUNT.times do |partition|
    StoreOutboxItemsPublishJob.enqueue(partition)
  end
end
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

همپوشانی شغل

همانطور که بحث شد، مکانیسم فعلی رسیدگی به وظایف ما از تجمع و همپوشانی شغلی بالقوه رنج می برد. این می تواند سیستم را تحت الشعاع قرار دهد و از پردازش همه رویدادها در زمان تعیین شده جلوگیری کند. برای اطمینان از پردازش کار روان‌تر و مطمئن‌تر، می‌توانیم از موارد محبوب استفاده کنیم sidekiq-uniq-jobs گوهر اگر کار قبلی با همان شناسه منحصر به فرد همچنان در حال اجرا باشد، این جواهر از شروع کار جدید جلوگیری می کند.

class StoreOutboxItemsPublishJob < ApplicationJob
  sidekiq_options lock: :until_executed

  def perform(partition)
    StoreOutboxItem.publish!
  end
end
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این رویکرد چندین مزیت را ارائه می دهد:

  • اتکا به ابزارهای استاندارد: پشتیبانی و استقرار را ساده می کند.
  • قابلیت مشاهده خوب: معیارهای داخلی بینش ارزشمندی را ارائه می دهند.
  • مقیاس پذیری: افزایش موازی سازی به آسانی افزودن پارتیشن های بیشتر به موضوع کافکا و افزایش همزمانی صف سیدیک است.

با این حال، علیرغم این مزایا، سیستم همچنان پیچیده است، با چندین نقطه بالقوه شکست.
همه چیز نسبتاً آرام پیش رفت تا اینکه یک روز … شکست. یک توسعه دهنده با تجربه Ruby به سرعت ضعیف ترین پیوند را شناسایی می کند. sidekiq-uniq-jobs ما را شکست داد ما با چالش جدیدی روبرو شدیم: کاهش نقاط شکست برای هموار کردن راه برای مقابله با چالش‌های در مقیاس بزرگتر.

انتقال پردازش سفارش از HTTP Async به Outbox

سیستم ما از دو جزء اصلی تشکیل شده است: یک فروشگاه و یک سیستم پشتی، که به طور فعال اطلاعات سفارش را با سرعتی در حدود 100 پیام در ثانیه رد و بدل می کند.

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

با درک اهمیت تبادل اطلاعات سفارش به موقع، تیم پلتفرم روبی تصمیم گرفت مکانیسم هماهنگ سازی سفارش را به کافکا منتقل کند. مشخص شد که راه‌اندازی Schked/Sidekiq/Uniq-jobs موجود ما نمی‌تواند قابلیت اطمینان و عملکرد مورد نیاز را ارائه دهد.

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

  • نقاط شکست را به یک کاهش دهید.
  • دیمون های مستقل، قابل تکرار و قابل تعویض را فعال کنید.

این تضمین می‌کند که شکست یک دیمون روی بقیه تأثیری نخواهد داشت.

مفهوم دیمون جدید ساده و مؤثر بود:

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

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

مهاجرت ایمن به کافکا

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

در اینجا نحوه نزدیک شدن ما به مهاجرت آمده است:

  1. ادغام صندوق خروجی: ما تنظیمات جزئی را در پیاده سازی Sidekiq+HTTP موجود خود انجام دادیم تا با الگوی صندوق خروجی ادغام شود.
  2. ارسال سفارش موازی: ما شروع به تکرار سفارشات از طریق کافکا کردیم.
  3. مقایسه عملکرد و برش: با نظارت و مقایسه معیارهای عملکرد (به عنوان مثال، سرعت پردازش سفارش)، برتری راه حل جدید مبتنی بر کافکا را تأیید کردیم. پس از اطمینان، سیستم همگام سازی قدیمی را با خیال راحت از کار انداختیم و تنها مکانیسم حمل و نقل جدید کافکا را باقی گذاشتیم.

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

قابلیت مشاهده و ردیابی

اجرای قبلی صندوق خروجی مبتنی بر Sidekiq ما از معیارهای داخلی ارائه شده توسط ابزارهایی مانند بهره مند شد yabeda-schked و yabeda-sidekiq. این ابزارها بینش های ارزشمندی را در مورد سلامت و عملکرد سیستم با کمترین تلاش ارائه کردند. با این حال، توسعه یک دیمون صندوق ارسال سفارشی از ابتدا به این معنی بود که ما مجبور بودیم تمام معیارهای مشاهده پذیری، از جمله معیارها و ردیابی توزیع شده را خودمان پیاده سازی کنیم.

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

در طول توسعه و راه اندازی دیمون جدید، ما روی جنبه های کلیدی زیر تمرکز کردیم:

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

صندوق خروجی گرافانا

در حالی که دیمون جدید ما پایداری و مقیاس‌پذیری افزایش‌یافته‌ای را ارائه می‌دهد، همچنین به تلاش بیشتری برای اطمینان از همان سطح مشاهده‌پذیری نیاز دارد – یک جنبه حیاتی برای حفظ یک سرویس با کیفیت بالا.

الگوی صندوق ورودی

بیایید دیدگاه خود را به مصرف کننده پیام تغییر دهیم، که با چالش های مشابهی مواجه است. برای مثال، کافکا ذاتاً معنای دقیق تحویل پیام را تضمین نمی کند. برای پرداختن به این موضوع، مفهوم “صندوق خروجی تراکنش” را به “صندوق ورودی تراکنش” تعمیم می دهیم، که مکانیسم را در سمت مصرف کننده منعکس می کند.

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

  1. ماندگاری پیام: پس از ورود، پیام ها در یک جدول اختصاصی “صندوق ورودی” در پایگاه داده ذخیره می شوند. هنگام استفاده از کافکا، این کار توسط یک «inbox-kafka-consumer» مدیریت می‌شود که تعدیل‌های پیام پردازش شده را در یک تراکنش انجام می‌دهد.
  2. پردازش صندوق ورودی: یک فرآیند صندوق ورودی جداگانه رویدادها را از پایگاه داده واکشی می کند و منطق تجاری مربوطه را برای هر کدام راه اندازی می کند. این فرآیند شبیه فرآیند صندوق خروجی است، اما به جای ارسال پیام به یک کارگزار، منطق تجاری مرتبط با پیام دریافتی را اجرا می کند.

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

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

توجه به این نکته مهم است که این رویکرد به ویژه زمانی ارزشمند است که تضمین های قوی برای پردازش صحیح پیام توسط مصرف کننده مورد نیاز باشد.

مقیاس بندی

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

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

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

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

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

پارتیشن های مجازی

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

برای پرداختن به این موضوع، مفهوم «سطل» را معرفی کردیم. به جای استفاده از اعداد پارتیشن، اکنون از اعداد سطلی هنگام ایجاد رکوردهای صندوق خروجی/صندوق ورودی استفاده می کنیم. تعداد سطل ها محدود است و به طور مشابه با شماره های پارتیشن محاسبه می شود – به عنوان باقیمانده تقسیم کلید رویداد. با این حال، چند سطل را می توان به یک پارتیشن کافکا نگاشت کرد.

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

سطل های صندوق خروجی

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

  • دیمون صندوق ورودی/صندوق ورودی: این کنترل کامل بر پردازش پیام را فراهم می کند و بهینه سازی های متناسب را امکان پذیر می کند.
  • مقیاس بندی صندوق خروجی مبتنی بر پارتیشن کافکا: ما می توانیم با تغییر تعداد پارتیشن ها، توان عملیاتی سیستم را به صورت پویا تنظیم کنیم.
  • پیاده سازی الگوی صندوق ورودی قوی: این دقیقاً یک بار پردازش پیام را تضمین می کند که برای فرآیندهای تجاری حساس به داده بسیار مهم است.
  • مقیاس بندی مستقل با پارتیشن های مجازی (سطل): این امکان مقیاس‌پذیری سیستم را بدون تغییر پیکربندی‌های کافکا یا راه‌اندازی مجدد تولیدکنندگان فراهم می‌کند.

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

پذیرش گسترده صندوق خروجی

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

  • اجرای موفقیت آمیز: در چندین سرویس روبی مستقر شده است.
  • پایداری اثبات شده: عملکرد قابل اعتماد را در محیط های تولید نشان داد.
  • قابلیت مشاهده و مقیاس پذیری: قابلیت های نظارت و مقیاس بندی قوی ارائه شده است.

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

  • تعداد پارتیشن های صندوق خروجی
  • تعداد سطل.
  • تعداد فرآیندهای دیمون
  • تعداد نخ ها در هر فرآیند.

این پیکربندی برای کاربران بسیار پیچیده بود و آنها را مجبور کرد تا جزئیات پیاده سازی فنی را بررسی کنند. کاربران اغلب سیستم را به اشتباه پیکربندی می‌کنند، پارتیشن‌های بسیار کمی یا پردازش‌های شبح زیادی را تنظیم می‌کنند که منجر به کاهش عملکرد می‌شود.

برای پذیرش گسترده‌تر، باید پیکربندی را ساده‌سازی کنیم و فرآیند مقیاس‌بندی را بصری‌تر و کاربرپسندتر کنیم.

معماری جدید

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

در ابتدا، واحد مقیاس ما برای منطق تجاری، خود نخ بود که هم نظرسنجی و هم پردازش را مدیریت می کرد. این رویکرد منجر به یک نتیجه نامطلوب شد: افزایش مقیاس، شدت هر دو عملیات را حتی در مواقع غیرضروری افزایش داد. به عنوان مثال، اگر اجرای منطق تجاری به یک گلوگاه تبدیل شود، افزایش تعداد رشته‌ها نیز فرکانس نظرسنجی پایگاه داده را تقویت می‌کند، حتی اگر گلوگاه مربوط به بازیابی پیام نباشد. این منجر به “مقیاس سازی بیش از حد” شد.

افزایش تعداد رشته ها به طور متناسب باعث افزایش پرس و جوهای پایگاه داده می شود. ما دائماً از پایگاه داده برای پیام‌های جدید نظرسنجی می‌کنیم، بدون اینکه بر سرعت پردازش تأثیر بگذاریم، فرکانس نظرسنجی را کاهش نمی‌دهیم.
معماری جدید ما این مسائل را با جداسازی دیمون به دو حوض رشته حل می کند: الف استخر رای گیری و الف استخر پردازش.

تفرقه بینداز و حکومت کن

معماری جدید ما مسئولیت های نظرسنجی و پردازش را با استفاده از دو مخزن رشته مجزا و استفاده از Redis به عنوان صف پیام واسطه جدا می کند.

استخر رای گیری:

  • Thread ها در این Pool وظیفه واکشی پیام های جدید از پایگاه داده برای هر پارتیشن را بر عهده دارند.
  • پس از کشف پیام‌های جدید، با استفاده از آن در Redis قرار می‌گیرند LPUSH فرمان
  • برای حفظ ثبات داده ها و نظم پردازش، یک قفل در پارتیشن در طول نظرسنجی به دست می آید.

استخر پردازش:

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

اکنون هر دیمون از دو thread pool تشکیل شده است: یک polling pool و یک processing pool. به‌طور پیش‌فرض، نسبت رشته‌های پردازشی به رشته‌های نظرسنجی 2:1 را حفظ می‌کنیم، اما این نسبت را می‌توان در صورت نیاز تنظیم کرد.

مزایای معماری جدید

معماری اصلاح شده ما چندین مزیت کلیدی را ارائه می دهد:

  • مقیاس بندی ساده شده: از آنجایی که رای گیری یک عملیات سبک وزن است، استخر نظرسنجی می تواند به طور قابل توجهی رشته های کمتری نسبت به استخر پردازش داشته باشد. مقیاس‌گذاری سیستم اکنون شامل افزایش تعداد کپی‌های دیمون است که هرکدام دارای استخرهای نظرسنجی و پردازش خاص خود هستند.
  • عملکرد پیشرفته: جداسازی نظرسنجی و پردازش، استفاده کارآمدتر از منابع را ممکن می‌سازد. رشته‌های نظرسنجی در طول پردازش پیام مسدود نمی‌شوند و رشته‌های پردازشی در انتظار داده‌های جدید بیکار نمی‌مانند.
  • پیکربندی انعطاف پذیر: نسبت نظرسنجی به رشته های پردازشی را می توان به راحتی بر اساس ویژگی های بار کاری و الزامات عملکرد تنظیم کرد.
  • مقیاس خودکار با Kubernetes HPA: استفاده از Redis به عنوان بافر بین استخرهای نظرسنجی و پردازش، مقیاس خودکار (HPA) بدون درز را در Kubernetes تسهیل می‌کند. اندازه صف Redis به دقت بار استخر پردازش را منعکس می کند. اگر صف رشد کند که نشان‌دهنده تنگناهای پردازش است، HPA می‌تواند به‌طور خودکار نسخه‌های شبح را افزایش دهد. برعکس، اگر صف خالی باشد، HPA می تواند کپی ها را برای بهینه سازی مصرف منابع کاهش دهد.

این معماری جدید سیستم پردازش پیام انعطاف پذیرتر، مقیاس پذیرتر و به راحتی قابل تنظیم را ارائه می دهد.

معماری کامل صندوق خروجی

سفر تکاملی

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

غذای کلیدی؟ روبی تقریباً از عهده هر کاری بر می آید. بسیاری از پروژه های منبع باز به عنوان راه حل هایی برای مشکلات تجاری خاص شروع می شوند و به ابزارهای همه کاره و آزمایش شده در نبرد تبدیل می شوند که در سیستم های تولیدی متعدد استفاده می شوند.

این مقاله سه مرحله از توسعه ابزار Inbox/Outbox ما را بررسی می‌کند که توسط تیم Ruby Platform برای رسیدگی به چالش‌های مدیریت پیام ایجاد شده است. هر مرحله بر افزایش قابلیت اطمینان، مقیاس پذیری و کاربر پسند بودن تمرکز داشت.

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

آرم صندوق خروجی

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا