امین برجیان
امین برجیان
خواندن ۱۲ دقیقه·۳ سال پیش

منبع‌کردن رویداد

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

نگهداری وضعیت

اکثر سامانه‌های متدوال امروزی، معمولا به این صورت طراحی می‌شوند که وضعیت سامانه را به صورت‌های مختلف مثل فایل، اطلاعات پایگاه‌داده، اطلاعات موجود در مموری و ... نگهداری می‌کنند. به این سامانه‌ها، سامانه‌های نگهداری داده مبتنی بر وضعیت (state based persistence) نیز گفته می‌شود.

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

تغییرات جدید بر روی داده‌های قدیمی

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

برای مثال فرض کنیم رستورانی داشته باشیم که اطلاعات سفارش مشتریان را نگهداری می‌کند. این اطلاعات شامل شناسه سفارش، مشتری، وضعیت سفارش و مواردی که سفارش داده شده‌اند، می‌باشد. بدیهی است که همین مدل ساده برای نگهداری داده‌های سامانه در اکثر موارد توسط اکثر افراد پذیرفته می‌باشد و همچنین در بیش‌تر نیازمندی‌ها کافی و قابل توسعه می‌باشد. اما فرض کنیم که روزی مسئول کسب و کار تصمیم می‌گیرد که تا حد امکان هدر رفت‌های کسب و کاری‌اش را کاهش دهد. او به این نکته توجه می‌کند که یک سفارش می‌تواند در چهار وضعیت مختلف باشد: یک سفارش اخیرا ایجاد شده است (created)، این سفارش تحویل آشپزخانه برای آماده‌سازی شده است (prepare)، سفارش تحویل داده شده و مشتری رستوران را ترک کرده است (closed) یا این سفارش در هر کدام از مراحل قبلی پیش از تحویل به مشتری، به دلایل مختلفی لغو شده است و حتی جریمه در صورت نیاز دریافت شده است (canceled). مسئول کسب و کار می‌داند که اگر یک سفارش در مرحله‌ای که به آشپزخانه تحویل داده شده است، لغو شود باعث هدر رفت مواد غذایی می‌شود. (با این که شاید جریمه نیز دریافت شود اما به هر حال وقت آشپزخانه، مواد مصرفی و ... مورد استفاده قرار گرفته‌اند اما الان مشتری برای آن وجود ندارد) مسئول کسب و کار تصمیم می‌گیرد که پیش از شروع به کار و تصمیم‌گیری روی این موضوع، مقداری وضعیت جاری سامانه را بهتر بشناسد. او نیاز دارد تا نرخ رخداد این اتفاق، میزان هدر رفت، خسارت وارد‌شده و ... را در اختیار داشته باشد تا بتواند تصمیم دقیق‌تری بگیرد. به همین دلیل از طراحان سامانه‌ی نرم‌افزاری می‌خواهد که اطلاعات مربوط به مشتریان یک سال اخیر رستورانش را در اختیار او قرار دهند. اما در این وضعیت و با توجه به مدل ساده‌ای که ما در ابتدا در نظر گرفتیم، چگونه می‌توانیم نیازمندی کارفرما را پاسخ دهیم؟! ما باید بدانیم چه تعداد از سفارشات بعد از قرار گرفتن در وضعیت آماده‌شدن (prepare)، توسط مشتری لغو شده‌اند (cancel) و این در حالی است که ما همواره وضعیت نهایی سامانه یعنی لغو‌شدن یا بسته‌شدن درخواست را نگهداشت داشته‌ایم. در عمل راهی برای پاسخ‌گویی به این نیازمندی کارفرما در وضعیت فعلی با توجه به طراحی صورت‌گرفته وجود نخواهد داشت!

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

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

وجود مشکلات نرم‌افزاری

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



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

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

منبع‌کردن رویداد

نگهدار منتبی برای وضعیت، اگر چه یک رویکرد عمومی مناسبی هست اما دارای محدودیت‌هایی می‌باشد. به همین دلیل ما به دنبال آن هستیم که به روش‌هایی این محدودیت‌ها را رفع یا کاهش دهیم. یکی از روش‌های معروف در این زمینه، منبع‌کردن رویدادها (event sourcing) می‌باشد.

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

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

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

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

چالش منبع حقیقی

به عنوان ساده‌ترین مثال، نکته‌ی اول این است که اگر لاگ و اطلاعات وضعیت سیستم به دلایلی با یکدیگر هماهنگ نباشند، کدام یک را باید به عنوان منبع حقیق (true source) در نظر بگیریم؟ ممکن است به عنوان مثال باگی در سامانه وجود داشته باشد که گاهی مواقع وضعیت برنامه را خراب می‌کند اما در لاگ‌های سامانه مشکلی ایجاد نمی‌کند یا برعکس.

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

چالش تغییر واحد

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



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

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

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

البته این روش، چالش‌های دیگری مثل میزان فضای لازم برای نگهداری را نیز به دنبال خواهد داشت. به همین دلیل شاید در اکثر موارد لازم باشد در طول زمان لاگ‌ها را فشرده کرد و تنها لاگ‌ها را تا بازه‌ی زمانی خاص (مثلا یک یا دو سال) نگهداری کرد.

نتیجه‌گیری

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

این مطلب، بخشی از تمرین‌های درس معماری نرم‌افزار در دانشگاه شهیدبهشتی است.

منابع



معماری_نرم_افزار_بهشتیرخدادمعماری
شاید از این پست‌ها خوشتان بیاید