رویکرد Event Sourcing (ES) ترکیبشده با Command Query Responsibility Segregation (CQRS) به ستون فقرات معماری بسیاری از سیستمهای با مقیاسپذیری بالا تبدیل شده است. در حالی که مزایای بخش خواندن بهخوبی مستند شده است، بخش نوشتن اغلب یک گلوگاه قابلتوجه را ارائه میدهد: نوشتن ترتیبی رویدادها در بستر ذخیرهسازی. وقتی میلیونها دستور همزمان دریافت میشوند، پیادهسازیهای ساده ممکن است با تداخل تراکنشها، تشدید قفلها و اشباع I/O مواجه شوند. در این پست، استراتژیهای پیشرفتهای را برای بهینهسازی این مسیرهای نوشتن بررسی میکنیم که بر دستهبندی و مکانیکهای فقط-افزودنی تمرکز دارند.
چالش تداخل در نوشتن
در یک پیادهسازی استاندارد Event Sourcing، هر دستور منجر به یک یا چند رویداد دامنه میشود که باید ذخیره شوند. اگر هر رویداد یک تراکنش پایگاه داده جداگانه را فعال کند، سربار دورهای شبکه و ثبت تراکنشها میتواند عملکرد را فلج کند. علاوه بر این، کنترل همزمانی بهینه (OCC) نیاز به مقایسه نسخهها در هر نوشتن دارد. اگر چندین دستور همزمان به یک اگریگیت (Aggregate) هدفگیری کنند، ممکن است اعتبارسنجی آنها شکست بخورد و نیاز به تلاش مجدد داشته باشند که منجر به مشکلات گروههای طوفانی میشود.
برای دستیابی به عملکرد بالا، باید از درج سطر-به-سطر به عملیات دستهبندیشده و فقط-افزودنی تغییر وضعیت دهیم. این رویکرد سربار قفلگذاری را به حداقل رسانده و کارایی I/O را به حداکثر میرساند.
استراتژیهای دستهبندی با عملکرد بالا
دستهبندی رویدادها تعداد دورهای شبکه به پایگاه داده را کاهش میدهد. به جای تأیید رویدادها به صورت جداگانه، برنامه رویدادها را در یک بافر محلی جمعآوری کرده و آنها را به صورت دستهای تخلیه میکند. با این حال، دستهبندی ساده میتواند تأخیر ایجاد کند. نکته کلیدی تعادل بین اندازه دسته و الزامات زمان واقعی است.
یک الگوی رایج شامل یک کارگر پسزمینه است که یک صف حافظه را نظارت میکند. وقتی صف به یک اندازه یا آستانه زمانی خاصی میرسد، کارگر دسته را به عنوان یک واحد اتمی واحد تأیید میکند. این اطمینان حاصل میکند که یا تمام رویدادهای دسته ذخیره میشوند یا هیچکدام، که ثبات داده را بدون قربانی کردن عملکرد حفظ میکند.
// کد شبه برای یک بوس رویداد با عملکرد بالا
class EventBatcher:
def __init__(self, batch_size=100, flush_interval_ms=50):
self.buffer = []
self.batch_size = batch_size
self.flush_interval = flush_interval_ms
def append(self, event):
self.buffer.append(event)
if len(self.buffer) >= self.batch_size:
self.flush()
def flush(self):
if not self.buffer:
return
# شروع یک تراکنش پایگاه داده واحد برای کل دسته
try:
with db.transaction():
for event in self.buffer:
db.insert("events", event)
db.commit()
except IntegrityError:
# مدیریت تداخلهای همزمانی در اینجا
retry_buffer(self.buffer)
finally:
self.buffer = []
این استراتژی سربار ثبت تراکنش را بهطور قابلتوجهی کاهش میدهد، زیرا پایگاه داده تنها نیاز دارد یک رکورد تأیید برای دهها یا صدها رویداد بنویسد. با این حال، توسعهدهندگان باید محتاط باشند: دستهبندی شعاع انفجار یک شکست را افزایش میدهد. اگر یک دسته شکست بخورد، تمام رویدادهای آن دسته باید بازگردانده شوند که ممکن است نیاز به منطق تلاش مجدد پیچیده داشته باشد.
مکانیکهای ذخیرهسازی فقط-افزودنی
رویکرد Event Sourcing ذاتاً فقط-افزودنی است. برخلاف پایگاههای داده رابطهای سنتی که سطرها را بهروزرسانی میکنند (که میتواند باعث شکست صفحه و پراکندگی شود)، افزودن رکوردهای جدید اجازه میدهد تا I/O ترتیبی انجام شود که بسیار سریعتر از نوشتنهای تصادفی در بیشتر رسانههای ذخیرهسازی است.
برای بهرهبرداری از این ویژگی، در نظر بگیرید که از ساختارهای لاگ فقط-افزودنی یا پایگاههای دادهای که برای نوشتنهای ترتیبی طراحی شدهاند، مانند Apache Kafka، Amazon Kinesis یا پایگاههای داده رویداد تخصصی مانند EventStoreDB استفاده کنید. این سیستمها با جداسازی کامل مسیر نوشتن از مسیر خواندن، برای عملکرد نوشتن بالا بهینه شدهاند.
هنگام پیادهسازی لاگهای فقط-افزودنی، اطمینان حاصل کنید که موتور پایگاه داده شما از پارتیشنبندی کارآمد پشتیبانی میکند. پارتیشنبندی بر اساس شناسه اگریگیت یا شناسه مستأجر تضمین میکند که نوشتن رویدادهای مرتبط محلی باقی میماند و تداخل قفل را بیشتر کاهش میدهد. به عنوان مثال، تمام رویدادهای برای یک اگریگیت سفارش واحد باید به یک پارتیشن یکسان نوشته شوند تا ثبات ترتیبی تضمین شود.
همزمانی بهینه با نوشتنهای دستهبندیشده
پیادهسازی OCC در یک محیط دستهبندیشده نیاز به مدیریت دقیق نسخهها دارد. به جای بررسی نسخه قبل از هر رویداد، میتوانید نسخه را در شروع دسته بررسی کنید. اگر نسخه از زمان دریافت دستور تغییر کرده باشد، کل دسته نامعتبر شده و تلاش مجدد میشود.
این رویکرد فرض میکند که در زمانی که طول میکشد تا دسته نوشته شود، وضعیت اگریگیت به احتمال زیاد به شدت تغییر نمیکند. اگرچه این یک مبادله است، اما اجازه میدهد همزمانی بسیار بالاتری وجود داشته باشد. برای سیستمهایی که به خطیابی خطی سختگیرانه نیاز دارند، ممکن است مکانیزمهای قفلگذاری اضافی یا شمارههای ترتیبی لازم باشد، اما برای اکثر دامنههای تجاری، ثبات نهایی ارائهشده توسط OCC دستهبندیشده قابل قبول است.
نتیجهگیری
بهینهسازی مسیر نوشتن در یک سیستم Event Sourcing برای دستیابی به مقیاسی که CQRS وعده میدهد حیاتی است. با ترکیب دستهبندی با عملکرد بالا با استراتژیهای ذخیرهسازی فقط-افزودنی، توسعهدهندگان میتوانند تأخیر را بهطور چشمگیری کاهش داده و عملکرد سیستم را افزایش دهند. کلید تعادل بین اندازه دستهها، مدیریت ظریف تداخلهای همزمانی و انتخاب موتورهای ذخیرهسازی است که در نوشتنهای ترتیبی عالی عمل میکنند. با رشد سیستم شما، این بهینهسازیها تفاوت بین یک برنامه کند و برنامهای که تحت بار سنگین بهطور بینقص مقیاس میپذیرد را رقم میزنند.