سعی می کنم به فراخور ان شاءالله چالش های معماری مختلفی رو در GoCasts راه اندازی کنم تا هم کمی بیشتر دانش خودمون رو به چالش بکشیم و هم اینکه از تجربیات و ایده های دیگران استفاده کنیم.
خب بی مقدمه بریم سراغ صورت مساله:
یک سری از دوستان پیشنهاد دادند که برای دیتابیس اصلی یک replica اضافه بشه و سرویس دوم دیتای مورد نیازش رو از replica بخونه.
از نظر من مزیت این روش اینه که همه دغدغه های consistency و availability بر عهده خود دیتابیس هست و لازم نیست برنامه نویس کار خاصی انجام بده. اما دو تا نکته منفی هم داره. اولیش اینه که ممکنه نگهداری سرویس در حالت replica چالش های خودشو داشته باشه و یه سربار اضافه مضاعف بشه. دومین و مهم ترین نکته منفی برای چالش پیش رو اینه که عموما دیتابیس های استفاده شده در هاست قابلیت رپلیکا شدن ندارند، پس این روش قابل استفاده نیست.
یکی از دوستان پیشنهاد دادند که یک event bus به سیستم اضافه بشه و همه eventهای مرتبط علاوه بر اعمال شدن روی دیتابیس اصلی روی event bus هم ذخیره بشه. در نتیجه سرویس دوم میتونه event هارو بخونه و مصرف کنه.
خب این راه حل تا حدودی به جواب مد نظر من نزدیکه، اما مشکل اینه که هیچ اشاره ای نشد که برای حفظ consistency چطور میخوان eventها رو بصورت transactional به event bus ارسال کنند. چون همانطور که اشاره شد یکپارچگی داده ها خیلی مهمه و در روشی که مطرح شد اشاره ای نشد که چطور میخوان این موضوع رو تضمین کنند.
یکی از دوستان پیشنهاد دادن که سرویس هایی که به داده نیاز دارند از read cache استفاده کنند و داده هایی که در cache موجود نیست رو از سرویس اصلی درخواست کنند. یکی از فرضیات مساله این بود که داده ها تغییرناپذیر هستند.
مزیت این روش اینه که پیاده سازیش ساده است و سرویس اصلی رو به هیچ وجه درگیر نمیکنه که خیلی مزیت مهمیه. عیبی که به نظر من میرسه اینه که این کار باعث میشه سرویس دوم به سرویس اول کاملا وابسته بمونه و اگه سرویس اول از دسترس خارج بشه کارایی سرویس دوم هم مختل بشه. ضمنا برای یک سری از درخواست ها latency زیاد میشه چون باید داده رو از سرویس اصلی درخواست کنه و داده ها از قبل در دسترس سرویس دوم قرار نگرفته اند. ایراد دیگه ای که به این روش هست که البته ایراد نیست، و واقعیت چالش هست اینه که داده های کاربر تغییر پذیرهستند و با فرض راه حل مغایرت داره.
یک سری از دوستان راه حل رو در استفاده از saga pattern دیدند که خب راه حل درستیه، اما مشکل اصلی اینه که پیچیدگی خیلی زیادی به مساله اضافه میکنه چون saga pattern جایی کاربرد داره که یک transaction چند سرویس مختلف رو درگیر کنه و برای اینکه atomic انجام بشه شما لازم دارید compensating transaction هارو هم پیاده سازی کنید که پیچیدگی زیادی به سیستم اضافه میکنه
راه حل ساده تر از saga pattern استفاده از transactional outbox هست که یکی از دوستان تقریبا به درستی روش رو توضیح دادند. این روش کجا کاربرد داره؟ وقتی که شما میخوای یه event یا message رو بصورت transactional و distributed بین ۲ تا دیتابیس یا بهتره بگیم ۲ تا storage service مختلف انجام بدی. خیلی وقت ها پیش میاد که ما لازم داریم دیتایی که در دیتابیس اصلی ذخیره میشه رو در یک storage دیگه مثلا مثل message broker یا event bus هم داشته باشیم. که این مورد خیلی به صورت مساله ما نزدیکه. چون ما لازم داریم که دیتای دیتابیس اصلی رو در سرویس دوم مون هم داشته باشیم و نمیخوام داده ای رو از دست بدیم.
حالا روش کار چطوریه؟ خب همانطور که گفتم ممکنه ذاتا ۲ تا سرویسی که قراره دیتا رو داشته باشن از ۲ جنس متفاوت باشن، مثلا یکیش دیتابیس mysql باشه و دیگری nats باشه، پس ما هیچ راهی نداریم که دیتارو بصورت atomic بین این ۲ سرویس ذخیره کنیم. از طرفی هم واقعا نمیخوایم atomic باشه چون اونوقت availability یکی روی کل تراکنش تاثیر میذاره، در حالیکه ما در صورت مساله اشاره کردیم که میخوام یه کپی از دیتارو در سرویس دوم داشته باشیم و availability سرویس دوم نباید روی تراکنش تاثیر بذاره. ایده transactional outbox اینه که شما تو همون دیتابیس اصلی ای که داری مثلا mysql یه جدول جداگانه در نظر بگیر به اسم مثلا outbox و بصورت atomic transaction داده ای که میخوای رو هم در جدول اصلی ذخیره کن مثلا جدول user و هم در جدول outbox ذخیره کن. بعدش یه سرویس به اسم message relay داشته باش که میتونه یه cron job باشه، این سرویس هر چند وقت یکبار(یا حتی بصورت آنی به کمک هوک های دیتابیس) جدول outbox رو چک میکنه و داده هایی که ارسال نشدند رو به سرویس دوم یا broker مورد نظر ارسال میکنه و وضعیت رکورد رو بروز میکنه. طبیعتا سرویس دوم باید نسبت به پیام های دریافتی خودکفا باشه، یعنی چی خودکفا باشه؟ یعنی اگه یه پیام رو ۲ بار دریافت کرد فقط یکبار اونو اعمال کنه. این قضیه به message relay کمک میکنه که با خیال راحت یه پیام رو بیش از یکبار بفرسته و خودشو درگیر چالش های ack گرفتن از سرویس دوم نکنه.
خب مزایای این روش چیه؟ پیاده سازی نسبتا ساده ای که داره و تضمین یکپارچگی داده ها از مزیت هاشه. عیبش چیه؟ عیبش اینه که سرویس اصلی رو برای پیاده سازی درگیر میکنه یه ذره، البته اگه سرویس اصلی مثلا بصورت pod روی kubernetes ارائه میشد اونوقت این message relay میتونست به صورت یه side car container بالا بیاد.
از لینک زیر میتونید بیشتر در موردش بخونید: Transactional Outbox
اگر در طول مسیر یادگیری احتیاج به کمک دارید می توانید با بنده از طریق ایمیل در ارتباط باشید. همچنین برای اطلاع از مطالب جدید می توانید کانال تلگرام GoCasts را دنبال کنید.