Bölme Modeli: Hataya Dayanıklı Mikro Hizmetlerin Tasarlanması
Mikro hizmet mimarisinde, tek bir uygulama düzinelerce veya yüzlerce bağımsız, işbirliği yapan hizmete bölünür. Bu tasarım modülerliği ve ölçeklenebilirliği geliştirirken aynı zamanda büyük bir riski de beraberinde getirir: bir hizmette meydana gelen bir arıza, tüm sistemi basamaklandırabilir ve çökertebilir.
Bir aşağı akış hizmeti yavaşlar veya yanıt vermez hale gelirse, yukarı akış hizmetlerinize gelen istekler birikmeye başlar. Hepsi aynı belleği, CPU’yu veya iş parçacığı havuzunu paylaşıyorsa, yavaş bir bağımlılık mevcut tüm kaynakları hızla tüketebilir ve tüm uygulamanızın çökmesine neden olabilir.
Bu basamaklı başarısızlık domino etkisi olarak bilinir. Bunu önlemek için sistem mimarları Bölme Modeli’ni kullanır.
Bu kılavuzda Bulkhead modelinin ne olduğunu, nasıl çalıştığını ve Java (Resilience4j) ve Go’daki basit benzetmeler, mimari kavramlar ve kod örneklerini kullanarak nasıl uygulanacağını keşfedeceğiz.
Gerçek Dünyadaki Benzetme: Su Geçirmez Gemi Bölmeleri
Bu desenin adı gemi inşa endüstrisinden gelmektedir.
Bölme, bir geminin gövdesinin içine inşa edilen su geçirmez bir duvardır. Geminin gövdesi içinde tek, büyük bir açık alan yerine, iç kısım birkaç bağımsız, yalıtılmış bölmeye bölünmüştür.
Eğer gemi bir engelle çarpışırsa ve gövdesi delinirse, hasarlı bölmeye su taşacaktır. Ancak su geçirmez bölmeler nedeniyle su bu tek bölmede tutulur. Geminin geri kalanı kuru ve yüzer durumda kalarak suyun üstünde kalmasına ve güvenliğe ulaşmasına olanak tanıyor.
Bölmeler olmasaydı, su tüm gövde boyunca serbestçe akacak ve sonunda gemi batacaktı.
Yazılım mühendisliğinde:
- Gemi uygulamanızın veya hizmetinizin tamamıdır.
- Bölmeler yalıtılmış kaynak havuzlarıdır (iş parçacıkları, bağlantılar, CPU).
- Gövde İhlali, aşağı yöndeki bir mikro hizmetteki bir arıza veya yavaşlamadır.
- Su baskını kaynakların tükenmesidir.
Sorun: Paylaşılan Kaynak Havuzları ve Konu Tükenmesi
Bölmelerin neden gerekli olduğunu anlamak için kaynaklar küresel olarak paylaşıldığında neler olduğuna bakalım.
Kullanıcı isteklerini işleyen bir API Ağ Geçidini veya bir web sunucusunu hayal edin. Gelen tüm çağrıları işlemek için 100 iş parçacığından oluşan tek bir küresel iş parçacığı havuzuna sahiptir. Sunucu üç aşağı akış hizmetiyle etkileşime girer:
- Katalog Hizmeti (hızlı, ürün listesini okur)
- Ödeme Hizmeti (hızlı, ödeme işlemleri)
- Öneri Hizmeti (yavaş, kişiselleştirilmiş öğeleri hesaplar)
Normalde her şey yolunda gider. Ancak Öneri Hizmetinin veritabanı kilitlenmesinden muzdarip olduğunu ve yanıt vermesinin 200 milisaniye yerine 30 saniye sürmeye başladığını varsayalım.
İşte olanlar:
- Kullanıcılar ana sayfayı ziyaret etmeye devam ederek Öneri Hizmetine yönelik talepleri tetiklerler.
- Sunucu, her isteğe genel havuzdan bir iş parçacığı atar.
- Tavsiye Hizmeti yavaş olduğundan, bu ileti dizileri yanıtları beklemektedir.
- Saniyeler içinde havuzdaki 100 iş parçacığının tümü Öneri Hizmetinde bekliyor.
- Yeni bir kullanıcı kataloğu satın almaya veya görüntülemeye çalıştığında, sunucuda bu isteği işleyecek iş parçacığı kalmaz.
Katalog ve Ödeme hizmetleri tamamen sağlıklı olmasına rağmen yavaş Öneri Hizmetinin paylaşılan ileti dizisi havuzunu tüketmesi nedeniyle artık erişilemiyor. Tüm sistem çevrimdışı oldu.
Çözüm: Bölme Modeli
Bölme Modeli, bir alandaki arızanın diğerlerini etkilememesi için kaynak havuzlarını bölümlere ayırarak bu sorunu çözer.
Tek bir global havuz yerine, her hizmet veya alt bağımlılık için ayrı, sınırlı havuzlar tahsis ediyoruz.
Tavsiye Hizmeti için özel olarak 10 iş parçacığı ayırırsak, bu durumda en fazla 10 iş parçacığı bu hizmette beklerken engellenebilir. Öneri Hizmeti yavaşlarsa, bu 10 ileti dizisi tükenecek ve sonraki öneri istekleri hemen reddedilecektir (başarısız).
Ancak geri kalan 90 ileti dizisi hâlâ Katalog ve Ödeme hizmetlerine ayrılmıştır. Öneri widget’ı geçici olarak kullanılamıyor olsa bile kullanıcılar yine de ürünlere göz atabilir ve satın alma işlemi gerçekleştirebilir.
Bölme İzolasyon Türleri
Yazılım sistemlerinde bölmeleri uygulamanın iki temel yolu vardır:
1. İş Parçacığı Havuzu İzolasyonu
Bu modelde, her aşağı akış bağımlılığına kendi özel iş parçacığı havuzu ve yürütme kuyruğu atanır.
- Nasıl çalışır: Ana uygulama iş parçacığı, görevi belirli bir iş parçacığı havuzuna devreder. Havuz doluysa istek sıraya alınır veya reddedilir.
- Artıları: Tam izolasyon sağlar. Bir hizmet yavaşlarsa yalnızca iş parçacığı havuzu etkilenir. İş parçacıkları işletim sistemi/JVM düzeyinde yalıtılmıştır.
- Eksileri: İş parçacığı zamanlaması, içerik değiştirme ve kuyruk yönetimi nedeniyle ekstra CPU yükü getirir.
2. Semafor İzolasyonu
Semafor yalıtımı, yeni iş parçacığı havuzları oluşturmak yerine, belirli bir hizmete izin verilen eşzamanlı çağrıların sayısını sınırlamak için bir sayaç (semafor) kullanır.
- Nasıl çalışır: Bir istek başladığında semafordan izin almaya çalışır. Bir izin mevcutsa, çağıran iş parçacığında isteği yürütür ve bittiğinde izni serbest bırakır. Herhangi bir izin mevcut değilse, talep derhal reddedilir.
- Avantajları: Hiçbir iş parçacığı içeriği değişimi söz konusu olmadığından neredeyse sıfır ek yük ile çok hafiftir.
- Eksileri: İplik ayrımı yok. Bir ağ soketinde bir çağrı uygun bir zaman aşımı olmadan engellenirse, arayan iş parçacığını yine de engelleyebilir.
Uygulama Örnekleri
İki popüler arka uç dilinde bölmelerin nasıl uygulanacağına bakalım.
1. Java (Resilience4j ve Spring Boot)
Resilience4j, Java için tasarlanmış hafif, kullanımı kolay bir hata toleransı kütüphanesidir. Aşağıda, Spring Boot uygulamasında bir alt ödeme hizmeti için bölmeyi nasıl yapılandıracağınız açıklanmaktadır.
Yapılandırma (application.yml)
resilience4j.bulkhead:
instances:
paymentService:
maxConcurrentCalls: 10
maxWaitDuration: 10ms
resilience4j.threadpoolbulkhead:
instances:
paymentService:
maxThreadPoolSize: 10
coreThreadPoolSize: 5
queueCapacity: 20
Kod Uygulaması
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
private final RestTemplate restTemplate;
public OrderService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// Apply semaphore bulkhead
@Bulkhead(name = "paymentService", fallbackMethod = "paymentFallback")
public String processPayment(OrderDetails details) {
return restTemplate.postForObject("http://payment-service/charge", details, String.class);
}
// Fallback method executed when the bulkhead is full
public String paymentFallback(OrderDetails details, Throwable throwable) {
return "Payment service is currently busy. Please try again later.";
}
}
2. Git (Golang)
Go’da ağır bir çerçeveye ihtiyacımız yok çünkü dil, Goroutine’ler ve ara belleğe alınmış kanallar gibi yerel eşzamanlılık ilkelleri sağlıyor. Tamponlu bir kanal kullanarak temiz bir semafor bölmesi uygulayabiliriz:
package main
import (
"errors"
"fmt"
"net/http"
"time"
)
// Bulkhead represents a concurrency limiter
type Bulkhead struct {
semaphore chan struct{}
}
// NewBulkhead initializes a bulkhead with a max concurrency limit
func NewBulkhead(maxConcurrency int) *Bulkhead {
return &Bulkhead{
semaphore: make(chan struct{}, maxConcurrency),
}
}
// Execute runs the task if resource permit is available, otherwise returns error
func (b *Bulkhead) Execute(task func() error) error {
select {
case b.semaphore <- struct{}{}:
// Acquired permit
defer func() { <-b.semaphore }() // Release permit
return task()
default:
// Bulkhead is full, reject immediately
return errors.New("bulkhead is full: request rejected")
}
}
func main() {
// Allow maximum of 3 concurrent calls
paymentBulkhead := NewBulkhead(3)
mockTask := func() error {
fmt.Println("Processing payment...")
time.Sleep(2 * time.Second) // Simulate network delay
return nil
}
// Simulate 5 rapid requests
for i := 1; i <= 5; i++ {
go func(reqID int) {
err := paymentBulkhead.Execute(mockTask)
if err != nil {
fmt.Printf("Request %d failed: %v\n", reqID, err)
} else {
fmt.Printf("Request %d completed successfully\n", reqID)
}
}(i)
}
// Keep main alive to watch output
time.Sleep(3 * time.Second)
}
Bölme Modeli için Yaygın Kullanım Durumları
Bölme modelinin uygulanmasının kritik olduğu bazı tipik senaryolar şunlardır:
- API Ağ Geçidi Yönlendirme: Farklı arka uç hizmetleri için yolları ayırma. Tavsiye Hizmetinin devre dışı kalması durumunda Ağ Geçidi üzerindeki Sipariş Hizmeti rotaları tamamen çalışır durumda kalır.
- Veritabanı Bağlantı Havuzları: Veritabanı bağlantı havuzlarını hizmete veya kiracıya göre bölme. Bir kiracıdan gelen ağır analitik sorguların artması, mevcut tüm bağlantı tanıtıcılarını tüketmez ve diğer kiracılar için işlem sorgularından tasarruf sağlar.
- Çok kiracılı SaaS Uygulamaları: Premium ve ücretsiz kiracılar için bilgi işlem kaynaklarını veya yürütme kuyruklarını ayırma. Ücretsiz katmandaki kaynak ani artışları, CPU veya bellekteki premium katman isteklerini ortadan kaldırmaz.
- Üçüncü Taraf API Entegrasyonları: Harici ödeme ağ geçitleri, gönderim sağlayıcıları veya bildirim motorları için ayrı HTTP istemci havuzları ayırma. Bir üçüncü taraf hizmeti yavaşlarsa diğer harici etkileşimler herhangi bir kesinti olmadan devam eder.
Kafka/Mesaj Aracıları Neden Bölme Desenini Değiştiremez?
Sık sorulan bir soru şudur: “Apache Kafka gibi mesaj aracılarımız varsa neden Bulkhead modeline ihtiyacımız var? İstekleri arabelleğe almak için kuyrukları kullanamaz mıyız?”
Mesaj aracıları sistemleri ayırırken Bölme modelinin yerini alamazlar. İşte nedeni:
1. Senkron ve Asenkron İletişim
Kafka eşzamansız, olaya dayalı mimariler için tasarlanmıştır. Üretici bir konuya bir mesaj aktarır ve tüketici sonunda onu işler. Ancak, kullanıcıya yönelik uygulamalar genellikle senkron (istek-yanıt) iletişim gerektirir (örneğin, bir ürün kataloğunun yüklenmesi veya bir REST/gRPC API aracılığıyla kredi kartından ödeme alınması). Kafka’yı burada tanıtmak, yüksek gecikme ve ek yük getiren karmaşık istek-yanıt kalıpları gerektirir. Bölmeler, bu senkronize yürütme iş parçacıklarını gerçek zamanlı olarak korumak için özel olarak tasarlanmıştır.
2. Kafka Tüketicilerinin İçindeki Konu Açlığı
Sisteminiz tamamen olay odaklı olsa ve Kafka kullanıyor olsa bile yine de bölmelere ihtiyacınız var!
Tek bir tüketici mikro hizmetinin birden çok Kafka konusunu dinlediğini varsayalım (ör. user-registrations ve video-transcoding). Tüketici, tüm dahili çalışan iş parçacıklarını büyük miktarda yavaş video-transcoding işleri işlemek için tahsis ederse iş parçacığı açlığı yaşayacaktır. Tüketici, bu bölüm sağlıklı olsa bile hafif user-registrations iletilerini işleyemeyecektir. İşi izole etmek için tüketici hizmeti içinde hala dahili bölmelere (ayrı iş parçacığı havuzları) ihtiyacınız var.
3. İstemci Tarafı Ek Yükü ve Hızlı Arıza Gereksinimi
Aşağı akışlı bir hizmet kapalı olduğunda, bir bölme, çağrı hizmetinin hızlı bir şekilde arızalanmasına ve anında bir geri dönüş yanıtı vermesine olanak tanır. Bunun yerine her şeyi Kafka’da sıraya koyarsanız, sıra sonsuza kadar büyüyebilir, bu da eski isteklere, yüksek bellek tüketimine ve sistem kurtarıldığında zaman aşımlarının gecikmesine neden olabilir.
Kısacası, Kafka ağ üzerinden sistemler arasındaki iletişimi ayırır, Bölmeler ise çalışan bir uygulama örneğinde kaynak yürütmeyi izole eder. Birbirini dışlayan değil, tamamlayıcıdırlar.
Bölmeleri Kullanırken En İyi Uygulamalar
- Zaman Aşımlarını Her Zaman Ayarla: Bölme eşzamanlılığı sınırlar, ancak yavaş soket okumalarını çözmez. İş parçacıklarını olabildiğince hızlı serbest bırakmak için bölmeleri katı ağ zaman aşımlarıyla birleştirin.
- Devre Kesicilerle Birleştirin: Devre kesicilerin yanında bölmeleri kullanın. Bir bölme, istekleri sürekli olarak reddetmeye başlarsa, devre kesicinin trafiği tamamen durdurmak için açma yapması ve aşağı yöndeki servis odasına iyileşmesi için izin vermesi gerekir.
- Havuz Doygunluğunu İzleyin: Bölme kuyruğu uzunluklarınıza ve etkin iş parçacığı sayılarınıza ilişkin uyarılar uygulayın. Bölme sürekli olarak doluysa altyapınızı ölçeklendirmeniz veya aşağı yöndeki hizmeti optimize etmeniz gerekebilir.
- Boyutları Ayrı Ayrı Ayarlayın: Herkese uyan tek bir sınır kullanmayın. Doğru bölme sınırlarını belirlemek için her bağımlılığın gecikmesini ve istek oranını ölçün.
Çözüm
Bölme Modeli, dayanıklı, bulut ölçeğinde sistemler oluşturmak için temel bir tasarım modelidir. Kaynaklarınızı bölümlendirerek arızaları izole eder, kademeli etkileri önler ve yerelleştirilmiş bir hatanın küresel bir kesintiye dönüşmemesini sağlarsınız.