Distributed data pattern-ləri
Distributed data pattern-ləri
Monolith-də "order yarat, pulu çıx, məhsulu rezerv et" tək database transaction-ıdır: ya hamısı olur, ya heç biri. Sistemi service-lərə böləndə order, payment və inventory ayrı database-lərə düşür və onları bir COMMIT ilə bağlamaq artıq mümkün deyil. Microservice-lərdə ən çətin problem şəbəkə də, deploy da deyil. Ən çətini service boundary-lərini aşan transaction-lardır.
Bu fəsil həmin problemə qarşı pattern-ləri izah edir: saga, CQRS və event sourcing. Hər biri real problemi həll edir və hər biri ciddi mürəkkəblik gətirir. Məqsəd hansının nə vaxt pulunu çıxardığını görməkdir.
Database-per-service və onun qiyməti
Ch04-dəki qayda burada başlanğıc nöqtəsidir: hər data parçasının tam olaraq bir writer-i var. Microservice-lərdə bunun praktik forması database-per-service-dir: hər service öz data-sına sahibdir, digərləri ona yalnız API və ya event vasitəsilə çatır. Ayrı server şərt deyil: başqa service-lərin birbaşa toxunmadığı ayrı schema da kifayətdir.
Bunun alternativi olan shared database, yəni bir neçə service-in eyni cədvəllərə yazması, anti-pattern-dir. Join-lər və transaction-lar yerində qalır, amma bir service-in schema dəyişikliyi hamının dəyişikliyinə çevrilir, deploy-lar yenidən birləşir və ch03-dəki distributed monolith alınır.
Database-per-service müstəqil deploy və müstəqil scale verir. Qiyməti isə bu fəslin qalan hissəsidir:
- Join yoxdur. "Müştərinin adı ilə son order-ləri" artıq iki service-ə çağırış və ya data kopyası tələb edir.
- Transaction-lar lokaldır. Bir neçə service-i əhatə edən atomik dəyişiklik yoxdur.
- Reporting çətinləşir. Bütün biznes üzrə hesabat üçün data-nı ayrıca yerə yığmaq lazımdır. Buna data pipeline-lar fəslində qayıdacağıq.
2PC-dən dual write-a: atomiklik problemi
İlk instinkt klassik həllə, two-phase commit-ə əl atmaqdır. Kağız üzərində problem həll olunur, praktikada isə microservice-lər üçün çox vaxt yanlış cavabdır:
- Blocking. Prepare mərhələsindən sonra iştirakçılar lock-ları saxlayır və coordinator-un qərarını gözləyir. Coordinator bu anda düşsə, onlar ilişib qalır.
- Coordinator single point of failure-dur. Bütün yazmalar onun sağlamlığından asılıdır.
- Availability vurulur. Ch07-dəki riyaziyyat burada amansızdır: transaction yalnız bütün iştirakçılar eyni anda sağlam olanda uğurlu olur. Dörd service-in hər biri 99.9%-dirsə, birlikdə təxminən 99.6% alınır.
- Dəstək yoxdur. Kafka kimi broker-lər və NoSQL store-ların çoxu XA transaction-larda iştirak etmir.
2PC-dən imtina edəndə daha gizli problem qalır: dual write. Service həm öz database-inə yazmalı, həm də digərlərinə xəbər vermək üçün event publish etməlidir. Bu iki yazmanı ardıcıl edəndə aralarında crash olarsa, ya event itir, ya da heç vaxt commit olunmamış dəyişiklik haqqında event yayılır. Ch05-dəki ADR nümunəsi məhz bu problemi outbox pattern ilə həll edirdi: event biznes dəyişikliyi ilə eyni lokal transaction-da outbox cədvəlinə yazılır, relay və ya change data capture isə onu broker-ə çatdırır. Nəticə at-least-once delivery-dir, ona görə consumer-lər idempotent olmalıdır. Etibarlı event publish etmək bu fəsildəki hər şeyin təməlidir: onsuz nə saga, nə də CQRS işləyir.
Saga: lokal transaction-lar zənciri
Saga distributed transaction-ı lokal transaction-lar ardıcıllığına bölür. Hər addım öz service-ində öz database-inə commit edir və növbəti addımı işə salır. Addımlardan biri uğursuz olanda, artıq bitmiş addımlar compensating transaction-larla tarazlanır: hər addımın onu "ləğv edən" cütü olur.
Saga ACID mənasında atomik deyil: sistem bir müddət aralıq vəziyyətdə qalır və sonda ya tam uğurlu, ya da tam kompensasiya olunmuş vəziyyətə çatır. Bu, biznes prosesi səviyyəsində eventual consistency-dir.
Choreography və orchestration
Saga-nı koordinasiya etməyin iki yolu var. Choreography-də mərkəz yoxdur: hər service event-ləri dinləyir və öz addımını atıb yeni event publish edir. Orchestration-da isə ayrıca orchestrator hər service-ə nə edəcəyini deyir və cavabları gözləyir. Eyni sifariş prosesi iki üsulla belə görünür:
| Addım | Choreography (event-lər) | Orchestration (command-lar) | Compensation |
|---|---|---|---|
| 1. Order | Order service order-i PENDING yaradır, OrderCreated publish edir | Orchestrator order-i PENDING yaradır | Order CANCELLED olur |
| 2. Payment | Payment service OrderCreated-i dinləyir, pulu bloklayır, PaymentAuthorized publish edir | Orchestrator payment-ə AuthorizePayment göndərir | Bloklanmış məbləğ azad edilir (çıxılıbsa, refund) |
| 3. Inventory | Inventory PaymentAuthorized-i dinləyir, məhsulu rezerv edir, StockReserved publish edir | Orchestrator inventory-yə ReserveStock göndərir | Rezerv buraxılır |
| 4. Shipping | Shipping StockReserved-i dinləyir, göndərişi planlaşdırır, ShipmentScheduled publish edir | Orchestrator shipping-ə ScheduleShipment göndərir | Göndəriş ləğv edilir |
| Uğursuzluq: anbarda məhsul yoxdur | Inventory StockReservationFailed publish edir; payment və order onu dinləyib öz compensation-larını edir | Orchestrator xətanı alır və compensation-ları tərs sırada çağırır: payment, sonra order | — |
Choreography-nin güclü tərəfi loose coupling-dir: yeni addım əlavə etmək üçün mövcud service-lərə toxunmadan yeni event-i dinləmək kifayətdir. Zəif tərəfi görünürlükdür. Proses heç bir yerdə yazılmayıb, o, beş service-in kodunda səpələnib. "Bu order hazırda harada ilişib?" sualına cavab üçün distributed tracing lazımdır. Addımlar artdıqca service-lər arasında cyclic dependency yaranır: payment inventory-nin event-lərini dinləyir, inventory isə payment-in.
Orchestration prosesi bir yerdə, oxuna bilən state machine kimi saxlayır: uğursuzluq və timeout-ları idarə etmək, order-in vəziyyətini göstərmək asanlaşır. Qiyməti mərkəzləşmədir: orchestrator bütün service-lər haqqında bilir və diqqət edilməsə, biznes məntiqi oraya axır, service-lər isə anemic CRUD-a çevrilir. Orchestrator həm də öz state-i olan critical komponentdir.
Praktik qayda: iki-üç addımlıq sadə axın üçün choreography kifayətdir. Dörd və daha çox addım, budaqlanma və ya ciddi compensation məntiqi varsa, orchestration daha az ağrı verir.
Compensation rollback deyil
Database rollback-i dəyişikliyin heç vaxt olmadığını iddia edir. Compensating transaction isə bunu edə bilməz, çünki dəyişiklik artıq commit olunub və başqaları onu görüb. O, semantic undo-dur: biznes baxımından əks hərəkət.
Bəzi addımları geri qaytarmaq ümumiyyətlə mümkün deyil. Müştəriyə "sifarişiniz təsdiqləndi" e-poçtu gedibsə, onu geri çağırmaq olmaz, yalnız ikinci, üzrxahlıq e-poçtu göndərmək olar. Refund da izsiz deyil: müştəri bank çıxarışında iki əməliyyat görür. Buna görə geri qaytarılmayan addımları saga-nın sonuna qoymaq lazımdır, uğursuz ola biləcək hər şeydən sonra.
İkinci problem isolation-ın olmamasıdır. Saga gedərkən onun aralıq nəticələri digər transaction-lara görünür. Başqa sorğu bir saniyə sonra buraxılacaq rezervi görə bilər (dirty read). Müdafiə vasitələri:
- "Pending" state-lər. Order
PENDINGvəziyyətindədirsə, UI və digər service-lər onu hələ bitmiş kimi qəbul etmir. - Semantic lock. Aralıq vəziyyətdəki obyekt digər saga-lar üçün "məşğul" işarələnir və onlar ya gözləyir, ya da imtina edir.
CQRS: oxuma ilə yazmanı ayırmaq
CQRS (Command Query Responsibility Segregation) yazma və oxuma üçün ayrı model saxlamaq deməkdir. Yazma tərəfi biznes qaydalarını yoxlayan normallaşdırılmış modeldir. Oxuma tərəfi bir və ya bir neçə read model-dir: konkret ekran və ya sorğu üçün hazır formada denormallaşdırılmış data. Yazma tərəfi event-lər publish edir, read model-lər onları dinləyib yenilənir.
Nə vaxt pulunu çıxarır:
- Oxuma ilə yazma arasında böyük asimmetriya. Saniyədə 50 yazma və 50 000 oxuma varsa, onları ayrıca scale etmək mənalıdır.
- Mürəkkəb query-lər. Order, customer və shipping data-sını birləşdirən dashboard join edə bilmir, amma üç service-in event-lərindən qurulan read model edə bilər.
- Fərqli storage ehtiyacları. Yazma Postgres-də, axtarış Elasticsearch-də.
Adi CRUD ekranlarında, oxuma və yazma modeli eyni görünəndə isə CQRS iki model, sinxronizasiya kodu və yeni failure rejimləri gətirir, əvəzində heç nə vermir.
Ən çox unudulan qiymət read model-in eventual consistency-sidir: istifadəçi order yaradır, siyahıya qayıdır və order orada yoxdur. Bu, ch10-dakı replication lag ilə eyni problemdir və həlləri də oxşardır: UI-da yeni order-i optimistic göstərmək, command cavabında versiya qaytarıb read model həmin versiyaya çatana qədər gözləmək, və ya "emal olunur" vəziyyətini açıq göstərmək.
Event sourcing: state əvəzinə tarixçə
Adi sistem cari state-i saxlayır: hesabın balansı 120 manatdır. Event sourcing isə state-ə gətirən event-ləri saxlayır: hesab açıldı, 200 manat mədaxil, 80 manat məxaric. Cari state bu event-lərin ardıcıl tətbiqi (replay) ilə alınır. Event-lər yalnız əlavə olunan, dəyişdirilməyən event store-da saxlanır.
Nə verir:
- Tam audit. Nəyin, nə vaxt və niyə dəyişdiyi data modelinin özündədir, sonradan əlavə edilmiş log deyil.
- Temporal query-lər. "Bu hesab 3 mart tarixində necə görünürdü?" sualı replay ilə cavablanır.
- Yeni read model-lər. Sabahın hesabatını bütün tarixçə üzərində qurmaq olar.
Uzun tarixçəni hər dəfə replay etmək baha olduğu üçün snapshot-lar saxlanır və replay sonuncusundan davam edir.
Risklər isə ciddidir. Birincisi, event schema təkamülü: event-lər əbədidir, ona görə beş il əvvəlki OrderPlaced formatını kod hələ də oxumalıdır. Versioning və upcasting daimi işə çevrilir. İkincisi, öyrənmə əyrisi: səhv data "sətri düzəltməklə" yox, düzəldici yeni event ilə düzəlir. Üçüncüsü, ən geniş yayılmış səhv: event sourcing-i hər yerə tətbiq etmək. O, mühasibat, ödəniş və hüquqi audit tələb olunan domain-lər üçündür, istifadəçi profilləri və parametrlər üçün yox.
Event sourcing və CQRS tez-tez birlikdə gəlir, çünki event store sorğu üçün əlverişsizdir və read model-lərə ehtiyac yaranır. Amma onlar bir-birindən asılı deyil: CQRS adi database ilə, event sourcing isə ayrıca read model olmadan, birbaşa replay ilə də işləyə bilər.
Mürəkkəblik büdcəsi
Hər pattern mürəkkəblik büdcəsindən xərcləyir. Onları dəb üçün yox, konkret problemə görə seçin:
| Problem | İlk baxılacaq həll | Nə vaxt lazım deyil |
|---|---|---|
| Dəyişiklik və event birlikdə yazılmalıdır | Outbox və ya CDC | Event publish etmirsinizsə |
| Biznes prosesi bir neçə service-i əhatə edir | Saga; addım çoxdursa orchestration | Addımlar bir service-ə sığırsa: boundary-ləri yenidən düşünün |
| Service-lər üzrə sorğu və ya oxuma yükü | CQRS read model | Bir database və bir neçə index kifayət edirsə |
| Audit, tarixçə, "o vaxt necə idi?" | Event sourcing | Audit log cədvəli kifayət edirsə |
İkinci sətirdəki qeyd təsadüfi deyil: saga-ya ehtiyac çox vaxt boundary-nin yanlış yerdə çəkildiyinin, ch04-dəki "chatty interface"-in əlamətidir. Bəzən ən yaxşı distributed transaction iki service-i birləşdirib onu lazımsız etməkdir. Hansı pattern-in hansı problem üçün seçildiyi isə ch05-dəki mənada ADR-ə layiq qərardır.
Hazırkı sisteminiz haqqında suallar
- Hansı cədvəllərinizə birdən çox service yazır və ya birbaşa oxuyur? Onların schema-sını dəyişmək üçün neçə komanda ilə danışmalısınız?
- Database-ə yazıb sonra event və ya mesaj göndərən kod yolunu tapın. Aralarında process crash olarsa, nə itir?
- Bir neçə service-dən keçən ən uzun biznes prosesiniz üçün hər addımın compensation-ını yazın. Hansı addım geri qaytarıla bilmir və o, prosesin sonundadırmı?
- Read model-iniz və ya cache-iniz geri qaldıqda istifadəçi nə görür? Bunu UI-da kimsə qəsdən dizayn edibmi?