Sertaç Yıldırım saha notları

Ana sayfa → Teknik

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.

Özet
  • 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 yaparsaHiç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”

Sık görülen
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.

Postgres ile en basit hali
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. WHERE satırındaki iki koşul bunu garanti ediyor.
  • :ben sü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.
  • donem alanı 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:

ZamanKopya AKopya BGerçek durum
00:00lider oldu, dönem 7bekliyorlider A
00:05yeniledibekliyorlider A
00:07GC duraklaması başladıbekliyorlider A (ama uyuyor)
00:20hâlâ duraklamadasüre doldu, lider oldu, dönem 8lider B
00:22uyandı, “ben liderim” sanıyorçalışıyoriki 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.

İki liderin oluşmasını engelleyemezsin. Engelleyebileceğin şey, ikinci liderin zarar vermesi.

Çö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:

Eski lider yazamıyor
-- 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:

Devralmanın ilk işi
-- 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?

Fencing gerekmez
  • Salt okuma sağlık kontrolü (ping atıp loglamak)
  • Önbellek ısıtma
  • Metrik toplama

İki kopya yaparsa sadece israf olur.

Fencing şart
  • 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ışıyorsunKullanNot
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:

  1. Liderlik lease’e bağlandı. 5 saniye yenileme, 15 saniye kira. Ping trafiği dörtte bire indi.
  2. 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.
  3. 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.
Küçük ama kritik detay

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

Leader election kurarken
  • 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?
  • :ben kimliğ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.