Event driven server درPHP

ترجمه و تالیف : فاطمه شیرزادفر
تاریخ انتشار : 26 مرداد 99
خواندن در 5 دقیقه
دسته بندی ها : پی اچ پی

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

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

بیشتر یادبگیریم!

Proof-of-concept چیست؟  proof-of-concept یا اثبات یک مفهوم یک رویکرد کلی است که شامل آزمایش یک فرض خاص، به منظور به‌دست آوردن تائید مبنی بر امکان‌پذیری وکاربردی بودن آن است. به عبارت دیگر این کار نشان می‌دهد که آیا این محصول نرم‌افزاری برای یک مشکل خاص تجاری مناسب است یا نه. 

بنابراین در اولویت اول، درباره این معماری صحبت می‌کنم. این یک سرور طولانی مدت(long-running PHP server)است که تمام state آن در حافظه بارگذاری شده، از رویدادهای ذخیره شده ساخته شده است. به عبارت دیگر: این event sourcing است که ما در php می‌شناسی، اما تمام aggregate‌ها و پیش‌بینی‌ها در memory بارگذاری می‌شوند و هرگز روی دیسک ذخیره نمی‌شوند.

بیایید این را بیشتر بررسی کنیم!

یک سرور طولانی مدت پی اچ پی (A long-running PHP server)

رکن اول این معماری یک سرور طولانی مدت (دائم اجرا) است. از منظر php مدرن چندین راه‌حل آزمایش شده برای مدیریت این دسته از پردازش‌ها قابل ارائه است: فریمورک‌هایی مثل ReactPHP، Amphp و Swoole به جامعه php اجازه می‌دهند تا به دنیایی، غیر کشف نشده بپردازند، درحالی که php روز به روز با چرخه‌‌ی سریع درخواست/پاسخ رو به رو بود.

البته این چرخه سریع درخواست/پاسخ یکی از مواردی است که php را عالی کرده است: شما هرگز leaking state یا سینک نگه داشتن همه چیز نیستید: وقتی یک درخواست وارد می‌شود، یک روند درست php شروع شده و برنامه شما از ۰ بوت می‌شود. پس از اینکه پاسخ فرستاده شد، برنامه به طور کامل از بین می‌رود.

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

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

از آن‌جا که همیشه کل حالت برنامه در حافظه بارگذاری شده است، دیگر نیازی به اجرای کوئری‌های پایگاه داده، استفاده از منابع در mapping data از پایگاه داده برای آبجکت‌ها یا مسائل مربوط به عملکرد مثل خطای Circular Reference که بین موجودیت‌های ORM ، نیست.

این از نظر تئوری خوب به نظر می‌رسد، اما هنوز هم نیاز داریم تا قادر به اجرای کوئری‌های پیچیده باشیم؛ چیزی که پایگاه‌داده‌ها برای آن بسیار بهینه شده‌اند. واضح است که این معماری به ما نیاز دارد درباره برخی از برنامه‌های PHP، از جنبه‌هایی که در گذشته به آن‌ها عادت کرده‌ایم، برگردیم و دوباره فکر کنیم. بعداً به این قسمت برمی‌گردم.

ابتدا،‌بیایید نگاهی به دومین رکن بیندازیم:event sourcing

Event sourcing

چرا من پیشنهاد می‌کنم که event sourcing را بخشی از هسته این معماری کنیم؟ شما به راحتی می‌توانید یک سرور درحال کار در طولانی مدت داشته باشید که تمام داده‌های آن در حافظه از یک پایگاه داده عادی بارگیری می‌شود.

بیایید لحظه‌ای از این جاده پایین برویم: به کلاینت بگویید یک بروزرسانی را انجام دهد و آن را به سرور بک-اند بفرستد. سرور باید داده‌ها را در دیتابیس ذخیره کند و همچنین وضعیت حافظه خود را نیز تازه (refresh) کند. چنین سیستم‌هایی باید به روزرسانی وضعیت برنامه را به درستی انجام دهند تا همه چیز پس از بروزرسانی صحیح باشد.

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

اگر ما به طور طبیعی به event-driven سیستم برای حفظ حالت در حافظه به طور همگام متمایل هستیم، پس چرا همه چیز به صورت overhead به هر چیزی که در یک دیتابیس ذخیره شده ،اضافه می‌کنیم و به یک ORM برای map کردن داده‌ها برای بازگرداندن آن به آبجکت‌ها نیاز داریم؟ به همین دلیل است که event sourcing روش بهتری است: همه‌ی مشکلات سینک‌کردن وضعیت را به صورت خودکار حل می‌کند، و از آنجایی که به برقراری ارتباط با یک پایگاه‌داده و کار با یک ORM نیست، کارایی را افزایش می‌دهد.

در مورد کوئری‌های پیچیده چطور؟ به عنوان مثال چگونه می‌توانید از یک فروشگاه حاوی محصولات که میلیون‌ها آیتم دارد، هنگامی که همه چیز در حافظه قرار دارد، چیزی را جستجو کنید. Php در خصوص این کارها برتری ندارد. اما مجدداً event sourcing راه‌حلی را ارائه می‌دهد: projection یا پیش‌بینی . شما کاملاً قادر هستید برای یک کار معین یک پیش‌بینی بهینه سازی شده انجام دهید و حتی آن را در یک پایگاه داده ذخیره کنید! این می‌تواند یک پایگاه داده سبک در حافظه مثلSQLite یا یک سرور کامل مثل MySQL یا PostgreSQL server باشد.

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

این مسأله دلیل نهایی را برای اینکه چرا event sourcing برای این معماری بسیار مناسب و هماهنگ است را می‌آورد. هنگامی نیاز به ریبوت شدن – به دلیل کرش یا بعد از توسعه – دارد؛ event sourcing راهی برای بازسازی وضعیت برنامه به شکل بسیار سریع‌تر ارائه می‌دهد:snapshots.

در این معماری یک snapshots از کل وضعیت برنامه یک یا دوبار در روز ذخیره می‌شود. این نقطه‌ای است که می‌توان بدون نیاز به پخش مجدد همه‌ی رویداد‌ها، از سرور مجدداً ساخته شود.

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

کلاینت‌ها

من این را قبلاً نیز ذکر کرده‌ام: با کلاینت‌ها، منظورم برنامه‌های php سمت سرور است که با سرور بک-اند ارتباط برقرار می‌کنند.آن‌ها برنامه‌های معمولی php هستند و فقط در یک چرخه درخواست/پاسخ معمولی، به مدت کوتاهی زندگی می‌کنند.

تا زمانی که به جای برقراری ارتباط مستقیم با سرور، راهی برای استفاده از event-server وجود دارد؛ شما می‌توانید از هر فریمورکی برای این کلاینت‌ها استفاده کنید. به جای استفاده از یک ORM مثل Doctrine در سیمفونی یا Eloquent در لاراول، می‌توانید از یک لایه‌ی ارتباطی کوچک برای ارتباط با سوکت‌ها با بک-‌اند سرور استفاده کنید.

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

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

 

final class AccountsController
{
    public function index(): View
    {
        $accounts = Account::all();

        return new View('accounts.index', [
            'accounts' => $accounts,
        ]);
    }
}

به خاط داشته باشید که من عمدتا با لاراول کار می‌کنم و به Eloquent ORM عادت کرده‌ام. اگر ترجیح می‌دهید از repository pattern استفاده کنید، این هم عالی است.

در پشت صحنه، Account::all() یا $accountRepository→all() کوئری‌های پایگاه داده را انجام نمی‌دهد، بلکه آن‌ها یک پیام کوچک به بک-‌اند سرور ارسال می‌کنند، که حساب‌ها را از حافظه به مشتری ارسال می‌کند.

اگر در حال تغییر در تراز حساب‌ها هستیم، اینگونه انجام می‌شود:

final class BalanceController
{
    public function increase(Account $account, int $amount): Redirect
    {
        $aggregateRoot = AccountAggregateRoot::find($account);
   
        $aggregateRoot->increaseBalance($amount);

        return new Redirect(
            [AccountsController::class, 'index'], 
            [$account]
        );
    }
}

در پشت‌ صحنه، AccountAggregateRoot::increaseBalance() رویدادی را به سرور ارسال می‌کند، که آن را ذخیره می‌کند و به مشترکان مربوطه اطلاع می‌دهد.

اگر متعجب هستید که AccountAggregateRoot چنین عملی را ممکن است انجام دهد،‌ در اینجا یک نسخه ساده شده از آن را آورده‌ام:

final class AccountAggregateRootRoot extends AggregateRoot
{
    public function increaseBalance(int $amount): self
    {
        $this->event(new BalanceIncreased($amount));

        return $this;
    }
}

و در آخر این همان چیزی است که موجودیت حساب به نظر می‌رسد. به عدم وجود پیکربندی به سبک ORM توجه کنید؛ این‌ها آبجکت‌های php ساده در حافظه هستند.

final class Account extends Entity
{
    public string $uuid;

    public string $name;

    public int $balance = 0;
}

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

در آخر

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

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

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

منبع

گردآوری و تالیف فاطمه شیرزادفر
آفلاین
user-avatar

تجربه کلمه‌ای هست که همه برای توصیف اشتباهاتشون ازش استفاده میکنن، و من همیشه دنبال اشتباهات جدیدم! برنامه‌نویس هستم و لینوکس‌ دوست

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید