Ana sayfa → Teknik
Monolitikten Mikroservise: 5 Ayda Yaşadıklarımız
Dokuz yıl önceki bir anımdan bahsedeceğim. Salı günü saat 16:40. Dört ekibin iki haftalık işi tek bir deploy’da canlıya çıkacak. Rapor ekibinin bir sorgusu üretimde kilitleniyor, geri alıyoruz. Geri alma 52 dakika. Ödeme ekibinin acil düzeltmesi de o pakette; o da geri gidiyor. Ertesi sabah üç yöneticinin ortak cümlesi: “Böyle devam edemeyiz.” Haklılardı. Sonra beş ay sürecek bir şey başladı.
- Sebep teknik değil örgütseldi. Dört ekip tek deploy treni paylaşıyordu; kod yavaş değildi, ekipler birbirini bekliyordu.
- İlk günden olay tabanlı gittik. Karar doğruydu; bedelini bilmiyorduk. Aynı süreç içindeki çağrının bedavaya verdiği üç şey sessizce kayboldu: tam bir kez çalışma, sıralama, anlık tutarlılık.
- Sınır kodda değil veride çizilir. Olay “kayıt değişti” diyor, tüketici ortak tablodan okuyorsa hâlâ monolitsin.
- Kenardan başla, çekirdekten değil. Sipariş dört hafta sürüp yarım kaldı; bildirim iki haftada canlıya çıktı.
- Her şey olay olmaz. Ödeme onayını beklemek için ekran yoklamaya düştü; iki çağrı senkrona geri döndü.
- İki servisi geri birleştirdik. Başarısızlık değil, öğrenmenin faturası.
Önce dur: mikroservis gerçekten gerekiyor mu?
Bu soruyu bize kimse sormadı; biz de kendimize sormadık. O günlerde mikroservis “modern olan” şeydi. Şimdi sorulsa cevabım şu tabloyla başlar:
| Belirti | Mikroservis çözer mi? | Önce dene |
|---|---|---|
| Uygulama yavaş | Hayır | Profil al; genelde bir sorgu, bir N+1, bir önbellek |
| Trafik artıyor | Hayır | Yatay kopya (monolit de ölçeklenir) |
| Kod karmaşık | Hayır | Modül sınırları; ağ eklemek karmaşayı dağıtır, azaltmaz |
| Bir modül farklı dil/çalışma zamanı istiyor | Belki | Yalnızca o parçayı ayır |
| Ekipler birbirinin deploy’unu bekliyor | Evet | Ayrı çıkabilen sınırlar |
| Bir ekibin hatası herkesin çıkışını geri alıyor | Evet | Ayrı hata alanı |
| Bir parça diğerlerinden 50 kat fazla yük alıyor | Evet | Sadece o parçayı ayrı ölçekle |
Kural şu: mikroservis bir performans aracı değil, bir bağımsızlık aracı. Bağımsız çıkması gereken ekipler yoksa aldığın şey hız değil, ağ üstünden dağıtılmış bir monolit. Bizde son üç satır gerçekten geçerliydi; karar doğruydu. Yapılış şekli değildi.
Neden başladık: bir deploy treni hikâyesi
Sekiz yaşında bir monolit, dört ekip, haftada bir deploy. Sayılar kötüydü ama asıl sorun sayılar değildi:
- Deploy 45 dakika, geri alma 52 dakika. Bu yüzden haftada bir; daha sık çıkmayı kimse göze alamıyordu.
- Haftalık pakette ortalama 60–80 değişiklik. Bir şey bozulunca hangisi olduğunu bulmak saatler alıyordu.
- Ödeme ekibinin acil düzeltmesi, rapor ekibinin yarım kalmış işini bekliyordu. Ya da tersi.
- Test ortamı tek. Bir ekip test ederken diğeri bekliyordu.
Yani problem “sistem yavaş” değil, “dört ekip birbirine kelepçeli” idi.
İlk gün verdiğimiz ikinci karar da buradan çıktı: servisler birbirini çağırmayacak, olay yayınlayacak. Broker olarak Kafka’yı seçtik. HTTP ile çağıran servis, çağırdığının o an ayakta olmasına bağlıdır; olay dinleyen servis değildir. Ekipler deploy’da birbirinden bağımsız olacaksa çalışma zamanında da bağımsız olmalıydı. Karar doğruydu. Bedelini bilmiyorduk.
Adım adım: yanlıştan doğruya
Deneme 1 — katmana göre böldük (bozuk)
web-api --(bekler)--> is-kurallari-servisi --(bekler)--> veri-erisim-servisi --> DB
# "Olayla konusacaklar" demistik.
# Katmanlar olayla konusamaz: is kurali, veri erisiminin CEVABINI beklemek zorunda.
İlk refleks kodu zaten bildiğimiz katmanlara ayırmaktı: API, iş kuralları, veri erişimi. Daha ilk sprintte fark ettik: bu parçalar birbirine olay yayınlayamıyor, çünkü her biri bir sonrakinin cevabına muhtaç. Katmana göre böldüğümüz bu sistem, hedeflediğimiz bağımsız olay tabanlı yapıya dönüşemedi; katmanlı bir sistem olay tabanlı olabilir, ama bizim katmanlarımız birbirinin cevabına muhtaçtı. Üstelik hiçbir ekip tek başına bir özellik çıkaramıyordu — başladığımız problemi ağ gecikmesiyle yeniden kurmuştuk. İki hafta sonra bıraktık.
Deneme 2 — alana göre böldük, veritabanını bölmedik (dağıtık monolit)
siparis-servisi --> yayinla(SiparisDegisti { id: 4711 })
kargo-servisi --> dinle, sonra: SELECT * FROM siparis WHERE id = 4711 <- ortak tablo
rapor-servisi --> dinle, sonra: SELECT * FROM siparis WHERE id = 4711 <- ortak tablo
Bu sefer alana göre ayırdık, doğru yön. Olaylar da akıyordu. Ama “veritabanını sonra böleriz” dedik ve olaylarımız yalnızca “4711 değişti” diyordu; tüketici gidip ortak tablodan okuyordu. Kâğıt üstünde olay tabanlı, altta yine tek tabloya bağlı altı servis. Üç hafta iyi gitti. Sonra bir Cuma:
| Saat | Olay | Sonuç |
|---|---|---|
| 14:10 | Müşteri ekibi musteri tablosunda bir kolonu yeniden adlandırdı | Kendi servisi güncel, testleri yeşil |
| 14:25 | Deploy | Müşteri servisi sağlıklı |
| 14:31 | Sipariş servisi olayı aldı, aynı tabloyu okudu | 500 — sipariş alınamıyor |
| 14:33 | Rapor ve kampanya tüketicileri de | Üç servis, tek değişiklik |
| 15:40 | Sebep bulundu, geri alındı | Bir saat sipariş kaybı |
Kimse hata yapmamıştı. Müşteri ekibi kendi tablosunu değiştirmişti. Ama o tablo kendinin değildi — dört servisin ortak sözleşmesiydi ve olay tabanlı görünen sistem bunu gizliyordu. Öğrendiğimiz cümle: servis sınırı kodda değil, veride çizilir. Olay, tüketicinin ihtiyacı olan veriyi taşımalı; “git kendin bak” diyen olay, olay değil, tabloya giden bir davetiye.
Deneme 3 — çekirdekten başladık (yarım kaldı)
Veriyi de böleceğimize karar verince en önemli alandan başladık: sipariş. Mantık şuydu — en çok değişen, en çok ağrıyan parça o. Dört hafta sürdü ve bitmedi. Sebep basit: sipariş her şeye bağlı. Müşteri, stok, kampanya, ödeme, kargo, fatura. Onu ayırmak, bütün olay sözleşmelerini ilk günde tasarlamak demekti. Dört haftanın sonunda elimizde hem monolitte hem serviste yaşayan, ikisi de tam olmayan bir sipariş kodu vardı.
Deneme 4 — kenardan, kademeli (doğru)
# monolit icinde, bildirim kodu:
if hash(siparisId) % 100 < BILDIRIM_SERVISI_YUZDESI: # 5 -> 25 -> 100
pass # yeni servis SiparisOlustu olayini dinliyor, o gonderecek
else:
eskiBildirimGonder() # monolitteki kod, henuz yerinde
Bildirim servisiyle başladık: e-posta, SMS, push. Bağımlılığı az, kendi verisi net,
ilk gerçek olay tüketicisi. İki haftada canlıdaydı. Monolit
SiparisOlustu yayınlıyor, yeni servis dinliyor; eski kod yüzde 95’i
göndermeye devam ediyor. Bir haftada yüzde 100’e çıktı. Ters giden bir şey
olsaydı geri dönüş tek bir ayar. Sonra dosya işleme, sonra rapor üretimi. Sipariş
servisini dördüncü ayın sonunda, etrafındaki her şey ayrıldıktan
sonra ayırdık ve iki hafta sürdü.
Sıra şuydu ve bir daha olsa yine bu olurdu: en az bağımlı olandan en çok bağımlı olana. Çekirdek en sona kalır, çünkü çekirdek ancak etrafı boşaldığında hareket edebilir.
Birinci bedel: aynı mesaj iki kez gelir
İkinci ayın başında bir Salı sabahı müşteri hizmetlerinden mesaj: “Aynı siparişe iki kargo çıkmış, müşteriye iki kutu gitmiş.” Tek sipariş değil; bir haftada 17 çift kargo. Kampanya tarafında daha sessiz bir şey: 300’den fazla müşteriye puan iki kez yüklenmiş, kimse fark etmemiş — şikâyet eden yok.
Sebep bir hata değil, sözleşmenin kendisi. Kargo servisi olayı aldı, etiketi oluşturdu, veritabanına yazdı ve broker’a “işledim” demeden önce deploy yedi. Broker cevap alamayınca aynı olayı yeniden teslim etti; yeni kopya onu ilk kez görüyormuş gibi işledi. Kullandığımız broker tam olarak vaat ettiği şeyi yapmıştı: en az bir kez teslim. Monolitte bu problem yoktu, çünkü fonksiyon çağrısı “yeniden teslim” edilmez.
onSiparisOlustu(olay):
etiket = kargo.olustur(olay.siparisId)
db.kaydet(etiket)
ack() # <- buraya gelemeden olurse
# olay yeniden gelir,
# etiket ikinci kez cikar
onSiparisOlustu(olay):
BEGIN
INSERT INTO islenen_olaylar (olay_id)
VALUES (olay.id) -- UNIQUE:
-- ikinci gelis burada patlar
etiket = kargo.olustur(olay.siparisId,
header "Idempotency-Key: " + olay.id) -- dis sistem: ayni anahtar,
-- ayni etiket, yeni etiket yok
db.kaydet(etiket)
COMMIT
ack()
# unique ihlali -> zaten islenmis, sessizce ack, gec
Anahtar üç şey. Olayın kimliğini üretici koyuyor, tüketici üretmiyor. “Gördüm” kaydı, işin veritabanı tarafıyla aynı işlemde; ayrı işlemde olursa aradaki çökme yine iki etiket üretir. Ve en geç fark ettiğimiz üçüncüsü: dış sistem çağrısı transaction’ın içinde durur ama ona dahil değildir.
İlk dedup’tan sonra çift kargo haftada 17’den 2’ye indi; sıfıra inmedi. Kalan ikisi
şuydu: INSERT geçti, kargo firmasının API’si etiketi kesti, COMMIT’ten
önce süreç öldü. Veritabanı geri alındı — dedup kaydı da onunla birlikte — ama kargo
firması geri almadı. Olay tekrar geldi, API bir daha çağrıldı, ikinci etiket. Çözüm dış
sistemin de idempotent olması: kargo API’sine olay kimliğini idempotency
anahtarı olarak geçtik; aynı anahtarla gelen ikinci istek aynı etiketi döndürüyor,
yenisini kesmiyor. Dış sistem bunu desteklemiyorsa iş ikiye bölünür: önce “istek
gönderiliyor” kaydını commit et, sonra çağır, sonra sonucu yaz — güvenli yeniden deneme
ancak öyle mümkün. Ayrıntısı
mesaj teslim garantileri yazısında; buradaki
ders sıralamayla ilgili: bu, ilk olay tüketicisiyle birlikte kurulmalıydı,
on yedi çift kargodan sonra değil.
Tekrar yalnızca tüketici tarafından gelmiyor. Siparişi yazıp olayı yayınlarken süreç ölürse olay hiç gitmez; bunu outbox ile çözdük — ve outbox relay’i de yeniden dener, o da aynı olayı iki kez yayınlayabilir. Yani tekrar iki uçtan da gelebiliyor ve tek savunma tüketicide. Kural basit, bedeli her servise bir tablo: olay tüketen her servis, aynı olayı iki kez alınca aynı sonucu üretmeli. Tercih değil, olaya geçişin giriş şartı.
İkinci bedel: sıra
Dedup’ı kurduktan iki hafta sonra yeni bir şikâyet: “İptal ettiğim sipariş kapıma geldi.” Log şunu gösteriyordu:
| Saat | Kargo servisine gelen olay | Ne yaptı |
|---|---|---|
| 11:02:14.310 | SiparisIptalEdildi (4711) | “Bilinmeyen sipariş” — atladı |
| 11:02:14.480 | SiparisOlustu (4711) | Kargo etiketi oluşturdu |
| — | Müşteri sepeti onayladıktan 3 saniye sonra iptal etmişti. İki olay iki farklı partition’a düştü, farklı tüketici kopyaları onları farklı hızda işledi; sonuç ters sıra. | |
Sebep: olayları yayınlarken partition anahtarı vermiyorduk; broker dağıtıyordu. Aynı siparişin iki olayı iki farklı partition’a düştü ve iki tüketici kopyası onları kendi hızında işledi. Kafka sırayı yalnızca aynı partition içinde garanti eder; bunu biliyorduk, ama “bize olmaz” diyorduk. İki olay arasındaki 170 milisaniye yetti.
yayinla(topic="siparis", key=siparisId, olay) # ayni siparisin butun olaylari ayni partition
Partition anahtarı, sırasının korunmasını istediğin şeyin kimliğidir: sipariş için sipariş numarası, müşteri için müşteri numarası. Bunun ötesinde sıra beklemek — “bütün siparişler oluşturulma sırasında gelsin” — tasarım hatasıdır; tek partition demektir, tek partition da aynı consumer group içinde paralelliği bire indirir.
Bir de sırayı bozmadan çözülemeyen bir durum kaldı: iptal olayı gerçekten önce gelirse
“bilinmeyen sipariş” diye atmak yerine iptali kaydedip beklemek.
Sonra SiparisOlustu gelince önce iptal var mı diye bakmak. Sıra artık
garanti olsa da bu koruma duruyor; tüketici ucuz, çift kargo pahalı.
Üçüncü bedel: “sipariş verdim ama listede yok”
Bu en çok destek bileti üreten madde oldu. Sipariş yazılıyor, olay yayınlanıyor, “Siparişlerim” sayfası ayrı bir okuma modelinden besleniyor ve o model olayı 1–3 saniye sonra işliyor. Müşteri “Sipariş ver”e basıp hemen listeye gidiyor: boş. Yeniden sipariş veriyor. Haftada 12 çift sipariş, hepsi bu yoldan.
Aynı ailenin ikinci üyesi daha kötüydü. “Her şey olay” deyince
ödeme sonucunu da olayla bekliyorduk: sipariş ekranı
OdemeTamamlandi gelene kadar bir dönen simge gösteriyor ve her saniye
sunucuya soruyordu. Ödeme onayının ekranda görünmesi p95’te 6 saniye;
monolitte 80 milisaniyeydi. Bu sürede müşteri sekmeyi kapatıyor, para çekilmiş, sipariş
“bekliyor”da kalıyordu.
POST /siparis
-> siparisi yaz
-> yayinla(SiparisOlustu)
-> 202 Accepted
# ekran: OdemeTamamlandi gelene kadar
# her saniye GET /siparis/4711/durum
Doğru cevap yok, sadece “sonra bak”. Kullanıcı bekliyor, sekme kapanıyor.
POST /siparis
-> stok.kontrol() # HTTP, 300 ms zaman asimi
-> odeme.al() # HTTP, devre kesici
-> siparisi yaz + outbox(SiparisOlustu)
-> 201 { siparisId, durum: "alindi" }
# kargo, bildirim, puan, rapor, okuma modeli:
# olayi dinler, kendi hizinda
Ekran 400 ms’de “alındı” görüyor; gerisi arkada akıyor.
Ölçü şu oldu: cevabı olmadan isteğe devam edilemeyen adım senkron; gerisi olay. Stok ve ödeme bu testi geçiyor, geri kalan yedi şey geçmiyor. İki senkron çağrıya zaman aşımı ve devre kesici kondu; ödeme servisi yavaşlarsa sipariş ekranı 300 ms’de “şu an alamıyoruz” diyor, 6 saniye dönmüyor.
“Listede yok” problemi için de iki küçük şey: sipariş sonrası ekran listeye değil sipariş detayına gidiyor ve detay, okuma modelinden değil doğrudan sipariş servisinden okunuyor (kendi yazdığını görmek). Listede gecikme hâlâ 1–3 saniye; ama artık kimse o sürede ikinci sipariş vermiyor.
Dördüncü bedel: bir işlemin dört servise dağılması
Monolitte sipariş oluşturma tek bir veritabanı işlemiydi: stok düş, sipariş yaz, kampanya kullan, ödeme kaydet — hepsi ya olur ya olmaz. Olaya dağıldığında bu garanti sessizce kayboldu; mutlu yolda her şey çalıştığı için kimse fark etmedi.
| Adım | Olay | Durum |
|---|---|---|
| 1 | StokDusuldu | ✓ |
| 2 | SiparisOlustu | ✓ |
| 3 | KuponKullanildi | ✓ |
| 4 | OdemeBasarisiz | yayınlandı — kimse dinlemiyordu |
| — | Sonuç: stok gitti, kupon yandı, sipariş “bekliyor”, para alınmadı. Müşteri tekrar deniyor: “kupon kullanılmış.” | |
İlk üç hafta bunu elle düzeltiyorduk — haftada 20-30 kayıt. Sonra telafi
olaylarına geçtik: OdemeBasarisiz yayınlanınca stok servisi
StokIadeEt, kampanya servisi kuponu serbest bırakıyor. Her ileri adımın bir
geri adımı var ve her servis kendi telafisini dinliyor. Bu
saga yazısının konusu; burada önemli olan şu:
bu maliyeti geçişten önce kimse hesaplamamıştı. Monolitte bedava olan
bir garanti, olay tabanlı sistemde tasarlanması gereken bir özellik.
Görünmezlik: hata ayıklama 10 dakikadan 3 saate
HTTP zincirinde en azından bir stack trace olur. Olayda hiçbir şey yok: kargo etiketi yanlış çıkmış, hangi olay üretmiş, o olayı hangi olay tetiklemiş? Üçüncü ayda aynı iş ortalama üç saat alıyordu; altı servisin logunda aynı saniyeye bakıp müşterinin siparişini elle eşleştiriyorduk.
Correlation ID ve dağıtık izlemeyi o yazıda anlattım; olay tabanlı sistemde bir adım daha var. Tek kimlik yetmiyor, üç tane gerekiyor ve üçü de olayın başlığında taşınıyor:
event_id bu olayin kendi kimligi (dedup bunu kullanir)
correlation_id is akisinin kimligi; ilk istekte dogar, zincir boyunca DEGISMEZ
causation_id bu olayi tetikleyen olayin event_id'si
SiparisOlustu event_id=E1 correlation_id=C1 causation_id=-
OdemeTamamlandi event_id=E2 correlation_id=C1 causation_id=E1
KargoEtiketiKesildi event_id=E3 correlation_id=C1 causation_id=E2
Tüketici, ürettiği olaya correlation kimliğini olduğu gibi kopyalıyor, causation alanına tükettiği olayın kimliğini yazıyor. Correlation ile “bu müşterinin isteği hangi olayları doğurdu” sorusuna tek sorguyla cevap alıyorsun; causation ile zinciri geriye doğru yürüyüp yanlış etiketin hangi olaydan çıktığını buluyorsun. Biz üçüncü ayda ekledik; aradaki üç ay boyunca “görmediğimiz için olmadığını sandığımız” hataları da ekleyince, bu gecikmenin faturası en pahalılardan biriydi.
Kimsenin hesaplamadığı bedel: operasyon
Bir monolitin bir pipeline’ı, bir panosu, bir nöbet defteri vardır. On dört servisin on dört tane — artı kuyruklar:
| Monolit | 4. ay | |
|---|---|---|
| Deploy pipeline | 1 | 14 |
| İzleme panosu | 1 | 14 (+ 1 “hepsi”) |
| Topic / kuyruk | 0 | 31 |
| Tüketici gecikmesi (lag) alarmı | — | her tüketiciye bir tane |
| Gece uyandıran alarm / ay | 4 | 11 |
| “Kim bunun sahibi?” sorusu | yok | haftada birkaç |
| Ortam kurulumu (yeni geliştirici) | yarım gün | 3 gün |
| Kütüphane güncellemesi | 1 PR | 14 PR |
Kuyruğun kendi hastalıkları da geldi. Bozuk bir olay (şeması eski bir üreticiden) kargo tüketicisini 40 dakika kilitledi: aynı mesajı tekrar tekrar deniyor, arkadaki 2.000 olay bekliyor. Zehirli mesaj kuyruğu (DLQ) o gün kuruldu — kuyruk hastalıkları yazısında hepsi var; buradaki not, hepsinin ilk üç ayda birer birer başımıza geldiği.
Alarm sayısı ilk üç ayda neredeyse üçe katlandı; sistem daha kırılgan olduğu için değil, daha çok parçası olduğu için. Bunu ancak bir platform ekibiyle geri çektik: ortak pipeline şablonu, ortak log/izleme paketi, tek komutla yeni servis iskeleti — içinde dedup tablosu, outbox ve lag alarmı hazır. O ekip planda yoktu, dördüncü ayda mecburiyetten kuruldu. Bugün olsa ilk haftada kurulurdu.
Bir de insan tarafı var: on dört servis, dört ekip. Her ekip üç dört servisin sahibi. Bu Conway’in kendini hatırlatma biçimi — servis sınırlarını ekip sınırlarına göre çizmeyince, ekip sınırları kendini servis sınırlarına uydurmaya başlıyor.
Geri birleştirdiklerimiz
Bunu yazmak biraz zor ama en faydalı ders bu. Beşinci ayda iki servisi geri birleştirdik:
-
Sepet ve fiyatlandırma. Sepet her değişince
SepetDegisti, fiyatlandırma cevap olarakFiyatHesaplandi, sepet onu bekliyor — iki servis arasında olay ping-pongu ve kullanıcı yeni fiyatı 1-2 saniye sonra görüyor. Birbirinden bağımsız hiç değişmemişlerdi: dört ayda 40 deploy, 38’i aynı gün. Tek servis oldu; fiyat anında, hiçbir şey kaybedilmedi. - Kargo ve teslimat takibi. Aynı ekip, aynı veri, aynı gün değişen iki servis. Ayrılma gerekçesi “ileride ayrı ekipler olur” idi. Olmadı.
Kural sonradan netleşti: iki servis deploy geçmişinde tek servis gibi davranıyorsa, muhtemelen tek servis olmalıdır. Bunu ölçmek kolay — deploy geçmişine bak. Ayrı çıkma özgürlüğü kullanılmıyorsa ayrılığın bedeli boşuna ödeniyor.
Sahadan: 5 ay sonra sayılar
| Başlangıç | 2. ay | 5. ay | |
|---|---|---|---|
| Servis sayısı | 1 | 8 | 12 (14’ten geri) |
| Deploy sıklığı | haftada 1 | günde 3 | günde 8 |
| Deploy + geri alma süresi | 45 + 52 dk | 8 + 3 dk | 4 + 1 dk |
| Bir ekibin hatasının diğerlerini durdurması | her hafta | ayda 2-3 | çeyrekte 1 |
| Ödeme onayının ekranda görünmesi (p95) | 80 ms | 6 sn | 400 ms |
| Gece alarmı / ay | 4 | 11 | 5 |
| Hata ayıklama (ortalama) | 10 dk | 3 saat | 25 dk |
| Elle düzeltilen tutarsız sipariş / hafta | 0 | 20-30 | 0-1 |
| Çift işlenen olay (kargo, puan) / hafta | — | 17+ | 0 |
| Çift sipariş (müşteri tekrar verdi) / hafta | 0 | 12 | 0 |
Tablonun orta sütunu, çoğu yazıda olmayan sütun. İkinci ayda her şey daha kötüydü: daha yavaş, daha çok alarm, daha uzun hata ayıklama, tutarsız veri, çift kargo. O dönemde “geri dönelim” diyen vardı ve haksız değildi. Geçiş, ortasında bırakılırsa iki dünyanın da kötüsünü veriyor; sonuna kadar götürülünce ilk sütunun asıl problemini — ekiplerin birbirine kelepçeli olmasını — çözüyor. Son sütundaki tek gerçek kazanç ikinci ve dördüncü satır; geri kalanı, ikinci ayda kaybettiklerimizi geri almak.
Bugün yeniden başlasam
- Önce modüler monolit. Sınırları ağ olmadan, aynı kod tabanında modül olarak çiz; modüller birbirine süreç içi olay yayınlasın, birbirinin tablosuna dokunamasın. Yanlış sınır burada ucuz, ağ üstünde pahalı.
- Olay veriyi taşısın. “4711 değişti” değil, tüketicinin ihtiyacı olan alanlar olayın içinde. Tüketici tabloya gidiyorsa sınır yok demektir.
-
Dedup ve partition anahtarı ilk günden. Servis iskeletinin içinde
gelsin: olay kimliği,
islenen_olaylartablosu, anahtar = kaydın kimliği — ve her dış sistem çağrısına idempotency anahtarı. - Her isteğe ve her olaya kimlik, birinci gün. Üç kimlik olayın başlığında: event, correlation (zincir boyunca aynı), causation (tetikleyen olay).
- Platform ekibi ilk haftada. Pipeline şablonu, izleme paketi, lag alarmı, DLQ — üçüncü servisten sonra elle yapmak çöküyor.
- Kenardan başla, strangler ile, trafiği yüzde yüzde kaydırarak. Çekirdek en sona.
- Veriyi kodla birlikte böl. “Veritabanını sonra” diyen her plan dağıtık monolitte bitiyor.
- Hangi adımın olay olmayacağına baştan karar ver. Cevabı olmadan devam edilemeyen adım senkron; zaman aşımı ve devre kesiciyle. Gerisi olay.
- Deploy geçmişine ayda bir bak. Deploy geçmişinde tek servis gibi davranan iki servis, ayrılığın faydasını kullanmıyordur; birleştir.
Kontrol listesi
- Ayırma sebebi bağımsız deploy mu, yoksa performans/modernlik mi? (İkincisiyse dur.)
- Bu parça monolitte önce modül olarak ayrıldı mı? Başka modülün tablosuna dokunuyor mu?
- Kendi verisi belli mi? Hangi tabloları yalnızca o kullanıyor?
- Yayınladığı olay tüketicinin ihtiyacı olan veriyi taşıyor mu, yoksa “git bak” mı diyor?
- Olay yayınlıyorsa outbox var mı?
- Olay tüketiyorsa dedup var mı? “Gördüm” kaydı işin veritabanı tarafıyla aynı işlemde mi? Dış sistem çağrısı idempotency anahtarı alıyor mu?
- Partition anahtarı, sırası korunacak kaydın kimliği mi?
- Sıra dışı gelen olay (iptal, oluşturmadan önce) atılıyor mu, bekletiliyor mu?
- Hangi adımlar senkron kalıyor? Her biri “cevapsız devam edilemez” testini geçiyor mu? Zaman aşımı ve devre kesici var mı?
- Kullanıcı kendi yazdığını hemen görebiliyor mu, yoksa okuma modelini mi bekliyor?
- Monolitte tek işlem olan hangi akış artık birden çok servise yayılıyor? Telafi olayları yazıldı mı, dinleyeni var mı?
- Olay başlığında event, correlation ve causation kimlikleri var mı? Tüketici correlation’ı kopyalıyor, causation’a tükettiği olayı yazıyor mu?
- Tüketici gecikmesi (lag) alarmı ve DLQ var mı?
- Trafik kademeli kaydırılabiliyor mu (yüzde 5 → 25 → 100)? Geri dönüş tek değişiklik mi?
- Sahibi hangi ekip? O ekip kaç servise daha bakıyor?
Sonuç
Beş ayın sonunda doğru kararı yanlış sırayla vermiş olduğumuzu gördük. Karar iki parçaydı ve ikisi de doğruydu: dört ekibin tek deploy trenine kelepçeli olması sürdürülemezdi, ve servisler birbirini çağırmak yerine olayla konuşmalıydı. Sıra yanlıştı: katman, sonra ortak veritabanı, sonra çekirdek — üç deneme, yaklaşık on hafta. Ve olayın üç bedelini — tam bir kez çalışma, sıralama, anlık tutarlılık — tasarlayarak değil, çift kargo, iptal edilmiş siparişin kapıya gelmesi ve boş sipariş listesiyle öğrendik.
Akılda kalacak sıra şu: önce sınırı monolitin içinde çiz, sonra veriyi böl, sonra kenardan başlayıp ağ ekle; olay tüketen ilk servisle birlikte dedup, partition anahtarı ve kimlik gelsin; hangi adımın olay olmayacağına baştan karar ver. Mikroservis, kuyruktan konuşan kod parçaları değil; birbirini beklemeden çıkabilen ekipler demek. Ekipler beklemeye devam ediyorsa ne servis sayısının ne de kuyruğun önemi var.