M0 · Requirements, capacity, dựng hạ tầng

5 câu hỏi~2–3 ngày codeTừ khóa: FR/NFR, SLO, QPS, capacity estimate, docker-compose, healthcheck, actuator, .env, skeleton service

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:

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

3. Đề bài

3.1. Tài liệu docs/requirements.md phải có các mục:

MụcNội dung tối thiểu
FRXem 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
NFRKhô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 viFrontend, hoàn tiền thật, multi-region (để M11)
CapacityBảng giả định + phép tính (xem dưới)
Rủi roHot 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:

ContainerImage gợi ýPortHealthcheck
postgrespostgres:165432pg_isready
redisredis:76379redis-cli ping
kafkaapache/kafka:3.7 (KRaft)9092list topics
rabbitmqrabbitmq:3-management5672, 15672rabbitmq-diagnostics ping
keycloakquay.io/keycloak/keycloak:258180/health/ready
toxiproxyghcr.io/shopify/toxiproxy8474 + proxy port/version
prometheusprom/prometheus9090/-/ready
grafanagrafana/grafana3000/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

ServicePortDependency tối thiểu
gateway8080spring-cloud-starter-gateway, actuator
catalog-service8081web, data-jpa, postgresql, flyway, actuator, micrometer-registry-prometheus
booking-service8082như catalog + data-redis, spring-kafka
payment-service8083như catalog
notification-service8084amqp, 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ó:

MethodPathResponse
GET/actuator/health{"status":"UP"}
GET/actuator/health/readinessUP khi DB/Redis/Kafka sẵn sàng
GET/actuator/prometheusmetrics 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

  1. docs/requirements.md có đủ FR, NFR (có số), ngoài phạm vi, capacity có phép tính, rủi ro.
  2. docker compose up -d trên máy sạch: cả 8 container hạ tầng healthy trong ≤ 3 phút.
  3. 5 service trả /actuator/health = UP; readiness = DOWN khi tắt Postgres.
  4. Prometheus target page: 5 service đều UP.
  5. Keycloak tự import realm ticket từ infra/keycloak/realm.json.
  6. grep -r "password:" */src/main/resources chỉ thấy ${...}.
  7. GET /api/events qua gateway trả 200.

5. Test cases

IDKịch bảnInput / điều kiệnKỳ vọng
M0-T1Context load@SpringBootTest + Testcontainers PostgresContext lên, Flyway chạy xong
M0-T2Health UPGET /actuator/health200, UP
M0-T3Readiness khi DB chếtDừng container Postgres/health/readiness 503, liveness vẫn 200
M0-T4Thiếu biến envBỏ BOOKING_DB_URLService fail-fast khi start, log rõ tên biến
M0-T5Gateway routeGET :8080/api/events200 []
@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ạoHành vi mong đợi
Postgres chưa sẵn sàng khi service startBỏ depends_on ... service_healthy, docker compose upCó healthcheck: service đợi. Không có: thấy lỗi, ghi lại vì sao cần
Postgres chết khi đang chạydocker compose stop postgresReadiness DOWN, liveness UP, không restart loop
Redis chếtdocker compose stop redisCatalog/booking readiness DOWN (hoặc degrade nếu bạn chọn vậy — ghi ADR)
Thiếu .envXóa .envCompose báo biến trống; không có default password ngầm

7. Số liệu cần thu

8. Checklist hoàn thành

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:

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 ý:

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â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


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}.