Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Kenapa read replica mengembalikan data lama?
Replica menerima dan me-replay write-ahead log dari primary secara asinkron. Primary mengonfirmasi commit sebelum replica menerapkannya, jadi untuk jeda singkat replica masih memegang row lama. Jeda itulah yang disebut replication lag.
02Apa itu read-your-writes consistency?
Itu jaminan bahwa setelah sebuah proses menulis, read berikutnya mencerminkan write tersebut. Dengan read replica hal ini tidak otomatis, jadi Anda menyediakannya dengan mengarahkan read user ke primary untuk jangka pendek setelah ia menulis, atau menunggu sampai replica me-replay LSN write itu.
03Bagaimana cara mengukur replication lag di Postgres?
Di primary, query pg_stat_replication untuk replay_lag, atau pakai pg_wal_lsn_diff antara pg_current_wal_lsn() dan replay_lsn untuk hasil dalam byte. Di replica, now() dikurangi pg_last_xact_replay_timestamp() memberi lag semu. Angka terakhir itu membesar pada primary yang sepi meski replica sudah menyusul.
04Haruskah saya membaca dari primary setelah setiap write?
Hanya untuk user yang baru menulis, dan hanya untuk jendela singkat atau sampai replica mencapai posisi write-nya. Mengirim semua read ke primary menggagalkan tujuan replica. Pin singkat per user atau per record menjaga sebagian besar read tetap di replica.
Ia membuat commit menunggu sampai standby sinkron menerapkan transaksi, sehingga standby itu bisa melayaninya. Harganya delay commit yang jauh lebih besar, dan hanya berlaku jika synchronous_standby_names diatur. Pakai untuk beberapa transaksi kritis, bukan sebagai default global.
Read Replica Mengembalikan Data Lama: Atasi Replication Lag
Kenapa read replica menampilkan data lama tepat setelah write, cara mengukur lag di Postgres, dan cara me-routing read agar user selalu melihat perubahannya.
Read replica mengembalikan data lama karena replica menerapkan write-ahead log dari primary secara asinkron, sehingga selalu tertinggal sejauh lag tertentu. Atasi dengan mengukur lag lewat pg_stat_replication, mengarahkan read setelah write user sendiri ke primary untuk jangka pendek, dan menjaga tiap sesi tetap di satu replica.
Bayangkan layar POS. Kasir menyimpan tiket cuci, aplikasi redirect ke halaman tiket, lalu halaman bilang tiketnya tidak ada. Dua detik kemudian di-refresh dan tiketnya muncul. Tidak ada data yang hilang; write masuk ke primary, sedangkan read dilayani replica yang belum menyusul.
Post ini membahas celah itu: kenapa ada, seperti apa gejalanya bagi user, cara mengukurnya di Postgres, dan tiga solusi dari yang termurah. Angka pada contoh hitungan adalah ilustrasi aritmetika, dan setiap klaim perilaku Postgres berasal dari dokumentasi resmi yang tercantum di akhir.
Kenapa read replica mengembalikan data lama?
Streaming replica adalah server kedua yang menerima write-ahead log (WAL) dari primary lalu me-replay-nya. Secara default primary tidak menunggu proses itu selesai sebelum memberi tahu client bahwa commit berhasil, jadi selalu ada jeda di mana primary sudah punya row-nya tetapi replica belum. Postgres melaporkan jeda itu dalam tiga tahap, satu per kolom di pg_stat_replication.
Kolom
Yang diukur
Level synchronous_commit yang sepadan
write_lag
WAL sudah diterima dan ditulis oleh standby, belum di-flush atau diterapkan
remote_write
flush_lag
WAL sudah ditulis dan di-flush ke penyimpanan durable di standby, belum diterapkan
on
replay_lag
WAL sudah diterapkan, sehingga perubahan terlihat oleh query di standby
remote_apply
Hanya tahap terakhir yang penting bagi user, karena query tidak bisa melihat perubahan sebelum di-replay. Replica bisa saja sudah menyimpan WAL di disk tetapi tetap mengembalikan row lama. Replay juga bisa tertunda oleh standby sendiri: pada hot standby, query yang berbenturan dengan WAL masuk dapat menahan replay hingga max_standby_streaming_delay, yang default-nya 30 detik.
Seperti apa replication lag bagi user?
Lag muncul sebagai dua bug berbeda yang butuh solusi berbeda. User yang menulis lalu membaca bisa tidak melihat perubahannya sendiri. User yang membaca dua kali bisa melihat data mundur, karena kedua read dilayani replica di posisi berbeda.
Gejala
Jaminan yang dilanggar
Solusi
Record sudah disimpan, halaman berikutnya bilang tidak ada
Read-your-writes
Arahkan read ke primary setelah write
Row muncul di satu refresh lalu hilang di refresh berikutnya
Monotonic reads
Jaga tiap sesi di satu replica
Total di dashboard tertinggal beberapa detik
Tidak ada, ini eventual consistency biasa
Terima saja, dan tampilkan waktu as-of
Contoh hitungan. User commit pada t = 0. Aplikasi redirect dan request berikutnya tiba pada t = 120 ms. Jika replay lag replica 800 ms, read itu berjalan 800 dikurangi 120 = 680 ms terlalu cepat dan tidak menemukan row. Sekarang tambah replica kedua dengan lag 50 ms. Request pada t = 500 ms masuk ke replica cepat dan melihat row; request pada t = 600 ms masuk ke replica lambat, yang baru menerapkan commit pada t = 800 ms, sehingga row menghilang. Read-your-writes berarti sebuah proses melihat write-nya sendiri sebelumnya; monotonic reads berarti read berikutnya tidak pernah menampilkan state lebih lama dari read sebelumnya. Keduanya didefinisikan per proses, itulah sebabnya solusi per sesi berhasil.
Bagaimana cara mengukur replication lag di Postgres?
Ukur dari kedua sisi. Primary tahu sejauh mana tiap standby sudah maju, dan replica tahu seberapa tua commit terbaru yang sudah di-replay. Query pertama di bawah melaporkan lag dalam byte dan waktu per standby; yang kedua bisa dijalankan saat Anda hanya punya akses ke replica; yang ketiga memeriksa apakah replica sudah me-replay posisi tertentu.
-- On the PRIMARY: one row per connected standby.
SELECT application_name,
state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
write_lag,
flush_lag,
replay_lag -- what a reader on that standby actually feels
FROM pg_stat_replication;
-- On the REPLICA: how old is the newest commit I can see?
SELECT now() - pg_last_xact_replay_timestamp() AS apparent_lag;
-- On the REPLICA: has it replayed at least the LSN I was handed after a write?
SELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), '0/3000148') >= 0 AS caught_up;
pg_wal_lsn_diff mengurangkan dua lokasi WAL dan mengembalikan byte, yang oleh dokumentasi dirujuk untuk menghitung lag. pg_last_xact_replay_timestamp mengembalikan waktu transaksi terakhir yang di-replay di-commit pada primary, jadi mengurangkannya dari now() memberi lag semu. Query ketiga adalah blok dasar untuk pengecekan tepat setelah write: ambil pg_current_wal_lsn() di primary setelah commit, lalu tanyakan ke replica apakah posisi replay-nya sudah mencapainya.
now() dikurangi pg_last_xact_replay_timestamp() hanya jujur selama primary sedang menulis. Pada primary yang sepi, commit terakhir yang di-replay terus menua, sehingga angkanya naik padahal replica sudah menyusul penuh. Demikian pula pg_stat_replication menampilkan waktu replay WAL terbaru, bukan nol, saat sudah menyusul, lalu NULL setelah beberapa saat. Perlakukan NULL sebagai tidak ada pengukuran terbaru, bukan lag nol.
Bagaimana cara mengarahkan read ke primary setelah write?
Solusi termurah yang berhasil adalah pin singkat. Setelah write apa pun untuk seorang user, ingat fakta itu beberapa detik, dan selama masih aktif arahkan read user itu ke primary. Service NestJS di bawah melakukannya dengan key Redis yang kedaluwarsa sendiri, dan memilih replica untuk semua orang lainnya. Durasi jendelanya adalah keputusan desain: 5 detik di sini berasal dari batas alert 2 detik, ditambah polling lag 1 detik, ditambah margin.
import { Injectable } from "@nestjs/common";
import { Pool } from "pg";
import Redis from "ioredis";
// Wrong: send every SELECT to the replica and hope the lag is small.
// Right: a user who just wrote is pinned to the primary for a short window.
const PIN_SECONDS = 5; // alert threshold 2 s + poll interval 1 s + margin
@Injectable()
export class DbRouter {
constructor(
private readonly primary: Pool,
private readonly replicas: Pool[],
private readonly redis: Redis,
) {}
// Call this after any committed write made on behalf of a user.
async markWritten(userId: string): Promise<void> {
await this.redis.set("ryw:" + userId, "1", "EX", PIN_SECONDS);
}
async poolFor(userId: string, sessionId: string): Promise<Pool> {
if (await this.redis.exists("ryw:" + userId)) return this.primary;
return this.pickReplica(sessionId);
}
// Same session, same replica: this is what keeps reads monotonic.
private pickReplica(sessionId: string): Pool {
let h = 0;
for (const c of sessionId) h = (h * 31 + c.charCodeAt(0)) >>> 0;
return this.replicas[h % this.replicas.length];
}
}
Pin menukar beban primary dengan kebenaran, dan hanya untuk user yang baru menulis, jadi primary menanggung sebagian kecil read, bukan semuanya. Jika jendela pin ternyata terlalu pendek, alternatif yang tepat adalah pengecekan LSN dari bagian sebelumnya: simpan LSN yang dikembalikan setelah commit, bukan boolean, dan baca dari replica hanya saat posisi replay-nya sudah mencapainya. Itu menghapus tebakan durasi dengan biaya satu query tambahan per read.
Pin berdasarkan data yang ditulis user, bukan seluruh user, bila bisa. Jika kasir mengedit satu tiket, pin read untuk id tiket itu beberapa detik melindungi layar yang membutuhkannya dan membiarkan sisa dashboard tetap di replica.
Bagaimana menjaga monotonic reads di banyak replica?
Monotonic reads gagal ketika request berurutan dari satu user mendarat di replica pada posisi berbeda. Solusinya adalah membuat pemilihan replica stabil per user, bukan acak. Tiga cara umum, dari yang paling sederhana:
Hash session atau user id ke satu replica, seperti pickReplica di atas. Tanpa state, dan tetap bekerja setelah aplikasi restart.
Sticky routing di load balancer, hasilnya sama tetapi logikanya pindah keluar dari aplikasi.
Kirim token LSN bersama tiap request dan hanya baca dari replica yang posisi replay-nya sudah mencapainya. Ini opsi terkuat dan paling banyak kerjanya.
Stabilitas ada harganya: jika replica tempat sesi menempel mati, sesi itu pindah ke replica lain dan mungkin sekali melihat data yang lebih lama. Biasanya itu kompromi yang bisa diterima, dan sama dengan kompromi yang dijelaskan Jepsen untuk jaminan ini, yang hanya berlaku selama client terus berbicara dengan server yang sama.
Bisakah synchronous_commit remote_apply memperbaiki stale read?
Bisa, dengan harga. Dengan remote_apply, commit menunggu sampai standby sinkron menerapkan transaksi, sehingga terlihat oleh query di sana. Itu mensyaratkan synchronous_standby_names tidak kosong; tanpa itu tidak ada efeknya. Dokumentasi memperingatkan bahwa ini menyebabkan delay commit jauh lebih besar dibanding level lain karena menunggu replay.
# postgresql.conf on the primary
synchronous_standby_names = 'ANY 1 (replica_a, replica_b)'
# Per transaction, only where a stale read is unacceptable:
# SET LOCAL synchronous_commit = 'remote_apply';
# Everything else keeps the default and does not wait for replay.
Karena itu saya menganggapnya pisau bedah. Mengaturnya untuk segelintir transaksi di mana stale read tidak boleh terjadi itu wajar; mengaturnya global membuat setiap write menunggu replica sinkron yang paling lambat, dan primary macet jika standby itu tidak terjangkau. Dengan ANY 1 pada dua replica, commit hanya butuh salah satunya, tetapi read masih bisa mengenai yang lain, jadi remote_apply saja tidak menjamin read di replica melihat write.
Kapan read harus kembali ke primary?
Gunakan checklist ini untuk menentukan, berurutan, ke mana sebuah read pergi. Ia merangkum solusi di atas agar aturannya ada di satu tempat, bukan di tiap endpoint.
Apakah user ini menulis dalam jendela pin terakhir? Baca dari primary.
Apakah read bagian dari transaksi yang juga menulis? Pakai koneksi primary.
Apakah replay_lag replica terpilih di atas threshold, atau state-nya bukan streaming? Fallback ke primary.
Selain itu pilih replica dengan hash session id agar read tetap monotonic.
Untuk data yang memang boleh berumur beberapa detik, seperti laporan, tampilkan timestamp as-of alih-alih menyembunyikan lag.
Langkah 3 melindungi primary dari replica yang down: jika semua replica tertinggal, semuanya fallback sekaligus, jadi ukur kapasitas primary untuk skenario terburuk itu atau buang read terberat lebih dulu.
Replication lag bukan bug yang harus dihapus, melainkan sifat yang harus diakali lewat routing. Ukur replay lag, pin user ke primary tepat setelah ia menulis, jaga tiap sesi di satu replica, dan pakai remote_apply hanya di tempat stale read lebih mahal daripada commit yang lebih lambat.