Skip to content

Lab: tự chạy thử ​

Nhiều trang trong sổ tay có số liệu và kết quả “đã chạy thật” trên MySQL và PostgreSQL. Toàn bộ script dùng để chạy nằm trong thư mục lab/ của repo. Trang này nói cách tự chạy lại chúng; code của từng script được nhúng ở cuối mỗi trang, trong mục Nguồn tham khảo.

Tự chạy có ích ở ba chỗ:

  • Kiểm chứng: số liệu trong trang được đo trên một laptop vào một ngày cụ thể; chạy lại trên máy khác hay phiên bản database khác để xem kết luận còn đúng không.
  • Thử biến thể: đổi isolation level, cỡ lô, kích thước bảng, rồi xem kết quả đổi ra sao. Cách này nhớ lâu hơn đọc bảng.
  • Khi nâng phiên bản: một số kết luận chỉ đúng với MySQL 8.0 (ví dụ cascade không có trong binlog, MySQL chỉ dùng hash join khi không có index), nên lên 8.4 hay 9.x cần chạy lại.

Script chỉ chạy được trên máy của mình, vì site này là file tĩnh, không có database phía sau. Phần lớn script cần Docker; riêng latency, process-thread và event-loop chỉ cần Node.js.

Cách chạy ​

Cần Docker Desktop đang chạy và Node.js 20 trở lên.

bash
npm run lab:up                        # cài thư viện, dựng 3 container, nối replica
npm run lab -- transaction            # chạy script của một trang (tên trong bảng dưới)
npm run lab -- all                    # chạy tất cả trừ dong-thuan, thoi-gian và redis, mất vài phút
npm run lab:down                      # xoá container và dữ liệu

lab:up dựng ba container: MySQL 8.0 làm source (cổng 33306), một MySQL replica dùng GTID (33307), và PostgreSQL 17 (55432). Tất cả chỉ mở trên 127.0.0.1, mật khẩu pw.

LệnhTrangChạy những gì
npm run lab -- viet-sqlViết SQL khi phỏng vấnDữ liệu mẫu, mọi câu SQL trong trang, các khác biệt giữa MySQL và PostgreSQL
npm run lab -- phan-tich-bai-toanPhân tích bài toánDung lượng thật của 1 triệu dòng so với dữ liệu thô, throughput của point select và insert từng dòng / theo lô trên MySQL và PostgreSQL; khoảng 1 phút
npm run lab -- time-seriesDữ liệu chuỗi thời gianPartition theo ngày: DELETE một ngày so với bỏ partition (thời gian, binlog/WAL), partition pruning; BRIN so với B-tree; bảng tổng hợp theo giờ; khoảng 1 phút
npm run lab -- bo-demBộ đếm, hot row & bảng tổng hợp32 connection cùng tăng một bộ đếm: một dòng, chia 4/16/64 dòng, cập nhật ở đầu và cuối transaction; COUNT(*) và số ước tính; hạng trong bảng xếp hạng; khoảng 1 phút
npm run lab -- full-text-searchFull-text search500.000 tên sản phẩm tiếng Việt: LIKE so với FULLTEXT, GIN trên tsvector, pg_trgm; từ hai chữ, tìm không dấu với collation ai_ci và unaccent; khoảng 30 giây
npm run lab -- da-ngon-nguĐa ngôn ngữBảng dịch và ngôn ngữ dự phòng, cột JSON có index và CHECK, thứ tự sắp xếp tiếng Việt theo từng collation; vài giây
npm run lab -- dia-lyDữ liệu địa lý1 triệu cửa hàng trên MySQL: tìm trong bán kính bằng ST_Distance_Sphere so với khung bao có spatial index; sai số của công thức phẳng; khoảng 3 phút
npm run lab -- migrationMigration trong teamALTER INSTANT, INPLACE, COPY trên bảng 1 triệu dòng khi có client ghi; ALTER xếp hàng sau transaction mở lâu, có và không có lock timeout, trên MySQL và PostgreSQL; khoảng 20 giây
npm run lab -- testTest với databaseCùng câu lệnh trên SQLite (node:sqlite), MySQL, PostgreSQL; chi phí mỗi test của rollback, TRUNCATE, DELETE, database từ template; khoảng 1 phút
npm run lab -- giam-satGiám sát databaseTải giả lập (N+1, query thiếu index, query thường) rồi xem performance_schema và pg_stat_statements; history list length và dead tuple khi có transaction mở lâu; hit rate; khoảng 30 giây, dựng tạm container lab-pg-stat
npm run lab -- backupBackup & khôi phụcmysqldump, xoá nhầm, replica nhận lệnh xoá sau bao lâu, khôi phục tới ngay trước lệnh xoá bằng START REPLICA UNTIL trên một MySQL tạm; pg_dump/pg_restore; khoảng 20 giây
npm run lab -- bao-matBảo mật databaseSQL injection khi nối chuỗi và khi dùng placeholder, câu lệnh chồng, ORDER BY ?, LIKE với %, quyền của user chỉ đọc ghi dữ liệu; vài giây
npm run lab -- mui-gioThời gian & múi giờTIMESTAMP/DATETIME, timestamptz/timestamp khi đổi múi giờ của phiên; giá trị mysql2 và pg ghi khi process Node có TZ khác nhau; CONVERT_TZ; doanh thu theo ngày UTC và ngày giờ Việt Nam; vài giây
npm run lab -- oltp-olapOLTP vs OLAP5 triệu dòng bán hàng trên PostgreSQL và DuckDB (lưu theo cột, chạy trong process): dung lượng, query báo cáo, tra theo khoá, UPDATE từng dòng; khoảng 20 giây
npm run lab -- view-triggerView, trigger, stored procedureView, trigger (kể cả cascade, TRUNCATE), procedure, binlog khi cascade, chi phí trigger
npm run lab -- transactionTransaction & isolation6 kịch bản × 4 isolation level × 2 database; ghi lại dữ liệu cho mô phỏng; DDL trong transaction
npm run lab -- cau-truc-luu-truCấu trúc lưu trữHeap PostgreSQL (ctid, HOT, bảng phình, Heap Fetches), InnoDB
npm run lab -- joinThuật toán join100.000 khách × 1.000.000 đơn; các thuật toán join và cách ép
npm run lab -- xu-ly-theo-loXử lý dữ liệu lớn theo lôMột câu so với chia lô (lock, replica lag), OFFSET so với keyset, import, export, xoá theo lô
npm run lab -- tach-doc-ghiTách đọc / ghiĐọc replica ngay sau khi ghi, chờ GTID, pool xoay vòng. Mặc định chỉ đo lúc rảnh; thêm FULL=1 để đo lúc có tải ghi nặng (khoảng 5 phút, ghi nhiều binlog)
npm run lab -- latencyLatencyĐộ bận và latency, fan-out, hedged request, coordinated omission. Chỉ cần Node.js, không cần lab:up; khoảng 5–6 phút
npm run lab -- process-threadProcess, thread & concurrencySố thread của process Node, chi phí process/worker/async function, concurrency với parallelism. Chỉ cần Node.js; khoảng 20 giây
npm run lab -- dong-thuanĐồng thuậnCluster etcd 3 node (dựng riêng bằng npm run lab -- raft:up, xoá bằng raft:down, không cần lab:up): latency, cắt follower, cắt leader và đo thời gian bầu leader mới, mất đa số; khoảng 30 giây
npm run lab -- thoi-gianThời gian & thứ tựĐồng hồ máy so với NTP (sntp, chỉ macOS), đồng hồ container Docker so với máy host bằng công thức NTP, Date.now() so với performance.now(); khoảng 40 giây, chỉ cần Docker. Trên Windows và Linux, phần sntp được bỏ qua
npm run lab -- redisRedisRedis 8.4 (dựng riêng bằng npm run lab -- redis:up, xoá bằng redis:down, không cần lab:up): thread của Redis, redis-benchmark có và không có pipelining, io-threads, ba cách gửi lệnh từ Node, lệnh chậm chặn client khác, encoding và bộ nhớ, MULTI/EXEC, WATCH, script Lua, lazy và active expire, noeviction và LRU xấp xỉ, fork và copy-on-write, RDB và AOF khi bị kill, WAIT, hash tag; khoảng 3 phút, khởi động lại lab-redis và pause replica
npm run lab -- commit-phan-tanCommit phân tánXA của MySQL và PREPARE TRANSACTION của PostgreSQL: chi phí, transaction in doubt giữ lock và còn sau restart, coordinator nhỏ chuyển tiền giữa hai database; khoảng 30 giây, restart container MySQL và PostgreSQL
npm run lab -- nhat-quanNhất quán & CAPSemi-sync replication của MySQL: latency commit so với async, docker pause replica để thấy commit chờ tới timeout rồi chuyển về async; khoảng 15 giây
npm run lab -- distributed-lockDistributed lockBảng lease kèm fencing token trên MySQL và PostgreSQL: tạo dòng, acquire lấy token trong một lệnh, hết hạn, release sai chủ, ghi bằng token cũ, 20 client tranh lock; khoảng 5 giây
npm run lab -- xu-ly-loiXử lý lỗi & idempotencyTimeout sau khi commit với và không có idempotency key, hai cách lưu key, retry storm với bốn chiến lược, circuit breaker, phát hiện node chết bằng heartbeat và SWIM (10–60 node UDP thật, mất gói, node bị treo). Phần idempotency cần lab:up, các phần còn lại chỉ cần Node.js; khoảng 3 phút
npm run lab -- event-loopEvent loopThứ tự callback (CommonJS, ES module), thread pool, event loop bị chặn, 10.000 request trên một thread. Chỉ cần Node.js; khoảng 1 phút
npm run lab -- kieu-so-sanhKiểu dữ liệu & so sánhKết quả thật của các phép so sánh và phép tính hay bị hỏi. Chỉ cần Node.js; vài giây
npm run lab -- scope-closureScope, closure, this, prototypeHoisting, TDZ, closure trong vòng lặp, this, prototype, closure giữ bộ nhớ. Chỉ cần Node.js; vài giây
npm run lab -- asyncPromise & async/awaitHành vi thật của Promise trên Node: thứ tự, lỗi không ai bắt, các hàm gộp promise. Chỉ cần Node.js; vài giây
npm run lab -- moduleModule: CommonJS & ESMCommonJS và ES module trên Node 24, vòng phụ thuộc, named import từ CommonJS. Chỉ cần Node.js; vài giây
npm run lab -- libuv-thread-poolNon-blocking I/O & thread poolThread pool dùng chung: pbkdf2 chiếm hết pool làm đọc file chậm theo; mỗi UV_THREADPOOL_SIZE một process. Chỉ cần Node.js; khoảng 10 giây
npm run lab -- streamStream & backpressureBộ nhớ khi chép file bằng readFile so với pipeline, đọc từng dòng, bỏ qua và tôn trọng backpressure, pipe() so với pipeline() khi nguồn lỗi. Tạo file mẫu khoảng 90 MB trong thư mục tạm rồi xoá. Chỉ cần Node.js; khoảng 25 giây
npm run lab -- bo-nho-gcBộ nhớ & GCGiới hạn heap, OOM, minor và major GC, leak của cache và listener, WeakMap, chụp heap snapshot (file khoảng 160 MB, xoá sau khi đo). Chỉ cần Node.js; khoảng 15 giây
npm run lab -- node-xu-ly-loiXử lý lỗi & graceful shutdownLỗi trong callback, nuốt uncaughtException làm pool cạn, bốn cách xử lý SIGTERM với client giữ keep-alive, Error có cause. Chỉ cần Node.js; khoảng 20 giây
npm run lab -- concurrencyRace condition trong Node.jsRút tiền cùng lúc qua await, mutex trong một và hai process, ghi có điều kiện ở "database" giả lập, giới hạn số việc song song. Chỉ cần Node.js; khoảng 10 giây
npm run lab -- scaleScale Node.jsThroughput của cluster theo số worker, cách chia request cho worker, session trong bộ nhớ khi có nhiều worker. Chỉ cần Node.js; khoảng 20 giây
npm run lab -- hieu-nangHiệu năng & profilingKhởi động lạnh, ảnh hưởng của cách ghi log, --cpu-prof khi có tải; ghi lại dữ liệu cho mô phỏng flame graph. Chỉ cần Node.js; khoảng 25 giây
npm run lab -- frameworkFramework: Express & NestJSHandler async ném lỗi trên Express 4 và 5, thứ tự middleware, error middleware, validation bằng zod, giới hạn body. Chỉ cần Node.js; vài giây
npm run lab -- node-bao-matBảo mật Node.jsPrototype pollution và các cách chặn, permission model, process con kế thừa biến môi trường, npm audit (cần mạng). Chỉ cần Node.js; vài giây
npm run lab -- nodejs-databaseNode.js với databaseTransaction bằng pool.query, quên release(), kích thước pool và max_connections, kiểu BIGINT/DECIMAL của pg và mysql2, AsyncLocalStorage, stream 500.000 dòng. Cần lab:up (database nodedb); khoảng 20 giây
npm run lab -- kafkaKafkaCụm Kafka 4.1 ba broker (dựng riêng bằng npm run lab -- kafka:up, xoá bằng kafka:down, không cần lab:up): thứ tự theo key, tăng số partition, consumer group 1–8 consumer, consumer bị kill, acks và min.insync.replicas khi tắt hay kill follower, ba cách commit offset, log compaction. Khoảng 7–8 phút
npm run lab -- hang-doiHàng đợi messageRabbitMQ 4.1 và ElasticMQ (giả lập SQS), dựng riêng bằng npm run lab -- hang-doi:up, xoá bằng hang-doi:down, không cần lab:up: prefetch, auto-ack và ack khi consumer chết, fan-out, message độc và delivery limit, thứ tự với hai consumer; visibility timeout, dead letter queue, FIFO theo message group, long polling. Khoảng 1 phút
npm run lab -- building-blocksBuilding blocksBốn cách chia request (round-robin, random, least outstanding, power of two choices) cho 4 server, khi các server như nhau và khi một server chậm. Chỉ cần Node.js; khoảng 1 phút
npm run lab -- chan-event-loopChặn event loop & xử lý CPUJSON, vòng lặp O(n²), ReDoS, chi phí gửi dữ liệu sang worker, CPU tăng tốc chậm sau lúc rảnh, so sánh chạy thẳng / chia nhỏ / pool worker khi có tải, --cpu-prof, --trace-sync-io, dừng regex chạy quá lâu. Chỉ cần Node.js; khoảng 2 phút

Chạy xong thì npm run lab:down: các phép đo ghi rất nhiều, và volume của MySQL có thể lớn nhanh nếu để lâu.

Đọc kết quả ​

  • Client mysql không in gì khi kết quả rỗng, còn psql in (0 rows). Mỗi bước trong file .sql có một dòng nhãn --- …, để biết bảng kết quả nào thuộc bước nào.
  • Các file .sql chạy tiếp khi gặp lỗi, vì nhiều bước cố ý gây lỗi để xem thông báo.
  • Thời gian dao động nhiều giữa các lần chạy, nhất là trên laptop. So bậc độ lớn và tỉ lệ giữa các cách làm, không so con số tuyệt đối. Các trang ghi khoảng giá trị qua nhiều lượt chạy vì lý do này.

Môi trường ​

lab/docker-compose.yml
yml
# Môi trường chạy thử cho sổ tay: MySQL 8.0 (source), một MySQL replica dùng GTID, PostgreSQL 17.
# Dựng: npm run up (trong lab/) hoặc npm run lab:up (ở thư mục gốc). Xoá: npm run down.
name: interview-lab

x-mysql-health: &mysql-health
  # Kết nối qua TCP: lúc khởi tạo lần đầu MySQL chạy một server tạm không mở TCP,
  # nên healthcheck chỉ qua khi server thật đã sẵn sàng.
  test: ['CMD', 'mysql', '--protocol=TCP', '-h127.0.0.1', '-uroot', '-ppw', '-e', 'SELECT 1']
  interval: 3s
  timeout: 5s
  retries: 60

services:
  mysql:
    image: mysql:8.0
    container_name: lab-mysql
    # Các phép đo cập nhật hàng triệu dòng, mỗi lần ghi vài trăm MB binlog. Không giới hạn thì binlog tích luỹ
    # qua nhiều lần chạy (đã từng lên hơn 40 GB): xoay file khi đạt 100 MB, xoá file cũ hơn 30 phút.
    command: --server-id=1 --gtid-mode=ON --enforce-gtid-consistency=ON --max-binlog-size=104857600 --binlog-expire-logs-seconds=1800
    environment:
      MYSQL_ROOT_PASSWORD: pw
    ports: ['127.0.0.1:33306:3306']
    healthcheck: *mysql-health

  replica:
    image: mysql:8.0
    container_name: lab-replica
    # Replica không cần binlog riêng (không có ai đọc từ nó); relay log tự xoá sau khi áp dụng.
    command: --server-id=2 --gtid-mode=ON --enforce-gtid-consistency=ON --skip-log-bin
    environment:
      MYSQL_ROOT_PASSWORD: pw
    ports: ['127.0.0.1:33307:3306']
    healthcheck: *mysql-health

  postgres:
    image: postgres:17-alpine
    container_name: lab-pg
    environment:
      POSTGRES_PASSWORD: pw
    ports: ['127.0.0.1:55432:5432']
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'postgres']
      interval: 2s
      timeout: 5s
      retries: 30
lab/common/run-sql.mjs: chạy file .sql bằng client trong container
js
// Chạy một file .sql bằng client dòng lệnh trong container (mysql hoặc psql) và in kết quả.
// Dùng client thay vì driver để giữ đúng thông báo lỗi và lệnh riêng của psql như \timing, \echo.
//   node common/run-sql.mjs mysql viet_sql viet-sql/common.sql
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { CONTAINERS } from './config.mjs'

const LAB = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')

function docker(args, input) {
  const r = spawnSync('docker', args, { input, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
  if (r.error) throw r.error
  return (r.stdout ?? '') + (r.stderr ?? '')
}

export function ensureDb(engine, db) {
  if (engine === 'mysql') {
    docker(['exec', '-i', CONTAINERS.mysql, 'mysql', '-uroot', '-ppw', '-e', `CREATE DATABASE IF NOT EXISTS ${db}`])
  } else {
    const has = docker(['exec', CONTAINERS.pg, 'psql', '-U', 'postgres', '-tAc', `SELECT 1 FROM pg_database WHERE datname = '${db}'`])
    if (!has.includes('1')) docker(['exec', CONTAINERS.pg, 'createdb', '-U', 'postgres', db])
  }
}

// --force: chạy tiếp sau lỗi, vì nhiều phép thử cố ý gây lỗi để xem thông báo.
// --default-character-set=utf8mb4: thiếu cờ này thì chữ có dấu bị lỗi bảng mã.
export function runSql(engine, db, file) {
  ensureDb(engine, db)
  const sql = fs.readFileSync(path.resolve(LAB, file), 'utf8')
  const out = engine === 'mysql'
    ? docker(['exec', '-i', CONTAINERS.mysql, 'mysql', '--default-character-set=utf8mb4', '--force', '-t', '-uroot', '-ppw', db], sql)
    : docker(['exec', '-i', CONTAINERS.pg, 'psql', '-U', 'postgres', '-q', '-d', db], sql)
  return out.split('\n').filter((l) => !l.includes('Using a password on the command line')).join('\n')
}

export function show(engine, db, file) {
  console.log(`\n========== ${engine === 'mysql' ? 'MySQL' : 'PostgreSQL'} · ${file} ==========`)
  console.log(runSql(engine, db, file))
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const [engine, db, file] = process.argv.slice(2)
  show(engine, db, file)
}