M11 · High availability

5 câu hỏi~4–5 ngày codeTừ khóa: Kafka RF=3, min.insync.replicas, quorum queue, Redis Sentinel, Postgres replica, Kubernetes, Helm, probe, PDB, HPA, GitHub Actions

Giờ mọi thứ đều chạy 1 bản: 1 Kafka, 1 Redis, 1 Postgres. Mình muốn làm M11: dựng cluster, deploy lên Kubernetes, kill thử từng thứ mà booking vẫn chạy.

Mục tiêu M11 không phải "nhiều bản cho đẹp". Mục tiêu là chứng minh bằng thực nghiệm: mất một node bất kỳ, khách vẫn giữ ghế và thanh toán được, không mất event, không bán trùng.

1. Bối cảnh & bài toán

2. Ôn lại trước khi code

3. Đề bài

3.1 Hạ tầng HA (docker compose profile ha, chạy local trước)

Thành phầnCấu hình bắt buộc
Kafka3 broker KRaft. Topic booking.events, payment.events: replication.factor=3, min.insync.replicas=2, unclean.leader.election.enable=false. Producer: acks=all, enable.idempotence=true.
RabbitMQCluster 3 node (cùng Erlang cookie từ env). Queue notification là x-queue-type: quorum, publisher confirm bật, DLX giữ nguyên từ M7.
Redis1 master + 2 replica + 3 Sentinel, quorum 2. Spring dùng spring.data.redis.sentinel.master/nodes.
Postgres1 primary + 1 streaming replica. Catalog đọc danh sách event từ replica (tùy chọn), booking chỉ đọc/ghi primary.
ServiceMỗi service ≥ 2 instance, stateless.
# application.yml (booking)
spring.kafka.bootstrap-servers: ${KAFKA_BOOTSTRAP}          # kafka-1:9092,kafka-2:9092,kafka-3:9092
spring.kafka.producer.acks: all
spring.rabbitmq.addresses: ${RABBIT_ADDRESSES}              # rabbit-1:5672,rabbit-2:5672,rabbit-3:5672
spring.data.redis.sentinel.master: ${REDIS_MASTER_NAME}
spring.data.redis.sentinel.nodes: ${REDIS_SENTINELS}
spring.lifecycle.timeout-per-shutdown-phase: 20s
server.shutdown: graceful

3.2 Kubernetes (kind hoặc minikube) + Helm

deploy/helm/
├── ticket-service/          # 1 chart dùng chung cho 5 service
│   ├── Chart.yaml
│   ├── values.yaml          # image, replicas, resources, probes, env
│   └── templates/
│       ├── deployment.yaml  # TODO: readiness/liveness/startup probe
│       ├── service.yaml
│       ├── pdb.yaml         # TODO: minAvailable: 1
│       ├── hpa.yaml         # TODO: min 2, max 6, CPU 70%
│       └── configmap.yaml   # secret lấy từ Secret, KHÔNG nằm trong values
├── values-booking.yaml
├── values-payment.yaml
└── ...
# hạ tầng: dùng chart có sẵn (bitnami/strimzi/...) hoặc giữ compose ngoài cluster, ghi lý do vào ADR
ProbeEndpointGợi ý
startup/actuator/health/livenessCho JVM đủ thời gian khởi động
liveness/actuator/health/livenessKhông phụ thuộc DB/Kafka (tránh restart dây chuyền)
readiness/actuator/health/readinessCó thể gồm DB; khi shutdown phải trả DOWN trước

3.3 CI (GitHub Actions) .github/workflows/ci.yml:

on: [push, pull_request]
jobs:
  test:      # TODO: setup-java 21 + cache Maven/Gradle, chạy unit + Testcontainers IT
  build:     # needs: test; TODO: build image từng service, tag ${{ github.sha }}
  push:      # chỉ trên main; login registry bằng ${{ secrets.REGISTRY_TOKEN }}
  helm-lint: # helm lint + helm template | kubeconform

4. Acceptance criteria

  1. kafka-topics --describe cho thấy RF=3, ISR=3 cho cả hai topic.
  2. Kill 1 broker trong lúc k6 chạy: không mất event (đếm event outbox đã gửi = event consumer nhận, theo id), booking vẫn confirm được.
  3. Kill 2 broker: producer bị từ chối (NotEnoughReplicas), outbox giữ event, khi broker sống lại thì event được gửi tiếp. Không mất, không trùng ở phía nghiệp vụ (inbox).
  4. Kill 1 node RabbitMQ: notification vẫn được xử lý, không mất message.
  5. Kill Redis master: Sentinel bầu master mới trong < 30 giây; tỉ lệ lỗi hold trong lúc failover được đo và ghi lại; không bán trùng ghế (DB vẫn là nguồn sự thật).
  6. Promote Postgres replica theo runbook docs/runbooks/postgres-failover.md, ghi RTO/RPO đo được.
  7. kubectl delete pod booking trong lúc k6 chạy: lỗi 5xx ≤ 0.5%.
  8. Rolling update phiên bản mới: 0 request lỗi (graceful shutdown + readiness).
  9. kubectl drain node: PDB giữ ít nhất 1 pod mỗi service.
  10. HPA tăng replica khi k6 tăng tải, giảm lại khi hết tải.
  11. CI xanh trên PR; image được tag theo SHA; không secret nào trong workflow.

5. Test cases

IDKịch bảnInput / điều kiệnKỳ vọng
H1Kafka mất 1 brokerk6 50 VU, docker stop kafka-2Không mất event, lỗi ≤ 0.5%
H2Kafka mất 2 brokerstop kafka-2, kafka-3Outbox pending tăng, không 5xx ở hold; hồi phục tự gửi hết
H3Redis failoverdocker stop redis-masterMaster mới < 30s, không trùng ghế
H4RabbitMQ mất nodestop rabbit-1 (leader của queue)Leader mới, message vẫn được tiêu thụ
H5Pod chếtkubectl delete pod -l app=booking (1 pod)Lỗi ≤ 0.5%
H6Rolling updatehelm upgrade đổi image tag0 lỗi
H7Drain nodekubectl drainBị PDB chặn đúng mức
H8Postgres primary chếtstop primary, promote replicaRTO, RPO ghi lại
// k6: chạy song song khi kill, check không trùng ghế sau đó
export const options = { scenarios: { holds: { executor: 'constant-arrival-rate', rate: 50, timeUnit: '1s', duration: '5m', preAllocatedVUs: 100 } } };
export default function () { /* TODO: hold ghế ngẫu nhiên, confirm, ghi metric lỗi theo status */ }

-- sau test: phải trả 0 dòng
SELECT seat_id, count(*) FROM bookings WHERE status = 'CONFIRMED' GROUP BY seat_id HAVING count(*) > 1;

// Testcontainers: Kafka 3 broker hơi nặng, có thể chạy test này riêng (tag "ha")
@Tag("ha") class KafkaBrokerLossIT { /* TODO: stop 1 container, gửi 1000 event, đếm nhận đủ */ }

6. Failure scenarios

Sự cốCách tạoHành vi mong đợi
Broker Kafka chếtdocker stop kafka-2Leader chuyển, producer retry, không mất event
Mất quorum Kafka ISRStop 2/3 brokerGhi bị từ chối, outbox giữ hàng, hồi phục tự động
Redis master chếtdocker stop redis-masterSentinel failover; client tự kết nối master mới
Split brain RedisCô lập master bằng docker network disconnectGhi vào master cũ có thể mất; DB vẫn chặn bán trùng
RabbitMQ node chếtdocker stop rabbit-1Quorum queue bầu leader mới
Pod bị OOMKillHạ limits.memory thấpRestart, readiness chặn traffic tới khi sẵn sàng
Pod bị SIGTERM giữa requestRolling updateRequest đang chạy hoàn tất, không nhận request mới
Image lỗiDeploy tag có app crash khi startRollout dừng, pod cũ vẫn phục vụ; helm rollback

7. Số liệu cần thu

Lưu docs/benchmarks/m11-ha.md: với mỗi sự cố H1–H8 ghi thời gian phát hiện, thời gian hồi phục, số request lỗi, p99 trong lúc sự cố, có mất/trùng dữ liệu không. So throughput acks=1 và acks=all (cái giá của độ bền). Chụp dashboard M10 trong lúc kill.

8. Checklist hoàn thành

Sentinel báo đã có master mới rồi, mà booking-service vẫn lỗi READONLY You can't write against a read only replica cả phút.

Gợi ý hướng điều tra:

Mình đã bật server.shutdown: graceful rồi mà rolling update vẫn có vài request 502.

Vẽ lại dòng thời gian khi pod nhận SIGTERM:

t0  kubelet gửi SIGTERM ─┐
t0  endpoint bị gỡ khỏi Service ─ lan tới kube-proxy/ingress mất một lúc ─┐
                         └─ Spring dừng nhận request ngay?

Câu hỏi cho bạn: trong khoảng giữa hai sự kiện đó, request mới đi đâu? Gợi ý tìm hiểu preStop hook và terminationGracePeriodSeconds. Ngoài ra kiểm tra Dockerfile: JVM có phải PID 1 nhận được signal không.

CI mất 25 phút, chủ yếu là integration test. Có cách nào?

Gợi ý, bạn tự đo trước và sau:

Xem lại Build & quality gate và Flaky test.

Mình làm xong rồi, review và phỏng vấn mình đi.

Checklist review AI sẽ soi

Câu hỏi phỏng vấn

1. RF=3, min.insync.replicas=2, acks=all nghĩa là gì? Mất 2 broker thì sao?

🇻🇳 Mỗi partition có 3 bản; ghi chỉ thành công khi ít nhất 2 bản trong ISR đã nhận. Mất 1 broker vẫn ghi được. Mất 2 thì từ chối ghi để không mất dữ liệu; outbox giữ event tới khi hồi phục.

🇬🇧 Each partition has three replicas and a write succeeds only when at least two in-sync replicas have it. Losing one broker is fine; losing two makes writes fail rather than risk data loss, and the outbox buffers events until recovery.

2. Redis Sentinel failover có thể làm mất dữ liệu không? Ảnh hưởng gì tới booking?

🇻🇳 Có, replication async nên ghi gần nhất có thể mất. Vì vậy Redis chỉ là cache/rate limit/lock tối ưu; chống bán trùng do DB đảm bảo.

🇬🇧 Yes, replication is asynchronous so recent writes can be lost. That is why Redis is only a cache and an optimisation; the database guarantees no double booking.

3. Liveness và readiness khác nhau thế nào?

🇻🇳 Liveness sai thì kubelet restart pod. Readiness sai thì pod bị gỡ khỏi Service nhưng không restart. Phụ thuộc bên ngoài chỉ nên ở readiness.

🇬🇧 A failing liveness probe restarts the container; a failing readiness probe only removes it from load balancing. External dependencies belong in readiness, not liveness.

4. PDB và HPA giải quyết vấn đề gì?

🇻🇳 PDB giới hạn số pod bị gỡ cùng lúc khi bảo trì (drain). HPA thay đổi số replica theo tải. Hai thứ bổ sung nhau.

🇬🇧 A PDB limits voluntary disruptions such as node drains; an HPA scales replicas with load. One protects availability during maintenance, the other during traffic changes.

Cách nhớ ngắn gọn

Kafka 3/2/all · Rabbit quorum 3 · Redis Sentinel 3 · Postgres primary+replica
Redis mất ghi được → DB là chốt chặn cuối
liveness = còn sống · readiness = nhận traffic
PDB = bảo trì · HPA = tải · preStop = khoảng trống khi SIGTERM
CI: test → build image (SHA) → helm lint

Tự kiểm tra

Vì sao tắt unclean leader election?

Để không bầu replica tụt hậu làm leader, tránh mất dữ liệu đã được xác nhận. Đổi lại có thể không ghi được một lúc.

Vì sao Redis Sentinel cần ít nhất 3 Sentinel?

Để đạt quorum và bầu được leader Sentinel khi một node chết, tránh split brain.

Pod nhận SIGTERM nhưng vẫn có request 502, sửa thế nào?

Thêm preStop sleep ngắn để endpoint được gỡ khỏi load balancer trước, kết hợp graceful shutdown và grace period đủ dài.

Quorum queue khác classic mirrored queue ở điểm nào?

Dùng Raft, ghi được xác nhận khi đa số node có; an toàn hơn khi node chết và mạng chia cắt.

Vì sao tag image bằng commit SHA thay vì latest?

Để biết chính xác code nào đang chạy, rollback được, và K8s nhận ra thay đổi để rollout.