Monolith'i Bölmeden Önce Sormam Gerekenler
Servis sayısı bir başarı metriği değil

Toplantıda cümle hep aynı yerden başlıyor: fatura kesme yavaş, stok kilidi siparişi tutuyor, o zaman faturalandırmayı ayrı servis yapalım. Tahtaya üç kutu çiziliyor, araya ok konuyor, herkes başını sallıyor. Ben o anda kutunun adından çok, yazının kime ait kalacağını soruyorum.
Kutunun adı değişince veri sahibi değişmiyor. Sadece çağrı bir HTTP sınırından geçmeye başlıyor. Timeout, retry, kısmi başarı, iki log sistemi. Bunlar 'ileride çözeriz' diye ertelenince, ilk kampanya günü eski monolith'ten daha gürültülü bir dağıtık kilit ortaya çıkıyor.
Bu yazı bir anti-mikroservis nutku değil. Bölmenin bazen doğru olduğu yerler var. Yanlış olan, soruları atlayıp slayttaki kutu sayısını ilerleme sanmak. Aşağıdaki beş soruyu geçirmeden process sınırını çizmiyorum.
Servis sayısı bir başarı metriği değil
Bir ekip büyüyünce 'artık mimari olarak olgunlaştık' cümlesi sık duyuluyor. Olgunluk genelde gözle görülür bir şey istiyor: yeni repo, yeni pipeline, yeni dashboard. Bunlar görünür. Görünmeyen kısım, aynı satırı iki yerde düzeltmek, aynı stoğu iki kez düşürmek, aynı kullanıcıyı iki serviste farklı isimle tutmak.
Monolith'te kötü olan şey tek process olması değil. Kötü olan, herkesin her tabloya yazması, her feature'ın her klasöre sızması, deploy'un tek korku gecesine dönmesi. Bunları process çoğaltarak çözmeye çalışmak, dağınıklığı çoğaltır. Önce dağınıklığın sınırını çizmek daha ucuz.
Küçük bir ekipte altı servis, altı tane yarım sahiplik demek. Gece 02:00'de bakan kişi hangisi olduğunu bilmiyorsa, o servis bağımsız değil; sadece uzak. Bağımsızlık, çağrı şekli değil, karar ve onarım yetkisi.
1. Yazının sahibi kim?
İlk sorum bu. Sipariş satırını kim insert ediyor? Stok adedini kim düşürüyor? Fatura numarasını kim üretiyor? Cevap 'ikisi birden, transaction ile' ise henüz iki servis yok; tek yazma yolu var. Yazma yolunu HTTP ile kesmek, dağıtık transaction davetiyesi.
Okuyan çok olabilir. Yazanın tek olması çoğu domain'de daha değerli. Katalogu vitrin, arama, admin, öneri okuyabilir. Fiyatı değiştiren yerin bir tane olması, cache invalidation'ından daha temel bir kural. Bunu konuşmadan 'katalog servisi' çizmek, aynı UPDATE'i iki API'nin arkasında bırakır.
Sahipliği kodda görmek istiyorum. Bir modülün dışarıya verdiği şey bir arayüz, içeriye sakladığı şey bir tablo. Orders.Place dışarıda, order_lines içeride. Başka bir ekip order_lines 'e join atıyorsa sahiplik kâğıtta kalmış demektir.
public interface IBillingModule
{
Task IssueForOrderAsync(OrderId orderId, CancellationToken ct);
}
internal sealed class BillingModule(AppDbContext db) : IBillingModule
{
public async Task IssueForOrderAsync(OrderId orderId, CancellationToken ct)
{
var order = await db.Orders.Include(o => o.Lines)
.SingleAsync(o => o.Id == orderId, ct);
var invoice = Invoice.FromOrder(order);
db.Invoices.Add(invoice);
await db.SaveChangesAsync(ct);
return invoice.Id;
}
}
Bu kod bir mikroservis değil. Aynı process, aynı veritabanı, tek transaction. Dışarıdan görünen şey bir sözleşme. İçerideki tabloyu başka bir feature doğrudan okumuyorsa, yarın process'e taşımak mümkün. Bugün taşımak zorunlu değil.
2. Hata sınırı nerede bitiyor?
İkinci soru: faturalandırma düşerse sipariş de mi düşmeli? Evet ise onları ayırmak, kullanıcıya 'siparişiniz alındı, faturanız belirsiz' demek. Hayır ise zaten asenkron bir sınır var demektir; o sınırı kuyrukla konuşabiliriz. Kararsızsak, önce ürün cümlesini yazmak gerekir, Dockerfile'ı değil.
Hata sınırını slaytta 'billing down' diye çizmek yetmez. Timeout 2 saniye mi, 30 mu? Kullanıcı bekliyor mu? Telafi var mı? Telafi yoksa ve kullanıcı bekliyorsa, o çağrı senkron kalmalı. Senkron kalacaksa ayrı process'in kazancı, bağımsız deploy ve ayrı ölçek. Bunlar yoksa kazanç ince, maliyet kalın.
Bir de sessiz hata var. Servis 200 dönüp işi yarım bırakırsa, çağıran 'oldu' sanır. Monolith'te bu, aynı stack trace içinde görünür. Ağırda, iki log deposunda iki yarım hikâye olur. Hata sınırını çizmek, gözlemlenebilirlik sınırını da çizmek zorunda.
3. Deploy bağımsızlığı gerçek bir ihtiyaç mı?
Üçüncü soru sık atlanıyor. 'Ayrı deploy edeceğiz' deniyor, sonra fatura alanı değişince sipariş API'si de aynı günde çıkıyor. İki pipeline, aynı release treni. O zaman ayrı process, ayrı bekleyiş, aynı kilitlenme.
Gerçek bağımsızlık şöyle durur: fatura şablonunu değiştiren kişi, sipariş testlerini kırmadan üretime çıkabilir. Sözleşme değişmedikçe karşı taraf habersizdir. Sözleşme her sprint değişiyorsa, o iki kutu henüz ayrı yaşamıyor; sadece ayrı derleniyor.
Haftada bir kez, aynı üç kişinin aynı PR'larla girdiği yerde bağımsız deploy bir hikâye. Günde birkaç kez, farklı ekiplerin birbirini beklemeden çıkması gerekiyorsa hikâye gerçek. Ekip iki kişiysa, iki pipeline çoğu gün tören.
4. Hangi işlemi kırıyorum?
Monolith'in verdiği şey, bir {SaveChanges} içinde sipariş + stok + fatura. Bunu bölünce üç yazma, üç başarı tanımı çıkar. İkisi olup biri olmayınca ne olacak? Kullanıcıya ne göstereceksiniz? Destek hangisini kaynak kabul edecek?
İki fazlı işlem, saga, outbox: bunlar çözüm, bedava değil. Outbox'ı erken eklememin nedenini başka yazıda anlatıyorum; burada önemli olan, işlemi kırmadan önce telafinin cümlesini kurmak. Cümle yoksa, dağıtık işlem 'biraz retry' ile gizlenir ve muhasebe ileride bağırır.
Bazı işlemler kırılmamalı. Para hareketi ile sipariş durumu aynı anda netleşmeliyse, onları aynı yazma yolunda tutuyorum. 'İleride event ile bağlarız' demek, bağın kopacağı günü ötelemek. Ötelemek bazen doğru; inkâr etmek değil.
5. Ekip sınırı veri sınırıyla örtüşüyor mu?
Beşinci soru insani. İki ekip aynı tabloya yazıyorsa, o tablo bir savaş alanı. Servis çizmek, savaşı HTTP üzerinden sürdürmek olabilir. Tersine, tek ekip altı servisi gezmek zorundaysa, o altı servis bir monolith'in dağınık hali.
Conway'i slogan gibi kullanmıyorum. Bakınan şey basit: kim, hangi dosyaya, kimin izni olmadan girebiliyor? Girebiliyorsa sahiplik yok. Giremiyor ama her feature için o kapıyı çalmak zorundaysa, sınır erken çizilmiş. İkisinin ortası, küçük bir sözleşmeli yüzey.
Ekip değişince sınırın da değişmesi normal. Dondurulmuş bir kutu haritası, altı ay sonra yalan söyler. Bu yüzden ilk çizdiğim şey process değil, modül ve tablo listesi. Process, o listenin istikrarsızlaştığı yerde gelir.
Bölmeden önce baktığım liste
Soruları ezbere sormuyorum. Bir sayfaya yazıyorum, toplantıda okuyoruz. Cevap 'bilmiyoruz' ise iş orada duruyor. Bilmiyorken bölmek, bilinmeyeni çoğaltmak.
{
"candidate": "billing",
"writeOwner": "billing-module",
"foreignWriters": [],
"failurePolicy": "order-accepted-invoice-later",
"userWaits": false,
"brokenTransaction": "order+invoice",
"compensation": "retry-issue-invoice",
"deployIndependentlyThisQuarter": false,
"teamCount": 1,
"splitNow": false,
"reason": "same team, same release train, write path still shared"
}
Bu JSON bir framework değil. Bir kararın altına imza. Üç ay sonra 'neden bölmedik' diye sorulunca, o günkü gerekçe duruyor. Gerekçe değişmişse tekrar konuşuruz. Slayt değişmişse konuşmayız; slayt zaten unutulur.
Ne zaman gerçekten böldüm
Böldüğüm yerler genelde sıkıcı. Bildirim gönderimi siparişi tutuyordu; SMS sağlayıcısı 8 saniye cevap verince checkout p95 bozuldu. Bildirimin yazarı ayrıydı, hata sınırı netti, kullanıcı beklememeliydi. Orada kuyruk haklıydı. Servis bile değil, bir worker yetmişti.
İkinci örnek, raporlama. Analitik sorgu sipariş yazmasını kilitliyordu. Okuma modeli ayrılsın diye process şart değildi; ayrı bağlantı ve ayrı tablo yetti. İleride ayrı depo geldi. İlk adım 'analytics servisi' değildi.
Üçüncü örnek, farklı ölçek. Görüntü işleme CPU yer, sipariş I/O yer. Aynı pod ikisini birden şişiriyordu. Burada process sınırı, kutu çizmek için değil, kaynak sınıfı için geldi. Veri sahibi hâlâ netti: iş bitince bir event, asıl yazı yine siparişte.
Bölmenin görünmeyen faturası
Yeni servisin ilk maliyeti repo değil. Sözleşme versiyonu, yerel çalıştırma, test verisi, kimlik iletme, iz sürme, uyarı eşiği, on-call rotası. Bunların hiçbiri tahtadaki okta yazılmaz. Yazılmayınca 'küçük bir extract' 6 haftaya yayılır.
Yerel geliştirme ikinci faturadır. Tek {docker compose} iken herkes ayağa kaldırıyordu. Beş servis, beş env dosyası, sırası bozulunca 'bende çalışıyor'. Bunu yaşamayan ekip, bağımsızlığı bedava sanır.
Üçüncü fatura, kısmi başarı. Destek ekranı tek sipariş numarası bekler. İki sistem iki numara üretince, müşteriye 'biraz bekleyin' denir. Beklemek bir ürün kararıdır; tesadüf olmamalı.
Kötü bir extract'in takvimi
Bir kez faturalandırmayı 'sadece taşıdık' diye ayırdık. İlk hafta mutlu yol. İkinci hafta iptal: sipariş iptal oldu, fatura kesilmişti, iade başka bir ekibin kuyruğundaydı. Üçüncü hafta numara üreteci iki host'ta çakıştı; fatura no unique'i production'da öğrendik. Dördüncü hafta, yerel {compose} 'u kimse ayağa kaldıramadı. Beşinci hafta extract'i geri aldık. Geri almak, ileri gitmekten uzun sürdü.
Geri alırken öğrendiğim şey, ilk soruların hepsinin cevabının o gün 'bilmiyoruz' olduğuydu. Yazarı iki yerde sandık. Hata sınırını 'billing down olursa order da down' diye geçiştirdik, sonra kullanıcıya 'siparişiniz var, faturanız yok' gösterdik. Deploy bağımsızlığı yoktu: proto değişince ikisi birden çıktı. İşlem kırılmıştı, telafi yoktu. Ekip hâlâ tekti. Beş hayır, bir extract. Slayt yine yeşildi.
Bu takvimi anlatmamın nedeni utanç değil, ucuzluk. Beş soruyu bir saatte sormak, beş haftalık geri almadan ucuz. Sormadan çıkan servis, bir demo; demo, ilk iptalde üretim olur.
Sözleşme yoksa bağımsızlık yok
Ayrı host, ayrı JSON demek değil. JSON her sprint değişiyorsa, o iki kutu evli. Evliliği gizlemek için {optional} alan yığmak, tüketicinin her alanı 'belki gelir' diye savunmasıdır. Savunmalı tüketici, bağımsız değildir; korkaktır. Korku, her release'te iki ekibi aynı odaya çağırır.
Sözleşmeyi dondurduğum yerde versiyon görünür. {v1/invoices} durur, {v2} yeni alan getirir, {v1} bir süre yaşar. Yaşatma maliyeti, erken bölmenin gizli faturasıdır. Maliyeti ödemeyeceksem, aynı process'te metod imzası değiştirmek daha dürüst: derleyici bağırır, iki pipeline bağırmaz.
public sealed record IssueInvoiceV1(Guid OrderId, decimal Amount);
public sealed record IssueInvoiceV2(Guid OrderId, decimal Amount, string Currency);
// v1 tüketicisi v2 host'a konuşuyorsa:
if (request.Currency is null)
request = request with { Currency = "TRY" };
Bu uyum katmanı, bir ay mazur. Altı ay, ikinci bir ürün. Uyum katmanını gateway'e koymak, beşinci yazıdaki yığına döner. Uyum, üreten tarafta veya açık bir anti-corruption'da dursun. Kenarda durmasın.
Toplantıya ölçüsüz girmemek
Bölme toplantısına boş elle girmiyorum. Son 14 günün p95'i, hata oranı, kim hangi dosyaya değmiş, hangi tabloya kim yazmış. 'Yavaş' bir duygu olabilir; duygu, host doğurmaz. Yavaşlık fatura şablonundan geliyorsa, extract değil, şablon önbelleği konuşulur.
{information_schema} ve git blame, slayttan daha kaba, daha işe yarar. Blame'de aynı üç isim her klasördeyse, Conway henüz iki ekip demiyor. Schema'da {invoice_} önekli kolonlar {orders} 'taysa, sahiplik zaten bulanık. Bulanık sahipliği ağ ile netleştirme.
Ölçü, kararı vermez. Kararın yalanını gösterir. Yalan görünce sorular kısa sürer. Görünmeyince herkes kendi slaydını anlatır. Benim işim, slayttan önce bir sayfa sayı bırakmak.
Önce sınır, sonra process
Bölmeden önce istediğim şey sıkıcı bir harita: tablolar, yazan fonksiyonlar, okuyan ekranlar, kabul edilen gecikme, kabul edilmeyen tutarsızlık. Harita yoksa kutu çizmek süs. Harita varsa, çoğu gün cevap 'şimdi değil' oluyor.
'Şimdi değil' bir erteleme hastalığı olmak zorunda değil. Modül sözleşmesi duruyorsa, yarın bölmek bir taşıma işi. Sözleşme yoksa, bugün bölmek bir keşif. Keşfi production'da yapmak pahalı.
Bu yüzden ilk PR'ım genelde yeni host değil. İçeride bir arayüz, dışarıda bir çağrı yeri, tabloda bir sahip kolonu. Bunu üç ay taşıyıp kimse acı çekmiyorsa, o sınır doğru demektir. Acı çekiliyorsa, process değil, sınırı yanlış çizmişizdir.
Kutu çizmeyi sevmiyorum diye yazmıyorum. Kutuyu erken sevmenin bedelini gördüğüm için yazıyorum. Sorular bitmeden oku çizmeyin. Ok, çoğu zaman bir dilek; dilek, timeout olunca gerçek olur.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap