آنچه در این مقاله میخوانید [پنهانسازی]
Saga Pattern در میکروسرویس روشی است برای مدیریت تراکنش های توزیع شده: به جای یک تراکنش سراسری بین سرویس ها، زنجیره ای از تراکنش های محلی با عملیات جبرانی تعریف می شود تا در صورت خطا اثرها برگردانده شوند و داده ها به سازگاری نهایی برسند. هماهنگی این زنجیره یا با ارکستراسیون توسط یک هماهنگ کننده مرکزی انجام می شود یا با کُرئوگرافی مبتنی بر رویداد بین خود سرویس ها.
Saga Pattern دقیقا چیست و چگونه کار می کند؟
سگا دنباله ای از چند تراکنش محلی مستقل است که هر کدام در پایگاه داده همان سرویس به صورت اتمیک اجرا می شوند. اگر هر قدم شکست بخورد، برای همه قدم های موفق قبلی عملیات جبرانی (Compensating Action) اجرا می شود؛ مثلا لغو رزرو موجودی یا بازگشت وجه. بنابراین به جای قفل طولانی داده ها و 2PC، با پذیرش سازگاری نهایی (Eventual Consistency) و تعریف جبران، کسب و کار پایدار می ماند.
دو سبک رایج هماهنگی عبارتند از:
- ارکستراسیون: یک هماهنگ کننده جریان را قدم به قدم پیش می برد و در خطا، جبران ها را فراخوانی می کند.
- کُرئوگرافی: سرویس ها با انتشار و مصرف رویدادها یکدیگر را هدایت می کنند؛ بدون کنترلر مرکزی.
چه زمانی از Saga استفاده کنیم و چه زمانی نه؟
از Saga استفاده کنید اگر:
- فرآیند شما چند سرویس و چند پایگاه داده را درگیر می کند و 2PC نمی خواهید یا نمی توانید.
- قدم ها ممکن است طولانی باشند (رزرو، پرداخت، ارسال) و تحمل سازگاری نهایی دارید.
- می توانید برای هر قدم موفق، عملیات جبرانی معنی دار تعریف کنید.
بهتر است استفاده نکنید اگر:
- یک سرویس و یک دیتابیس دارید؛ یک تراکنش ساده کفایت می کند.
- الزام قانونی یا فنی برای سازگاری آنی و قوی بین چند مرجع داده دارید و جبران کافی نیست.
- عملیات شما غیرقابل جبران است (مثلا تماس خارجی بدون امکان لغو) و کنترل ریسک کافی ندارید.
دو سبک اجرا: ارکستراسیون یا کُرئوگرافی
ارکستراسیون
هماهنگ کننده مسیر را کنترل می کند. مزایا: ساده تر برای استدلال، رصد و نسخه بندی فرآیند. معایب: وابستگی مرکزی و نیاز به مقیاس پذیری هماهنگ کننده.
کُرئوگرافی مبتنی بر رویداد
هر سرویس با دریافت یک رویداد، کار خودش را انجام می دهد و نتیجه را به صورت رویداد جدید منتشر می کند. مزایا: کوپلینگ کمتر و تکامل پذیری بالا. معایب: مشاهده پذیری دشوارتر و احتمال حلقه یا انفجار رویداد اگر قراردادها شفاف نباشند.
یک سناریوی عملی: پردازش سفارش
سرویس ها: Order، Inventory، Payment، Shipping.
- Order ثبت می شود و وضعیت Pending می گیرد.
- Inventory موجودی را رزرو می کند.
- Payment مبلغ را کسر می کند.
- Shipping برنامه ارسال را ثبت می کند.
- Order به Confirmed تغییر می کند.
- اگر هر قدم شکست بخورد، قدم های موفق قبلی جبران می شوند: آزادسازی موجودی، بازگشت وجه، لغو ارسال و در نهایت لغو سفارش.
ارکستراسیون با مثال کد ساده (آموزشی)
نمونه زیر ایده ارکستراسیون Saga را با فراخوانی سرویس ها و جبران نمایش می دهد. این کد برای فهم الگو است و جایگزین زیرساخت پیام رسان، ذخیره وضعیت Saga و کنترل خطا در محیط تولید نیست.
// pseudo-orchestrator.ts (آموزشی)
type OrderId = string;
interface Services {
order: {
createPending: (orderId: OrderId) => Promise<void>;
confirm: (orderId: OrderId) => Promise<void>;
cancel: (orderId: OrderId, reason: string) => Promise<void>;
};
inventory: {
reserve: (orderId: OrderId) => Promise<void>;
release: (orderId: OrderId) => Promise<void>;
};
payment: {
capture: (orderId: OrderId) => Promise<void>;
refund: (orderId: OrderId) => Promise<void>;
};
shipping: {
schedule: (orderId: OrderId) => Promise<void>;
cancelSchedule: (orderId: OrderId) => Promise<void>;
};
}
export async function placeOrderSaga(orderId: OrderId, sv: Services) {
let step = 0;
try {
await sv.order.createPending(orderId); // step 1
step = 1;
await sv.inventory.reserve(orderId); // step 2
step = 2;
await sv.payment.capture(orderId); // step 3
step = 3;
await sv.shipping.schedule(orderId); // step 4
step = 4;
await sv.order.confirm(orderId); // step 5
step = 5;
} catch (err: any) {
// Compensation (برعکس قدم های موفق)
if (step >= 4) {
await safe(() => sv.shipping.cancelSchedule(orderId));
}
if (step >= 3) {
await safe(() => sv.payment.refund(orderId));
}
if (step >= 2) {
await safe(() => sv.inventory.release(orderId));
}
if (step >= 1) {
await safe(() => sv.order.cancel(orderId, reasonFrom(err)));
}
throw err;
}
}
async function safe(fn: () => Promise<unknown>) {
try { await fn(); } catch { /* log and continue */ }
}
function reasonFrom(err: any): string {
return (err && err.message) || "saga_failed";
}
در عمل باید وضعیت هر Saga را در یک استور مطمئن ذخیره کنید (State و Step)، با پیام رسان قابل اعتماد کار کنید، و جبران ها را ایمن و تکرار پذیر بنویسید.
کُرئوگرافی مبتنی بر رویداد: قرارداد رویداد و مصرف کننده مقاوم
رویدادهای اصلی:
{
"OrderCreated": { "orderId": "o123", "items": [...], "total": 420000 },
"InventoryReserved": { "orderId": "o123" },
"InventoryFailed": { "orderId": "o123", "reason": "out_of_stock" },
"PaymentCaptured": { "orderId": "o123" },
"PaymentFailed": { "orderId": "o123", "reason": "card_declined" },
"ShipmentScheduled": { "orderId": "o123", "tracking": "T-001" },
"ShipmentFailed": { "orderId": "o123", "reason": "address_invalid" },
"OrderConfirmed": { "orderId": "o123" },
"OrderCancelled": { "orderId": "o123", "reason": "..." }
}
نمونه مصرف کننده با کنترل ایندمپوتنسی (پردازش تکراری بی خطر):
// inventory-consumer.js (آموزشی)
async function handleOrderCreated(evt, deps) {
const id = evt.meta.id; // شناسه یکتا رویداد
if (await deps.store.isProcessed(id)) return; // ایندمپوتنسی
try {
const ok = await deps.inventory.tryReserve(evt.orderId, evt.items);
if (ok) {
await deps.outbox.publish("InventoryReserved", { orderId: evt.orderId });
} else {
await deps.outbox.publish("InventoryFailed", { orderId: evt.orderId, reason: "out_of_stock" });
}
await deps.store.markProcessed(id);
} catch (e) {
// خطا را لاگ کنید و اجازه دهید سیستم با Retry پیام دوباره را تحویل دهد
throw e; // at-least-once
}
}
برای ارسال مطمئن رویدادها از Outbox Pattern استفاده کنید تا نوشتن در دیتابیس و درج رویداد در صف منطقا اتمیک باشد.
-- outbox table (آموزشی)
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
یک پردازشگر Outbox رکوردهای pending را به بروکر پیام ارسال می کند و پس از موفقیت، وضعیت را به sent تغییر می دهد.
الگوهای مکمل و نکات اجرایی مهم
- Idempotency: همه عملیات های جانبی مثل رزرو، کسر وجه و لغو باید در صورت Retry نتیجه یکسان بدهند (با کلید ایندمپوتنسی یا ثبت eventId).
- Timeout و Retry با Backoff: هر قدم باید زمان انتظار و سیاست Retry معقول داشته باشد. پیام های شکست خورده را به Dead Letter Queue بفرستید.
- ذخیره وضعیت Saga: وضعیت جاری، قدم بعدی، دلیل خطا و تلاش های انجام شده را ذخیره کنید تا از دست رفتن هماهنگ کننده باعث بلاتکلیفی نشود.
- Correlation Id: برای ردیابی یک سفارش در لاگ و تریس، شناسه همبستگی ثابت در همه پیام ها عبور دهید.
- مشاهده پذیری: متریک های تعداد Saga های موفق، در حال اجرا، لغوشده و زمان تکمیل را پایش کنید. تریس توزیع شده بسیار کمک می کند.
- قرارداد رویداد نسخه پذیر: تغییر فیلدها را با حفظ سازگاری عقبرو انجام دهید و مصرف کننده ها را به تدریج به نسخه جدید ببرید.
- جبران قابل اتکا: جبران ها هم ممکن است شکست بخورند؛ برای آنها نیز Retry، لاگ و هشدار طراحی کنید.
- محدودیت ایزوله سازی: در بازه بین قدم ها ممکن است کاربر داده موقتا ناسازگار ببیند. این را در تجربه کاربری مدیریت کنید (وضعیت در حال پردازش، قفل نمایشی).
خطاهای رایج و محدودیت ها
- تعریف نکردن جبران معنادار: اگر قدمی اثر خارجی غیرقابل لغو دارد، Saga مناسب نیست یا باید از رزرو قابل لغو استفاده شود.
- فرض تحویل دقیقا یک بار: اغلب بروکرها at-least-once هستند؛ بدون ایندمپوتنسی دچار دوباره کاری می شوید.
- وابستگی بیش از حد به هماهنگ کننده: در ارکستراسیون اگر مقیاس پذیری و تحمل خطا رعایت نشود، گلوگاه می شود.
- انفجار رویداد در کُرئوگرافی: نبود قرارداد روشن یا فیلترهای مناسب باعث چرخه بی پایان یا طوفان رویداد می شود.
- نادیده گرفتن داده خوانده قدیمی: مصرف تصمیمات کسب و کاری حساس با داده قدیمی می تواند خطا ایجاد کند؛ لازم است استراتژی خواندن و قفل منطقی داشته باشید.
چک لیست سریع پیاده سازی Saga
- فرآیند را به قدم های محلی با مرز سرویس ها بشکنید و برای هر قدم، جبران تعریف کنید.
- سبک هماهنگی را انتخاب کنید: ارکستراسیون برای کنترل و مانیتورینگ ساده، کُرئوگرافی برای چابکی و کوپلینگ کمتر.
- قرارداد پیام ها یا API هماهنگ کننده را مستند و نسخه پذیر کنید.
- Idempotency، Outbox، Retry، DLQ و Correlation Id را به عنوان زیرساخت پایه پیاده کنید.
- وضعیت Saga و تاریخچه قدم ها را در یک ذخیره ساز مطمئن نگه دارید.
- آلارم و داشبورد برای نرخ شکست، زمان تکمیل و Backlog صف ها بسازید.
- سناریوهای شکست را به صورت خودکار در تست های یکپارچه شبیه سازی کنید.
چطور صحت اجرا را بررسی کنیم؟
- تست یکپارچه مرحله ای: شکست عمدی هر قدم و بررسی اجرای جبران های متناظر.
- تست ایندمپوتنسی: ارسال دوباره همان رویداد یا تکرار درخواست و انتظار خروجی بدون اثر مضاعف.
- تست تاخیر و Timeout: تاخیر کنترل شده در سرویس ها و ارزیابی Retry و پیام های DLQ.
- ردیابی سراسری: با Correlation Id دنباله کامل سفارش از رویداد آغازین تا پایان را بازسازی کنید.
از کجا شروع کنیم؟
یک فرآیند محدود ولی واقعی مثل ثبت سفارش را انتخاب کنید، قدم ها و جبران ها را روشن بنویسید، سبک هماهنگی را بر اساس اولویت مانیتورینگ یا چابکی برگزینید، زیرساخت پیام رسان و Outbox را آماده کنید و سناریوهای شکست را قبل از انتشار به تولید به طور خودکار آزمایش کنید. با همین چرخه کوچک، الگوی پایدار خود را شکل می دهید و سپس آن را به سایر فرآیندها توسعه می دهید.







