← blog · 21 Ekim 2026

Okuma Replikası Gecikmesi: Kullanıcının Kendi Yazdığını Görmesi, Yapışkan Oturum ve Günlük Konumu

Okumaları replikaya taşıyınca kullanıcı kaydettiği değeri göremez. Gecikmenin neden sıfır olmadığı, yapışkan oturum, WAL konumu takibi ve senkron uygulama beklemesi arasında seçim, önbellek, arka plan işi ve yük devri tuzakları.

Okuma yükünü hafifletmek için veritabanına bir okuma replikası eklemek en ucuz ölçeklendirme adımlarından biridir. Uygulama yazmaları birincil sunucuya, okumaları replikaya gönderir ve birincil rahatlar. Sorun ilk gün görünmez: kullanıcı profilini günceller, kaydet düğmesine basar, sayfa yenilenir ve eski adı görür. Bir iki saniye sonra yenilediğinde yeni değer gelir. Destek ekibine "kaydetmiyor" diye bilet açılır, geliştirici kendi makinesinde tekrar üretemez, çünkü yerelde replika yoktur. Bu yazı bu gecikmenin neden kaçınılmaz olduğunu, kendi yazdığını okuma (read-your-writes) tutarlılığını sağlamanın yollarını ve hangisinin nerede doğru olduğunu anlatıyor.

Gecikme neden hiç sıfır olmaz

Akışlı replikasyonda birincil sunucu değişiklikleri önce kendi günlüğüne (PostgreSQL'de WAL) yazar, commit'i onaylar, sonra bu günlük replikaya akar ve orada yeniden oynatılır. Varsayılan asenkron kurulumda commit onayı replikayı beklemez. Yani her yazmanın ardından, en iyi durumda bile milisaniyeler süren bir pencere vardır ve o pencerede replika eski veriyi döner.

Bu pencere sakin saatlerde fark edilmeyecek kadar küçüktür, yük altında ise büyür. Replikada uzun süren bir rapor sorgusu, büyük bir toplu güncelleme, ağdaki bir dalgalanma ya da replikanın disk G/Ç sınırına dayanması gecikmeyi saniyelere, kötü günlerde dakikalara çıkarır. Bu yüzden "bizde gecikme hep birkaç milisaniye" ölçümü tasarım kararına dayanak olamaz. Tasarımı en kötü gecikmeye göre yapın.

Yaklaşım 1: Yazmadan sonra bir süre birincile yapış

En yaygın ve en basit çözüm şudur: bir istek ya da oturum yazma yaptıysa, o istekteki ve ardından gelen kısa bir süre içindeki okumalar da birincilden yapılır. Bazı ORM'lerde bu hazır gelir; örneğin Laravel'in veritabanı bağlantı ayarındaki sticky seçeneği, aynı istek içinde yazma yapıldıktan sonraki okumaları birincile yönlendirir.

İstek sınırının ötesine taşımak için oturuma ya da bir çereze "son yazma zamanı" bilgisi koyarsınız. Okuma katmanı bu zamandan beri belirli bir süre geçmediyse birincili seçer.

Bu yaklaşımın güçlü yanı uygulamanın kolay olmasıdır. Zayıf yanı süreyi tahminle seçmenizdir. Beş saniye derseniz ve gecikme bir gün sekiz saniyeye çıkarsa hata geri döner. Süreyi uzun tutarsanız yoğun yazan kullanıcılar neredeyse hiç replikaya gitmez ve kazancın bir kısmı kaybolur. Yine de çoğu web uygulaması için doğru başlangıç budur.

Yaklaşım 2: Günlük konumunu taşı ve replikanın yetişmesini bekle

Daha kesin olan yöntem zamana değil günlük konumuna bakar. Yazma işlemi bittikten sonra birincilin o anki WAL konumunu alırsınız:

SELECT pg_current_wal_lsn();

Bu değeri istemciye ya da oturuma bağlarsınız. Sonraki okumada replikaya bu konuma kadar oynatıp oynatmadığını sorarsınız:

SELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), '0/3A2B1C8'::pg_lsn) >= 0 AS yetisti;

Sonuç doğruysa okuma replikadan güvenle yapılır. Değilse ya kısa bir süre bekleyip tekrar sorarsınız ya da o okumayı birincile yönlendirirsiniz. MySQL tarafında aynı fikir GTID ile kurulur: yazmadan sonra yürütülmüş GTID kümesini alırsınız, replikada WAIT_FOR_EXECUTED_GTID_SET ile zaman aşımlı bekleme yaparsınız.

Bu yöntem gecikme ne kadar olursa olsun doğrudur ve gereksiz yere birincile gitmez. Bedeli karmaşıklıktır: konumu bir yerde taşımanız, birden fazla replika varsa hangisinin yetiştiğine bakmanız ve beklemenin bir üst sınırını koymanız gerekir. Bekleme sınırı koymazsanız gecikmeli bir replika yüzünden isteğiniz asılı kalır. Doğru davranış genelde birkaç yüz milisaniye bekleyip sonra birincile düşmektir.

Yaklaşım 3: Commit'i replikanın uygulamasına bağla

PostgreSQL'de senkron replikasyon ile synchronous_commit ayarını remote_apply yaparsanız, commit onayı senkron replika değişikliği uygulayana kadar gelmez. O andan sonra o replikadan yapılan okuma yeni veriyi görür. Bu ayar işlem bazında da verilebilir, dolayısıyla yalnızca kritik yazmalarda kullanabilirsiniz:

BEGIN;
SET LOCAL synchronous_commit = remote_apply;
UPDATE hesaplar SET ad = 'Yeni Ad' WHERE id = 42;
COMMIT;

Burada iki şeye dikkat edin. Birincisi, bu ayarın etkisi için synchronous_standby_names ile senkron bir replika tanımlanmış olmalıdır; tanımlı değilse ayar sessizce yerel commit gibi davranır ve siz korunduğunuzu sanırsınız. İkincisi, her yazmanın gecikmesi artık replikanın uygulama süresine bağlıdır ve senkron replika düşerse yazmalar bekler. Bu yöntemi genel çözüm olarak değil, az sayıda ama kesinlikle tutarlı olması gereken yazma için düşünün.

Hangisini seçmeli

Çoğu uygulama için önerim yapışkan oturumla başlamak ve süreyi gözlediğiniz en kötü gecikmenin birkaç katı tutmaktır. Ölçtüğünüzde birincil hâlâ fazla yükleniyorsa ya da gecikmenin uç değerleri yapışma süresini aşıyorsa günlük konumu yöntemine geçin. Senkron uygulama beklemesini ise ödeme durumu, yetki değişikliği gibi eski okumanın gerçekten zarar verdiği birkaç akışa saklayın.

Bir istisna: okumaların küçük bir kısmı gerçekten ağırsa, her şeyi replikaya yönlendirmek yerine yalnız o ağır okumaları (raporlar, arama, dışa aktarma) replikaya göndermek çoğu zaman daha iyi sonuç verir. Bu sorguların birkaç saniyelik eskiliğe tahammülü vardır ve kendi yazdığını okuma sorunu hiç doğmaz.

Tuzaklar

Gecikmeyi yanlış ölçmek. Replikada now() - pg_last_xact_replay_timestamp() ifadesi sık kullanılır, ama birincilde yazma yoksa bu değer de sürekli büyür ve gece boyu sahte alarm üretir. Birincildeki pg_stat_replication görünümünün replay_lag sütunu ya da iki taraftaki WAL konumlarının farkı daha güvenilirdir.

Önbelleği replikadan doldurmak. Yazmadan sonra önbelleği silip bir sonraki okumada doldururken, o okuma replikaya giderse eski veri önbelleğe girer ve artık replikayı değil önbelleğin süresini beklersiniz. Önbelleği dolduran okumayı birincilden yapın ya da günlük konumunu kontrol edin.

Arka plan işleri. Bir kayıt oluşturup hemen kuyruğa iş atarsınız, işçi birkaç milisaniye içinde işi alır ve kaydı replikada arar. Bulamaz, "kayıt yok" diye işi düşürür. İşçinin, kendisini tetikleyen yazmadan sonraki ilk okumayı birincilden yapmasını sağlayın ya da işe günlük konumunu ekleyin.

Replikada iptal edilen sorgular. Uzun sorgular replikada, birincilden gelen temizlik kayıtlarıyla çakışınca "conflict with recovery" hatasıyla iptal edilir. hot_standby_feedback açmak iptalleri azaltır ama birincilde tablo şişmesine yol açabilir. max_standby_streaming_delay değerini büyütmek de iptali azaltır, karşılığında gecikmeyi büyütür. Hangisini seçtiğinizi bilerek seçin.

Bağlantı havuzlayıcısı ve işlemler. Okuma ve yazmayı aynı işlem içinde farklı sunuculara bölemezsiniz. Bir işlem başladıysa içindeki her sorgu birincilde kalmalıdır, yoksa işlemin kendi yazdığını göremediği garip hatalar çıkar.

Yük devri. Birincil düşüp bir replika terfi ettiğinde, asenkron kurulumda son commit'lerin bir kısmı kaybolabilir. Günlük konumuna bakan mantığınız, yeni birincilin daha geride bir konumdan başladığı durumu tanımalı; yoksa hiç gelmeyecek bir konumu beklemeye devam eder.

Ne zaman replikaya hiç gitmemeli

Birincil sunucunuz okuma yüküyle henüz zorlanmıyorsa replika eklemek size ölçeklendirme değil, bir tutarlılık problemi kazandırır. Önce yavaş sorguları, indeksleri ve uygulama önbelleğini ele alın. Replikayı yüksek erişilebilirlik için kuruyorsanız ve okuma için kullanmanız şart değilse, uygulamayı ona yönlendirmeyin; yalnızca yük devri hedefi olarak bekletin. Okumayı replikaya taşımak bir mimari karardır ve her okuma yolunun "birkaç saniye eski veri burada kabul edilir mi" sorusuna cevap vermesini gerektirir. Bu soruyu sormadan yapılan yönlendirme, yukarıdaki biletlerin kaynağıdır.