M0 · Requirements, capacity, dựng hạ tầng
Bắt đầu M0 nhé. Mình muốn có tài liệu yêu cầu, ước lượng tải và dựng xong hạ tầng trước khi code nghiệp vụ.
Đúng thứ tự. M0 không có logic đặt vé, nhưng mọi milestone sau đều đứng trên nó. Mình giao bài nhé.
1. Bối cảnh & bài toán
Ticket Booking bán vé concert. Bình thường vài chục request/giây. Lúc mở bán, hàng chục nghìn người vào cùng lúc. Nếu bỏ qua M0:
- Không có NFR → không biết "đủ nhanh" là bao nhiêu, benchmark M1/M7/M9 không có mốc so sánh.
- Không ước lượng QPS → chọn sai công cụ (ví dụ dùng DB lock cho 50k QPS ghi).
- Hạ tầng dựng tay, mỗi lần một kiểu → test không lặp lại được, failure scenario không chạy được.
- Secret nằm trong
application.yml→ lộ khi push GitHub.
M0 cần ra 3 thứ:
docs/requirements.md ─ FR, NFR, SLO, capacity
docker-compose.yml ─ 8 thành phần hạ tầng, có healthcheck
5 service skeleton ─ /actuator/health = UP
2. Ôn lại trước khi code
- Interview framework — đi từ requirements → estimate → design, không nhảy vào vẽ box.
- Requirements, SLO & capacity — cách tính QPS trung bình/đỉnh, storage, bandwidth.
- Ticket booking — các luồng chính: xem ghế, giữ ghế, thanh toán.
- Docker Compose —
depends_onvớicondition: service_healthy, network, volume. - Actuator — health group liveness/readiness, expose endpoint an toàn.
- Profile & config — đọc config từ
${ENV}, profilelocal/test. - Keycloak docker setup — import realm khi khởi động.
- Prometheus & Grafana — scrape
/actuator/prometheus.
3. Đề bài
3.1. Tài liệu docs/requirements.md phải có các mục:
| Mục | Nội dung tối thiểu |
|---|---|
| FR | Xem sự kiện, xem sơ đồ ghế, giữ ghế (TTL 10 phút), xác nhận + thanh toán, hủy, nhận thông báo, admin tạo sự kiện |
| NFR | Không bán trùng ghế (correctness > availability ở bước giữ ghế); p99 xem ghế < 200 ms; p99 giữ ghế < 500 ms; availability 99.9%; thanh toán không trừ tiền 2 lần |
| Ngoài phạm vi | Frontend, hoàn tiền thật, multi-region (để M11) |
| Capacity | Bảng giả định + phép tính (xem dưới) |
| Rủi ro | Hot seat, PSP timeout, mất event, flash sale |
Giả định để tính (bạn có thể đổi, nhưng phải ghi rõ):
1M user hoạt động/ngày, mỗi user xem 20 trang ghế, 10% giữ ghế
Flash sale: 100k người trong 5 phút đầu, sự kiện 20k ghế
Tự tính và điền:
QPS đọc trung bình = ? (20M / 86 400)
QPS đọc đỉnh = ? (hệ số peak bạn chọn, giải thích)
QPS ghi giữ ghế đỉnh = ?
Storage bookings/năm = ? (bytes/row × số booking)
Số instance booking cần = ? (QPS đỉnh / QPS 1 instance đo ở M8)
3.2. docker-compose.yml — 8 thành phần, tất cả có healthcheck:
| Container | Image gợi ý | Port | Healthcheck |
|---|---|---|---|
| postgres | postgres:16 | 5432 | pg_isready |
| redis | redis:7 | 6379 | redis-cli ping |
| kafka | apache/kafka:3.7 (KRaft) | 9092 | list topics |
| rabbitmq | rabbitmq:3-management | 5672, 15672 | rabbitmq-diagnostics ping |
| keycloak | quay.io/keycloak/keycloak:25 | 8180 | /health/ready |
| toxiproxy | ghcr.io/shopify/toxiproxy | 8474 + proxy port | /version |
| prometheus | prom/prometheus | 9090 | /-/ready |
| grafana | grafana/grafana | 3000 | /api/health |
Skeleton (bạn tự điền phần còn lại):
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./infra/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
retries: 10
# redis, kafka, rabbitmq, keycloak, toxiproxy, prometheus, grafana: TODO
booking-service:
build: ./booking-service
env_file: .env
depends_on:
postgres: { condition: service_healthy }
# TODO: kafka, redis
volumes:
pgdata:
Mỗi service một database riêng (catalog, booking, payment, notification) tạo trong init.sql. Toxiproxy đứng giữa booking → postgres và booking → payment (dùng ở M4).
3.3. .env.example (commit file này, không commit .env):
POSTGRES_USER=ticket
POSTGRES_PASSWORD=change-me
REDIS_PASSWORD=change-me
RABBITMQ_DEFAULT_USER=ticket
RABBITMQ_DEFAULT_PASS=change-me
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=change-me
GRAFANA_ADMIN_PASSWORD=change-me
PSP_WEBHOOK_SECRET=change-me
BOOKING_DB_URL=jdbc:postgresql://postgres:5432/booking
3.4. Skeleton 5 service
| Service | Port | Dependency tối thiểu |
|---|---|---|
| gateway | 8080 | spring-cloud-starter-gateway, actuator |
| catalog-service | 8081 | web, data-jpa, postgresql, flyway, actuator, micrometer-registry-prometheus |
| booking-service | 8082 | như catalog + data-redis, spring-kafka |
| payment-service | 8083 | như catalog |
| notification-service | 8084 | amqp, actuator |
# application.yml (mỗi service)
spring:
application.name: booking-service
datasource:
url: ${BOOKING_DB_URL}
username: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
management:
endpoints.web.exposure.include: health,info,prometheus
endpoint.health.probes.enabled: true
# TODO: health group readiness gồm db, redis
API của M0 chỉ có:
| Method | Path | Response |
|---|---|---|
| GET | /actuator/health | {"status":"UP"} |
| GET | /actuator/health/readiness | UP khi DB/Redis/Kafka sẵn sàng |
| GET | /actuator/prometheus | metrics text |
| GET | /api/events (qua gateway) | [] — route gateway → catalog chạy |
Flyway migration đầu tiên của catalog (schema dùng xuyên suốt):
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
starts_at TIMESTAMPTZ NOT NULL,
venue TEXT NOT NULL
);
-- seats nằm ở booking DB (M1): seats(id, event_id, code, status, version, held_by, hold_expires_at)
4. Acceptance criteria
docs/requirements.mdcó đủ FR, NFR (có số), ngoài phạm vi, capacity có phép tính, rủi ro.docker compose up -dtrên máy sạch: cả 8 container hạ tầnghealthytrong ≤ 3 phút.- 5 service trả
/actuator/health=UP; readiness =DOWNkhi tắt Postgres. - Prometheus target page: 5 service đều
UP. - Keycloak tự import realm
tickettừinfra/keycloak/realm.json. grep -r "password:" */src/main/resourceschỉ thấy${...}.GET /api/eventsqua gateway trả 200.
5. Test cases
| ID | Kịch bản | Input / điều kiện | Kỳ vọng |
|---|---|---|---|
| M0-T1 | Context load | @SpringBootTest + Testcontainers Postgres | Context lên, Flyway chạy xong |
| M0-T2 | Health UP | GET /actuator/health | 200, UP |
| M0-T3 | Readiness khi DB chết | Dừng container Postgres | /health/readiness 503, liveness vẫn 200 |
| M0-T4 | Thiếu biến env | Bỏ BOOKING_DB_URL | Service fail-fast khi start, log rõ tên biến |
| M0-T5 | Gateway route | GET :8080/api/events | 200 [] |
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class BookingHealthIT {
@Container @ServiceConnection
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16");
@Autowired TestRestTemplate rest;
@Test
void healthIsUp() {
// TODO: gọi /actuator/health, assert status UP
}
@Test
void readinessDownWhenDbStopped() {
// TODO: pg.stop()? hay dùng Toxiproxy? Chọn cách và giải thích
}
}
6. Failure scenarios
| Sự cố | Cách tạo | Hành vi mong đợi |
|---|---|---|
| Postgres chưa sẵn sàng khi service start | Bỏ depends_on ... service_healthy, docker compose up | Có healthcheck: service đợi. Không có: thấy lỗi, ghi lại vì sao cần |
| Postgres chết khi đang chạy | docker compose stop postgres | Readiness DOWN, liveness UP, không restart loop |
| Redis chết | docker compose stop redis | Catalog/booking readiness DOWN (hoặc degrade nếu bạn chọn vậy — ghi ADR) |
Thiếu .env | Xóa .env | Compose báo biến trống; không có default password ngầm |
7. Số liệu cần thu
- Thời gian
docker compose upđến khi mọi thứ healthy. - Thời gian start mỗi service, RAM mỗi container (
docker stats). - Lưu ở
docs/benchmarks/m0-baseline.md. Đây là mốc để biết hạ tầng có vừa máy bạn không.
8. Checklist hoàn thành
- ☐ Acceptance criteria có test tự động và xanh
- ☐ Failure scenario đã chạy thật, có ghi kết quả
- ☐
docs/benchmarks/m0-baseline.md - ☐ ADR-001: chọn database-per-service; ADR-002: Kafka cho domain event, RabbitMQ cho notification
- ☐ Không hardcode secret, có
.env.example,.envtrong.gitignore - ☐
docs/requirements.mdcó phép tính capacity - ☐ Prometheus scrape được 5 service
- ☐ Đã qua review + phỏng vấn của AI
8 container cộng 5 service, máy mình 16 GB chạy nổi không? Keycloak với Kafka nặng lắm.
Lo đúng chỗ. Gợi ý hướng, bạn tự đo:
- Dùng compose profiles:
core(postgres, redis),messaging(kafka, rabbitmq),auth,obs. M1–M3 chỉ cầncore+ kafka. - Kafka KRaft một node, không cần ZooKeeper. Đặt heap nhỏ qua biến env của image.
- Keycloak chạy
start-devở local là đủ. - Service Java: giới hạn
-XX:MaxRAMPercentagevàmem_limittrong compose. Xem lại bài cgroups để hiểu JVM thấy bao nhiêu RAM.
Câu hỏi để bạn tự trả lời: chạy service Java trong IDE hay trong compose thì tiện debug hơn? Ghi lựa chọn vào README.
Ước lượng QPS mình không chắc hệ số peak lấy bao nhiêu. Lấy đại 10 được không?
Gần đúng, nhưng flash sale khác traffic thường. Đừng lấy một hệ số chung. Gợi ý:
- Tách 2 kịch bản: ngày thường (peak ≈ 2–3× trung bình) và flash sale (tính trực tiếp: 100k người, mỗi người refresh bao nhiêu lần trong 5 phút?).
- Tách đọc và ghi. Đọc có thể cache, ghi giữ ghế thì không.
- Quan trọng nhất: số ghế chỉ 20k. Số giữ ghế thành công bị giới hạn bởi số ghế, còn số thử thì không. Con số nào quyết định thiết kế?
Trả lời được câu cuối là bạn đã có lý do cho M7.
Healthcheck của Kafka viết sao? Image không có curl.
Đúng, nhiều image tối giản không có curl. Hướng đi: dùng chính script CLI có sẵn trong image (tìm trong /opt/kafka/bin), gọi một lệnh nhẹ tới localhost:9092. Lệnh trả exit code 0 là healthy. Đặt start_period đủ dài vì Kafka khởi động chậm. Tương tự cho Keycloak: bật health endpoint bằng biến KC_HEALTH_ENABLED rồi tìm cách gọi nó không cần curl.
Mình làm xong rồi, review và phỏng vấn mình đi.
OK. Mình soi theo checklist này trước:
Checklist review
- Có password/secret nào trong YAML, compose, Dockerfile, git history không?
depends_oncó dùngservice_healthyhay chỉ thứ tự start?- Liveness có phụ thuộc DB không? (Không được — DB chết sẽ làm restart loop.)
- Actuator có expose
env,heapdumpra ngoài không? - Capacity có phép tính và giả định rõ, hay chỉ có con số?
- Port, tên DB, tên topic có khớp quy ước chung không?
Câu hỏi phỏng vấn
1. Bạn ước lượng tải hệ thống thế nào?
🇻🇳 Tiếng Việt:
Em tách đọc và ghi, ngày thường và flash sale. Ngày thường khoảng 230 QPS đọc trung bình, peak 3 lần. Flash sale tính theo số người và số lần refresh. Ghi giữ ghế bị chặn bởi số ghế nên em tập trung vào chống tranh chấp, còn đọc thì cache.
🇬🇧 English:
I split reads from writes and normal days from flash sales. Normal reads are about 230 QPS with a 3x peak. For flash sales I estimate from users and refresh rate. Successful holds are bounded by seat count, so the hard part is contention, while reads are cached.
2. Liveness và readiness khác gì?
🇻🇳 Tiếng Việt:
Liveness hỏi "process còn sống không", fail thì restart. Readiness hỏi "nhận traffic được chưa", fail thì bỏ khỏi load balancer. DB chết chỉ làm readiness fail, không restart.
🇬🇧 English:
Liveness asks whether the process is alive; failing it triggers a restart. Readiness asks whether it can serve traffic; failing it removes the instance from the load balancer. A database outage should only fail readiness.
3. Vì sao mỗi service một database?
🇻🇳 Tiếng Việt:
Để service độc lập về schema và deploy. Đổi lại không join chéo được và phải dùng event, saga để giữ nhất quán.
🇬🇧 English:
It keeps schemas and deployments independent. The trade-off is no cross-service joins, so consistency needs events and sagas.
4. Secret được quản lý thế nào?
🇻🇳 Tiếng Việt:
Local dùng file .env không commit, có .env.example. Lên K8s dùng Secret hoặc vault. Code chỉ đọc biến môi trường.
🇬🇧 English:
Locally I use an uncommitted .env with a committed .env.example. In Kubernetes I use Secrets or a vault. The code only reads environment variables.
Cách nhớ ngắn gọn
- Requirements có số, capacity có phép tính.
- Healthcheck cho mọi container,
service_healthycho mọi phụ thuộc. - Liveness không phụ thuộc DB.
- Secret chỉ qua
${ENV}.
Tự kiểm tra
Vì sao liveness probe không nên kiểm tra DB?
DB chết làm mọi instance restart liên tục mà không giải quyết gì. Chỉ readiness nên phụ thuộc DB để rút instance khỏi traffic.
depends_on không có condition đảm bảo điều gì?
Chỉ đảm bảo container được start trước, không đảm bảo nó sẵn sàng nhận kết nối.
Trong flash sale, con số nào quyết định thiết kế ghi?
Số lần thử giữ ghế đồng thời trên cùng ít ghế (tranh chấp), không phải số ghế bán được.
Commit gì và không commit gì về config?
Commit .env.example với giá trị giả; không commit .env; YAML chỉ chứa ${ENV}.