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çimNə vaxt mənalıdırƏsas risk
SaxlaSistem stabildir, xərci məqbuldur, biznesə mane olmurBilik itkisi, köhnəlmiş platformanın dəstəyinin bitməsi
RefactorKod dəyişməyə çətindir, amma struktur düzgündürTestsiz refactor — reqressiya
ModullaşdırSərhədlər bulanıqdır, hamı hər şeyi çağırırUzun müddət görünməyən fayda
Bir funksiyanı ayırKonkret funksiyanın fərqli dəyişmə tempi, yükü və ya sahibi varPaylanmış sistemin əməliyyat xərci, data consistency
Yenidən yazPlatforma dəstəklənmir və ya biznes modeli tam dəyişibUzun "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: notifications cədvəlinə kim yazır, kim oxuyur? Başqa modul bu cədvəli birbaşa JOIN edirmi? 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

  1. Texnologiyadan başlamaq. "Kubernetes, Kafka, microservices" — problemi adlandırmadan.
  2. Ortaq bazanı saxlamaq. Kod ayrılır, cədvəl ortaq qalır — paylanmış monolit.
  3. Ə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.
  4. Exactly-once fərz etmək. Outbox və broker at-least-once verir. Consumer idempotent olmalıdır.
  5. Rollback-ı test etməmək. Plan kağızda qalır, lazım olanda işləmir.
  6. Bitməyən keçid. İki sistem illərlə yanaşı qalır; köhnə kod və routing flag-i silinmir.
  7. Ə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).