بک اند

معماری میکروسرویس‌ها

میکروسرویس‌ها مشکل «کد بزرگ» را با مشکل «سیستم توزیع‌شده» عوض می‌کنند، و این معامله فقط گاهی به سود شماست. این مقاله می‌گوید کی ارزشش را دارد، مرزها را چطور بکشید و چه چیزهایی بعد از تقسیم‌کردن باید حل شوند.

  • نویسنده تیم تتا
  • ۸ دقیقه مطالعه
  • به‌روزرسانی:
معماری میکروسرویس‌ها

چه زمانی میکروسرویس ارزش دارد؟

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

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

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

مرز سرویس‌ها را چگونه بکشیم؟

رایج‌ترین اشتباه، برش بر اساس لایه‌ی فنی است (سرویس دیتابیس، سرویس API، سرویس UI). این کار باعث می‌شود هر تغییر ساده چند سرویس را درگیر کند. به‌جای آن، بر اساس قابلیت کسب‌وکار ببرید؛ مثلاً در یک فروشگاه: کاتالوگ، سبد و سفارش، پرداخت، اعلان‌ها. اصطلاح «محدوده‌ی مفهومی» (bounded context) در طراحی دامنه‌محور همین را می‌گوید.

نشانه‌های مرز درست

  • بیشتر تغییرات یک ویژگی فقط یک سرویس را لمس می‌کند.
  • سرویس می‌تواند بدون هماهنگی با تیم دیگر مستقر شود.
  • سرویس دیتابیس خودش را دارد و دیگران مستقیم از آن نمی‌خوانند.
  • اگر دو سرویس برای هر درخواست با هم صحبت می‌کنند، احتمالاً باید یکی باشند.
Illustration of small glowing service cubes connected by colourful data streams around a central message bus
سرویس خوب کوچک نیست؛ مستقل است و دلیل تغییرش فقط یک بخش از کسب‌وکار است.

ارتباط بین سرویس‌ها

دو الگوی اصلی وجود دارد. در ارتباط همگام (REST یا gRPC) سرویس A منتظر پاسخ B می‌ماند؛ ساده است و برای پرسش‌هایی که نتیجه‌شان همین الان لازم است مناسب است. در ارتباط ناهمگام (رویداد روی یک message broker مثل RabbitMQ یا Kafka) سرویس A فقط می‌گوید «چیزی رخ داد» و هر سرویس علاقه‌مند خودش واکنش نشان می‌دهد.

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

بیرون از سازمان، مشتری‌ها معمولاً فقط با یک API Gateway حرف می‌زنند که مسیریابی، احراز هویت و محدودیت نرخ را یک‌جا انجام می‌دهد. قراردادهای API را از ابتدا نسخه‌دار کنید؛ نکته‌هایش را در طراحی API های مقیاس‌پذیر آورده‌ایم، و برای احراز هویت بین سرویس‌ها JWT یک گزینه‌ی رایج است.

داده و سازگاری

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

  • Saga: توالی مراحل محلی، هرکدام با یک عمل جبرانی. اگر پرداخت رد شد، سفارش لغو و موجودی برگردانده می‌شود.
  • Outbox: رویداد در همان تراکنش محلی کنار داده ذخیره می‌شود و یک فرایند جدا آن را منتشر می‌کند؛ این‌طور دیگر نمی‌شود داده ثبت شود ولی رویدادش گم شود.
sql
-- same transaction: the order and the event that announces it
BEGIN;
INSERT INTO orders (id, user_id, total) VALUES (42, 7, 1500);
INSERT INTO outbox (aggregate_id, type, payload, published_at)
VALUES (42, 'order.created', '{"orderId":42,"total":1500}', NULL);
COMMIT;

-- a worker polls unpublished rows, sends them to the broker, then marks them
SELECT id, type, payload FROM outbox WHERE published_at IS NULL ORDER BY id LIMIT 100;

چون رویدادها ممکن است دوبار تحویل شوند، مصرف‌کننده‌ها باید idempotent باشند: پردازش دوباره‌ی همان پیام نتیجه‌ی متفاوتی نسازد. برای خواندن‌های پرتکرار هم می‌توانید یک نسخه‌ی خواندنی را با کش Redis نگه دارید.

تاب‌آوری در برابر شکست

در سیستم توزیع‌شده، «کند» از «خراب» بدتر است؛ چون درخواست‌ها روی هم انباشته می‌شوند و یک سرویس کند کل زنجیره را می‌خواباند. سه ابزار پایه:

  1. Timeout روی هر تماس خروجی، همیشه کوتاه‌تر از timeout بالادست.
  2. Retry فقط برای عملیات idempotent، با تأخیر نمایی و مقدار تصادفی (jitter) تا همه‌ی کلاینت‌ها هم‌زمان دوباره تلاش نکنند.
  3. Circuit breaker: بعد از چند شکست پشت‌سرهم، مدتی تماس را قطع کنید تا سرویس مقصد فرصت بازیابی داشته باشد.
js
async function callWithRetry(url, { retries = 3, timeoutMs = 800 } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
      if (res.ok) return await res.json();
      if (res.status < 500) throw new Error("client error " + res.status); // do not retry 4xx
    } catch (err) {
      if (attempt === retries) throw err;
    }
    // exponential backoff with full jitter: 0..(100ms * 2^attempt)
    const delay = Math.random() * 100 * 2 ** attempt;
    await new Promise((r) => setTimeout(r, delay));
  }
}

استقرار و مشاهده‌پذیری

هر سرویس باید با یک فرمان ساخته و مستقر شود؛ برای همین کانتینر تقریباً پیش‌شرط است. اگر هنوز Docker را شروع نکرده‌اید، دیپلوی پروژه با Docker نقطه‌ی شروع خوبی است و مرجع رسمی مستندات Docker همه‌ی جزئیات را دارد.

مهم‌تر از آن، دیدن سیستم است. یک درخواست کاربر از پنج سرویس عبور می‌کند و بدون شناسه‌ی همبستگی (correlation ID) در لاگ‌ها و ردیابی توزیع‌شده، پیداکردن علت خطا شبیه جست‌وجوی سوزن در انبار کاه است. جزئیاتش را در مانیتورینگ و لاگ‌گیری حرفه‌ای آورده‌ایم؛ این‌ها را قبل از جدا کردن اولین سرویس آماده کنید، نه بعد از آن.

مهاجرت تدریجی از مونولیت

بازنویسی کامل تقریباً همیشه شکست می‌خورد. الگوی «strangler» امن‌تر است: جلوی مونولیت یک لایه‌ی مسیریابی می‌گذارید و قابلیت‌ها را یکی‌یکی به سرویس‌های جدید منتقل می‌کنید تا چیزی از قدیمی باقی نماند یا باقی‌مانده کوچک و بی‌خطر شود.

  1. ابتدا داخل مونولیت ماژول‌ها را با مرز و رابط روشن جدا کنید.
  2. قابلیتی را انتخاب کنید که کم‌وابسته و پربار یا پرتغییر است (مثلاً اعلان‌ها یا جست‌وجو).
  3. سرویس را کنار مونولیت اجرا کنید و ترافیک را درصدی منتقل کنید.
  4. داده را جابه‌جا کنید، مسیر قدیمی را حذف کنید و قبل از قدم بعدی اندازه بگیرید که چه چیزی بهتر شد.

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

پرسش‌های متداول

تفاوت مونولیت و میکروسرویس چیست؟

در مونولیت کل اپلیکیشن یک واحد است که با هم ساخته و مستقر می‌شود. در میکروسرویس چند سرویس مستقل با دیتابیس جدا داریم که از راه شبکه با هم ارتباط دارند. مونولیت ساده‌تر است و میکروسرویس استقلال تیمی و مقیاس انتخابی می‌دهد.

آیا یک استارتاپ کوچک به میکروسرویس نیاز دارد؟

معمولاً نه. تیم کوچک با مونولیت ماژولار سریع‌تر پیش می‌رود و هزینه‌ی عملیاتی کمتری دارد. وقتی تیم‌ها رشد کردند یا یک بخش مقیاس متفاوتی خواست، می‌توانید همان ماژول را جدا کنید.

هر میکروسرویس باید دیتابیس جدا داشته باشد؟

بله، اگر استقلال می‌خواهید. دیتابیس مشترک سرویس‌ها را به ساختار جداول وابسته می‌کند و استقرار مستقل را عملاً غیرممکن می‌سازد. جدایی لزوماً سرور جدا نیست؛ می‌تواند schema یا کاربر جدا باشد، به شرط اینکه دسترسی مستقیم دیگران ممنوع باشد.

داده در میکروسرویس‌ها چطور سازگار می‌ماند؟

معمولاً با سازگاری نهایی: هر سرویس تراکنش محلی انجام می‌دهد و با رویداد به بقیه خبر می‌دهد. الگوی Saga شکست‌ها را با عمل جبرانی برمی‌گرداند و الگوی Outbox تضمین می‌کند ثبت داده و انتشار رویداد هم‌زمان رخ دهند.

دیدگاه‌ها

    هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.

    دیدگاه‌ها پس از بررسی منتشر می‌شوند.