M11 · High availability
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
- 1 broker Kafka chết = outbox dồn lại, không có payment event, booking treo PENDING_PAYMENT.
- Redis chết = rate limit và cache hỏng; nếu lock giữ ghế dựa vào Redis, có thể bán trùng khi failover.
- Postgres chết = toàn hệ thống dừng. M11 chỉ yêu cầu replica + quy trình promote, không cần auto-failover hoàn hảo.
- Deploy bằng tay = mỗi lần release là một lần downtime. Cần K8s rolling update, probe, PDB và CI build image tự động.
2. Ôn lại trước khi code
- High Availability: SPOF, redundancy, failover, đo availability.
- Kafka replication: ISR,
acks=all,min.insync.replicas, unclean leader election. - RabbitMQ quorum queue: Raft, vì sao cluster 3 node chịu được 1 node chết.
- Replication, Sentinel, Cluster: quorum Sentinel, client phải hỏi Sentinel để biết master.
- Distributed lock: lock Redis mất khi failover async replication.
- Postgres replication: streaming replica, replication lag, đọc từ replica.
- Pod & probe: liveness khác readiness, startup probe cho JVM.
- Deployment, StatefulSet: stateless dùng Deployment.
- Resource & autoscaling: request/limit, HPA theo CPU hoặc metric custom.
- Helm: chart, values theo môi trường,
helm upgrade --install. - CI pipeline và CD & zero-downtime: cache build, tag image theo commit SHA.
- PID 1 & signal: graceful shutdown khi pod bị kill.
3. Đề bài
3.1 Hạ tầng HA (docker compose profile ha, chạy local trước)
| Thành phần | Cấu hình bắt buộc |
|---|---|
| Kafka | 3 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. |
| RabbitMQ | Cluster 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. |
| Redis | 1 master + 2 replica + 3 Sentinel, quorum 2. Spring dùng spring.data.redis.sentinel.master/nodes. |
| Postgres | 1 primary + 1 streaming replica. Catalog đọc danh sách event từ replica (tùy chọn), booking chỉ đọc/ghi primary. |
| Service | Mỗ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
| Probe | Endpoint | Gợi ý |
|---|---|---|
| startup | /actuator/health/liveness | Cho JVM đủ thời gian khởi động |
| liveness | /actuator/health/liveness | Không phụ thuộc DB/Kafka (tránh restart dây chuyền) |
| readiness | /actuator/health/readiness | Có 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
kafka-topics --describecho thấy RF=3, ISR=3 cho cả hai topic.- 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.
- 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). - Kill 1 node RabbitMQ: notification vẫn được xử lý, không mất message.
- 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).
- Promote Postgres replica theo runbook
docs/runbooks/postgres-failover.md, ghi RTO/RPO đo được. kubectl delete podbooking trong lúc k6 chạy: lỗi 5xx ≤ 0.5%.- Rolling update phiên bản mới: 0 request lỗi (graceful shutdown + readiness).
kubectl drainnode: PDB giữ ít nhất 1 pod mỗi service.- HPA tăng replica khi k6 tăng tải, giảm lại khi hết tải.
- CI xanh trên PR; image được tag theo SHA; không secret nào trong workflow.
5. Test cases
| ID | Kịch bản | Input / điều kiện | Kỳ vọng |
|---|---|---|---|
| H1 | Kafka mất 1 broker | k6 50 VU, docker stop kafka-2 | Không mất event, lỗi ≤ 0.5% |
| H2 | Kafka mất 2 broker | stop kafka-2, kafka-3 | Outbox pending tăng, không 5xx ở hold; hồi phục tự gửi hết |
| H3 | Redis failover | docker stop redis-master | Master mới < 30s, không trùng ghế |
| H4 | RabbitMQ mất node | stop rabbit-1 (leader của queue) | Leader mới, message vẫn được tiêu thụ |
| H5 | Pod chết | kubectl delete pod -l app=booking (1 pod) | Lỗi ≤ 0.5% |
| H6 | Rolling update | helm upgrade đổi image tag | 0 lỗi |
| H7 | Drain node | kubectl drain | Bị PDB chặn đúng mức |
| H8 | Postgres primary chết | stop primary, promote replica | RTO, 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ạo | Hành vi mong đợi |
|---|---|---|
| Broker Kafka chết | docker stop kafka-2 | Leader chuyển, producer retry, không mất event |
| Mất quorum Kafka ISR | Stop 2/3 broker | Ghi bị từ chối, outbox giữ hàng, hồi phục tự động |
| Redis master chết | docker stop redis-master | Sentinel failover; client tự kết nối master mới |
| Split brain Redis | Cô lập master bằng docker network disconnect | Ghi vào master cũ có thể mất; DB vẫn chặn bán trùng |
| RabbitMQ node chết | docker stop rabbit-1 | Quorum queue bầu leader mới |
| Pod bị OOMKill | Hạ limits.memory thấp | Restart, readiness chặn traffic tới khi sẵn sàng |
| Pod bị SIGTERM giữa request | Rolling update | Request đang chạy hoàn tất, không nhận request mới |
| Image lỗi | Deploy tag có app crash khi start | Rollout 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
- ☐ Acceptance criteria đều có test hoặc script tái tạo được, kết quả xanh.
- ☐ Mọi failure scenario đã chạy thật, có số liệu.
- ☐ Benchmark lưu trong
docs/benchmarks/. - ☐ ADR: "Hạ tầng stateful chạy trong K8s hay ngoài" và "acks=all + min.insync=2".
- ☐ Không hardcode secret; K8s Secret/GitHub Secrets, không nằm trong values.yaml.
- ☐ Runbook cho Kafka broker, Redis failover, Postgres promote trong
docs/runbooks/. - ☐ Helm chart lint sạch, deploy được bằng 1 lệnh.
- ☐ CI chạy test + build image trên mỗi PR.
- ☐ Đã qua lượt review và phỏng vấn của AI.
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:
- App đang kết nối tới Redis qua Sentinel hay qua địa chỉ cố định của master cũ? Xem lại cấu hình thật sự được nạp (
/actuator/env). - Master cũ sống lại sẽ thành replica. Connection cũ trong pool vẫn trỏ vào nó. Lettuce có lắng nghe sự kiện
+switch-masterkhông? - Trong docker/K8s, Sentinel quảng bá IP nào? IP nội bộ container có đúng với IP app nhìn thấy không (
announce-ip)?
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:
- Cache dependency của build tool (
actions/setup-javacócache:). - Container có được dùng lại giữa các test class không (static container, singleton pattern)?
- Chạy song song từng service bằng
matrix. - Test HA nặng (3 broker) gắn tag riêng, chạy nightly thay vì mỗi PR.
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
- Liveness có gọi DB/Kafka không (nếu có: DB chậm là toàn bộ pod bị restart).
- Resource request/limit có hợp lý với heap (
-XX:MaxRAMPercentage)? - Tính đúng bán ghế có phụ thuộc Redis lock không, hay DB constraint/version vẫn là chốt chặn cuối?
- Producer có
acks=all+ idempotence; consumer vẫn dedup bằng inbox. - Secret có lọt vào values.yaml, log CI, image layer không.
- HPA dựa trên CPU có hợp lý với service chủ yếu chờ I/O không.
- Số liệu sự cố có đi kèm ảnh dashboard và lệnh tái tạo.
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.