SET lock_timeout = '3s';
SET statement_timeout = '15min';
ALTER TABLE orders ADD COLUMN channel text;
Scenarios: Zero-Downtime Migration & Zero Data Loss
draftedMock interview tình huống production: đổi schema Postgres không downtime (lock queue, CONCURRENTLY, NOT VALID), backfill bảng lớn, đổi DB engine bằng snapshot + CDC, strangler fig cho legacy, outbox, drain Kafka backlog 10M, durability knobs và cutover/rollback.
0/57 rated · mastery 0%
Bài học
6 bài · ~94 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~18 phút· 9 câuverify
Playbook: đổi schema Postgres khi traffic vẫn chạy
Chẩn đoán lock queue, lock_timeout + retry, CONCURRENTLY, NOT VALID, fast default, migration tool, MySQL/SQL Server online DDL và guardrail cho cả team.
- 02~18 phút· 10 câuverify
Playbook: expand–contract, backfill và rollback qua nhiều deploy
Thứ tự migration so với rollout, rename/split cột với writer không kiểm soát, int → bigint, backfill tự điều tiết, rollback forward-only và rollout theo tenant.
- 03~13 phút· 9 câuverify
Playbook: chuyển database bằng snapshot + CDC, verify và cutover
Chọn dump/restore, logical replication, DMS hay Debezium; chứng minh dữ liệu giống nhau; runbook cutover dưới 5 phút; sequence, collation, time zone và đường lui.
- 04~12 phút· 11 câuverify
Playbook: legacy, API cũ và các kho dữ liệu khác
Strangler fig cho monolith PHP, adapter XML/JSON, shadow traffic an toàn, deprecation, canary đổi format dữ liệu, reindex Elasticsearch, chuyển S3 và nâng cấp hash password.
- 05~17 phút· 11 câuverify
Playbook: chuỗi durability và chứng minh zero data loss
RPO/RTO, no ack until durable, synchronous_commit, Multi-AZ vs replica, PITR và restore drill, outbox đúng cách, Kafka acks/ISR, reconciliation.
- 06~15 phút· 7 câuverify
Playbook: drain Kafka backlog 10M mà không mất message
Tính ETA, chẩn đoán lag theo partition, giới hạn consumer ≤ partition, song song theo key, poison message, không tăng partition giữa backlog, ưu tiên traffic mới.
Tóm tắt & cheat sheet của track
TL;DR
- "Zero-downtime" = mọi bước đều tương thích với cả code version N và N-1 đang chạy song song (rolling deploy, blue/green, canary). Công cụ chính: expand → migrate → contract, mỗi bước deploy riêng và rollback được.
- Postgres hiếm khi chết vì DDL chậm; nó chết vì lock queue: một
ALTER TABLEcầnACCESS EXCLUSIVExếp hàng sau một query dài và chặn mọi query đến sau nó. Luôn đặtlock_timeout(vài giây) + retry, dùngCONCURRENTLY/NOT VALID+VALIDATE. - Đổi DB engine / cluster không downtime = snapshot + CDC (Debezium, AWS DMS, logical replication) → verify (count, checksum theo chunk, shadow read) → cutover ngắn (write freeze vài giây–phút) → reverse replication để có đường lui. Dual-write từ app là phương án cuối vì không atomic.
- "Zero data loss" không phải một config mà là một chuỗi durability: client retry + idempotency key → API chỉ ack khi đã durable → outbox / Kafka
acks=all+min.insync.replicas=2→ consumer commit sau xử lý → DB sync replication / PITR → reconciliation để chứng minh. - Kafka backlog 10M: tính ETA trước (
backlog / (consume − produce)), scale consumer tối đa bằng số partition, song song theo key bên trong partition nếu cần ordering, bảo vệ downstream bằng rate limit, đừng tăng partition vội vì nó đổi mapping key → partition.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Expand–contract (parallel change) | Thêm cái mới song song cái cũ, chuyển dần, rồi mới xoá cái cũ | zip → thêm postal_code, dual-write, backfill, đổi read, drop zip |
| Lock queue | Lock request chờ sẽ chặn các request đến sau xung đột với nó | ALTER chờ sau SELECT 10 phút → mọi SELECT mới cũng chờ |
lock_timeout | Hủy statement nếu chờ lock quá N ms | SET lock_timeout = '3s' rồi retry có backoff |
NOT VALID + VALIDATE | Tạo constraint chỉ áp cho row mới, validate row cũ sau với lock nhẹ | FK / CHECK trên bảng 500M rows |
CREATE INDEX CONCURRENTLY | Build index không chặn write, đổi lại chậm hơn và có thể để lại index INVALID | Không chạy được trong transaction block |
| Backfill | Điền dữ liệu cho cột/bảng mới theo batch, idempotent, resume được | UPDATE ... WHERE id BETWEEN $1 AND $2 AND new_col IS NULL |
| CDC | Đọc change log (WAL / binlog / SQL Server CDC) để stream thay đổi | Debezium → Kafka → sink Postgres |
| Shadow read | Đọc cả nguồn cũ và mới, so sánh, vẫn trả kết quả cũ | Log diff 0.02% → tìm bug mapping |
| Strangler fig | Proxy route từng endpoint từ hệ cũ sang hệ mới | /v1/orders/* → Node, còn lại → PHP |
| RPO / RTO | Mất tối đa bao nhiêu dữ liệu / khôi phục trong bao lâu | RPO = 0, RTO = 5 phút cho payments |
| Transactional outbox | Ghi business row + event trong cùng transaction, relay publish sau | orders + outbox trong 1 COMMIT |
| Reconciliation | Job so sánh định kỳ nguồn và đích để phát hiện lệch | Count + sum amount mỗi giờ theo tenant |
| Rehash-on-login | Nâng cấp hash password dần khi user đăng nhập | MD5 → argon2id |
Cơ chế hoạt động
Expand–contract qua nhiều lần deploy
Lock queue: vì sao một ALTER "0.1 giây" làm sập API
Đổi DB engine: snapshot + CDC + cutover + đường lui
Chuỗi durability (không ack cho tới khi durable)
Khung trả lời
Dùng lại cho mọi câu "migrate X không downtime / không mất dữ liệu":
- Làm rõ ràng buộc: kích thước dữ liệu (rows, GB), QPS đọc/ghi, RPO/RTO, có được freeze write vài phút không, các app/consumer nào đang dùng, yêu cầu ordering.
- Liệt kê điều phải tương thích song song: code N và N-1, schema cũ và mới, consumer cũ và mới, client cũ (mobile không update được).
- Chia bước reversible: expand → dual (write / replicate) → backfill → verify → switch read → switch write → contract. Mỗi bước: cách rollback, tiêu chí go/no-go.
- Bảo vệ production khi chạy:
lock_timeout, batch + throttle, theo dõi replica lag / WAL / consumer lag / error rate, kill switch (feature flag). - Chứng minh đúng: count, checksum theo chunk, shadow read diff, reconciliation job, audit log.
- Cutover + rollback: runbook, người quyết định, thời gian freeze, reverse replication, khi nào xoá đường lui.
- Nói trade-off bằng số: "freeze 2 phút đổi lấy không phải dual-write 6 tuần", "sync commit thêm ~1–3 ms mỗi write trong cùng region".
Trade-offs
| Cách đồng bộ khi migrate | Consistency | Độ phức tạp | Rủi ro chính | Hợp khi |
|---|---|---|---|---|
| Downtime window (dump/restore) | Tuyệt đối | Thấp | Downtime tỉ lệ với data size | DB nhỏ, business chấp nhận maintenance |
| Dual-write từ app | Yếu (không atomic) | Trung bình | Lệch khi 1 bên fail, race ordering | Chỉ khi không có CDC, có reconciliation mạnh |
| CDC (Debezium / DMS / logical replication) | Tốt, theo commit order | Cao (vận hành connector) | Type mapping, DDL, lag, replication slot giữ WAL | Đa số migrate engine / cluster |
| Snapshot restore + native replication | Tốt | Thấp–TB | Chỉ cùng engine / version tương thích | Self-hosted PG → RDS/Aurora |
| Transactional outbox | Tốt, at-least-once | TB | Relay lag, bảng outbox phình | Sync SQL → NoSQL / event cho service khác |
| Knob "zero data loss" | Được gì | Trả giá gì |
|---|---|---|
PG synchronous_commit=on + sync standby | RPO ≈ 0 khi mất primary | Latency mỗi commit + write dừng nếu standby chết (trừ khi có nhiều candidate) |
Kafka acks=all + min.insync.replicas=2 + RF=3 | Không mất message đã ack khi mất 1 broker | Latency cao hơn, mất 2 broker → producer bị từ chối |
unclean.leader.election.enable=false | Không bầu replica lỗi thời làm leader | Partition offline cho tới khi replica ISR quay lại |
| Consumer commit sau xử lý | Không mất khi crash | Duplicate → cần idempotent sink |
| PITR (WAL archive) | Khôi phục tới thời điểm trước lỗi người dùng | Chi phí lưu trữ, RTO phụ thuộc lượng WAL replay |
Pitfalls
- Gộp migration và code dùng schema mới trong một deploy → pod cũ crash / pod mới crash tùy thứ tự.
- Migration không có
lock_timeout→ mộtALTER"instant" chặn toàn bộ bảng phía sau một report query. CREATE INDEX CONCURRENTLYtrong migration tool mặc định bọc transaction → lỗi ngay; hoặc chạy fail để lại indexINVALIDvẫn làm chậm write.- Backfill bằng một
UPDATEkhổng lồ hoặc vòng lặpOFFSET→ lock lâu, WAL bùng, replica lag, bloat. - Tin rằng "sync xong row count bằng nhau" = đúng. Count bằng nhau vẫn có thể sai collation, time zone, precision, trailing space.
- Quên reset sequence / identity sau khi copy →
duplicate keyngay khi app ghi vào DB mới. - Outbox relay "publish rồi mark done" coi là exactly-once; polling
id > last_idbỏ sót event commit muộn. - Tăng partition Kafka giữa lúc backlog để "chạy nhanh hơn" → phá ordering theo key, không giảm lại được.
- Có backup nhưng chưa từng restore thử → không biết RTO thật, không biết backup hỏng.
- Traffic mirroring cho endpoint ghi → hệ mới gửi email/charge thẻ thật lần hai.
Cheat sheet
- Mọi DDL Postgres trong migration:
SET lock_timeout = '3s'; SET statement_timeout = '15min';+ retry có jitter. - Thêm cột: nullable hoặc
DEFAULThằng/stable (PG11+ không rewrite). Default volatile (gen_random_uuid(),clock_timestamp()) → rewrite bảng. - NOT NULL cột có sẵn:
ADD CONSTRAINT ... CHECK (c IS NOT NULL) NOT VALID→VALIDATE CONSTRAINT→SET NOT NULL(PG12+ bỏ qua scan) → drop CHECK. - FK:
ADD CONSTRAINT ... FOREIGN KEY ... NOT VALID→VALIDATE CONSTRAINT(lock nhẹSHARE UPDATE EXCLUSIVE). - Index:
CREATE INDEX CONCURRENTLYngoài transaction; fail →DROP INDEX CONCURRENTLYrồi làm lại; kiểmpg_index.indisvalid. - MySQL:
ALGORITHM=INSTANT/INPLACE, LOCK=NONE, bảng lớn dùng gh-ost / pt-online-schema-change. SQL Server:WITH (ONLINE = ON, RESUMABLE = ON)(edition-dependent). - Backfill: keyset theo PK, batch 1k–10k, commit mỗi batch, sleep / throttle theo
replay_lag, checkpoint để resume,WHERE new IS NULLđể idempotent. - DB migration: snapshot + CDC → count + checksum theo chunk + shadow read → freeze → lag 0 →
setval→ flip → reverse replication. - Kafka zero loss:
acks=all, idempotent producer, RF=3,min.insync.replicas=2,unclean.leader.election.enable=false, commit sau xử lý, sink idempotent. - Backlog: ETA = backlog / (consume − produce); consumers ≤ partitions; per-key concurrency; rate limit downstream; park poison message; ưu tiên traffic mới.
- Legacy: strangler proxy, contract test, shadow traffic chỉ cho read, adapter XML ↔ JSON,
Deprecation/Sunsetheader, feature flag theo tenant, canary.
References
- PostgreSQL docs — ALTER TABLE ↗
- PostgreSQL docs — Explicit Locking ↗
- PostgreSQL docs — CREATE INDEX (Building Indexes Concurrently) ↗
- PostgreSQL docs — Client Connection Defaults (lock_timeout, statement_timeout) ↗
- PostgreSQL docs — WAL configuration (synchronous_commit) ↗
- PostgreSQL docs — Continuous Archiving and Point-in-Time Recovery ↗
- PostgreSQL docs — Logical Replication ↗
- PostgreSQL docs — Sequence Manipulation Functions (setval) ↗
- Apache Kafka documentation — Producer configs ↗
- Apache Kafka documentation — Broker / topic configs ↗
- Debezium documentation — SQL Server connector ↗
- AWS DMS — Best practices ↗
- Amazon RDS — Multi-AZ deployments ↗
- Amazon S3 — Replicating objects (including Batch Replication) ↗
- Elasticsearch — Aliases ↗
- Elasticsearch — Reindex API ↗
- Martin Fowler — Strangler Fig Application ↗
- Martin Fowler — Parallel Change ↗
- microservices.io — Transactional outbox ↗
- OWASP — Password Storage Cheat Sheet ↗
- RFC 8594 — The Sunset HTTP Header Field ↗
- gh-ost — GitHub's online schema migration for MySQL ↗
- Knex.js — Migrations ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 57 / 57
async function verify(user: User, pw: string): Promise<boolean> {
const ok =
user.scheme === "argon2" ? await argon2.verify(user.hash, pw)
: await argon2.verify(user.hash, md5hex(pw)); // wrapped legacy
if (ok && user.scheme !== "argon2") {
await users.update(user.id, { hash: await argon2.hash(pw), scheme: "argon2" });
}
return ok;
}
POST _aliases
{ "actions": [
{ "remove": { "index": "products_v1", "alias": "products" } },
{ "add": { "index": "products_v2", "alias": "products" } }
] }
-- migration 20260901_add_fk.sql
ALTER TABLE order_items
ADD CONSTRAINT order_items_order_fk
FOREIGN KEY (order_id) REFERENCES orders(id);
export const dataSource = new DataSource({
type: "postgres",
url: process.env.DATABASE_URL,
entities: [__dirname + "/entities/*.js"],
synchronize: true, // "so dev and prod stay in sync"
migrationsRun: false,
});
// Entity change in this release:
// @Column() referralCode: string; (renamed from `refCode`)
// migrations/20260910_orders_status_idx.ts
import type { Knex } from "knex";
export async function up(knex: Knex) {
await knex.schema.alterTable("orders", (t) => {
t.index(["tenant_id", "status", "created_at"], "orders_tenant_status_created_idx");
});
}
export async function down(knex: Knex) {
await knex.schema.alterTable("orders", (t) => t.dropIndex([], "orders_tenant_status_created_idx"));
}
const BATCH = 50_000;
await db.transaction(async (tx) => {
for (let offset = 0; ; offset += BATCH) {
const rows = await tx.query(
`SELECT id, total FROM orders ORDER BY id LIMIT $1 OFFSET $2`, [BATCH, offset]);
if (rows.length === 0) break;
for (const r of rows) {
await tx.query(`UPDATE orders SET total_cents = $1 WHERE id = $2`,
[Math.round(r.total * 100), r.id]);
}
}
});
-- Postgres side, per 100k-id chunk
SELECT id / 100000 AS chunk, count(*),
md5(string_agg(concat_ws('|', id, status, amount_cents,
to_char(created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS.US')), ',' ORDER BY id))
FROM orders GROUP BY 1 ORDER BY 1;
await consumer.run({
autoCommit: true, // commits periodically in the background
eachBatchAutoResolve: true,
eachBatch: async ({ batch }) => {
for (const m of batch.messages) {
buffer.push(JSON.parse(m.value!.toString())); // in-memory
}
if (buffer.length >= 5000) {
await db.insertOrders(buffer.splice(0)); // flushed later
}
},
});
app.get("/api/orders/:id", async (req, res) => {
const order = await orders.get(req.params.id); // one core model
if (req.accepts(["json", "xml"]) === "xml") {
res.type("application/xml").send(toLegacyXml(order)); // matches PHP output
} else {
res.json(toJsonV2(order));
}
});
SELECT pid, pg_blocking_pids(pid) AS blocked_by, state,
now() - xact_start AS xact_age, left(query, 60) AS query
FROM pg_stat_activity
WHERE datname = current_database() AND wait_event_type = 'Lock'
ORDER BY xact_start;
async function waitForReplicas(maxLagSec = 5) {
for (;;) {
const { rows } = await pg.query(
`SELECT coalesce(max(extract(epoch FROM replay_lag)), 0) AS lag FROM pg_stat_replication`);
if (Number(rows[0].lag) < maxLagSec) return;
await sleep(2_000);
}
}
ERROR: duplicate key value violates unique constraint "orders_pkey"
DETAIL: Key (id)=(1) already exists.
-- fix: move the sequence past the copied data (plus headroom)
SELECT setval(pg_get_serial_sequence('orders', 'id'),
(SELECT coalesce(max(id), 0) + 1000 FROM orders), false);
// runs every 200 ms in each of 3 replicas
async function relayOnce() {
const last = await state.get("last_id"); // shared checkpoint
const { rows } = await pg.query(
`SELECT * FROM outbox WHERE id > $1 ORDER BY id LIMIT 500
FOR UPDATE SKIP LOCKED`, [last]);
for (const e of rows) await kafka.send({ topic: e.topic, messages: [{ value: e.payload }] });
await pg.query(`UPDATE outbox SET published_at = now() WHERE id = ANY($1)`, [rows.map(r => r.id)]);
await state.set("last_id", rows.at(-1)?.id ?? last);
}
eachBatch: async ({ batch, resolveOffset, heartbeat }) => {
const byKey = new Map<string, KafkaMessage[]>();
for (const m of batch.messages) {
const k = m.key?.toString() ?? "";
byKey.set(k, [...(byKey.get(k) ?? []), m]);
}
await pMap([...byKey.values()], async (msgs) => {
for (const m of msgs) await handleIdempotent(m); // in order per key
}, { concurrency: 32 });
resolveOffset(batch.lastOffset()); // whole batch done
await heartbeat();
}
const queue: Order[] = [];
setInterval(async () => {
const batch = queue.splice(0, 500);
if (batch.length) await producer.send({ topic: "orders", messages: batch.map(toMsg) });
}, 1000);
app.post("/orders", (req, res) => {
queue.push(validate(req.body));
res.status(202).json({ status: "accepted" }); // acked before durable
});