تصویرسازی فضاییِ ماژول‌های مستقل و گذرگاه‌های محدود میان آن‌ها

azibom ·

معماری نرم‌افزار برای تغییر: ناوردا، مرز و هزینهٔ هماهنگی

معماری نرم‌افزار را معمولاً با شکل اجزا توصیف می‌کنیم: لایه‌ها، سرویس‌ها، صف‌ها و پایگاه‌های داده. اما ارزش معماری وقتی آشکار می‌شود که یک فرض عوض شود، دو درخواست هم‌زمان برسند، یا یک پاسخ در شبکه گم شود. سؤال اصلی این نیست که چند سرویس داریم؛ این است که برای حفظ کدام حقیقت، چه کسانی باید از چه تغییری خبر داشته باشند.

۱. مرز خوب، تصمیمی را پنهان می‌کند؛ نه صرفاً چند فایل را

پارناس در مقالهٔ کلاسیک ۱۹۷۲ معیار تجزیه را از «مراحل پردازش» به «تصمیم‌های طراحی پنهان‌شده پشت رابط» تغییر می‌دهد. در این نگاه، ماژول صرفاً یک ایستگاه در نمودار جریان نیست: صاحب دانشی است که بقیه نباید برای استفاده از آن بدانند. این تمایز، نقطهٔ شروع مناسبی برای نقد معماری‌هایی است که از نظر پوشه‌بندی تمیزند اما هر تغییر واقعی در آن‌ها به همه‌جا سرایت می‌کند.[۱]

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

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

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

۲. معماری یک مسئلهٔ بهینه‌سازی زیر عدم‌قطعیت است

برای دقیق‌تر فکرکردن، یک مدل تصمیم‌گیری ساده می‌سازیم؛ این مدل پیشنهادی این مقاله است، نه فرمول تجربیِ اندازه‌گیری‌شده برای همهٔ تیم‌ها. فرض کنید سناریوی تغییر s با نرخ تقریبی λₛ رخ می‌دهد. هزینهٔ تغییر در معماری A را در سه جزء بشکنیم: ویرایش، هماهنگی بین صاحبان و مهاجرت داده یا قرارداد.

J(A) = Σₛ λₛ · [Eₛ(A) + Hₛ(A) + Mₛ(A)]
       + O(A) + R(A)

در این‌جا E هزینهٔ ویرایش و آزمون، H هزینهٔ هماهنگی، M هزینهٔ مهاجرت، O هزینهٔ عملیاتی در یک افق زمانی مشخص و R زیان موردانتظار ناشی از ریسک است. همهٔ مؤلفه‌ها باید با واحد قابل‌مقایسه، مثلاً نفرساعت یا هزینهٔ پولی، بیان شوند. جمع‌زدن «تعداد سرویس»، «میلی‌ثانیه» و «درصد ریسک» بدون تبدیل واحد، فقط ظاهری ریاضی دارد.

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

یک نمونهٔ فرضی: تفکیک قیمت‌گذاری از سفارش، هزینهٔ هر تغییر تعرفه را از ۱۲ به ۵ نفرساعت کاهش می‌دهد. با ۲۰ تغییر در سال، صرفه‌جویی ۱۴۰ نفرساعت است. اگر عملیات، هماهنگی نسخه‌ها و مهاجرت سالانهٔ اضافه ۲۰۰ نفرساعت باشد، این توجیه به‌تنهایی تفکیک استقرار را تأیید نمی‌کند. البته استقلال مقیاس یا کاهش ریسک ممکن است نتیجه را عوض کند؛ باید آن‌ها را صریح وارد مدل کرد، نه با برچسب «آینده‌نگری» از محاسبه معافشان کرد.

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

۳. ناوردا مشخص می‌کند کدام تصمیم باید یک‌جا گرفته شود

ناوردا یا invariant گزاره‌ای دربارهٔ حالت‌های مجاز سیستم است. «پرداخت موفق معمولاً ثبت می‌شود» ناوردا نیست؛ «برای هر شناسهٔ برداشت، حداکثر یک اثر مالی ثبت می‌شود» نزدیک‌تر است. ناوردا باید آن‌قدر دقیق باشد که بتوان نمونهٔ نقض آن را ساخت.

برای رزرو موجودی، این گزاره را انتخاب کنیم: مجموع تعهدهای فعال برای یک کالا از ظرفیت قابل‌فروش بیشتر نشود. حالا اگر سفارش، پرداخت و انبار هرکدام با خواندن یک نسخهٔ قدیمی از موجودی تصمیم قطعی بگیرند، صرفاً سه سرویس مستقل نداریم؛ سه مرجع رقیب برای یک حقیقت داریم.

مالکیت داده به معنی «این جدول در کدام دیتابیس است» کافی نیست. مالکیت مهم‌تر این است که چه کسی مجاز است تصمیمی بسازد که ناوردا را تغییر می‌دهد. سرویس دیگر می‌تواند نمایش کش‌شدهٔ موجودی داشته باشد، اما این مشاهده لزوماً اختیار تعهددادن روی همان موجودی را ایجاد نمی‌کند.

  1. حالت‌های مجاز
  2. عملیات تغییر‌دهنده
  3. مرجع تصمیم
  4. قرارداد و مرز

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

۴. مثال رزرو: دو اجرای محلی درست، یک نتیجهٔ جهانی غلط

ظرفیت قابل‌فروش را ۱۰ واحد بگیریم. دو پردازش هم‌زمان، هرکدام ۷ واحد درخواست می‌کنند. هر دو عدد ۱۰ را می‌خوانند؛ هر دو شرط «۷ کمتر یا مساوی موجودی است» را درست می‌بینند؛ هر دو رزرو می‌کنند. هر تصمیم محلی قابل‌توجیه بوده، ولی مجموع تعهدها ۱۴ شده است. مشکل با افزودن یک کش سریع‌تر حل نمی‌شود؛ تصمیم‌گیری روی مشاهدهٔ قدیمی انجام شده است.

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

UPDATE stock
SET available = available - :quantity
WHERE sku = :sku
  AND :quantity > 0
  AND available >= :quantity;

تعداد ردیف‌های تغییرکرده، نتیجهٔ رزرو را تعیین می‌کند. اما این قطعه به‌تنهایی سامانهٔ رزرو نیست: باید رکورد رزرو، شناسهٔ درخواست، مالک، زمان انقضا و کاهش موجودی در همان تراکنش محلی ثبت شوند؛ محدودیت یکتایی شناسهٔ درخواست هم جلوی اثر تکراری را بگیرد. پشتیبانی موتور از تراکنش و رفتار هم‌زمانی این عملیات باید واقعاً آزموده شود.

راه دیگر، تقسیم حق رزرو است: منطقهٔ اول ۶ و منطقهٔ دوم ۴ سهم دریافت کند. تا وقتی هرکدام بیش از سهم خود تعهد نمی‌دهد، مجموع از ۱۰ عبور نمی‌کند. اما انتقال سهم، بازپس‌گیری از گرهٔ خراب و جلوگیری از دو مالک هم‌زمان برای یک سهم مسئله‌های واقعی‌اند. حذف هماهنگی از مسیر هر درخواست، به معنی حذف هماهنگی از کل چرخهٔ زندگی حق نیست.

انتخاب میان این دو طراحی با «کدام مدرن‌تر است» حل نمی‌شود. اگر ظرفیت کمیاب است و عدم‌فروش موقت قابل‌تحمل است، مرجع واحد شاید ساده‌تر باشد. اگر قطع ارتباط منطقه‌ای باید تحمل شود و تخصیص سهم معنا دارد، حق‌های تقسیم‌شده گزینهٔ جدی‌تری‌اند. حتی ممکن است سهم آزاد در یک منطقه تمام شود و منطقهٔ دیگر سهم استفاده‌نشده داشته باشد؛ ایمنی حفظ شده ولی استفادهٔ کامل از ظرفیت موقتاً قربانی شده است.

تصویرسازی واقع‌گرایانهٔ تقاطع شهری و هماهنگی مسیرهای عبوری
تصویرسازی مفهومی: در محل برخورد مسیرها، تصمیم‌های مستقل به هماهنگی نیاز دارند.

۵. هماهنگی را از معنای عملیات استخراج کنید، نه از شعار معماری

پژوهش Coordination Avoidance in Database Systems با مفهوم invariant confluence این پرسش را صورت‌بندی می‌کند: آیا حالت‌های معتبرِ قابل‌دستیابی از یک جد مشترک، پس از ادغام هم معتبر می‌مانند؟ در چارچوب و فرض‌های همان مقاله، این خاصیت معیار امکان اجرای بدون هماهنگی همراه با اعتبار، دسترس‌پذیری تراکنشی و همگرایی است. این نتیجه را نباید به «هر سیستم eventual consistency امن است» ترجمه کرد؛ مجموعهٔ عملیات، شیوهٔ ادغام و ناورداهای کامل جزئی از شرط‌اند.[۲]

در مثال رزرو، ادغام دو تعهد مستقل ۷ واحدی، حالت نامعتبر می‌سازد. در مقابل، اگر عملیات فقط افزودن مشاهده به یک مجموعه با شناسه‌های متمایز باشد و ناوردا صرفاً «مشاهده‌های معتبر حذف نشوند» باشد، اجتماع مجموعه‌ها این شرط را نگه می‌دارد. به محض اضافه‌کردن شرطی مثل «در هر بازه حداکثر ۱۰ مشاهده پذیرفته شود»، تحلیل عوض می‌شود.

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

اتمیک‌بودن هم با پیشرفت‌کردن یکی نیست. گری و لمپورت در بررسی commit تراکنش نشان می‌دهند که نسخهٔ کلاسیک Two-Phase Commit با خرابی هماهنگ‌کننده می‌تواند مسدود شود و صورت‌بندی مقاوم‌تری مبتنی بر اجماع ارائه می‌کنند. نتیجهٔ عملی برای طراحی ما این است که انتخاب یک تضمین، هزینهٔ خرابی و شرط پیشرفت هم دارد؛ نباید فقط نام پروتکل را در نمودار بنویسیم.[۳]

۶. عبور از مرز یعنی پذیرفتن زمان و نادانی

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

الگوی عملی برای این مثال، ثبت رزرو و رکورد «رویدادِ آمادهٔ ارسال» در یک تراکنش محلی است. فرستنده بعداً این رکورد را ارسال می‌کند. این طراحی شکاف میان تغییر داده و ایجاد قصد ارسال را می‌بندد؛ ولی ارسال تکراری، ترتیب دریافت و اجرای اثر در مصرف‌کننده را خودبه‌خود حل نمی‌کند. مصرف‌کننده هنوز به شناسهٔ رویداد، ثبت پردازش و یک سیاست روشن دربارهٔ ترتیب نیاز دارد.

هلند در مقالهٔ موضعی ۲۰۰۷، طراحی بیرون از تراکنش‌های توزیع‌شده را از منظر موجودیت‌ها، فعالیت‌ها و پیام‌رسانی بررسی می‌کند. توجه او به مرزهای اتمی محلی و حافظهٔ تعاملات، برای فکرکردن به این مسئله مفید است؛ این مقاله حکم ممنوعیت همیشگی تراکنش توزیع‌شده نیست.[۴]

مسئلهٔ دشوارتر وقتی است که اثر خارجی انجام شده ولی پاسخ گم شده است. timeout لزوماً به معنی شکست عملیات نیست. ممکن است پرداخت انجام شده باشد و فقط رسید به ما نرسیده باشد. بنابراین حالت «نتیجه نامعلوم» باید در مدل وجود داشته باشد. اجرای دوباره بدون شناسهٔ پایدار می‌تواند برداشت دوم بسازد؛ اعلام شکست قطعی هم می‌تواند تعهد واقعی را از دفتر ما حذف کند.

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

۷. Saga زمان را به عقب برنمی‌گرداند

مقالهٔ اصلی Sagas تراکنش طولانی را به زیرتراکنش‌هایی با عملیات جبرانی تقسیم می‌کند. این ایده راهی برای مدیریت اجرای طولانی بدون نگه‌داشتن یک تراکنش بزرگ است؛ اما قرارداد هر جبران و امکان دخالت تراکنش‌های دیگر اهمیت دارد.[۵]

در سامانهٔ رزرو ما، آزادکردن موجودی شاید اثر رزرو را جبران کند؛ اما پیام ارسالی به مشتری را از حافظهٔ او پاک نمی‌کند. پس‌دادن پول نیز معادل «هیچ پرداختی رخ نداده» نیست: رسید، هزینهٔ پردازش و زمان انتظار ممکن است باقی بمانند. واژهٔ rollback اگر برای همهٔ این‌ها استفاده شود، تفاوت اثر فنی و اثر دنیای واقعی را می‌پوشاند.

برای هر گام، سه قرارداد جدا بنویسید: شرط اجرای اولیه، شرط جبران، و رفتار وقتی خود جبران شکست می‌خورد. رزرو منقضی‌شده و رزروی که قبلاً مصرف شده، یک عملیات آزادسازی یکسان ندارند. جبران باید روی وضعیت معتبر عمل کند، نه اینکه فقط «برعکسِ عملیات قبلی» را اجرا کند.

این‌جا معیار موفقیت هم تغییر می‌کند. «تمام گام‌ها بدون exception تمام شدند» کافی نیست. باید بدانیم آیا تعهد خارجی با حالت داخلی سازگار شده، آیا پروندهٔ نامعلوم باقی مانده و چه کسی مسئول تطبیق آن است. یک صف بی‌نهایت از جبران‌های در حال retry، راه‌حل مسئله نیست؛ بدهی عملیاتیِ بدون صاحب است.

۸. یک مرز معماری را چگونه آزمایش کنیم؟

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

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

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

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

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

۹. نتیجه: مرز را از تعهد طراحی کنید

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

آیا این استدلال همیشه به نفع مونولیت است؟

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

آیا eventual consistency یعنی پذیرفتن دادهٔ غلط؟

نه لزوماً. دیررسیدن مشاهده با نامعتبرشدن تعهد فرق دارد. می‌توان نمایش را دیر به‌روز کرد و حق رزرو را دقیق نگه داشت. باید روشن کنیم کدام ناوردا در تمام لحظه‌ها لازم است و کدام نتیجه اجازه دارد بعداً همگرا شود.

قدم عملی از کجاست؟

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

منابع

  1. پارناس — معیار تجزیهٔ سیستم به ماژول‌ها، ۱۹۷۲
  2. Bailis و همکاران — Coordination Avoidance، VLDB، ۲۰۱۴
  3. گری و لمپورت — Consensus on Transaction Commit، ۲۰۰۶
  4. پت هلند — Life beyond Distributed Transactions، CIDR، ۲۰۰۷
  5. Garcia-Molina و Salem — Sagas، SIGMOD، ۱۹۸۷