رویداد-محوری (Event Sourcing) از شما میخواهد که از بازنویسی دادههای خود دست بردارید. در یک برنامه سنتی CRUD، بهروزرسانی آدرس ارسال یک کاربر به معنای یافتن آن ردیف، تغییر مقدار و دور ریختن حالت قبلی است. رویداد-محوری مسیر متفاوتی را در پیش میگیرد. این الگو هر تغییر را به عنوان یک واقعیت تغییرناپذیر ذخیره میکند: یک کاربر حساب کاربری ایجاد کرد، آدرس خود را بهروز کرد یا ایمیل خود را تأیید کرد. حالت فعلی سیستم مستقیماً ذخیره نمیشود، بلکه با بازپخش (replay) این رویدادها به ترتیب، محاسبه میشود.
این الگو مشکلات واقعی را حل میکند. ردپای حسابرسی (Audit trails) به محصولات جانبی رایگان تبدیل میشوند. شما میتوانید حالت یک سفارش را در هر لحظه از گذشته بازسازی کنید. میتوانید با بازپخش دقیق آنچه اتفاق افتاده، عیبیابی کنید. هزینه این کار، پیچیدگی است. شما اکنون به جای ردیفهای ساده، با جریانهایی از واقعیتها، مدلهای خواندن و سازگاری نهایی (eventual consistency) سروکار دارید.
PostgreSQL میتواند به عنوان ذخیرهساز رویداد (event store) شما عمل کند. اکثر تیمها در حال حاضر از آن استفاده میکنند. این پایگاه داده تراکنشهای ACID، قابلیت JSONB برای بار دادههای (payloads) منعطف و ابزارهای پشتیبانگیری اثباتشده را ارائه میدهد. نیازی نیست از روز اول Kafka، Cassandra یا یک پایگاه داده تخصصی ذخیرهساز رویداد را وارد چرخه کنید. یک نمونه استاندارد Postgres بدون گسترش زیرساخت شما، تضمینهای تراکنشی و ردپای حسابرسی مورد نیاز رویداد-محوری را فراهم میکند.
ساختار یک ذخیرهساز رویداد در Postgres
طرحواره (Schema) میتواند به طرز خجالتآوری ساده باشد. در حداقل حالت، شما به جدولی نیاز دارید که رویدادها را اضافه (append) کند و هرگز آنها را در محل تغییر ندهد. یک طراحی کاربردی به این صورت است:
idبه عنوانbigserialیاUUIDکه به عنوان ترتیب جهانی عمل میکند.stream_idبرای گروهبندی رویدادهای مرتبط، مانند تمام تغییرات مربوط به یک کاربر یا سفارش خاص.event_typeبه صورت متن ساده:UserEmailChanged،PaymentReceived،InventoryAdjusted.payloadبه صورتJSONBکه دادههای خاص مربوط به آن اتفاق را نگه میدارد.occurred_atبا دقت منطقه زمانی (timezone).versionبرای هر جریان (stream) جهت اعمال همزمانی خوشبینانه (optimistic concurrency).
شما قانون تغییرناپذیری را در کد برنامه یا با یک محدودیت (constraint) پایگاه داده اعمال میکنید. یک ایندکس یکتا روی (stream_id, version) از اینکه دو نویسنده همزمان یک شماره ترتیب یکسان را اضافه کنند، جلوگیری میکند. وقتی دستوری میرسد، نسخه فعلی آن جریان را میخوانید، آن را افزایش میدهید و رویداد جدید را درون یک تراکنش درج میکنید. اگر فرآیند دیگری زودتر از شما این کار را انجام داده باشد، محدودیت یکتا با خطا مواجه میشود و شما دستور را دوباره امتحان کرده یا آن را رد میکنید.
یک مثال ملموس را در نظر بگیرید. شما یک سیستم موجودی (inventory) را مدیریت میکنید. به جای داشتن یک ردیف واحد inventory با ستون quantity ، رویدادها را به جدول inventory_events اضافه میکنید. رویداد ItemReceived ده واحد اضافه میکند. ItemReserved دو واحد کم میکند. ItemShipped سه واحد کم میکند. برای دانستن موجودی فعلی برای SKU-42، شما مجموع بار دادههای (payloads) رویدادهای مرتبط را محاسبه میکنید. برای دانستن موجودی سه روز پیش، فقط مجموع رویدادها را تا آن برچسب زمانی (timestamp) محاسبه میکنید. اگر یک باگ در منطق ارسال شما باعث بروز خطا در سهشنبه گذشته شده باشد، رویدادها را از طریق کد اصلاحشده بازپخش میکنید تا به حالت واقعی برسید. شما نمیتوانید این کار را با یک دستور ساده UPDATE انجام دهید.
اصولی که شما را از دردسر دور نگه میدارد
ساختن سیستم روی Postgres نیاز به انضباط را از بین نمیبرد. اصول زیر مستقیماً در سیستمهای رویداد-محور صدق میکنند.
ساده نگه دارید. پیچیدگی، قابلیت اطمینان را از بین میبرد. در برابر وسوسه ساختن یک چارچوب رویداد عمومی (generic event framework) قبل از اینکه حتی یک جریان کاری عملی را عرضه کنید، مقاومت کنید. یک جدول واحد، یک تابع مخزن (repository function) برای اضافه کردن رویدادها و یک کارگر پروجکشن (projection worker) برای ساخت مدلهای خواندن برای اثبات ارزش کافی است. ابزارها را فقط زمانی اضافه کنید که یک مشکل واقعی ظاهر شود.
از کوچک شروع کنید. کل سیستم یکپارچه (monolith) خود را بازنویسی نکنید. یک بافت محدود (bounded context) را انتخاب کنید که در آن ردپای حسابرسی، هزینههای اضافی را جبران کند. یک دفتر کل صورتحساب، یک موتور گردش کار یا یک سیستم رزرو موجودی کاندیداهای خوبی هستند. آن یک خط لوله (pipeline) را از ابتدا تا انتها بسازید. اجازه دهید در محیط عملیاتی اجرا شود. سپس تصمیم بگیرید که آیا باید آن را گسترش دهید یا خیر.
ابتدا موفقیت را تعریف کنید. رویداد-محوری یک معماری پیشفرض نیست؛ بلکه راه حلی برای نیازهای خاص است. اگر نیاز شما فقط ردیابی آخرین حالت است، CRUD سریعتر و ارزانتر است. اگر به پرسوجوهای زمانی (temporal queries)، قابلیت حسابرسی دقیق یا توانایی بازسازی مدلهای خواندن در صورت نیاز دارید، آنگاه رویدادها منطقی هستند. قبل از تعهد، بدانید که در حال حل کدام مشکل هستید.
قبل از بهینهسازی، اندازهگیری کنید. PostgreSQL مدرن روی سختافزاری معمولی میتواند هزاران رویداد را در ثانیه با یک جدول سادهی فقط-افزایشی (append-only) دریافت کند. تا زمانی که مانیتورینگ شما ثابت نکرده است که راهحلهای سادهتر را امتحان کردهاید، ذخیرهساز رویداد خود را تکهتکه (shard) نکنید یا طرحهای پارتیشنبندی پیچیده وارد نکنید. فیلدهایی را که پرسوجو میکنید ایندکسگذاری کنید. autovacuum را برای بارهای کاری فقط-افزایشی تنظیم کنید. سپس دوباره اندازهگیری کنید.
همه چیز را تست کنید. هندلرهای رویداد (event handlers) خود را با تست واحد (unit test) بررسی کنید. مسیر append را با تست یکپارچگی (integration test) بسنجید. از همه مهمتر، سناریوهای شکست را تست کنید. وقتی دو گره (node) بهطور همزمان به یک استریم (stream) داده اضافه (append) میکنند، چه اتفاقی میافتد؟ وقتی یک کارگرِ تصویرسازی (projection worker) در میانهی یک دسته (batch) کرش میکند، چه میشود؟ تستهایی بنویسید که همزمانسازی خوشبینانه (optimistic concurrency) و تضمینهای تحویل حداقل یکباره (at-least-once delivery) شما را تأیید کنند.
در محیط عملیاتی مانیتورینگ انجام دهید. جدول رویدادها بزرگ خواهد شد. برخلاف یک شمای نرمالشده (normalized schema) که در آن بهروزرسانیها تعداد ردیفها را ثابت نگه میدارد، رویدادسرا (event sourcing) بهطور عمدی افزایشی است. اندازه جدول، ورودی/خروجی دیسک (disk I/O) و تأخیر (lag) بین مدل نوشتن (write model) و تصویرسازیهای مدل خواندن (read model projections) را دنبال کنید. پیش از آنکه کاربران متوجه دادههای قدیمی (stale data) شوند، برای تأخیرِ تصویرسازی هشدار (alert) تنظیم کنید.
کارهای دستی را خودکار کنید. تغییرات دستیِ شمای دیتابیس، بازسازی دستیِ تصویرسازیها و بازپخش (replay) دستیِ رویدادها، بمبهای ساعتی هستند. استراتژی مهاجرت خود را اسکریپتنویسی کنید. اگر شمای یک رویداد را تکامل میدهید، فرآیند upcasting یا تبدیل را خودکار کنید تا رویدادهای قدیمی بتوانند بدون نیاز به مداخله انسانی در نیمهشب، از طریق منطق جدید بازپخش شوند.
انتخابهای خود را مستند کنید. بنویسید که چرا استریمهای خاصی وجود دارند، هر نوع رویداد چه معنایی دارد و چه زمانی تیم باید CRUD را به رویدادها ترجیح دهد. رویدادسرا بار شناختی (cognitive load) ایجاد میکند. مستندسازی خوب مانع از آن میشود که یک مهندس جدید حدس اشتباه بزند و رویدادهای بدشکل (malformed) را به یک استریم حیاتی اضافه کند.
تلههایی که ماهها وقت تلف میکنند
رویدادسرا در نمودارها بسیار ظریف و زیبا به نظر میرسد، اما در محیط عملیاتی میتواند دردسرساز باشد. مراقب این تلهها باشید.
دستکم گرفتن پیچیدگی. بازپخش رویدادها برای بازسازی وضعیت (state) از نظر مفهومی ساده است، اما مدیریت یکسانسازی (idempotency)، اسنپشاتگیری (snapshotting) برای عملکرد بهتر و تراکنشهای جبرانی (compensating transactions) در میان مجموعهها (aggregates) ساده نیست. سیستم خود را به قطعات کوچک تقسیم کنید. هر بار فقط یک استریم را حل کنید.
مهندسی بیش از حد (Over-engineering). صرفاً به این دلیل که تصور میکنید حجم رویدادهایتان روزی به آن نیاز خواهد داشت، یک کلاستر چند گرهای Kafka راه نیندازید. Postgres میتواند شما را به طرز شگفتآوری جلو ببرد. زیرساخت جدید را فقط زمانی معرفی کنید که با یک گلوگاه (bottleneck) اندازهگیری شده مواجه شوید که نمیتوانید در ساختار فعلی خود آن را رفع کنید.
نادیده گرفتن بدهی فنی. شمای قدیمی رویدادها برای همیشه باقی میمانند. اگر محتوای (payload) OrderCreated را تغییر دهید، همچنان ده میلیون رویداد تاریخی با ساختار قدیمی دارید. این بدهی را دنبال کنید. خوانندههای سازگار با نسخههای قبلی (backward-compatible readers) یا اسکریپتهای مهاجرت را برنامهریزی کنید. اجازه ندهید بار رویدادهای قدیمی، سرعت توسعه هر ویژگی جدید را کاهش دهد.
انتخاب ابزارهایی که تیم قادر به مدیریت آنها نیست. بهترین معماری هم اگر فقط یک نفر آن را بفهمد، شکست میخورد. اگر تیم شما Postgres و SQL بلد است، از همانجا شروع کنید. اگر یک ذخیرهساز رویداد (event store) تخصصی معرفی میکنید، مطمئن شوید که تخصص عملیاتی لازم برای عیبیابی (debug) آن را در ساعت دو صبح دارید.
یک نقطه شروع کاربردی
اگر این رویکرد با مشکل شما سازگار است، منتظر بازنویسی در پنج فصل آینده نمانید. همین هفته شروع کنید.
سیستمهای فعلی خود را بررسی (audit) کنید. جایی را پیدا کنید که وجود یک ردپای حسابرسی (audit trail) درد واقعی را برطرف کند. شاید یک ماشین وضعیت (state machine) سفارش باشد که در حال حاضر تنها یک ستون status دارد. شاید یک دفتر کل مالی باشد که در آن اصلاح موجودی مستلزم وصلههای دستی دیتابیس است. یک شکاف را انتخاب کنید که در آن بازنویسی وضعیت (overwriting state) به شما آسیب زده است.
سپس یک بهبود کوچک را انتخاب کنید که میتوانید امروز انجام دهید. یک جدول رویداد بسازید. یک استریم را مدلسازی کنید. یک تصویرسازی (projection) بنویسید که یک مدل خواندن از آن رویدادها بسازد. آن را پشت یک پرچم ویژگی (feature flag) مستقر کنید. تماشا کنید که چگونه ترافیک واقعی را مدیریت میکند.
رویدادسرا با PostgreSQL جادو نیست. این یک ابزار کاربردی برای تیمهایی است که نیاز دارند نه تنها بدانند چیزها کجا هستند، بلکه بدانند چگونه به آنجا رسیدهاند. آهسته بسازید، صادقانه اندازهگیری کنید و اجازه دهید نیازهای واقعی شما، معماری را هدایت کنند.
