پرش به محتوای اصلی

پروژهٔ ۱۱

Distributed Microservices Platform

یک سامانهٔ فروش بلیت را با هفت میکروسرویس مستقل می‌سازید: API gateway، ارتباط gRPC، رویدادها روی Kafka، saga برای سفارش و اجرا روی Kubernetes، با این شرط که هیچ بلیتی دو بار فروخته نشود.

Seniorزمان پیشنهادی: ۵۰ ساعتزمان مطالعه: ۱۱ دقیقه
در این صفحه

هدف

در پایان این پروژه یک سامانهٔ توزیع‌شده دارید که:

  • از هفت سرویس مستقل ساخته شده و هر سرویس پایگاه دادهٔ خودش را دارد.
  • درخواست‌های بیرونی را از یک API gateway می‌گیرد و درون خود با gRPC حرف می‌زند.
  • تغییرها را با رویداد روی Kafka به بقیهٔ سرویس‌ها خبر می‌دهد.
  • سفارش را با saga پیش می‌برد و در صورت شکست، کار انجام‌شده را جبران می‌کند.
  • با از کار افتادن یک سرویس، بقیه را با خود پایین نمی‌کشد.
  • روی Kubernetes بدون قطعی به‌روزرسانی می‌شود.

این پروژه پایهٔ پروژهٔ ۱۲ است. اینجا سامانه را درست می‌سازید و آنجا آن را تا میلیون‌ها کاربر مقیاس می‌دهید.

چه چیزی می‌سازید

یک سامانهٔ فروش بلیت رویداد: کاربر رویدادها را می‌بیند، صندلی انتخاب می‌کند، پرداخت می‌کند و بلیت می‌گیرد.

این مسئله عمداً انتخاب شده، چون سه سختی واقعی سیستم‌های توزیع‌شده را با هم دارد:

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

سرویس‌ها

سرویسمسئولیتداده
gatewayورودی بیرونی، احراز هویت، rate limit و مسیردهیندارد
identityکاربران و صدور tokenPostgreSQL
catalogرویدادها و نقشهٔ صندلی‌ها، با بار خواندن زیادPostgreSQL و Redis
inventoryرزرو و آزادسازی صندلی، منبع حقیقت موجودیPostgreSQL
orderچرخهٔ عمر سفارش و هماهنگی sagaPostgreSQL
paymentپرداخت با یک درگاه شبیه‌سازی‌شدهPostgreSQL
notificationارسال پیام پس از رویدادهاندارد

معماری

در حال رسم نمودار…

چهار قاعده این معماری را سالم نگه می‌دارد:

  • هر سرویس مالک دادهٔ خودش است. هیچ سرویسی جدول سرویس دیگر را نمی‌خواند. اگر داده‌ای لازم دارد، یا می‌پرسد یا به رویدادها گوش می‌دهد.
  • فراخوانی هم‌زمان فقط برای پرسش‌های ضروری است. هر جا پاسخ فوری لازم نیست، رویداد می‌فرستید.
  • هر فراخوانی deadline دارد. context از gateway تا آخرین سرویس منتقل می‌شود و با تمام شدن زمان، همهٔ زنجیره متوقف می‌شود.
  • هر عملیات تغییردهنده idempotent است. تکرار یک درخواست با همان کلید، نتیجهٔ تازه‌ای نمی‌سازد.

saga سفارش

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

در حال رسم نمودار…

وضعیت saga در پایگاه دادهٔ order ذخیره می‌شود. اگر سرویس وسط کار از کار بیفتد، پس از بالا آمدن از همان گام ادامه می‌دهد.

outbox

سرویسی که هم پایگاه داده را تغییر می‌دهد و هم رویداد می‌فرستد، دو نوشتن جدا دارد و ممکن است یکی انجام شود و دیگری نه. راه‌حل، الگوی outbox است:

در حال رسم نمودار…

رویداد در همان تراکنش تغییر داده، در جدول outbox نوشته می‌شود. یک relay آن را به Kafka می‌رساند. پیام ممکن است دو بار برسد، پس مصرف‌کننده باید تکرار را تشخیص دهد.

مقایسه با دنیای Python

موضوعGoPython
شکل رایجچند سرویس کوچک با binary مستقلیک monolith با Django و Celery
ارتباط درونیgRPC با قرارداد Protobufفراخوانی تابع یا REST با DRF
کار پس‌زمینهمصرف‌کنندهٔ Kafka در goroutinetask در Celery
تراکنشsaga و outbox میان سرویس‌هاtransaction.atomic روی یک پایگاه داده
لغو و مهلتcontext در همهٔ لایه‌هاتنظیم timeout جدا برای هر کتابخانه
حجم image و زمان بالا آمدنچند ده مگابایت و چند میلی‌ثانیهچند صد مگابایت و چند ثانیه
همان سامانه با Go و با Django

پیش‌نیازها

  • فاز ۳ کامل: goroutine، channel، context و الگوهای لغو.
  • فاز ۴: net/http، PostgreSQL، migration و احراز هویت.
  • فاز ۵: gRPC، observability و Docker.
  • پروژهٔ ۷: نخستین تجربهٔ چند سرویس با gRPC و صف پیام.
  • از فاز ۷: درس‌های ۱ تا ۷، از مبانی مقیاس‌پذیری تا saga.

گام‌های پروژه

  1. مرزبندی سرویس‌ها و قراردادها

    ساختار مخزن، قراردادهای Protobuf، بررسی سازگاری قرارداد در CI و یک قالب مشترک برای سرویس با health check، پیکربندی و graceful shutdown.

    معیار پذیرش: تغییر ناسازگار در یک قرارداد، CI را قرمز می‌کند. هر سرویس خالی با یک دستور بالا می‌آید و با SIGTERM درخواست‌های در جریان را تمام می‌کند.

  2. سرویس‌های پایه و gRPC

    سرویس‌های identity و catalog، هر کدام با پایگاه داده و migration خودش، و interceptor برای log، احراز هویت و انتقال deadline.

    معیار پذیرش: درخواستی که مهلتش در gateway تمام شده، در سرویس پایین‌دست هم لغو می‌شود. این با تست یکپارچه نشان داده می‌شود.

  3. API gateway

    تبدیل REST به gRPC، اعتبارسنجی token، rate limit، شناسهٔ درخواست و سقف زمان برای هر مسیر.

    معیار پذیرش: هیچ سرویس درونی از بیرون در دسترس نیست. درخواست بدون token معتبر به هیچ سرویسی نمی‌رسد.

  4. رزرو بدون فروش دوباره

    سرویس inventory با رزرو مدت‌دار، قفل در سطح ردیف و کلید idempotency.

    معیار پذیرش: در تست هم‌روندی، ده هزار خریدار هم‌زمان برای پانصد صندلی رقابت می‌کنند و دقیقاً پانصد رزرو موفق ثبت می‌شود. تکرار یک درخواست با همان کلید، رزرو دوم نمی‌سازد.

  5. رویدادها با Kafka و outbox

    جدول outbox، relay، مصرف‌کنندهٔ idempotent و صف پیام‌های ناموفق.

    معیار پذیرش: با قطع و وصل کردن Kafka وسط کار، هیچ رویدادی گم نمی‌شود و هیچ رویدادی دو بار اثر نمی‌گذارد.

  6. saga سفارش

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

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

  7. تاب‌آوری

    circuit breaker، تلاش دوباره با backoff و jitter، جداسازی منابع و بودجهٔ زمان.

    معیار پذیرش: با از کار انداختن payment، دیدن رویدادها و انتخاب صندلی همچنان کار می‌کند و خطاها در کمتر از یک ثانیه برمی‌گردند، نه پس از timeout طولانی.

  8. observability و Kubernetes

    trace سرتاسری، metric نرخ و خطا و مدت برای هر سرویس، log با شناسهٔ trace و manifestهای Kubernetes با probe و انتشار تدریجی.

    معیار پذیرش: مسیر یک خرید از gateway تا Kafka در یک trace دیده می‌شود. هنگام به‌روزرسانی یک سرویس زیر بار، هیچ درخواستی شکست نمی‌خورد.

ساختار مخزن

Text
projects/11-distributed-platform/
├── api/proto/            قراردادهای Protobuf
├── services/
│   ├── gateway/
│   ├── identity/
│   ├── catalog/
│   ├── inventory/
│   ├── order/
│   ├── payment/
│   └── notification/
├── pkg/                  کد مشترک: interceptor، outbox، پیکربندی
├── deploy/
│   ├── compose.yaml      اجرای محلی با یک دستور
│   └── k8s/              manifestهای Kubernetes
├── test/                 تست‌های یکپارچه و هم‌روندی
└── docs/decisions/       یادداشت تصمیم‌های مهندسی

تصمیم‌های مهندسی که باید از آن‌ها دفاع کنید

  • مرز سرویس‌ها: چرا inventory از catalog جداست؟ چه چیزی این دو را از هم مستقل می‌کند؟
  • هماهنگی saga: orchestration در سرویس order یا choreography با رویداد؟ هر کدام چه چیزی را سخت می‌کند؟
  • هم‌زمان یا رویدادی: کدام فراخوانی‌ها باید gRPC بمانند و کدام باید رویداد شوند؟
  • کد مشترک: چه چیزی حق دارد در pkg باشد؟ کد مشترک زیاد، سرویس‌ها را دوباره به هم می‌چسباند.
  • یک مخزن یا چند مخزن: برای تیم کوچک و تیم بزرگ پاسخ یکی نیست.

اشتباه‌های رایج

نتیجهٔ نهایی

  • هفت سرویس با تست واحد، تست یکپارچه و -race سبز
  • تست هم‌روندی که نبودِ فروش دوباره را ثابت می‌کند
  • اجرای محلی با یک دستور و manifestهای Kubernetes
  • trace سرتاسری و داشبورد metric
  • گزارش آزمون شکست: چه چیزی را از کار انداختید و چه شد
  • یادداشت تصمیم‌های مهندسی

ویرایش این صفحه در GitHub