Fəsil 16 · 11 dəq oxu

Case study: bilet satışı platformasını dizayn etmək

Case study: bilet satışı platformasını dizayn etmək

Əvvəlki fəsillərdə hər mövzuya ayrıca baxdıq. Real layihədə isə onlar ayrı-ayrı gəlmir: capacity hesablaması data modelinə, data modeli , SLO-lara təsir edir. Bu fəsildə kitabın bütün alətlərini bir sistem üzərində ardıcıl tətbiq edəcəyik: konsert və idman biletləri satan platforma.

Bu sistem yaxşı nümunədir, çünki çətinlikləri bir-biri ilə toqquşur. Gün ərzində yük azdır, amma populyar tədbirin satışı açılan dəqiqədə yük yüz, hətta min dəfə artır. Inventory məhduddur və eyni yeri iki nəfərə satmaq olmaz. Ödəniş xarici provider-dən keçir. Bot-lar biletləri alıb yenidən satmaq istəyir, istifadəçilər isə prosesin ədalətli olmasını gözləyir. Dərin izahlar artıq fəsillərdədir, burada qərarları və onların səbəbini göstərəcəyik.

Requirement-lər və constraint-lər

Fəsil 6-dakı kimi üç növ driver-dən başlayırıq.

: tədbirləri axtarmaq və baxmaq; zalın sxemində yer seçmək və ya "ən yaxşı yer" rejimində avtomatik təklif almaq; yerləri rezerv etmək; ödəmək; bileti e-poçt və mobil tətbiq vasitəsilə almaq.

:

  • Komanda kiçikdir: altı backend mühəndisi, ayrıca platform komandası yoxdur.
  • Sistem cloud-da işləyir, managed database və managed broker istifadə olunur.
  • PCI DSS-in həcmini kiçik saxlamaq üçün kart data-sı bizim sistemə ümumiyyətlə daxil olmur: kart məlumatları birbaşa payment provider-in səhifəsinə və ya SDK-sına gedir, biz yalnız və nəticəni görürük.
  • Büdcə məhduddur: ildə bir neçə saatlıq peak üçün bütün il boyu nəhəng infrastructure saxlamaq olmaz.

Quality attribute-lərin sıralanması

Fəsil 2-nin tələbi ilə attribute-ləri sadalamırıq, sıralayırıq:

  1. Inventory consistency. Eyni yeri iki nəfərə satmaq xəbərlərə düşəcək failure-dur: pul geri qaytarılır, müştəri itirilir, təşkilatçı ilə müqavilə risk altına düşür.
  2. Checkout . Satış açıq olarkən checkout-un yıxılması birbaşa gəlir itkisidir.
  3. Peak . Sistem satış açılışının yükünə dözməlidir, amma "dözmək" hamıya eyni anda xidmət etmək demək deyil.
  4. Axtarışın . Vacibdir, amma bir neçə saniyəlik köhnə data qəbulediləndir.

Bu sıra artıq qərar verir: konflikt olanda consistency qalib gəlir. Yerin statusunu bilmədiyimiz halda "satıldı" deməkdənsə "bir az gözləyin" demək yaxşıdır. Sıralamanı iki scenario ilə ölçülə bilən edirik:

Consistency: Satış açılışında (environment) iki istifadəçi (source) eyni yeri eyni millisaniyədə rezerv etmək istəyir (stimulus). Inventory module-u () yalnız birinə hold verir, digərinə isə qonşu yerləri təklif edir (response). İkiqat satılmış yer sayı sıfırdır və bu, hər satışdan sonra reconciliation ilə yoxlanılır (measure).

: Satış açılışının ilk beş dəqiqəsində (environment) 1 milyon istifadəçi (source) tədbir səhifəsinə gəlir (stimulus). Sistem () hamını qəbul edir və növbə ilə checkout-a buraxır (response). Front door 5xx qaytarmır, checkout-a buraxılmış istifadəçilər üçün hold-un 500 ms-dən aşağı qalır, manual müdaxilə tələb olunmur (measure).

Capacity estimation

Fəsil 6-dakı üsulla iki rejimi ayrıca hesablayırıq.

Normal gün. 300 000 ziyarətçi, hər biri təxminən 20 request edir: 6M request/gün. 6M / 86 400 ≈ 70 req/s orta, peak faktoru 3 ilə ≈ 200 req/s. Gündə təxminən 20 000 bilet satılır: saniyədə bir sifarişdən də az.

Böyük satış açılışı. 50 000 yerlik stadion, orta sifariş 2 bilet: təxminən 25 000 sifariş. 1 milyon istifadəçi ilk beş dəqiqədə gəlir və hər biri səhifəni təxminən 3 saniyədən bir yeniləyir: 1M / 3 ≈ 330 000 req/s. Bu, normal peak-dən ≈ 1 650 dəfə çoxdur.

Checkout tərəfi. saniyədə 100 istifadəçi buraxır. Onların 60%-i sifarişi tamamlayır: ≈ 60 sifariş/s, yəni 25 000 sifariş təxminən 420 saniyədə, yeddi dəqiqədə satılır. Konflikt səbəbindən hər sifarişə orta hesabla iki hold cəhdi düşür: ≈ 120 hold transaction-ı/s, hər biri iki sətir dəyişir: ≈ 240 sətir yazması/s.

Payment provider. 60 ödəniş/s, provider-in 3 s. Little qanununa görə eyni anda ≈ 60 × 3 = 180 açıq çağırış olur.

Rəqəmlər əsas fikri göstərir. Yazma yükü kiçikdir: saniyədə bir neçə yüz sətir yazmasını tək relational database rahat emal edir, lazım deyil. Problem oxuma və yeniləmə dalğası, bir də eyni yerlər uğrunda rəqabətdir. 330 000 req/s-ni application-a buraxmaq mənasızdır: 1 milyon nəfərdən yalnız 25 000-i bilet alacaq. Həm də 1M-dən 40 000-ə yaxını checkout-a çatana qədər biletlər bitəcək, qalan 960 000 nəfərə bunu tez və ucuz demək lazımdır.

Yüksək səviyyəli arxitektura

Fəsil 3-dəki seçimə görə -dən başlayırıq: catalog, inventory, checkout, ticketing və payment adapter ayrı module-lardır, öz cədvəllərinə sahibdir və bir-biri ilə yalnız interface vasitəsilə danışır. Kiçik komanda üçün operational vergisi əsassızdır.

Amma yükün forması iki fərqli hissəni göstərir. Catalog və axtarış oxuma-ağırdır və köhnə data-ya dözür. Inventory və checkout yazma-kritikdir və consistency tələb edir. Onları eyni iki ayrı kimi işlədirik: catalog instance-ları read replica-lardan oxuyur və ayrıca scale olunur, checkout instance-ları isə primary-yə yazır. Bir hissə ilk gündən ayrıca komponentdir: . O, çətinlikdə olanda belə işləməlidir.

Fəsil 9-dakı building block-ların rolu:

istifadəçi
  → CDN: statik səhifələr, şəkillər, waiting room səhifəsi
  → load balancer
  → API gateway: auth, rate limiting, queue token-in yoxlanması
      ├→ waiting room: növbə, mövqe, buraxılma sürəti (in-memory store)
      ├→ catalog deployment → cache → read replica + search index
      └→ checkout deployment → primary DB (+ başqa AZ-da standby)
                                 └→ outbox → broker → bilet/e-poçt worker-i
                     payment adapter ⇄ xarici payment provider (webhook)

peak-i hamarlayır. İstifadəçi növbə səhifəsini alır, statusunu isə 10 saniyədən bir yüngül endpoint-dən soruşur: 1M / 10 = 100 000 req/s, amma hər cavab yaddaşdakı sayğacdan qaytarılan bir neçə yüz baytdır. Növbəsi çatan istifadəçi imzalanmış, qısa ömürlü alır və checkout-a yalnız bu token-la buraxır. Buraxılma sürəti checkout-un tutumuna görə tənzimlənir: bu, Fəsil 11-dəki istifadəçi səviyyəsində tətbiqidir.

Ədalətlilik və bot-lar da burada həll olunur. Açılışdan əvvəl gələnlər açılış anında təsadüfi qaydada növbəyə düzülür, sonra gələnlər isə FIFO ilə. Belə olanda 09:59:59-da sürətlə yeniləyən bot-un üstünlüyü qalmır. Növbəyə girişdə bot yoxlaması aparılır, hesaba və cihaza bağlanır, hər hesab üçün bilet limiti qoyulur, gateway isə tətbiq edir.

Data modeli və storage

Fəsil 10-un qaydası: storage access pattern-dən seçilir. Inventory üçün relational database seçirik, çünki tələb transaction və şərti yazmadır. Hər yer seats cədvəlində bir sətirdir: event_id, seat_id, status, hold_id, held_until, version. Hold bir şərti update-dir:

UPDATE seats
SET status = 'HELD', hold_id = :hold, held_until = now() + interval '10 minutes'
WHERE event_id = :event AND seat_id IN (:seats)
  AND (status = 'AVAILABLE' OR (status = 'HELD' AND held_until < now()));

Dəyişən sətir sayı istənən yer sayına bərabər deyilsə, transaction olunur və istifadəçi alternativ təklif alır. Bu, optimistic concurrency-dir: uzun lock yoxdur, rəqabət ən qısa anda, bir sətir səviyyəsində həll olunur. Vaxtı keçmiş hold-lar ayrıca job gözləmədən elə həmin şərtlə yenidən satışa çıxır.

Catalog üçün , axtarış üçün isə search index istifadə olunur. Index event-ləri ilə yenilənir və bir neçə saniyə geri qala bilər.

Ən maraqlı problem -dır. Data-nı event_id ilə bölsək belə, bütün yük bir tədbirə, deməli bir partition-a düşür. bu problemi həll etmir. Həll başqa qatlardadır: eyni anda yazan istifadəçi sayını məhdudlaşdırır; "ən yaxşı yer" rejimi istifadəçiləri sektorlara paylayır ki, hamı eyni ön sıranı tutmağa çalışmasın; zal sxemindəki yerlərin vəziyyəti bir-iki saniyəlik oxunur, həqiqət isə yalnız hold anında yoxlanılır. event_id kimi gələcək üçün yenə də faydalıdır: tədbirlər arasında transaction olmadığından, lazım olarsa onları bir neçə database-ə paylamaq asan olacaq.

Rezerv və ödəniş saga-sı

Hold, order və bilet eyni database-dədir, onlar lokal transaction-dır. yalnız xarici payment provider-ə görə lazımdır (Fəsil 12). Checkout module-u onu orchestration ilə idarə edir:

  1. Hold: yerlər HELD olur (TTL 10 dəqiqə), order PENDING yaradılır.
  2. Ödəniş: payment adapter provider-ə order_id-ni kimi göndərir (Fəsil 8).
  3. Təsdiq: bir transaction-da yerlər SOLD, order CONFIRMED olur və TicketIssued event-i yazılır (Fəsil 5). Bilet yaradılması və e-poçt asinxron gedir.

Compensation-lar: ödəniş rədd edilsə hold buraxılır. Ödəniş uğurlu, amma hold artıq vaxtı keçib başqasına satılıbsa, avtomatik refund edilir. Geri qaytarılmayan addım, yəni e-poçt, ən sonda gəlir.

Ən təhlükəli hal : nəticə bilinmir. Bu halda hold dərhal buraxılmır. Adapter eyni ilə statusu soruşur, gözləyir, reconciliation job-u isə ilişib qalan order-ləri yoxlayır. Fəsil 11-in pattern-ləri burada belə tətbiq olunur: provider çağırışına 5 saniyəlik , və ilə ən çox iki ( sayəsində təhlükəsiz), hesablamadakı 180 açıq çağırışa görə təxminən 200 yerlik və . Breaker açılanda yeni istifadəçi buraxmağı dayandırır ki, insanlar ödəyə bilməyəcəkləri yerləri tutmasınlar.

Availability və SLO-lar

Fəsil 7-nin qaydası ilə hər axın üçün ayrıca seçirik:

  • və front door: 99.95%. Bu, sistemin qapısıdır və əsasən verilir.
  • Checkout: satış pəncərəsində uğurlu request-lərin 99.9%-i, hold-un 500 ms-dən aşağı. "Yer artıq tutulub" cavabı uğursuzluq deyil, düzgün cavabdır. Hədəfi 99.9%-dən yuxarı qoymuruq, çünki critical path-də 99.9%-lik provider var.
  • Axtarış və catalog: 99.5%, 300 ms-dən aşağı, 60 saniyəyə qədər köhnə data qəbulediləndir.

Böyük satış zamanı planı əvvəlcədən yazılır: tövsiyələr və personal bloklar söndürülür, axtarış verilir, zal sxemi yük artanda "ən yaxşı yer" rejiminə keçir və yükü checkout-un tutumunda saxlayır. Yük daha da artanda prioriteti aydındır: əvvəl catalog, ən son checkout.

Deployment və observability

Satış günü riski azaltmaq günüdür. Böyük satışdan 24 saat əvvəl və satış bitənə qədər freeze olur. Bütün dəyişikliklər edilir (Fəsil 14): hər tədbir üçün ayrıca flag ilə açılır, "ən yaxşı yer" rejimi və tövsiyələr isə . Satışdan bir həftə əvvəl gözlənilən yükün iki qatı ilə load test, sonra isə keçirilir: payment provider-in yıxılması (breaker açılmalı, növbə dayanmalıdır) və yük altında database yoxlanılır.

(Fəsil 15) əsas çətinlik pəncərənin qısalığıdır. 30 günlük alert-i yarım saatlıq satış üçün gecdir. Satış zamanı 5 dəqiqəlik və 1 dəqiqəlik pəncərələrlə alert işləyir. Ayrıca satış dashboard-u biznes metric-lərini göstərir: hold/s, vaxtı keçən hold-lar, ödəniş uğuru, növbənin uzunluğu. "İkiqat satış = 0" invariant-ı isə daim yoxlanılır. Hər checkout bir trace-dir: gateway → yoxlanması → hold → payment provider → təsdiq → . order_id kimi provider-in qədər ötürülür.

Bu layihənin üç əsas ADR-i

Fəsil 5-in formatında, qısa şəkildə:

ADR-001: Modular monolith ilə başlamaq
Context: 6 nəfərlik komanda, platform komandası yoxdur; yazma yükü kiçikdir.
Decision: Bir artifact, domain module-ları; catalog və checkout ayrı deployment kimi.
Consequences: + sadə operation, lokal transaction-lar. − module sərhədləri
  fitness function ilə qorunmalıdır; checkout-u ayırmaq gələcəkdə migration tələb edir.

ADR-002: Inventory üçün TTL-li hold və optimistic concurrency
Context: Eyni yer iki nəfərə satılmamalıdır; uzun lock-lar peak-də darboğaz yaradır.
Decision: 10 dəqiqəlik hold, şərti UPDATE; vaxtı keçmiş hold yenidən satışa çıxır.
Consequences: + oversell yoxdur, lock qısadır. − ödəniş gec gələndə refund
  compensation-ı lazımdır; TTL biznes ilə razılaşdırılmalıdır.

ADR-003: Virtual waiting room
Context: Açılışda normal peak-dən ~1 650 dəfə çox yük; tələbat sifariş sayından ~40 dəfə çoxdur.
Decision: Ayrıca waiting room, imzalı token, checkout tutumuna görə buraxılma sürəti.
Consequences: + checkout stabil qalır, ədalətli sıra. − əlavə komponent və
  istifadəçi gözləməsi; waiting room-un özü critical path-dədir və test olunmalıdır.

Nə etmədik və niyə

Fəsil 12-dəki mürəkkəblik büdcəsi burada da işləyir. Bəzi məşhur həllərdən şüurlu imtina etdik:

  • Erkən . Kiçik komanda üçün network, distributed transaction və vergisi heç bir problemi həll etmədən gələrdi. Checkout-u ayrıca service-ə çıxarmaq üçün səbəb komandanın böyüməsi və ya fərqli ritmi olacaq.
  • . Audit tələbi cədvəli və event-ləri ilə ödənir. versioning və replay qiymətinə ehtiyac yoxdur.
  • Multi-region active-active. İki region arasında eyni yerin statusunu sinxron saxlamaq ya , ya da consistency itkisi deməkdir və bu, sıralamamızın birinci yerindəki attribute-u pozur. Multi-AZ və başqa region-a hazır, amma passiv bərpa planı kifayətdir.

Yekun: qərarlar və onların səbəbi

QərarFəsilXidmət etdiyi attribute
Attribute-lərin sıralanması, consistency birinci2Bütün qərarların əsası
İki rejimli capacity hesablaması6Peak , büdcə
, iki 3, 4Maintainability, kiçik komandanın sürəti
, gateway, 9, 11Peak , checkout , ədalətlilik
Şərti UPDATE, TTL-li hold10Inventory consistency
, search index, 10Axtarışın
Ödəniş , , 5, 8, 12Consistency, etibarlılıq
, , , 11Checkout
Axın üzrə SLO-lar, degradation planı7, 11
freeze, , 14Satış gününün riski
Qısa pəncərəli , checkout trace15

Son fikir

Bu fəslin heç bir qərarı "düzgün cavab" deyil. Daha böyük komanda seçə bilərdi, qlobal satış edən platforma isə multi-region-a ehtiyac duyardı. Qərarlarımız yalnız bizim prioritetlərimiz, rəqəmlərimiz və üçün yaxşıdır. Hər birinin qiymətini də bilirik və yazmışıq.

Fəsil 1-də dedik ki, mücərrəd mənada yaxşı architecture yoxdur, yalnız müəyyən prioritetlər üçün yaxşı olan architecture var. Kitab boyu öyrəndiyimiz texnikalar bu cümlənin tətbiq yoludur: əvvəl nəyi optimize etdiyinizi sıralayın, sonra rəqəmlə yoxlayın, sonra seçib qeyd edin. Qalanı təcrübədir.

Hazırkı sisteminiz haqqında suallar

  1. Sisteminizin ilk dörd sıralayın. Son böyük texniki qərarınız bu sıraya uyğun idimi?
  2. Sisteminizin ən pis günü hansıdır? Onun yükünü normal günlə müqayisədə hesablayın: bu rəqəm hansı qərarı məcbur edir?
  3. Sisteminizdə "eyni şeyi iki dəfə satmaq" növündən olan invariant hansıdır? Onu hansı mexanizm qoruyur və pozulduğunu necə bilərdiniz?
  4. Bu fəsildəki kimi qısa sisteminizin üç əsas qərarını yazın. Hansının Consequences hissəsini yazmaq ən çətin oldu?