Leader Election: Bu İşi Kim Yapacak?
Streaming-engine'in dört kopyası çalışıyor. Bridge’lerin sağlık kontrolünü (health check) dördü birden yapıyor. Aynı köprüye saniyede dört ping, aynı alarma dört bildirim, aynı yeniden bağlanma dört kez. Ölçekledik ama işi bölmedik.
- Bazı işler tam olarak bir kere yapılmalı. Hepsi yaparsa gürültü, hiçbiri yapmazsa ölü köprü fark edilmez.
- Liderlik süresiz verilmez, kiralanır. Lider yenilemeyi bırakırsa süre dolar, yerine biri geçer.
- İki liderin aynı anda olması engellenemez, sadece zararsız hale getirilir. Cevabı fencing token.
- Çoğu ekip bunu kendi yazmamalı. Kubernetes Lease, etcd veya Postgres zaten var. Ama fencing token her yerde senin işin.
Problem nereden çıkıyor?
Bir servisi yatayda büyütmek genelde bedava gelir: iki kopya iki kat iş yapar. Ama her iş bölünebilir değil. Bazı işler tam olarak bir kere yapılmalı ve kopya sayısı arttıkça bu işler sessizce bozulur.
Trade tarafından tanıdık gelecek örnekler:
| İş | Hepsi yaparsa | Hiçbiri yapmazsa |
|---|---|---|
| Bridge sağlık kontrolü | Köprüye 4 kat ping, 4 kat alarm, gereksiz yük | Ölü köprü fark edilmez, emirler boşluğa gider |
| Gün sonu mutabakat işi | Aynı rapor 4 kez üretilir, 4 kez mail gider | Mutabakat hiç yapılmaz |
| Sembol listesini dış kaynaktan çekme | Sağlayıcının hız sınırına takılırsın | Fiyat listesi eskir |
| Zaman aşımına (timeout) uğramış emirleri temizleme | Aynı emri 4 kopya iptal etmeye çalışır | Emirler askıda kalır |
Sağ sütun ilk bakışta daha korkutucu görünüyor ama sol sütun daha sinsi: hata vermez, sadece pahalıya patlar. Sağlık kontrolünün dört kopyadan gitmesi ilk gün fark edilmez; köprü sağlayıcısı hız sınırı uygulamaya başladığında fark edilir.
Kolay ama yanlış üç çözüm
1. “Sadece bir kopya çalıştıralım”
İşe yarar — o kopya ölene kadar. Pod çökerse Kubernetes yeni bir tane açar, arada 20–60 saniye geçer. Node çökerse daha kötü: varsayılan eviction eşikleri yüzünden pod’un taşınması 5 dakikayı bulur. O sürede kimse sağlık kontrolü yapmaz. Kabul edilebilir mi? Bazı işler için evet. Bridge health check için hayır.
2. “Ortam değişkeniyle işaretleyelim”
if os.getenv("IS_LEADER") == "true":
health_check_dongusunu_baslat()
Basit ve çoğu zaman çalışıyor. Sorun şu: o kopya ölünce liderlik ölür.
Kubernetes yeni pod açar ama yeni pod’un değişkeni false’tur.
Kimse haber vermez; bunu gece 3’te, ölü köprü yüzünden emirler gitmeyince
öğrenirsin.
3. “İlk gelen alsın, Redis’e bayrak koyalım”
Doğru yöne gidiyorsun ama yarısı eksik. SETNX lider 1 ile bayrak koyup
süre vermezsen, süreç çöktüğünde bayrak sonsuza kadar kalır ve
kimse lider olamaz. Bunun tamamı
dağıtık kilit (distributed lock) yazısında anlatılıyor; buradaki
fark, liderliğin bir kilit değil süreli bir kira olması.
Doğru model: lease (liderlik kiralanır)
Lease mantığı şu: liderlik sonsuza kadar verilmez, belirli bir süreliğine kiralanır. Lider bu kirayı düzenli olarak yeniler. Yenileyemezse — çöktüğü, ağdan koptuğu ya da takıldığı için — süre dolar ve başkası devralır.
CREATE TABLE liderlik (
is_adi text PRIMARY KEY,
sahip text NOT NULL,
gecerlilik timestamptz NOT NULL,
donem bigint NOT NULL -- ← fencing token
);
-- Her kopya 5 saniyede bir bunu calistirir.
-- :ben = pod adi + surec basinda uretilen UUID (sadece pod adi DEGIL)
INSERT INTO liderlik (is_adi, sahip, gecerlilik, donem)
VALUES ('bridge_health', :ben, now() + interval '15 seconds', 1)
ON CONFLICT (is_adi) DO UPDATE
SET sahip = :ben,
gecerlilik = now() + interval '15 seconds',
-- donem: yenilemede sabit, lider degisince YA DA sure dolmussa artar
donem = liderlik.donem
+ CASE WHEN liderlik.sahip = :ben
AND liderlik.gecerlilik >= now() THEN 0 ELSE 1 END
WHERE liderlik.sahip = :ben -- ya zaten benim (yenileme)
OR liderlik.gecerlilik < now() -- ya da suresi dolmus (devralma)
RETURNING donem;
-- Satir donduyse: lider benim, donen donem'i fencing token olarak sakla.
-- Satir donmediyse: baskasi lider, sen bekle.
Yirmi satır ve ek bir altyapı yok. Dikkat edilecek beş detay var:
- Kira süresi, yenileme aralığının en az 3 katı olmalı. Yukarıda 5 saniyede bir yenileme, 15 saniyelik kira. İki yenileme kaçarsa hâlâ liderim; üçüncüde düşerim. Bu oran daha dar olursa tek bir ağ gecikmesi liderliği boşuna el değiştirir; buna flapping denir.
-
Devralma süre dolmadan yapılamaz.
WHEREsatırındaki iki koşul bunu garanti ediyor. -
:bensüreç kimliği olmalı, pod adı değil. StatefulSet’te hostname sabittir; aynı adla restart eden yeni süreç eski dönemi devralır ve fencing boşa çıkar. Kimliğe süreç başında üretilen bir UUID ekle. - Süresi dolmuş lease’i eski sahibi yenilese bile dönem artar. Süre dolduysa arada başkası lider olmuş olabilir; kimin yenilediğine bakmadan dönemi artırmak daha güvenli. Sadece süresi dolmamış yenileme dönemi sabit tutar.
-
donemalanı boşuna değil. Bir sonraki bölümün konusu.
Split-brain: iki lider aynı anda
Şimdi kötü haber. Yukarıdaki kod doğru ama iki kopyanın aynı anda kendini lider sanmasını engellemiyor. Ve hiçbir kod engelleyemez. Sebep:
| Zaman | Kopya A | Kopya B | Gerçek durum |
|---|---|---|---|
| 00:00 | lider oldu, dönem 7 | bekliyor | lider A |
| 00:05 | yeniledi | bekliyor | lider A |
| 00:07 | GC duraklaması başladı | bekliyor | lider A (ama uyuyor) |
| 00:20 | hâlâ duraklamada | süre doldu, lider oldu, dönem 8 | lider B |
| 00:22 | uyandı, “ben liderim” sanıyor | çalışıyor | iki lider! |
A yanlış bir şey yapmadı; sadece uyudu ve uyandığında dünyanın değiştiğini bilmiyor. Bu duruma split-brain deniyor ve dağıtık sistemlerde kaçınılmaz.
Çözüm: fencing token (dönem numarası)
Her liderlik devrinde artan bir numara üret ve korunan kaynağa bu numarayı yazdır. Kaynak, gördüğünden daha küçük numaralı isteği reddetsin:
-- A (donem 7) uyanip bridge durumunu guncellemeye calisiyor:
INSERT INTO bridge_durum (bridge_id, saglik, son_kontrol, donem)
VALUES ('LP-3', 'DOWN', now(), 7)
ON CONFLICT (bridge_id) DO UPDATE
SET saglik = EXCLUDED.saglik,
son_kontrol = EXCLUDED.son_kontrol,
donem = EXCLUDED.donem
WHERE bridge_durum.donem <= EXCLUDED.donem; -- ← tabloda 8 yaziyor, kosul tutmaz
-- 0 satir etkilendi. A yazamadi.
-- Uygulama bunu gorup "artik lider degilim" diyip donguyu durduruyor.
Neden düz UPDATE değil de upsert? Çünkü düz UPDATE’te
“0 satır” iki farklı şey demek: ya fence’lendin ya da o bridge için
henüz satır yok. İlk açılışta lider kendini lider değil sanır ve durur. Upsert’te
satır yoksa INSERT çalışır; 0 satır artık sadece
“fence’lendin” demek.
Buradaki incelik: A’nın kendi liderliğini sorgulamasına gerek yok. Yazmaya kalkıyor, reddediliyor, anlıyor. Doğruluk zamana değil sıraya bağlanmış oluyor — dağıtık kilitteki fikrin aynısı.
Boşluk: yeni lider ilk kez yazana kadar koruma yok
Tabloya tekrar bak. B 00:20’de lider oldu ama bridge_durum.donem
hâlâ 7; çünkü B henüz hiçbir şey yazmadı. A 00:22’de uyanırsa donem <= 7
koşulu tutar ve bayat yazma geçer. Korunan kaynaktaki token, liderlik
devrinden sonra gecikmeyle güncelleniyor. Çözüm: liderliği alır almaz, iş yapmadan
önce korunan satırlardaki dönemi yükselt:
-- B lider oldu (donem 8). Health check dongusune girmeden ONCE:
UPDATE bridge_durum
SET donem = 8
WHERE donem < 8;
-- Artik A'nin donem 7 ile yazmasi hicbir satirda tutmaz.
Yan etkisi olmayan işlerde gerek yok
Her iş için fencing gerekmiyor. Ayrım şu: iş dışarıya bir etki bırakıyor mu?
- Salt okuma sağlık kontrolü (ping atıp loglamak)
- Önbellek ısıtma
- Metrik toplama
İki kopya yaparsa sadece israf olur.
- Emir iptal etme, pozisyon kapatma
- Bildirim/e-posta gönderme
- Mutabakat kaydı yazma
- Dış sisteme durum bildirme
İki kopya yaparsa para veya güven kaybı.
Bunu kendin yazma (çoğu zaman)
Yukarıdaki Postgres yöntemi öğretici ama gerçekte muhtemelen zaten elinde hazır bir araç var:
| Nerede çalışıyorsun | Kullan | Not |
|---|---|---|
| Kubernetes | coordination.k8s.io/Lease |
API server Lease’i süresi dolunca silmez, expire etmez;
renewTime + leaseDurationSeconds kontrolünü client-go’nun
leaderelection paketi yapar ve tek lider garantisi vermediğini
açıkça yazar. Fencing yine senin işin: spec.leaseTransitions
tam olarak buradaki dönem numarasıdır, onu token olarak taşı |
| Postgres var | Yukarıdaki tablo ya da pg_try_advisory_lock |
Advisory lock bağlantı kopunca kendiliğinden düşer ama fencing token üretmez. Session-level olduğu için PgBouncer transaction pooling arkasında çalışmaz |
| etcd / Consul / ZooKeeper | Yerleşik lider seçimi | En sağlamı; ama sırf bunun için kurulmaz |
| Sadece Redis var | SET ... NX PX + yenileme |
Çalışır, ama fencing token’ı sen eklemelisin |
Kendi Raft uygulamanı yazmak neredeyse hiçbir zaman doğru cevap değil. O kod doğru görünür, testlerden geçer ve altı ay sonra bir ağ bölünmesinde seni yalnız bırakır.
Sahadan: health check’e ne yaptık
Baştaki probleme dönelim. Dört streaming-engine, hepsi köprüleri yokluyor. Yaptığımız üç adım:
- Liderlik lease’e bağlandı. 5 saniye yenileme, 15 saniye kira. Ping trafiği dörtte bire indi.
- Durum yazımına dönem numarası eklendi. Bir kopya duraklamadan çıkıp “LP-3 çalışmıyor” yazmaya kalkarsa reddediliyor. Bu değişiklikten önce, uykudan uyanan bir kopyanın sağlıklı bir köprüyü ölü işaretleyip trafiği kesmesi yaşandı — en pahalı ders buydu.
- Liderlik metriğe döküldü. Her kopya “lider miyim” bilgisini metrik olarak yayıyor. Grafikte toplamın 1 olması gerekiyor. 0’a düşerse kimse yapmıyor, 2 olursa split-brain var. Tek bir grafik, iki farklı arızayı da gösteriyor. Kural “anlık 1” değil, “30 saniye boyunca 1 değilse alarm”: normal devirde de toplam birkaç saniye 0 olur (eski lider bıraktı, yeni lider henüz almadı), bu arıza değil. Ters yönde de dürüst olmak gerek: scrape aralığından kısa süren bir split-brain bu grafikte hiç görünmez. Onun sigortası grafik değil, fencing.
Liderliği bırakırken haber ver. Kopya düzgün kapanıyorsa (deploy sırasında) kiranın süresini geçmişe çeksin. Yoksa yeni lider 15 saniye boşuna bekler. Tek satırlık bir iş ve deploy sırasındaki boşluğu sıfırlıyor:
UPDATE liderlik
SET gecerlilik = now() - interval '1 second'
WHERE is_adi = 'bridge_health' AND sahip = :ben;
İlk sürümde burada DELETE vardı ve bu bir bug’dı: satır silinince
sıradaki aday ON CONFLICT yoluna girmiyor, düz INSERT yapıyor
ve donem 1’e dönüyor. bridge_durum’da ise hâlâ 8
yazıyor. Yeni liderin ilk yazımı donem <= 1 koşuluna takılıyor, 0 satır
dönüyor, uygulama “artık lider değilim” deyip duruyor. Her yeni liderde aynı
şey: deploy sonrası kimse sağlık kontrolü yapmıyor ve hiçbir yerde hata görünmüyor.
Tam olarak bu yazının uyardığı türden sessiz arıza.
Daha da sağlamı, dönemi tablodan bağımsız bir SEQUENCE’tan almak
(nextval('liderlik_donem')): tablo silinse ya da taşınsa bile numara
geri gitmez.
Kaç lider olmalı? Bir tane, ama hangi konuda?
Sık yapılan bir tasarım hatası: tek bir global lider seçip bütün özel işleri ona yükleme. Sonuç, yatayda büyüyen bir sistemin içinde tek bir dar boğaz oluşması.
Daha iyisi liderliği iş bazında vermek: sağlık kontrolünün lideri ayrı, mutabakatın lideri ayrı, sembol senkronunun lideri ayrı. Böylece yük dağılır ve bir işin liderinin takılması diğerlerini etkilemez.
Bunu bir adım öteye götürmek istersen zaten sharding konusuna girmiş olursun: liderlik “tek kopya yapsın” demek, sharding “her kopya kendi payını yapsın” demek. İkisi birbirinin rakibi değil; hangi işin bölünebildiğine göre seçiyorsun.
Kontrol listesi
- Bu iş gerçekten tek kopya mı istiyor, yoksa bölünebilir mi?
- Liderlik süreli mi (lease), yoksa süresiz bir bayrak mı?
- Kira süresi, yenileme aralığının en az 3 katı mı?
- Lider çökerse kaç saniye içinde yerine geçiliyor? Bu süre kabul edilebilir mi?
- İş dışarıya etki bırakıyorsa fencing token var mı? Yeni lider iş yapmadan önce korunan satırlardaki dönemi yükseltiyor mu?
:benkimliği süreç bazında mı (UUID), yoksa restart’ta aynı kalan pod adı mı?- Düzgün kapanışta liderlik bırakılıyor mu? Bırakırken dönem korunuyor mu?
- “Kaç lider var” metriği var mı? Alarm “30 saniye boyunca 1 değil” mi, yoksa her deploy’da çalıyor mu?
- Liderlik iş bazında mı, yoksa tek global lider mi?
Sonuç
Leader election kulağa akademik geliyor ama pratik karşılığı çok basit bir soruya dayanıyor: bu işi kim yapacak? Cevabı “hepsi” olursa gürültü, “hiçbiri” olursa sessiz bir arıza çıkıyor.
Ve akılda kalacak tek şey şu olsun: liderlik bir mülk değil, bir kiradır. Süresi dolar, el değiştirir ve eski sahibi bunu bilmeyebilir. Sistemin sağlamlığı, eski sahibin geç uyandığında ne yapabildiğiyle ölçülür.