Read replica — memisahkan baca dari tulis
Portalmu 95% membaca. Read replica adalah cara termurah menambah kapasitas baca — asalkan kamu tahu kueri mana yang tidak boleh ke sana.
Intisari
- Replica RDS MySQL bersifat asinkron. Ia tertinggal beberapa milidetik sampai beberapa detik.
- Konsekuensinya: membaca dari replica tepat setelah menulis bisa tidak menemukan yang baru saja kamu tulis.
- Aturan: setelah operasi tulis dalam satu alur pengguna, baca dari writer sampai alur itu selesai.
- Dua pool terpisah: satu ke endpoint writer, satu ke endpoint reader. Dua instance Kysely.
- Pantau
ReplicaLag. Lag yang melonjak biasanya berarti ada transaksi tulis besar di writer.
Bentuknya di RDS
┌──── tulis ────► portal.abc.rds.amazonaws.com (writer)
Astro (ECS) ───┤ │ replikasi asinkron
└──── baca ─────► portal-ro.abc.rds.amazonaws.com (reader)
Replica RDS MySQL adalah instance terpisah yang memutar ulang binlog dari writer. Ia punya endpoint sendiri, kelas instance sendiri, dan biaya sendiri. Kamu bisa punya lebih dari satu, dan menaruhnya di Availability Zone berbeda.
Ini bukan failover. Read replica adalah untuk kapasitas baca, bukan ketersediaan. Untuk failover otomatis kamu butuh Multi-AZ, yang merupakan fitur berbeda dan bisa dipakai bersamaan. Banyak orang memasang replica lalu mengira mereka sudah punya HA — tidak.
Dua pool, dua instance Kysely
// src/lib/db.ts
import { Kysely, MysqlDialect } from "kysely";
import { createPool, type PoolOptions } from "mysql2";
import type { DB } from "./db-types";
const dasar: PoolOptions = {
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectTimeout: 5_000,
timezone: "Z",
ssl: { rejectUnauthorized: true },
};
function buat(host: string, limit: number) {
const pool = createPool({ ...dasar, host, connectionLimit: limit });
pool.on("connection", (c) => {
c.query("SET SESSION MAX_EXECUTION_TIME = 10000");
c.query("SET SESSION time_zone = '+00:00'");
});
return new Kysely<DB>({ dialect: new MysqlDialect({ pool }) });
}
// Baca jauh lebih banyak, jadi pool-nya lebih besar.
export const dbBaca = buat(process.env.DB_HOST_RO!, 12);
export const dbTulis = buat(process.env.DB_HOST_RW!, 4);
Ingat aritmetika koneksi dari materi sebelumnya — sekarang ada dua pool per task. 12 + 4 = 16
koneksi per task, bukan 10. Dengan 12 task itu 192 koneksi, tapi terbagi ke dua instance database:
144 ke reader, 48 ke writer. Hitung batas keduanya secara terpisah, karena
max_connections-nya diturunkan dari memori masing-masing instance dan reader-mu boleh
berkelas lebih kecil.
Kelas bug yang hanya muncul setelah replica ada
// Member baru saja berlangganan lewat Midtrans.
await dbTulis.updateTable("langganan")
.set({ status: "aktif" })
.where("id", "=", idLangganan)
.execute();
// Redirect ke halaman artikel; server island membaca status langganan.
const member = await dbBaca
.selectFrom("langganan")
.select("status")
.where("member_id", "=", memberId)
.executeTakeFirst();
// status masih "menunggu" — replica belum menyusul.
// Member yang BARU SAJA MEMBAYAR melihat paywall.
Ini keluhan pelanggan yang paling merusak kepercayaan, dan paling sulit direproduksi: di laptopmu lag replica nol, di produksi ia 300 ms — cukup untuk kalah dari redirect browser.
Perbaikan: tandai alur yang baru saja menulis
// src/lib/db.ts
const KUNCI_SEGAR = "baca-writer";
export function tandaiBaruMenulis(cookies: AstroCookies) {
cookies.set(KUNCI_SEGAR, "1", {
maxAge: 15, // cukup untuk melampaui lag normal
httpOnly: true,
sameSite: "lax",
secure: true,
path: "/",
});
}
export function pilihDb(cookies: AstroCookies) {
return cookies.has(KUNCI_SEGAR) ? dbTulis : dbBaca;
}
// Setelah pembayaran dikonfirmasi:
await aktifkanLangganan(idLangganan);
tandaiBaruMenulis(Astro.cookies);
return Astro.redirect("/akun?status=aktif");
Selama 15 detik berikutnya, pembaca itu membaca dari writer dan pasti melihat data terbarunya. Setelah cookie kedaluwarsa, ia kembali ke replica. Bebannya dapat diabaikan — hanya pembaca yang baru saja menulis yang terkena, dan mereka segelintir.
Alternatif yang lebih kuat, kalau kamu butuh: MySQL 8.0 punya GTID-based consistent reads — catat GTID setelah menulis, lalu suruh replica menunggu sampai ia mencapai GTID itu sebelum menjawab. Lebih presisi, tapi jauh lebih rumit dan menambah latensi ke pembacaan yang menunggu. Untuk portal berita, cookie 15 detik hampir selalu cukup.
Kueri mana yang ke mana
| Kueri | Ke | Alasan |
|---|---|---|
| Daftar artikel, halaman artikel, kategori | Reader | Basi beberapa detik tidak masalah |
| Data perusahaan, statistik | Reader | Diperbarui harian |
| Pencarian | Reader | Mahal, dan jangan mengganggu writer |
| Cek status langganan di paywall | Reader, kecuali baru menulis | Pola cookie di atas |
| Login & pembuatan sesi | Writer | Menulis, dan langsung dibaca sesudahnya |
| Webhook Midtrans | Writer | Menulis, dan idempotensinya bergantung pada baca-tulis yang konsisten |
| Panel admin & redaksi | Writer | Editor harus melihat hasil suntingannya seketika |
Memantau lag
-- Dari sisi replica
SHOW REPLICA STATUS\G
-- Seconds_Behind_Source: 0 itu sehat
Di CloudWatch, metrik yang dipantau adalah ReplicaLag. Pasang alarm di 5 detik. Lonjakan lag
hampir selalu berarti salah satu dari tiga hal:
- Ada transaksi tulis besar di writer — misalnya
UPDATEmassal tanpaLIMIT, atau migrasi skema yang berjalan di jam sibuk. - Replica-nya terlalu kecil untuk mengikuti laju tulis.
- Ada kueri baca panjang di replica yang memblokir penerapan binlog.
Sabuk pengaman yang layak dipasang: kalau ReplicaLag melewati ambang, alihkan
sementara pembacaan ke writer. Lebih baik writer bekerja lebih keras selama beberapa menit daripada
pembaca melihat artikel yang hilang dari daftar karena replica tertinggal. Bacalah metriknya sekali per
menit, bukan per request.
Kapan replica belum perlu
Jujur: kalau cache Cloudflare-mu sudah bekerja seperti yang direncanakan di Fase 8, beban baca yang sampai ke database akan turun 5–6 kali lipat. Ada kemungkinan nyata kamu tidak butuh replica sama sekali setelah migrasi — dan menghemat biaya satu instance RDS penuh.
Urutan yang benar: perbaiki cache dulu, ukur, baru putuskan. Menambah replica untuk menutupi cache yang buruk adalah membayar bulanan untuk masalah yang bisa diselesaikan sekali.
Latihan: pisahkan src/lib/db.ts jadi dbBaca dan
dbTulis, lalu telusuri seluruh fungsi di src/lib/ dan tandai tiap satu harus
memakai yang mana. Buat daftarnya. Untuk yang jawabannya "tergantung", tuliskan kenapa — daftar itu
adalah spesifikasi pola cookie segar untuk portalmu.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.