اکبر دیزجی
اکبر دیزجی
خواندن ۱۲ دقیقه·۲ ماه پیش

معماری LMAX

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

این پلتفرم یک پلتفرم جدید معاملاتی مالی برای مشتریان است که تعداد بسیاری معامله را با کمترین تاخیر پردازش کند. این سیستم بر روی پلتفرم JVM ساخته شده و حول یک پردازشگر منطق کسب و کار (Business Logic Processor) متمرکز است که می‌تواند ۶ میلیون سفارش را در ثانیه روی یک نخ (Thread) واحد پردازش کند. پردازشگر منطق کسب و کار به صورت کاملاً درون‌حافظه‌ای عمل می‌کند و از روش Event Sourcing بهره می‌برد. پردازشگر منطق کسب و کار توسط Disruptors احاطه شده است - یک مؤلفه همزمانی که شبکه‌ای از صف‌ها را بدون نیاز به قفل پیاده‌سازی می‌کند. در جریان طراحی، تیم به این نتیجه رسید که جهت‌های اخیر در مدل‌های همزمانی با کارایی بالا که از صف‌ها استفاده می‌کنند، به طور اساسی با طراحی مدرن CPU در تضاد هستند.

در یک سطح کلی، معماری سه بخش دارد:

  • پردازشگر منطق کسب و کار
  • بخشهای disruptor ورودی
  • بخشهای disruptor های خروجی

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

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

پردازشگر منطق کسب و کار به صورت متوالی پیام‌های ورودی را (به صورت یک فراخوانی متد) دریافت کرده، منطق کسب و کار را روی آن اعمال می‌کند و رویدادهای خروجی را منتشر می‌کند. تمام این عملیات به صورت درون‌حافظه‌ای انجام می‌شود و هیچ پایگاه داده یا ذخیره‌سازی دائمی وجود ندارد. حفظ تمام داده‌ها در حافظه دو مزیت مهم دارد. اول اینکه بسیار سریع است - هیچ پایگاه داده‌ای برای اجرای عملیات IO کند وجود ندارد و هیچ رفتاری مربوط به تراکنش وجود ندارد زیرا تمام پردازش‌ها به صورت متوالی انجام می‌شوند. مزیت دوم این است که برنامه‌نویسی را ساده‌تر می‌کند - هیچ نگاشت شی‌ء/رابطه‌ای (ORM) نیاز نیست و تمام کدها را می‌توان با استفاده از مدل شی‌ء جاوا بدون نیاز به سازگاری با پایگاه داده نوشت.


استفاده از یک ساختار درون‌حافظه‌ای یک پیامد مهم دارد - اگر سیستم به‌طور کامل از کار بیفتد چه اتفاقی می‌افتد؟ حتی مقاوم‌ترین سیستم‌ها هم در مقابل قطع برق آسیب‌پذیر هستند. قلب راه‌حل این مسئله، روش Event Sourcing است که به این معنی است که وضعیت فعلی پردازشگر منطق کسب‌وکار کاملاً با پردازش رویدادهای ورودی قابل بازسازی است. تا زمانی که جریان رویدادهای ورودی در یک ذخیره‌سازی بادوام نگه داشته شود (که یکی از وظایف disruptor ورودی است)، شما همیشه می‌توانید با بازپخش رویدادها وضعیت فعلی پردازشگر منطق کسب‌وکار را بازسازی کنید.

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

بنابراین، در تئوری، شما همیشه می‌توانید وضعیت پردازشگر منطق کسب‌وکار را با پردازش مجدد تمام رویدادها بازسازی کنید. با این حال، در عمل، این کار زمان زیادی طول می‌کشد اگر بخواهید یکی از آنها را از نو راه‌اندازی کنید. بنابراین، مشابه با سیستم‌های کنترل نسخه، LMAX می‌تواند از وضعیت پردازشگر منطق کسب‌وکار snapshot هایی ایجاد کند و آنها را بازیابی کند. هر شب در دوره‌های کم‌کاری، یک snapshot از وضعیت پردازشگر منطق کسب‌وکار گرفته می‌شود. راه‌اندازی مجدد پردازشگر منطق کسب‌وکار سریع است؛ یک راه‌اندازی کامل - شامل راه‌اندازی مجدد JVM، بارگذاری یک snapshot اخیر و بازپخش رویدادهای روزانه - کمتر از یک دقیقه طول می‌کشد.

اسنپ شات ها راه‌اندازی سریع‌تری برای پردازشگر منطق کسب‌وکار فراهم می‌کنند، اما به اندازه کافی سریع نیستند اگر یک پردازشگر در ساعت ۲ بعد از ظهر از کار بیفتد. به همین دلیل LMAX همیشه چندین پردازشگر منطق کسب‌وکار را به صورت همزمان اجرا می‌کند. هر رویداد ورودی توسط چند پردازشگر پردازش می‌شود، اما خروجی همه به‌جز یکی از آنها نادیده گرفته می‌شود. اگر پردازشگر زنده از کار بیفتد، سیستم به سرعت به پردازشگر دیگری منتقل می‌شود. این قابلیت برای مدیریت fail-over یکی از مزایای استفاده از Event Sourcing است.

با استفاده از Event Sourcing در سیستم‌های تکراری، می‌توان بین پردازشگرها در چند میکروثانیه جابه‌جا شد. علاوه بر گرفتن snapshot ها هر شب، آنها هر شب پردازشگرهای منطق کسب‌وکار را نیز مجدداً راه‌اندازی می‌کنند. تکرار (Replication) به آنها اجازه می‌دهد این کار را بدون زمان خاموشی انجام دهند، بنابراین معاملات را به صورت ۲۴/۷ پردازش می‌کنند.

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

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


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

برای افزایش عملکرد به یک سطح بالاتر، ترفندهای بیشتری لازم بود. یکی از کارهایی که تیم LMAX مفید یافت، نوشتن پیاده‌سازی‌های سفارشی از مجموعه‌های جاوا بود که برای کش حافظه مناسب و با مدیریت زباله (Garbage) دقیق طراحی شده بودند. مثالی از این کار، استفاده از primitive های long جاوا به عنوان کلیدهای hashmap با پیاده‌سازی خاصی که از آرایه‌ها برای پشتیبانی از Map استفاده می‌کرد (LongToObjectHashMap) بود. به طور کلی آنها دریافتند که انتخاب ساختارهای داده معمولاً تفاوت زیادی ایجاد می‌کند، در حالی که اکثر برنامه‌نویسان هر لیستی که دفعه قبل استفاده کرده‌اند را دوباره انتخاب می‌کنند، به جای اینکه فکر کنند کدام پیاده‌سازی برای این زمینه مناسب است.

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

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

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

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

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

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

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

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

وظایف تکرار (replication) و ژورنالینگ شامل عملیات IO هستند و بنابراین به نسبت کند هستند. ایده اصلی پردازشگر منطق کسب و کار این است که از انجام هر گونه IO جلوگیری کند. همچنین، این سه کار (تکرار، ژورنالینگ، و تبدیل پیام) نسبتاً مستقل هستند. همه آنها باید قبل از اینکه پردازشگر منطق کسب و کار روی یک پیام کار کند انجام شوند، اما می‌توانند به هر ترتیبی انجام شوند. برخلاف پردازشگر منطق کسب و کار که هر معامله وضعیت بازار را برای معاملات بعدی تغییر می‌دهد، این وظایف به طور طبیعی برای همزمانی مناسب هستند.

برای مدیریت این همزمانی، تیم LMAX یک مؤلفه همزمانی ویژه‌ای توسعه داده که آن را Disruptor می‌نامند.

تیم LMAX کد منبع Disruptor را با مجوز متن باز منتشر کرده است.

به طور خلاصه، می‌توانید Disruptor را به عنوان یک گراف چندپخشی (multicast graph) از صف‌ها در نظر بگیرید که تولیدکنندگان (producers) اشیاء را روی آن قرار می‌دهند و این اشیاء برای مصرف‌کنندگان مختلف به طور همزمان در صف‌های جداگانه برای مصرف فرستاده می‌شوند. وقتی به درون آن نگاه کنید، متوجه می‌شوید که این شبکه از صف‌ها در واقع یک ساختار داده‌ی واحد است - یک بافر حلقه‌ای (ring buffer). هر تولیدکننده و مصرف‌کننده یک شمارنده دنباله دارد که نشان می‌دهد در حال حاضر روی کدام قسمت از بافر کار می‌کند. هر تولیدکننده/مصرف‌کننده شمارنده دنباله خود را می‌نویسد، اما می‌تواند شمارنده‌های دیگر را بخواند. به این ترتیب، تولیدکننده می‌تواند شمارنده‌های مصرف‌کنندگان را بخواند تا مطمئن شود که جایگاه مورد نظر برای نوشتن آماده است بدون اینکه نیازی به قفل روی شمارنده‌ها باشد. به طور مشابه، یک مصرف‌کننده می‌تواند اطمینان یابد که فقط زمانی پیام را پردازش کند که مصرف‌کننده دیگری کارش را با آن تمام کرده باشد، با مشاهده شمارنده‌ها.

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

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

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

.

کسب کارمعماریlmax
Software Developer
شاید از این پست‌ها خوشتان بیاید