Problem: "hamısını microservice-lərə keçirək"
Uydurma ssenari. On beş illik kredit platforması müraciətləri, skorinqi, müqavilələri, ödənişləri, bildirişləri və hesabatları bir Java monolitində idarə edir. Sistem stabildir, gəlir gətirir. Bir gün təklif gəlir: "Bu köhnə texnologiyadır. Hamısını microservice-lərə keçirək."
Bu, arxitektura qərarı deyil, həll axtaran təklifdir. Arxitektin ilk sualı başqadır: biz hansı biznes problemini həll edirik? Bu məqalə həmin sualdan başlayan qərar prosesini və bir funksiyanın ayrılmasının texniki detallarını göstərir.
Beş seçim — yalnız biri "microservice"
| Seçim | Nə vaxt mənalıdır | Əsas risk |
|---|---|---|
| Saxla | Sistem stabildir, xərci məqbuldur, biznesə mane olmur | Bilik itkisi, köhnəlmiş platformanın dəstəyinin bitməsi |
| Refactor | Kod dəyişməyə çətindir, amma struktur düzgündür | Testsiz refactor — reqressiya |
| Modullaşdır | Sərhədlər bulanıqdır, hamı hər şeyi çağırır | Uzun müddət görünməyən fayda |
| Bir funksiyanı ayır | Konkret funksiyanın fərqli dəyişmə tempi, yükü və ya sahibi var | Paylanmış sistemin əməliyyat xərci, data consistency |
| Yenidən yaz | Platforma dəstəklənmir və ya biznes modeli tam dəyişib | Uzun "big bang", unudulmuş davranış, biznesin gözləməsi |
AWS Prescriptive Guidance da bunu qeyd edir: monolit müəyyən hallarda etibarlı seçim ola bilər. Modernizasiyaya başlamazdan əvvəl monolitin biznes istifadəsini, texnologiyasını və digər tətbiqlərlə asılılıqlarını tam başa düşmək lazımdır. Martin Fowler isə tam yenidən yazma planları haqqında xəbərdar edir: onlar çox vaxt baş tutmur, çünki uzun çəkir, mövcud davranışı tam təsvir etmək çətindir və biznes gözləyə bilmir.
Qərar meyarları
Biznes:
- Dəyişiklik sürəti: kiçik dəyişiklik neçə gün, neçə komanda və hansı release tələb edir?
- Dayanıqlılıq: bir modulun xətası bütün sistemi dayandırırmı?
- Miqyas: hansı hissə fərqli yük altındadır?
- Xərc və risk: platformanın dəstəyi, lisenziya, insan bilikləri.
Texniki:
- Modul sərhədləri nə dərəcədə aydındır?
- Kimin hansı cədvəlin sahibi olduğu bilinirmi?
- Avtomatik testlərin əhatəsi dəyişiklikləri qorumağa kifayət edirmi?
- Monitorinq: dəyişikliyin təsirini görə biləcəyikmi?
Bu ssenaridə problem konkretdir: bildiriş şablonunun mətnini dəyişmək üçün bütün platformanı release etmək lazımdır. Release ayda bir dəfədir və hər release-də bütün modullar risk altındadır. Bu, sürət problemidir və bir funksiya ilə məhdudlaşır. Bütün sistemi yenidən yazmağa əsas yoxdur.
Asılılıq və data ownership xəritəsi
Ayırmadan əvvəl xəritə çıxarılır:
┌──────────── Monolit (bir deploy, bir baza) ─────────────┐
│ Müraciətlər ──► Skorinq ──► Müqavilələr ──► Ödənişlər │
│ │ │ │ │
│ └──────────────► Bildirişlər ◄─────────────┘ │
│ │ │
│ applications contracts payments notifications ... │
└──────────────────────────────────────────────────────────┘Suallar:
- Statik asılılıqlar: hansı paket hansını çağırır?
jdeps, IDE analizi və ya ArchUnit testləri bunu göstərir. - İcra zamanı asılılıqları: real trafikdə hansı çağırışlar var? Bunu tracing və log-lardan görmək olar.
- Data ownership:
notificationscədvəlinə kim yazır, kim oxuyur? Başqa modul bu cədvəli birbaşaJOINedirmi? Ortaq baza ən çətin bağlılıqdır. Kod ayrılsa belə, iki sistem eyni cədvələ yazırsa, onlar hələ də bir sistemdir. - Biznes prosesləri: bildiriş hansı hadisələrdən yaranır (müraciət qəbul olundu, kredit təsdiqləndi, ödəniş gecikdi)?
Bildirişlər yaxşı namizəddir: sərhədi aydındır, başqa modullar ona yalnız "bu hadisə baş verdi" deyir, öz cədvəli var və tez-tez dəyişir. Müqavilələr və ödənişlər isə güclü tranzaksiya bağlılığına malikdir. Onları ilk addımda ayırmaq riski artırır.
Strangler Fig, yoxsa Branch by Abstraction?
- Strangler Fig: yeni funksionallıq köhnə sistemin yanında qurulur və çağırışlar kənarda tutulub tədricən yeni sistemə yönləndirilir. Çağırışları sistemin perimetrində (gateway, routing layer) tutmaq mümkün olanda uyğundur.
- Branch by Abstraction: komponent monolitin dərinliyindədir və onu yuxarıdakı kod çağırır. Əvvəl abstraksiya (interfeys) yaradılır, müştərilər ona keçirilir, sonra yeni implementasiya qoşulur və açar (feature flag) ilə keçid edilir. AWS-in qeydinə görə, data consistency məsələsi olan yerdə bu pattern çətinləşir.
Bildirişlər monolitin daxilindən çağırılır, ona görə praktikada ikisi birləşir: daxildə NotificationSender abstraksiyası yaradılır, routing qərarı onun arxasında verilir.
Yeni servis: outbox və idempotent consumer
Monolit bildiriş servisini sinxron çağırmır. O, hadisəni öz tranzaksiyasında transactional outbox cədvəlinə yazır. Ayrıca relay hadisəni broker-ə göndərir:
@Transactionalpublic void approve(LoanId id) { Loan loan = loans.get(id); loan.approve(); outbox.save(OutboxEvent.of("LoanApproved", loan.id(), loan.customerId())); // kredit statusu və hadisə eyni lokal tranzaksiyada yazılır}Outbox "hadisə itmir" problemini həll edir: status yazılıbsa, hadisə də yazılıb. Amma o, exactly-once çatdırılma demək deyil. Relay hadisəni göndərib uğur qeydini yazmadan çöksə, hadisə yenidən göndəriləcək. Çatdırılma at-least-once-dır. Ona görə yeni servis hadisəni idempotent emal etməlidir:
@Transactionalpublic void on(LoanApproved event) { int inserted = jdbc.update( "INSERT INTO processed_events(event_id) VALUES (?) ON CONFLICT DO NOTHING", event.eventId()); if (inserted == 0) return; // artıq emal olunub notifications.enqueue(event.customerId(), Template.LOAN_APPROVED, event.eventId());}processed_events cədvəli və bildiriş növbəsi yeni servisin öz bazasındadır. Burada vacib bir hüdud var: SMS və ya email provayderinə real göndəriş bazadan kənardadır. Servis göndərib, qeydi yazmadan çöksə, dublikat yenə mümkündür. Bunu provayderin idempotency açarı (məsələn, eventId) və ya "bəlkə iki dəfə" riskini qəbul edən biznes qərarı ilə idarə edin. Ətraflı: idempotency pattern-ləri və Kafka çatdırılma semantikası.
Nəticə: sistem eventual consistency ilə işləyir. Kredit təsdiqlənəndən bir neçə saniyə sonra bildiriş gedir. Bu gecikmə biznes üçün qəbul edilə bilən olmalıdır. Bunu əvvəlcədən soruşun.
Mərhələli routing
Keçid bir anda edilmir:
boolean useNew = flags.isEnabledFor("notifications.new-service", customerId);if (useNew) { // hadisə yeni servisə gedir; monolit modulu bu müştəri üçün göndərmir} else { legacyNotifications.send(customerId, template);}Məsələn: əvvəl daxili test müştəriləri, sonra 1%, 5%, 25%, 50% və nəhayət bütün müştərilər. Hər addımda bir müştəri üçün yalnız bir sistem göndərir. Paralel "shadow" rejimində yeni servis nəticəni hesablayır, amma göndərmir. Onun qərarı köhnə sistemin real göndərişi ilə müqayisə olunur.
Hər addımda baxılan göstəricilər: göndərilməyən və ya dublikat bildirişlər, gecikmə, xəta dərəcəsi, provayder xərci. Bunun üçün hər iki sistemdə eyni korrelyasiya id-si ilə log, metric və trace lazımdır. Bax: observability-nin üç sütunu.
Rollback: sürətli, amma pulsuz deyil
Routing flag-i geri çevirmək trafiki bir neçə saniyəyə köhnə modula qaytarır. Amma rollback-ın hüdudları var:
- Yeni servisin öz bazasına yazdığı data (göndəriş tarixçəsi, şablon versiyaları) köhnə sistemdə yoxdur. Onu uzlaşdırmaq və ya qəbul etmək lazımdır.
- Artıq emal olunmuş hadisələr geri qaytarılmır. Keçid anında eyni hadisəni iki sistem emal etməməlidir.
- Sxem dəyişiklikləri (köhnə cədvəldən sütunun silinməsi) geri dönüşü bağlayır. Belə "bir tərəfli qapıları" keçidin ən sonuna saxlayın.
- Köhnə modul uzun müddət söndürülübsə, rollback onun da işlək olduğunu tələb edir. Onu da test edin.
Uğuru biznes ölçür
Texniki "servis ayrıldı" uğur metriki deyil. Bu ssenaridə ölçülənlər:
- Şablon dəyişikliyindən production-a qədər keçən vaxt (lead time).
- Bildiriş dəyişikliklərinin yaratdığı insidentlərin sayı (change failure rate).
- Göndərilməyən və dublikat bildirişlərin payı.
- Əməliyyat xərci: yeni servisin infrastrukturu, monitorinqi, on-call yükü.
Metrik yaxşılaşmırsa, ayrılmanı dayandırmaq da düzgün qərardır. Bunu əvvəlcədən ADR-də yazın: ADR yazmaq.
Tez-tez edilən səhvlər
- Texnologiyadan başlamaq. "Kubernetes, Kafka, microservices" — problemi adlandırmadan.
- Ortaq bazanı saxlamaq. Kod ayrılır, cədvəl ortaq qalır — paylanmış monolit.
- Ən çətin modulu birinci ayırmaq. Ödənişlər əvəzinə bildirişlər kimi aşağı riskli, aydın sərhədli funksiya ilə başlayın.
- Exactly-once fərz etmək. Outbox və broker at-least-once verir. Consumer idempotent olmalıdır.
- Rollback-ı test etməmək. Plan kağızda qalır, lazım olanda işləmir.
- Bitməyən keçid. İki sistem illərlə yanaşı qalır; köhnə kod və routing flag-i silinmir.
- Əməliyyat xərcini saymamaq. Hər yeni servis şəbəkə, monitorinq, on-call və paylanmış data deməkdir.
Qısa xülasə
Modernizasiya texnologiya seçimi deyil. Biznes problemindən başlayır. Stabil və mane olmayan monoliti saxlamaq da arxitektura qərarıdır. Problem varsa, aydın sərhədli bir funksiya seçilir, köhnə və yeni sistem yanaşı işləyir, trafik mərhələli keçir, data consistency ölçülür və rollback-ın hüdudları əvvəlcədən bilinir. Strangler Fig resept deyil — konkret problemə seçilmiş, geri alına bilən addımdır.
Modul sərhədlərinin daxildən necə qorunduğu üçün mikroservis sərhədləri, hadisə yönümlü arxitekturanın qiyməti üçün event-driven trade-off-lar, arxitektin rolu üçün Solution Architect məqalələrinə baxın.
Mənbələr
- Martin Fowler — Strangler Fig Application.
- AWS Prescriptive Guidance — Decomposing monoliths into microservices (decompose by business capability, strangler fig, branch by abstraction).
- Martin Fowler — Branch By Abstraction.
- Chris Richardson — Transactional outbox və Idempotent consumer pattern-ləri (microservices.io).