8 lab RabbitMQ và Kafka
Từ routing đến incident drill: chủ động tạo lỗi, xác định dữ liệu nằm ở đâu tại thời điểm crash, rồi dùng log, ledger và metric để chứng minh khả năng phục hồi.
Trước khi chạy: giao thức bằng chứng chung
Chọn bất kỳ ngôn ngữ, thư viện client, database có transaction/unique constraint và công cụ triển khai phù hợp. Không bắt buộc Spring, Java, Docker, Kubernetes hay một hệ thống monitoring cụ thể. Cần broker thật cho kiểm thử semantics; mock chỉ dùng để điều khiển downstream và failpoint.
| Thành phần | Điều kiện phải ghi trong manifest |
|---|---|
| RabbitMQ | AMQP 0-9-1; ghi chính xác broker/client version, queue type, policy, durability và giới hạn. Lab 02/04 cần quorum queue có 3 replica thực sự; kiểm tra membership, không suy ra từ số node trong cluster. |
| Kafka | Consumer group thông thường, không dùng Share Consumer. Ghi broker/client version, partitioner, serializers, assignment strategy. Lab 05–07 dùng replication factor 3, min.insync.replicas=2, acks=all, producer idempotence bật rõ ràng. Tắt unclean leader election cho profile này. |
| HA độc lập | Cần 3 broker Kafka và controller quorum vẫn đủ đa số khi dừng 2 broker dữ liệu: dùng controller process riêng. Một máy chạy nhiều process chỉ chứng minh lỗi process, không chứng minh độc lập host/AZ hoặc disaster recovery. |
| Điều khiển lỗi | Test controller bên ngoài process bị kill, kênh điều khiển riêng, quyền chỉ trong namespace lab. Đặt failpoint theo event_id + stage, rồi chờ xác nhận đã dừng đúng stage trước khi gây lỗi. |
| Ngưỡng thử nghiệm | Đề xuất ban đầu: chờ gate 30 giây, phục hồi 120 giây, watchdog mỗi ca 300 giây; scrape 2 giây. Khai báo trước ngưỡng RAM, processing age và thời gian drain phù hợp máy chạy. Đây là ngưỡng lab, không phải SLO production hay benchmark đã đo. |
Fixture, định danh và reset
Mỗi ca có fixture bất biến, UTF-8, SHA-256 và thứ tự gửi được lưu lại. Envelope tối thiểu: event_id, accountId, sequence, schema_version, payload. Mỗi lần gửi có publish_attempt_id riêng; retry/replay của cùng nghiệp vụ không đổi event_id. Dedupe theo (handler_id, event_id); lưu thêm payload hash để phát hiện cùng ID nhưng khác nội dung. Không lấy delivery tag, offset mới hoặc cờ redelivered làm business idempotency key.
Chạy r1, r2, r3 trong namespace tách biệt, cùng fixture và cấu hình; reset cả broker state lẫn DB giữa các ca độc lập. Trong một ca crash/replay phải giữ nguyên ledger và state cần phục hồi. Retention/dedupe retention phải dài hơn toàn bộ cửa sổ thử nghiệm. Không xóa topic, purge queue hoặc reset offset của ứng dụng thật.
Hook có xác nhận, không “sleep rồi kill”
PREPARE(case, namespace, fixture_hash, config_hash)
ARM(event_id, stage) # Hook chỉ kích hoạt một lần
WAIT(controller_received_STAGE_REACHED) # Worker đang bị giữ tại barrier
SNAPSHOT(broker_state, committed_DB, durable_publish_ledger)
INJECT(fault); VERIFY(process_exit_or_fault_state)
RECOVER(); WAIT(terminal_invariants, deadline)
ASSERT_ALL(); EXPORT(raw_evidence, assertions, hashes)
FINALLY(remove_faults, restore_policies, stop_load_generator)
| Artifact | Nội dung kiểm chứng |
|---|---|
manifest.json | Version, commit của runner, CPU/RAM/disk, namespace, topology/config hash, fixture hash, ngưỡng đã chốt, timeout, thời điểm bắt đầu/kết thúc, fault đã bật và đã gỡ. |
events.jsonl | Chuỗi sự kiện có số thứ tự, process/instance, monotonic time, event/attempt ID, stage, kết quả publish, DB transaction, ack/commit, lỗi và owner generation. UTC chỉ để tương quan giữa máy; không lấy clock lệch giữa host để kết luận thứ tự. |
state-before.json, state-after.json, metrics.csv | Snapshot topology, queue/partition/ISR/assignment/checkpoint; metric có timestamp, đơn vị, nhãn; counter reset phải được đánh dấu. Không dùng ảnh chụp thay raw data. |
ledger.csv, assertions.json | Export dữ liệu đã commit bằng connection độc lập; mỗi acceptance ID có expected, actual, PASS/FAIL/BLOCKED và evidence path. Kiểm cả tập ID, số lần side effect, hash nội dung, không chỉ tổng count. |
reproduce.md, checksums.txt | Lệnh setup/run/verify/cleanup thật của implementation, exit code kiểm tra và hash evidence. Không đưa token, private key, password hay payload nhạy cảm vào hồ sơ. |
(queue, event_id); với Kafka, tách record vật lý khỏi event nghiệp vụ. Event có publish outcome chưa rõ phải được đối soát/retry, không được tự gắn “đã mất” hoặc “đã thành công”.Mỗi hàng acceptance là một gate; các điều kiện nối bằng “và” trong cùng hàng đều bắt buộc. Có 8 gate/lab × 8 lab = 64 gate; ba lần chạy tương đương 192 kết quả gate, chưa tính các subcase hoặc hàng benchmark. Một gate chỉ PASS khi đủ evidence ở cả ba lần. Negative control PASS khi tái hiện đúng lỗi đã dự đoán, rồi bản sửa vượt qua oracle tương ứng.
Lab 01 · RabbitMQ routing
Goal · Mục tiêu
Tạo direct/topic/fanout topology, mandatory publish và alternate exchange. Kiểm chứng route đúng, unroutable được quan sát và redeclare sai attributes làm channel fail.
Mô hình cần chứng minh. Routing quyết định queue nào nhận message; confirm quyết định kết quả publish ở phía broker. Hai câu hỏi này phải có hai oracle riêng. Message được alternate exchange route thành công không còn là unroutable đối với mandatory. [R1] [R2] [R11]
Fixture và cấu hình
Dùng bốn message cố định: M1=order.created, M2=order.paid, M3=stock.created, M4=no.route. Gửi đúng một lần mỗi message vào từng exchange trong subcase riêng, bật confirms và mandatory=true. Giữ consumer tắt đến khi đã ghi hết confirm/return để tách routing khỏi processing.
| Exchange / queue | Binding | Tập nhận |
|---|---|---|
ex.direct → q.created | order.created | M1 |
ex.direct → q.paid | order.paid | M2 |
ex.topic → q.orders | order.* | M1, M2 |
ex.topic → q.created.any | *.created | M1, M3 |
ex.fanout → q.a, q.b | Không lọc theo routing key | M1–M4 ở mỗi queue: tổng 8 queue copies |
ex.ae → q.unrouted | Fanout; AE của exchange thử riêng | M4 khi primary không route được |
Việc phải làm
- Khai báo exchange/queue/binding bằng cấu hình versioned; dùng durable queue, không auto-delete. Export cấu hình, chạy lại cùng declaration và xác nhận không tạo tài nguyên mới.
- Chạy ba subcase direct/topic/fanout từ state sạch. Lưu publish sequence → event ID, confirm, return và routing key; sau đó drain vào ledger theo
(queue, event_id)và so set với bảng oracle. - Tạo exchange thử AE riêng: gửi M4 khi AE có binding, rồi xóa binding của AE và gửi M4 trong subcase sạch khác. Không dùng metric aggregate để suy ra có return hay không; phải bắt callback/protocol event của chính message đó.
- Mở channel dùng riêng cho negative control. Khai báo lại một queue đã tồn tại với
durabletrái ngược; sau lỗi, mở channel mới và khai báo đúng để kiểm tra recovery. [R3]
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Không có route | Topology được xác nhận không có AE; publish M4. | Theo dõi cả basic.return và confirm; không coi confirm là bằng chứng message đã vào queue. |
| F02 · AE mất binding | Gỡ binding của q.unrouted, đọc lại topology rồi publish. | Lưu route snapshot trước/sau; M4 phải quay lại publisher khi không có queue đích nào. |
| F03 · Declaration xung đột | Queue đã tồn tại; gửi declaration khác durable trên channel thử. | Bắt 406 PRECONDITION_FAILED; chứng minh lỗi channel, không xóa queue để che lỗi. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L01-A01Topology tái lập | Export đủ ba loại exchange và binding trong oracle; redeclare cùng attributes hai lần không lỗi, tập tài nguyên không đổi. | topology.json, declaration log và diff. |
| L01-A02Direct chính xác | q.created={M1}, q.paid={M2}; tập return là {M3,M4}. Không có queue copy ngoài oracle. | direct-deliveries.csv, returns.jsonl. |
| L01-A03Topic chính xác | q.orders={M1,M2}, q.created.any={M1,M3}; chỉ M4 bị return. M1 xuất hiện ở hai queue là fan-out hợp lệ, không phải lỗi dedupe. | topic-deliveries.csv, set diff rỗng. |
| L01-A04Fanout chính xác | Mỗi queue a/b nhận đúng bốn ID; 8 cặp (queue,event_id), không thiếu hoặc dư cặp. | fanout-deliveries.csv, count và pair-set assertion. |
| L01-A05Mandatory khác confirm | Ở subcase M4 không có AE: có một return NO_ROUTE và confirm ack tương ứng; return xảy ra trước confirm ack trên channel. Queue copies bằng 0. | protocol-order.jsonl có sequence/correlation ID. |
| L01-A06AE có route | Trong subcase AE có binding, M4 vào q.unrouted đúng một lần, payload/ID giữ nguyên, không có return và publish được confirm. | ae-routed.csv, payload hash, callback log. |
| L01-A07AE không route | Trong subcase AE mất binding, M4 được return, không nằm trong queue nào; alert/event unroutable chứa exchange và routing key đúng. | ae-unbound.jsonl, topology snapshot, alert/event record. |
| L01-A08Channel fail và phục hồi | Declaration sai tạo 406 trên channel thử; queue cũ vẫn giữ attributes ban đầu. Channel mới khai báo đúng và publish/consume probe thành công. | channel-error.jsonl, attributes diff, probe ledger. |
Deliverables. topology.*, fixture bốn message, runner setup/reset, routing oracle đọc được bằng máy, callback log, tám assertion và routing-decision.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. Giải thích khi nào dùng return để producer xử lý trực tiếp, khi nào dùng AE để giữ message cho vận hành. Cả hai đều cần ownership và cảnh báo; “không bị return” không chứng minh consumer đã hoàn thành.
↑ Mục lục 8 labLab 02 · Confirm, ack và crash window
Goal · Mục tiêu
Manual ack + publisher confirm. Crash consumer sau DB commit trước ack; chứng minh redelivery nhưng chỉ một side effect nhờ unique idempotency key.
Mô hình cần chứng minh. Đặt business update và inbox marker trong cùng DB transaction, rồi mới ack delivery. Sau crash giữa commit và ack, message có thể được giao lại nhưng business update không được lặp. Confirm và consumer ack độc lập; quorum queue cần đa số replica để tiếp tục tiến triển. [R2] [R4]
Fixture và cấu hình
Fixture chuẩn có E001…E100, mỗi event tăng counter đúng 1. Inbox có unique key (handler_id,event_id); counter và marker commit nguyên tử. Dùng một consumer, manual ack, prefetch 1 cho các ca crash; publisher lưu trạng thái PENDING bền vững trước send, rồi cập nhật kết quả confirm. Các ca HA dùng quorum queue ba replica và tạm tắt consumer để giữ backlog đã confirm.
on_delivery(event):
BEGIN DB TRANSACTION
inserted = INSERT inbox(handler_id, event_id, payload_hash) IF ABSENT
if inserted: APPLY business_effect(+1)
else: ASSERT stored_payload_hash == event.payload_hash
COMMIT DB TRANSACTION
HIT_HOOK("AFTER_DB_COMMIT_BEFORE_ACK", event.id)
ACK(delivery_on_its_original_channel)
Việc phải làm
- Chạy baseline 100 event. Trước khi bật consumer, chụp đủ 100 confirm cùng queue state; sau khi drain, đối chiếu 100 ID với inbox và counter. Không chỉ kiểm số row inbox.
- Đặt hook E050 sau DB commit nhưng trước ack; test controller đọc DB bằng connection khác, xác nhận side effect đã có và ack chưa gửi, rồi kill consumer. Restart và giữ nguyên DB. Chạy thêm ca E025 trước commit: rollback phải được chứng minh trước khi recovery.
- Tạo các negative control riêng: gửi hai bản cùng E005 khi không có unique constraint; và ack E001 trước khi DB commit rồi crash. Lưu kết quả sai, reset, bật thiết kế đúng và chạy lại; không coi negative control là cấu hình ứng dụng đạt yêu cầu.
- Tạo ca một event U001: publisher đã nhận confirm callback nhưng bị giữ trước khi ghi kết quả vào durable publish ledger. Kill publisher, kiểm ledger vẫn PENDING, restart và gửi lại cùng event ID. Đây là outcome không rõ với publisher sau phục hồi, không phải bằng chứng đã mô phỏng mất packet confirm trên mạng.
- Với fixture HA riêng, xác nhận ba replica online và backlog 100 event đã confirm; dừng leader rồi đọc hết dữ liệu. Sau reset, dừng hai replica, chờ trạng thái mất đa số, mới gửi 10 ID mới; phục hồi replica và đối soát/retry pending.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Commit → ack | Hook E050 báo DB committed, ack chưa gửi. | Kill consumer, xác nhận process đã chết và broker phát hiện disconnect; sau restart tìm delivery lặp của E050. |
| F02 · Trước DB commit | Hook E025 giữ transaction chưa commit. | Kill và chứng minh DB không thấy marker/effect chưa commit. |
| F03 · Confirm → publish ledger | U001 đã được broker confirm; durable ledger còn PENDING. | Kill publisher; retry cùng ID tạo ít nhất hai bản giao nhưng chỉ một business effect. |
| F04 · Ack quá sớm | Ack E001 và đợi broker không còn unacked; giữ DB trước commit. | Kill để tạo loss ở negative control; không dùng graceful shutdown tự flush transaction. |
| F05 · Mất replica | Lần lượt một leader chết hoặc hai replica không hoạt động. | Ghi leader/membership trước-sau; không tự hạ replication/durability để làm test “xanh”. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L02-A01Confirm không chờ business effect | Khi consumer tắt, có 100 confirm, 100 event nằm trong backlog, DB effects bằng 0. Khi consumer chạy, set effects đúng E001–E100 và counter=100. | confirm-ledger.csv, queue snapshots, DB export. |
| L02-A02Crash sau commit | E050 đã commit trước kill; sau restart có ít nhất hai delivery attempts của E050, đúng một side effect. Cuối ca có 100 ID và counter=100, queue drain hết. | crash-E050.jsonl, independent DB snapshot, dedupe ledger. |
| L02-A03Crash trước commit | Tại thời điểm kill E025 chưa có effect đã commit; sau recovery E025 có đúng một effect, tổng counter=100, không còn transaction dở. | crash-E025.jsonl, before/after ledger. |
| L02-A04Duplicate có chủ đích | Với 10 ID và gửi thêm E005: negative control không dedupe tạo counter=11; bản sửa tạo counter=10, E005 có một effect. Hai request đồng thời cùng ID cũng không tạo hai effect. | duplicate-control.csv, unique-conflict/transaction log. |
| L02-A05Outcome không rõ của publisher | Ca U001: durable status PENDING sau restart; resend giữ ID; nhận ít nhất hai bản, effect=1. Kết thúc không còn publish attempt chưa đối soát. | publisher-crash.jsonl, durable ledger trước/sau, inbox hash. |
| L02-A06Ack sớm làm mất nghiệp vụ | Ca sai một event: broker đã nhận ack, crash trước DB commit, sau restart DB effect=0 và không có redelivery trong cửa sổ quan sát đã chốt. Ca sửa ack sau commit khôi phục effect=1. | early-ack-negative.jsonl, queue/DB snapshots; không suy kết luận chỉ từ timeout. |
| L02-A07Mất một leader | Tập 100 event đã confirm trước lỗi vẫn được xử lý đủ một lần ở mức business effect; có leader mới, phục hồi trong ngưỡng khai báo, không recreate queue hoặc xóa data. | quorum-one-down.json, 100-ID set diff, recovery timeline. |
| L02-A08Mất đa số rồi phục hồi | Sau khi đã xác nhận mất đa số, không gắn success cho 10 publish mới khi chưa có confirm; lưu timeout/error là failed hoặc unknown đúng thực tế. Phục hồi đa số, retry/đối soát và đạt đủ 10 effect duy nhất. | quorum-two-down.json, attempt states, membership, terminal assertions. |
Deliverables. inbox-schema.*, consumer/publisher adapter, năm nhóm failpoint, script HA có cleanup, ledger nghiệp vụ, publish ledger và guarantee-boundary.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. Ghi rõ idempotency bảo vệ handler nào và trong bao lâu. Unique constraint phải bao trùm transaction nghiệp vụ; check-then-insert không có ràng buộc nguyên tử là chưa đủ. Ack phải gắn đúng channel/delivery; không “khôi phục” bằng ack delivery tag cũ trên channel mới.
↑ Mục lục 8 labLab 03 · Prefetch và backpressure
Goal · Mục tiêu
Benchmark prefetch 1/20/200 với consumer nhanh/chậm. Đo ready/unacked, memory, fairness, throughput; kích hoạt downstream chậm và chọn giới hạn.
Mô hình cần chứng minh. Prefetch giới hạn số delivery chưa ack, không tự giới hạn mọi buffer trong ứng dụng hoặc tốc độ producer. Lab phải đo cả broker backlog, bộ nhớ process và công việc đang giữ ở client. Không mặc định prefetch lớn hơn sẽ tốt hơn. [R5] [R6]
Fixture và cấu hình
Mỗi ô benchmark dùng E0001…E6000, serialized payload được pad đến đúng 1.024 byte/message, cùng TLS, persistence, confirms và topology. Hai consumer, mỗi consumer một channel và một worker xử lý tuần tự, manual ack; không bật single-active-consumer. Prefetch per consumer lần lượt 1 / 20 / 200. Profile F: cả hai downstream stub xử lý 5 ms; profile S: C1 xử lý 5 ms, C2 xử lý 50 ms. Timer của stub là workload có chủ đích, không dùng để đoán thời điểm kill.
Warm-up 200 event trong namespace riêng, không trộn vào số liệu. Prefill 6.000 event đã confirm; đăng ký đủ hai consumer và giữ worker tại barrier để cả hai có backlog trước khi bắt đầu đo. Có 3 prefetch × 2 profile × 3 lần chạy = 18 hàng benchmark, không phải 18 tiêu chí nghiệm thu.
throughput = 6000 / (last_effect_time - worker_release_time)
fairness_J = (n_C1 + n_C2)^2 / (2 * (n_C1^2 + n_C2^2))
backlog_growth ≈ max(0, ingress_rate - sustainable_processing_rate)
catch_up_time ≈ backlog / (recovery_processing_rate - ongoing_ingress_rate)
Việc phải làm
- Chốt trước resource budget, p95 processing-age budget và drain deadline. Ghi rõ queue type, policy, payload bytes, thời gian downstream stub và định nghĩa clock đo age. Thu raw metric mỗi 2 giây cùng log mỗi delivery/effect.
- Chạy đủ ma trận 18 hàng. Với mỗi hàng, lưu ready/unacked, client admitted/in-flight, application buffer bytes, RSS, broker memory, số completion mỗi consumer, throughput, p50/p95 end-to-end age và thời gian drain. Không điền số liệu ước lượng vào cột actual.
- Chạy subcase saturation riêng: chặn cả hai downstream ngay từ đầu, cho broker giao message đến hạn mức rồi chụp state. Với 2 consumer và prefetch P, kiểm broker unacked không vượt
2×P; giữ gate tới khi đã có snapshot, sau đó release. - Chạy subcase producer liên tục, tối đa 6.000 ID và watchdog 300 giây. Khi downstream bị giữ tại barrier, bật admission control: application buffer tối đa 2 MiB payload, số công việc tối đa
2×P, producer outstanding-confirm tối đa 100. Ghi quyết định pause/throttle/reject hoặc lưu pending bền vững; không drop thầm. - Gỡ lỗi, drain toàn bộ event đã được chấp nhận và tính recovery capacity. Chọn cấu hình trên kết quả median/range của ba lần chạy cùng các budget; lưu cả kết quả không đạt thay vì chỉ giữ cấu hình thắng.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Downstream dừng | Cả hai worker đã nhận tín hiệu hold, không cấp thêm completion permit. | Chụp ready/unacked và client buffers sau khi credit đầy; backlog phải được quan sát, không che bằng tăng memory. |
| F02 · Consumer chậm | Profile S dùng latency stub cố định 50 ms cho C2. | So số việc, age và throughput với profile F; đo ảnh hưởng việc giữ nhiều delivery ở consumer chậm. |
| F03 · Ingress vượt khả năng xử lý | Load generator còn event trong fixture; downstream vẫn bị chặn. | Kích hoạt giới hạn đã chốt; dừng phát khi hết fixture hoặc watchdog. Sau release, kiểm mọi ID accepted hoặc pending đều có trạng thái cuối. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L03-A01Ma trận so sánh hợp lệ | Có đủ 18 hàng benchmark, cùng fixture/config ngoại trừ prefetch và profile. Mỗi hàng truy được tới raw metrics, thời gian bắt đầu/kết thúc và namespace sạch. | benchmark.csv, 18 manifest/config hashes. |
| L03-A02Đúng giới hạn credit | Ở subcase saturation, broker unacked ≤2, ≤40, ≤400 tương ứng P=1,20,200; mỗi consumer ≤P. Chụp khi state đứng yên; không dùng một sample thiếu nhãn để suy ra giới hạn. | credit-trace.jsonl, broker snapshots, client counters. |
| L03-A03Không mất dữ liệu trong benchmark | Mỗi hàng hoàn tất có đúng 6.000 business effects, set ID khớp fixture, ready=unacked=0 sau drain. Ca hết deadline phải FAIL/BLOCKED, không được rút bớt mẫu. | ledger.csv, set/hash assertions theo hàng benchmark. |
| L03-A04Đủ metric nguồn | Có ready, unacked, RSS, broker memory, application buffer bytes, per-consumer completions, throughput, processing-age p50/p95 và drain time; khai báo đơn vị và cách tính. | metrics.csv, metric-definitions.md. |
| L03-A05Fairness được giải thích | Tính n_C1/n_C2 và J từ ledger cho cả hai profile; chỉ ra trade-off giữa phân phối công việc và tail age. Kết luận dựa số đo, không bắt buộc “20 luôn tốt nhất” hay tỉ lệ 50/50. | fairness.csv, quyết định có đối chiếu raw data. |
| L03-A06Backpressure hữu hạn | Trong incident subcase, application payload buffer ≤2 MiB, admitted work ≤2P, outstanding confirms ≤100; RSS không vượt budget đã chốt. Khi không nhận thêm được, có throttle/reject/pending ledger rõ ràng, không drop. | admission.jsonl, high-water marks, pending/rejection ledger. |
| L03-A07Phục hồi và catch-up | Sau release, accepted IDs khớp effects, pending được retry/đối soát, queue drain trong deadline. Có đồ thị hoặc CSV tốc độ ingress/processing và phép tính catch-up với đơn vị. | recovery.csv, state-after, accepted-ID set diff. |
| L03-A08Chọn giới hạn có căn cứ | Chọn một prefetch đáp ứng đồng thời budget RAM, age, throughput/drain; ghi headroom và điều kiện phải retest. Nếu không ô nào đạt, kết luận chưa đạt và nêu thay đổi capacity, không tự nới ngưỡng sau khi đo. | capacity-decision.md, bảng PASS/FAIL theo budget. |
Deliverables. load-fixture.*, downstream stub có barrier, runner ma trận 18 hàng, raw metric/ledger, cấu hình backpressure và capacity-decision.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. Một mẫu RSS thấp chưa chứng minh bộ nhớ bị chặn. Phân biệt broker credit, buffer của client library, worker queue và publisher buffer; đo tất cả phần có thể tăng. Giữ cùng durability/security profile khi so hiệu năng.
↑ Mục lục 8 labLab 04 · Retry/DLX operations
Goal · Mục tiêu
Tách transient/permanent error, retry có TTL và bounded attempts, DLQ metadata. Viết replay CLI có dry-run, rate limit và audit; tái hiện dead-letter loop rồi sửa.
Mô hình cần chứng minh. Retry phải phân loại lỗi và có điểm dừng; redelivery do crash không được tự biến thành một lần retry nghiệp vụ mới. DLX không mặc nhiên là chuyển giao không mất dữ liệu: profile an toàn ở đây dùng quorum source queue, dead-letter-strategy=at-least-once và overflow=reject-publish. TTL là điều kiện hết hạn, không phải bộ hẹn giờ chính xác. [R4] [R7] [R8]
Fixture và cấu hình
| Nhóm | Hành vi xác định trước | Kết quả cuối |
|---|---|---|
| G01–G10 · 10 event | Thành công ở attempt 1 | 10 effects |
| T01–T04 · 4 event | Transient ở attempt 1,2; thành công ở attempt 3 | 4 effects, 8 retry transitions |
| P01–P03 · 3 event | Permanent: phiên bản handler chưa hỗ trợ | 3 event quarantine ngay attempt 1 |
| X01–X02 · 2 event | Transient ở cả ba attempts | 2 event quarantine sau attempt 3, 4 retry transitions |
Baseline không có transport fault: 31 business attempts, 14 effects, 5 event quarantine và 12 retry transitions. Hai queue retry có queue-level TTL lần lượt 1.000 ms và 2.000 ms, không có consumer; DLX route trở lại work queue. Không trộn TTL khác nhau trong cùng retry queue. Lưu application attempt và quyết định theo (event_id,attempt); x-death dùng để chẩn đoán, không thay business retry counter.
DLQ collector lưu raw payload + metadata vào quarantine store bền vững rồi mới ack. Replay CLI đọc snapshot từ store này, không consume/requeue DLQ để giả lập dry-run. Mỗi entry chứa event/attempt ID, payload hash, error class, first/last seen, origin exchange/routing key, schema version, death history và owner.
Việc phải làm
- Tạo work/retry/DLQ topology và export policy thực tế. Trong error handler, giữ nguyên event ID, tăng business attempt chỉ khi chuyển sang lần thử kế tiếp. Republish retry/DLQ bằng confirms + mandatory routing check, rồi mới ack delivery gốc; publish fail/unknown thì không xác nhận mất dữ liệu.
- Chạy fixture baseline để đạt đúng oracle 31/14/5/12. Ghi thời điểm vào/ra mỗi retry queue; kiểm không xử lý trước TTL trong cùng clock đo đã chốt, có tolerance đo clock, và kết thúc trong deadline. Không assert đúng chính xác 1 hoặc 2 giây.
- Dừng tại hook sau retry publish confirm, trước ack gốc rồi kill worker. Kiểm transport duplicate nhưng decision ledger và idempotency giữ đúng tập side effect; attempt nghiệp vụ tối đa 3 vẫn giữ. Tái hiện thêm retry queue không route được về work bằng cách gỡ binding DLX rồi khôi phục.
- Tái hiện loop trong topology cách ly:
loop.work → consumer reject(requeue=false) → DLX → loop.delay(TTL) → loop.work. Mỗi vòng có consumer rejection; controller dừng sau 5 lần rejection hoặc watchdog. Sau đó sửa bằng bounded attempts và terminal DLQ; không dựa vào cơ chế phát hiện chu kỳ không có rejection của broker. [R7] - Viết replay CLI có hai chế độ dry-run/execute, filter theo snapshot ID, allowlist schema, giới hạn số event, throttle 2 event/giây với burst=1, checkpoint và audit. Nâng cấp handler cho P, sửa downstream cho X; không sửa payload hoặc đổi event ID để né dedupe.
- Dry-run năm entry quarantine, execute, rồi kill CLI ở hook sau publish confirm trước audit checkpoint và chạy lại. Lưu duplicate attempt do replay; chỉ đánh dấu hoàn tất khi có bằng chứng downstream đã xử lý hoặc đã đối soát, không coi confirm là business success.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Transfer window | Retry message đã được confirm và kiểm routing; ack delivery gốc chưa gửi. | Kill worker, restart cùng state; bản gốc và retry copy có thể cùng xuất hiện, nhưng effect không được nhân đôi. |
| F02 · DLX target không có route | Gỡ binding từ retry DLX về work, chờ policy/topology được đọc lại. | Để retry hết TTL; source quorum giữ dữ liệu chờ dead-letter transfer. Khôi phục binding và đối soát, chấp nhận transport duplicate. |
| F03 · Dead-letter loop | Topology loop đã có một message, consumer chủ động reject ở mỗi vòng. | Đếm 5 vòng rồi dừng an toàn; sửa attempt budget trước khi bật lại. Không chạy loop không giới hạn trên shared broker. |
| F04 · Replay checkpoint window | Replay publish đã confirm, entry chưa đánh dấu/checkpoint. | Kill CLI, chạy lại cùng snapshot; audit phải thể hiện retry, không giấu duplicate bằng cách tạo ID mới. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L04-A01Phân loại và attempt budget | Baseline đạt 31 logical business attempts, 14 effects, 5 quarantine IDs, 12 retry transitions. P không retry; không event nào có business attempt >3. | attempt-ledger.csv, effect/quarantine set assertions. |
| L04-A02Delay có kiểm chứng | Tất cả retry baseline đi đúng queue TTL theo attempt; không quay lại xử lý trước TTL ngoài tolerance đã khai báo, đều kết thúc trong deadline. Log có enqueue/dequeue/cause, không chỉ biểu đồ count. | retry-timing.csv, TTL policy export, clock/tolerance note. |
| L04-A03Metadata đủ để vận hành | Năm entry quarantine có đủ các field đã quy định; raw payload hash đối chiếu được. Collector chỉ ack sau durable store commit; cùng entry được giao lại không làm mất bản gốc. | quarantine.jsonl, collector transaction/ack trace. |
| L04-A04Transfer crash không mất nghiệp vụ | Ca F01 có transport delivery lặp, business effect vẫn đúng 14 ID; mỗi effect một lần, logical attempt ≤3. Không ack delivery gốc khi publish chuyển tiếp còn failed/unknown. | transfer-crash.jsonl, publish outcomes, attempt/effect ledger. |
| L04-A05Dead-letter target phục hồi | Ca F02: export xác nhận at-least-once DLX và reject-publish; message hết TTL chưa biến mất khi không có route. Sau restore, tất cả ID chờ chuyển xuất hiện ở đích, set diff rỗng. | dlx-outage.jsonl, source/target snapshots, policy và terminal ledger. |
| L04-A06Loop được tái hiện rồi chặn | Có trace 5 lần rejection của cùng ID trong topology cách ly; watchdog/stop hoạt động. Bản sửa dừng ở attempt 3 và đưa đúng một logical event vào quarantine, không tiếp tục quay vòng. | loop-before.jsonl, loop-after.jsonl, topology diff. |
| L04-A07Dry-run thực sự không ghi | Dry-run chọn đúng 5 entry, xuất plan có schema/owner/reason/destination. Trước-sau: broker, checkpoint, entry status và business DB không đổi; không publish và không ack DLQ. | replay-plan.json, read-only operation log, state hashes. |
| L04-A08Replay an toàn khi chạy lại | Sau fix và execute/restart, 5 ID được phục hồi, tổng effects của fixture=19 và mỗi ID một effect. Khoảng cách phát ≥500 ms theo monotonic clock (burst=1); audit đầy đủ actor/snapshot/attempt/result, mọi entry có trạng thái cuối. | replay-audit.jsonl, rate trace, final 19-ID reconciliation. |
Deliverables. Topology/policies, error classifier, attempt ledger, DLQ collector, replay CLI có dry-run, execute, rate limit, checkpoint và audit, loop reproducer có watchdog, fixture/oracle và replay-runbook.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. Nêu chi phí giữ dead letters ở source, nguy cơ retry amplification và cách cảnh báo DLQ age. At-least-once transfer vẫn có thể duplicate; replay phải dùng lại idempotency contract của live traffic, không phải đường tắt bỏ kiểm tra.
↑ Mục lục 8 labLab 05 · Kafka partition và lag
Goal · Mục tiêu
Chọn key accountId, tạo hot key, đo lag per partition. Tăng partitions và ghi rõ ảnh hưởng mapping/order; scale consumers vượt partition count.
Mô hình cần chứng minh. Lag cần nhìn theo partition và theo tuổi dữ liệu, không chỉ cộng thành một số. Tăng partition không di chuyển record cũ và có thể đổi partition của cùng key cho record mới. Consumer group thông thường không tăng số partition được xử lý song song chỉ bằng cách thêm member vượt partition count. [K1] [K2]
Fixture và cấu hình
Dùng topic ba partition và key accountId. Để oracle độc lập ngôn ngữ, lab định nghĩa partitioner riêng: partition = số ở cuối accountId mod P; ví dụ account-0003 vào p0 khi P=3, vào p3 khi P=6. Ghi rõ đây là partitioner của fixture, không phải thuật toán mặc định của mọi Kafka client.
| Fixture | Quy tắc tạo | Oracle |
|---|---|---|
| B · Balanced, 360 event | i=0…359; account=i mod 12; sequence=floor(i/12)+1 | 12 account, 30 event/account; 120 record/partition khi P=3 |
| H · Hot key, 360 event | 216 event của account-0000, sau đó 144 event luân phiên account-0001/0002 | p0=216, p1=72, p2=72; sequence tăng trong từng account |
| X · Expansion | 360 event B ở P=3, rồi tăng P=6 và gửi 12 event sequence=31, một/account | 372 event; 6/12 account đổi mapping cho record mới |
| I · ISR | Topic riêng một partition, RF=3, min ISR=2, acks=all; 10 ID A và 10 ID B | Kiểm riêng cửa sổ ISR=2 và ISR=1, không trộn với benchmark lag |
Các topic mới, không có transaction/control record trong fixture B/H/X, không compaction và không hết retention trong lúc chạy. Khi consumer chưa chạy, committed offset được khởi tạo 0; lưu cả log start/end và checkpoint để diễn giải lag. Trong dữ liệu có transaction hoặc compaction, chênh offset không tự bằng số business event chưa xử lý.
Việc phải làm
- Chạy B rồi H trong namespace riêng. Lưu producer delivery metadata gồm event ID, key, partition, offset; dừng consumer trước snapshot lag. Với H, chỉ resume p1/p2, giữ p0 bằng barrier để có oracle lag p0=216 và p1=p2=0.
- Chạy B với group lần lượt 1, 3, 5 member, một topic và assignment strategy đã pin. Đợi group ổn định, export assignment và completions. Báo số member có partition và member idle; không khẳng định throughput tăng tuyến tính.
- Trong ca X không an toàn, giữ consumer của record cũ, tăng partition lên 6 và đợi producer thấy metadata mới. Gửi 12 record sequence=31; cho p3–p5 chạy trước p0–p2. Quan sát ít nhất account-0003 có sequence 31 được áp dụng trước record cũ.
- Làm lại X an toàn: ngừng producer, drain và xác nhận đủ 360 effect/checkpoint cũ, thay partition count, chờ metadata mới rồi mới phát 12 record tiếp theo. Ghi kế hoạch rollback không giả định có thể giảm partition count của topic cũ.
- Với I, dừng một follower, chờ ISR=2, publish 10 ID A. Tiếp tục dừng follower thứ hai, giữ controller quorum còn đa số, chờ ISR=1 rồi thử 10 ID B. Ghi từng response/error/timeout; phục hồi follower, chờ ISR đủ và retry/đối soát B bằng event ID.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Partition bị giữ | H đã được publish đầy đủ, consumer chưa commit; pause p0 và chạy p1/p2. | Đọc lag per partition và processing age; tổng lag không được che hotspot. |
| F02 · Mapping đổi khi còn backlog | Có record cũ chưa xử lý, P tăng 3→6, producer đã nhận metadata mới. | Cố ý chạy partition mới trước partition cũ để tạo bằng chứng reorder theo key. |
| F03 · ISR co lại | ISR đọc từ broker lần lượt bằng 2 rồi 1; controller quorum vẫn khỏe. | Không hạ min ISR/acks để tiếp tục ghi. Chờ ngưỡng recovery trước khi retry, lưu unknown outcome thay vì đoán mất dữ liệu. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L05-A01Key và partition đúng oracle | B có 360 event, 120 record/partition và 30/account; metadata thực tế khớp partitioner fixture. Với xử lý tuần tự theo partition, sequence của mỗi account tăng đúng 1…30. | partition-map.csv, producer metadata, per-key sequence assertions. |
| L05-A02Hotspot quan sát được | H trước consume có lag 216/72/72; khi p1/p2 đã drain và p0 vẫn giữ, lag là 216/0/0. Có processing-age và tốc độ từng partition để giải thích bottleneck. | hot-key-lag.csv, pause/commit log, per-partition metrics. |
| L05-A03Scale vượt số partition | Group ổn định với 1/3/5 member có lần lượt 1/3/3 member được gán partition; ca 5 member có 2 member không được gán partition. Mỗi partition chỉ có một owner hợp lệ tại một thời điểm ổn định. | assignments-1-3-5.json, stable group metadata, completion ledger. |
| L05-A04Expansion không di chuyển record cũ | Trong X, 360 record cũ vẫn giữ partition/offset cũ; mapping mới khác cho đúng các account 3,4,5,9,10,11. Có metadata refresh trước khi phát 12 record mới. | mapping-before-after.csv, old-record metadata diff, producer log. |
| L05-A05Ordering failure được tái hiện | Ca X không an toàn: ít nhất account-0003 có sequence 31 trước sequence cũ; vẫn nhận đủ 372 ID. Không đánh đồng đủ count với giữ đúng thứ tự. | unsafe-apply-order.csv, barrier trace, 372-ID reconciliation. |
| L05-A06Cutover an toàn | Ca X có drain barrier: mỗi account được áp dụng đúng sequence 1…31, 372 effect duy nhất; checkpoint cũ được lưu trước mở producer, có rollback/cutover plan. | safe-cutover.jsonl, sequence checker, partition-change.md. |
| L05-A07ISR=2 vẫn chấp nhận ghi | Topic I có RF=3/min ISR=2/acks=all; khi ISR ổn định bằng 2, cả 10 ID A nhận success và còn đọc được sau khi follower phục hồi. Controller quorum không bị mất đa số. | isr-two.json, config export, A receipts và read-back. |
| L05-A08ISR=1 không báo success giả | Khi đã xác nhận ISR=1 trước send, không đánh dấu 10 ID B thành công nếu không có broker success; lưu lỗi/timeout thật. Sau ISR phục hồi, đối soát/retry đạt đúng 20 logical effects A+B, không duplicate side effect. | isr-one-recovery.jsonl, response ledger, ISR timeline, effect diff. |
Deliverables. Fixture generator có partitioner xác định, metadata/lag collector, group-scaling runner, expansion order checker, ISR fault script và partition-capacity-decision.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. acks=all chờ tập ISR hiện hành, không đồng nghĩa mọi replica được cấu hình luôn online; min.insync.replicas là điều kiện durability của profile ghi này, không được đồng nhất với RabbitMQ quorum. Nêu lựa chọn giữ ordering per account hay chia nhỏ key để giảm hotspot, và chi phí cutover. [K3] [K4]
Lab 06 · Offset và rebalance
Goal · Mục tiêu
Manual commit sau DB transaction. Cho processing vượt max.poll.interval hoặc kill member; quan sát revoke/reassign/duplicate và sửa bằng bounded worker + pause/resume.
Mô hình cần chứng minh. Committed offset là vị trí tiếp theo cần đọc, không phải “message đã nhận từ poll”. Với worker song song, chỉ commit qua dải record liên tục đã hoàn tất. Pause/resume giúp chặn nạp thêm việc, nhưng không thay nghĩa vụ tiếp tục poll. Rebalance cũng không tự rollback DB effect của worker cũ. [K5] [K2]
Fixture và cấu hình
Topic mới có ba partition, mỗi partition 20 record ở offset 0…19, tổng 60 event; không transaction, compaction hoặc retention deletion trong ca thử. Dùng dynamic membership, group.protocol=classic, không đặt group.instance.id, tắt auto commit và đặt max.poll.interval.ms=5000, max.poll.records=10. Ghi rõ client thực sự hỗ trợ các lựa chọn này; client không hỗ trợ phải ghi BLOCKED, không âm thầm đổi protocol.
Baseline xử lý tuần tự và commit sau DB transaction. Bản sửa dùng owner loop gọi consumer API, hai worker, application admission cap 50 record; pause khi admitted≥40, resume khi ≤20, mỗi poll nạp tối đa 10 record. Admitted bao gồm việc đang chạy và đang chờ. Chọn fetch-byte limits và RSS budget riêng vì max.poll.records không giới hạn toàn bộ dữ liệu client đã fetch/cache. [K5]
on_worker_done(partition, offset, generation):
if generation != current_owner_generation:
discard_offset_completion() # DB effect vẫn phải được inbox bảo vệ
else:
mark_DB_committed(partition, offset)
next = first_not_completed_from_current_checkpoint(partition)
commit_next_offset(partition, next) # Chỉ owner loop thực hiện
on_partition_revoked(partition):
stop_admission(partition)
drain_with_deadline_or_cancel_workers(partition)
commit_only_contiguous_completed_offsets_if_still_allowed()
invalidate_generation(partition)
Việc phải làm
- Tạo inbox + business effect trong một DB transaction; chỉ commit offset sau commit DB thành công. Lưu topic/partition/offset cùng event ID, nhưng dedupe bằng event ID khi đó là định danh nghiệp vụ ổn định.
- Giữ p1/offset7 sau DB commit trước offset commit, kill member rồi khởi động lại. Chạy ca poll-timeout riêng: giữ owner loop không poll cho đến khi vượt 5 giây và coordinator xác nhận reassignment. Không thay bằng chỉ giữ worker trong khi owner vẫn poll.
- Chạy ca kill member đột ngột khi có record in-flight; dùng assignment ở member sống và coordinator để chứng minh chuyển quyền. Không đòi callback revoke từ process đã bị kill.
- Tạo completion gap riêng trên p0: hoàn tất và commit offset 0…6 để checkpoint=7; giữ offset7, cho offset8/9 hoàn tất DB trước. Kiểm checkpoint vẫn bằng 7, rồi release offset7 và cho phép checkpoint tiến tới 10.
- Bật bounded worker + pause/resume; chặn downstream 20 giây nhưng owner loop vẫn poll. Lưu mọi poll interval và worker-queue size. Thực hiện thêm controlled rebalance khi worker còn dở để kiểm generation fencing, cancellation và contiguous commit.
- Sau mọi lỗi, resume và drain; đối chiếu 60 ID, business effects, checkpoint cuối, assignment ổn định và không còn worker cũ commit offset sau mất quyền. Đánh dấu rõ DB effect đã commit của worker cũ có thể tồn tại và được inbox dedupe.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · DB commit → offset commit | p1/offset7 đã hiện trong DB bằng connection độc lập; checkpoint vẫn chưa qua 7. | Kill member, giữ nguyên DB/group offsets; sau reassignment chứng minh event được đọc lại. |
| F02 · Vượt poll interval | Owner loop dừng poll; chờ coordinator/reassignment, có watchdog. | Quan sát timeout, revoke nếu process còn sống, assign sang member khác và commit stale bị từ chối/bỏ qua. |
| F03 · Kill member | Coordinator ghi member đang sở hữu partition và có in-flight event. | Kill ngay, không graceful close; quan sát reassignment ở phía còn sống. |
| F04 · Out-of-order completion | Checkpoint p0=7, offset7 giữ ở hook, offset8/9 đã commit DB. | Không được commit 10 trước khi offset7 hoàn tất; sau release mới tiến checkpoint. |
| F05 · Revoke khi worker còn chạy | Bản sửa có task chưa xong, chủ động thêm/rút member. | Đổi generation; completion cũ không được đẩy offset của owner mới, DB vẫn được unique inbox bảo vệ. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L06-A01Baseline commit đúng thời điểm | 60 event có DB transaction commit trước offset commit tương ứng; sau drain, checkpoint ba partition đều bằng 20, set effects đủ 60 và không có side effect lặp. | db-offset-order.jsonl, group-offsets.json, ledger. |
| L06-A02Crash làm đọc lại, không nhân effect | Ca F01 có ít nhất hai processing attempts cho event tại p1/offset7; event có đúng một effect. Sau recovery đủ 60 ID, checkpoint 20/20/20. | commit-window.jsonl, independent DB export, duplicate ledger. |
| L06-A03Poll timeout và reassignment thật | Ca F02 có poll gap >5 giây, group chuyển assignment/generation; trace gắn rõ member cũ/mới. Không có ứng dụng tự coi stale commit thất bại là success. | poll-timeout.jsonl, coordinator/client error log, assignment snapshots. |
| L06-A04Kill khác graceful leave | Ca F03 có process-exit evidence và reassignment; không dựa vào revoke callback của member đã chết. Bất kỳ record in-flight đọc lại đều được dedupe, cuối ca đủ 60 effect. | member-kill.jsonl, new-owner assignment, final ledger. |
| L06-A05Không commit qua completion gap | Khi 8/9 đã commit DB nhưng 7 chưa xong, checkpoint p0 vẫn=7; sau 7 hoàn tất, checkpoint có thể lên 10. Restart từ checkpoint trước release không bỏ sót offset7. | gap-timeline.csv, checkpoint snapshots, replay trace. |
| L06-A06Pause vẫn duy trì poll | Bản sửa giữ downstream 20 giây; admitted≤50, poll gap luôn <5 giây và không có timeout-driven rebalance trong subcase membership cố định. Có pause/resume theo ngưỡng 40/20; RSS trong budget. | bounded-workers.csv, poll timestamps, pause/resume log, RSS. |
| L06-A07Mất quyền thì không commit muộn | Ca F05: worker completion generation cũ bị bỏ cho mục đích offset; không commit vượt contiguous checkpoint hoặc commit thay owner mới. DB duplicate được unique inbox chặn, không tuyên bố generation token tự fence DB. | generation-fence.jsonl, revoke/assign trace, inbox conflicts. |
| L06-A08Recovery đầy đủ và tái lập | Mỗi subcase bản sửa kết thúc với 60 ID đúng một effect/ID, checkpoint 20/20/20, group ổn định và không còn task vô chủ; cả ba lần chạy đạt recovery deadline đã chốt. | recovery-summary.json, terminal state, reproducibility log. |
Deliverables. Consumer có manual commit, DB inbox, failpoint ở DB/offset/poll, bounded-worker implementation, owner-generation trace, contiguous-offset checker và rebalance-runbook.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. Nêu đánh đổi giữa batch size, throughput, replay khi crash và thời gian rebalance. Tăng max.poll.interval chỉ thay cửa sổ timeout; nó không sửa worker queue vô hạn, commit sớm hay thiếu idempotency.
↑ Mục lục 8 labLab 07 · Kafka transactions và external DB
Goal · Mục tiêu
Read-process-write Kafka transaction với read_committed; sau đó thêm DB side effect để chứng minh nó ngoài EOS. Áp outbox/inbox hoặc reconciliation.
Mô hình cần chứng minh. Kafka transaction có thể gộp output records và consumer offsets trong Kafka. Nó không tự gộp transaction của database ngoài Kafka. read_committed lọc record của transaction bị abort/chưa commit, không phải bộ lọc duplicate business event cho mọi nguồn dữ liệu. [K6] [K7]
Fixture và cấu hình
Input topic mới có một partition, 10 event E01…E10, mỗi event có value=1; transform tạo một output cùng event ID với value=2. Dùng một processing slot với transactional.id ổn định trong phạm vi run, không trùng slot khác; output checker dùng isolation.level=read_committed. RF/min ISR áp dụng cả các topic nghiệp vụ; cấu hình transaction/offset internal topics cũng phải đủ replication cho cluster lab.
initialize_transactional_producer(stable_slot_id)
for input_record:
begin_Kafka_transaction()
send(output_record_with_same_event_id)
wait_send_result()
HIT_HOOK("AFTER_OUTPUT_SEND_BEFORE_TX_COMMIT", event_id)
send_next_input_offset_to_transaction(current_group_metadata)
commit_Kafka_transaction()
Hai checker có group độc lập: một read_committed, một read_uncommitted. Checker không được làm thay đổi group offset của processor. Đếm event visible theo ID, không lấy chênh offset làm số business output vì transaction có thể tạo khoảng trống/control records. [K5]
Việc phải làm
- Chạy baseline Kafka-only: output và input offset commit trong cùng Kafka transaction. Sau đó giữ E05 ở hook sau send, trước transaction commit; để checker read_uncommitted xác nhận có bản chưa commit rồi kill processor.
- Restart cùng transactional ID theo slot và đúng recovery API của client. Đợi transaction cũ được giải quyết/abort; processor đọc lại từ committed input offset. Lưu cả record vật lý được read_uncommitted thấy và tập output read_committed cuối cùng.
- Chạy ca crash sau transaction commit nhưng trước ghi progress log cục bộ. Chạy thêm ca hai producer dùng cùng transactional ID: khởi tạo producer thứ hai rồi buộc producer cũ tiếp tục thao tác để kiểm fencing; không để hai slot hợp lệ chia sẻ ID.
- Thêm DB counter ngoài Kafka transaction, cố ý chưa có unique inbox. Với E05, commit DB rồi kill trước Kafka transaction commit. Recovery xử lý lại input; đo output Kafka vẫn đúng 10 trong khi counter DB=11. Đây là negative control có chủ đích.
- Sửa bằng DB transaction chứa inbox + business update + outbox; commit input offset chỉ sau DB commit. Relay publish outbox sang Kafka; sink dùng inbox theo event ID. Kiểm cả crash trước commit DB, sau commit DB trước offset, và relay sau publish success trước đánh dấu outbox đã gửi.
- Viết reconciliation so tập input, inbox, business effects, outbox, output receipts và sink effects. Relay có thể gửi lại cùng event ID; downstream không được cộng lại. Ghi rõ đây là atomicity tại DB cộng eventual delivery, không phải distributed transaction tự động giữa DB và Kafka.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Output chưa commit | E05 đã được checker read_uncommitted thấy; Kafka transaction còn mở. | Kill processor; checker read_committed chưa được thấy E05 từ transaction này. Restart và đối soát sau recovery. |
| F02 · Commit xong, mất progress log | Kafka transaction commit đã trả success, local progress chưa flush. | Kill rồi restart từ group offset trong Kafka; không dùng local log cũ để rewind tùy tiện. |
| F03 · Producer bị fence | Producer thứ hai khởi tạo cùng transactional ID với producer đang sống. | Buộc producer cũ send/commit để quan sát lỗi fencing theo client/version; đóng producer lỗi, không retry vô hạn cùng epoch. |
| F04 · External DB window | DB counter đã commit cho E05, Kafka transaction chưa commit. | Kill tạo counter lặp ở bản sai; chạy lại cùng window với inbox/outbox để chứng minh bản sửa. |
| F05 · Outbox relay window | Relay đã publish success cho E05, outbox sent/checkpoint chưa commit. | Kill relay, resend cùng ID sau restart; checker phải quan sát duplicate vật lý nhưng sink effect vẫn duy nhất. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L07-A01Kafka-only atomic boundary | Baseline read_committed thấy đúng 10 ID, value=2; input next offset=10. Không có output visible được ghi tách rời khỏi offset transaction của processor. | tx-baseline.jsonl, output ledger, committed input offset. |
| L07-A02Abort/crash không lộ output sai | Ca F01: trước recovery, read_uncommitted đã thấy E05 nhưng read_committed chưa thấy bản đó; sau recovery read_committed có đúng 10 ID một lần/ID. Read_uncommitted có thể thấy bản abort thêm; có evidence transaction được giải quyết. | two-isolations.csv, tx lifecycle, restart and offset trace. |
| L07-A03Không phụ thuộc progress log cục bộ | Ca F02 restart từ committed group offset, không tạo thêm output read_committed cho record đã commit; terminal set vẫn 10 ID và next offset=10. | post-commit-crash.jsonl, offset snapshots, output-ID counts. |
| L07-A04Fencing được xử lý | Ca F03 producer cũ gặp lỗi fencing/epoch tương ứng phiên bản khi tiếp tục thao tác và bị dừng; producer mới tiến triển. Slot hợp lệ có transactional ID riêng, restart cùng slot giữ identity theo thiết kế. | producer-fence.jsonl, slot-ID manifest, error handling assertion. |
| L07-A05Chứng minh external DB ngoài EOS | Bản sai ở F04: Kafka read_committed cuối cùng có 10 output, DB counter=11 và riêng E05 có 2 effects. Kết luận nêu đúng hai transaction boundary, không gọi toàn workflow là exactly-once. | external-db-negative.csv, DB commit và Kafka tx trace. |
| L07-A06Inbox/outbox cùng DB transaction | Bản sửa: crash trước DB commit để lại 0 phần đã commit của event; crash sau commit trước offset có đủ inbox/effect/outbox, không ghi effect lần hai khi đọc lại. Cuối fixture có 10 inbox, 10 outbox, counter=10. | db-atomicity.jsonl, independent DB snapshots, unique-key proof. |
| L07-A07Relay retry không nhân effect | Ca F05 được tái hiện: cùng event ID có ít nhất hai output records được publish bởi relay qua các lần gửi, sink có đúng một effect/ID và tổng=10. Không thay ID hoặc dựa producer idempotence để dedupe application resend. | relay-crash.jsonl, physical output receipts, sink inbox/effects. |
| L07-A08Đối soát đóng mọi khoảng trống | Sau recovery, set input/inbox/outbox/sink đều khớp 10 ID; không có outbox pending quá deadline hoặc effect thiếu/dư. Tài liệu phân biệt Kafka-only EOS, DB atomicity, at-least-once relay và retention của dedupe. | reconciliation.json, empty set diffs, guarantee-scope.md. |
Deliverables. Kafka transaction processor, hai isolation checkers, external-DB negative control, inbox/outbox schema, relay và sink idempotency, reconciliation script, fault traces và guarantee-scope.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. So sánh chi phí transaction latency, fencing, outbox cleanup và reconciliation. Dedupe record phải sống đủ lâu cho replay; xóa inbox sớm có thể làm một event cũ tạo side effect mới. Với side effect không giao dịch được như email/payment API, cần idempotency contract của đích hoặc workflow đối soát riêng.
↑ Mục lục 8 labLab 08 · Schema và incident drill
Goal · Mục tiêu
Test v1/v2 rolling compatibility, inject malformed event vào DLQ, broker/consumer restart và disk/lag alert. Viết runbook chẩn đoán, recovery và safe replay.
Mô hình cần chứng minh. Compatibility là quan hệ giữa writer, reader và cách giải mã thực tế; “thêm field là an toàn” không phải kết luận áp dụng cho mọi format. Recovery chỉ hoàn tất khi schema, quyền truy cập, dữ liệu và checkpoint đều đúng; trạng thái process “running” chưa đủ. [S1] [K8]
Fixture và cấu hình
Chọn JSON có validation hoặc một schema format khác và pin serializer/reader. Fixture Kafka gồm 10 event v1 + 10 event v2, ID không trùng; mỗi event có amountMinor=100. v1 có event_id, accountId, amountMinor; v2 thêm currency. Contract lab: reader v1 bỏ qua field không biết; reader v2 dùng currency mặc định VND khi v1 không có; toàn bộ v2 fixture cũng dùng VND. Đây là contract cần test, không suy từ tên format.
Ba poison records riêng: bytes không parse được, amountMinor="one hundred" trái kiểu số, và schema_version=99 chưa hỗ trợ. Reader phải đọc raw bytes và deserialize trong lớp có thể quarantine; không để lỗi deserializer làm poll mắc kẹt mãi. Poison không có event ID dùng định danh quarantine theo (source topic, partition, offset).
Fixture incident khác có 200 valid event để tạo lag xác định; không trộn với 20 event schema. Dùng lại profile quorum/ISR và security của các lab trước, nhưng mỗi subcase có namespace riêng. Tất cả tải được giới hạn bởi fixture/watchdog.
Việc phải làm
- Chạy ma trận writer/reader 2×2 trong state sạch: v1→v1, v1→v2, v2→v1, v2→v2. Mỗi ô dùng 10 event đúng writer version và kiểm field/giá trị sau decode. Sau đó rolling upgrade consumer trước producer, lưu version đang chạy và trạng thái xử lý trong suốt rollout.
- Inject ba poison records xen giữa valid records. Với Kafka, dùng transaction gộp DLQ record và source next offset; với pipeline RabbitMQ tương đương, chỉ ack sau quarantine bền vững hoặc publish confirm đã kiểm routing. Giữ raw bytes/hash, original coordinates, error class và schema fingerprint để replay có chọn lọc.
- Dừng/restart lần lượt một RabbitMQ quorum leader, một Kafka broker và một consumer; không xóa volume hoặc đổi group ID để che recovery. Đọc lại tập ID đã confirm/ack-success, kiểm dedupe và checkpoint. Tách restart từng thành phần thành subcase để biết nguyên nhân.
- Tạo lag thật: publish 200 ID, giữ consumer ở barrier, chờ alert lag>100 liên tục 10 giây với scrape 2 giây. Release và drain. Tạo disk alarm RabbitMQ bằng cách nâng ngưỡng disk-free trong sandbox vượt free bytes hiện tại, không lấp đầy ổ hệ điều hành; đọc alarm thật, thử publisher probe, rồi khôi phục ngưỡng trong finally. [R9]
- Chạy security matrix trên cả hai broker: TLS verification, authentication và least privilege theo resource. Dùng admin chỉ để dựng tài nguyên, account ứng dụng không có admin. Xoay credential, kiểm bằng connection mới để không nhầm session cũ còn sống với credential cũ vẫn hợp lệ.
- Safe replay 20 valid schema events từ archive/offset range đã chốt vào topic/group cách ly. Một mode dùng lại business ledger để kiểm dedupe; một mode rebuild projection sạch để so hash/kết quả. Không rewind group live, không tự động replay ba poison records chưa có bản sửa.
Failure injection · Chủ động gây lỗi
| Ca lỗi | Điểm kích hoạt xác định | Thao tác / oracle |
|---|---|---|
| F01 · Rolling reader/writer | Version reader được ghi rõ; phát xen kẽ v1/v2 theo fixture. | Bắt mismatch field/default/type; có negative control schema sai kiểu và version chưa hỗ trợ. |
| F02 · Poison giữa valid records | Biết chính xác source coordinates của ba poison records. | Lưu raw bytes vào quarantine; inject crash sau DLQ send trước transaction commit để kiểm không bỏ qua dữ liệu. |
| F03 · Restart broker/consumer | Tập accepted IDs và checkpoint đã được snapshot. | Kill một thành phần/lần, khởi động cùng data/state; đối soát accepted set, không chỉ chờ health xanh. |
| F04 · Lag và disk alert | Consumer bị giữ; hoặc disk-free threshold lab cao hơn free bytes đo được. | Quan sát alert firing/resolved và publisher outcome; restore threshold bằng cleanup bắt buộc, không tạo disk-full thật trên host. |
| F05 · Quyền/TLS/credential sai | Tài nguyên allow/deny đều đã tồn tại, endpoint reachable. | Chạy probe với CA sai, credential đã thu hồi và resource ngoài quyền; lưu nguyên nhân từ client/broker, không coi network timeout là deny hợp lệ. |
| F06 · Replay lặp | Replay snapshot gồm đúng 20 valid ID; mode dedupe giữ DB cũ. | Chạy replay hai lần, so effect/hash; mode rebuild dùng projection mới, không dùng inbox cũ để vô tình bỏ qua toàn bộ dữ liệu. |
Acceptance / evidence · 8 tiêu chí nghiệm thu
| ID / tiêu chí | Điều kiện PASS | Evidence bắt buộc |
|---|---|---|
| L08-A01Compatibility 2×2 có oracle | Cả 4 ô×10 decode kiểm đúng field và amountMinor; v2 reader đọc v1 ra currency=VND, v1 reader đọc v2 giữ đúng field đã biết. Ghi rõ default/unknown-field behavior của serializer thật và phạm vi schema được kiểm. | compatibility-matrix.json, reader/writer versions, decoded snapshots. |
| L08-A02Rolling upgrade không mất nghiệp vụ | Roll consumer rồi producer trên 20 valid ID: có đúng 20 effect, tổng amountMinor=2000, không thiếu/dư ID. Version timeline và rollback giữ được khả năng đọc v1/v2 trong phạm vi contract. | rollout-timeline.jsonl, schema ledger, rollback record. |
| L08-A03Poison được quarantine an toàn | Ba poison records tạo đúng 3 logical quarantine entries có raw bytes/hash và source coordinates; valid records phía sau vẫn tiến triển. Crash trước DLQ transaction commit không làm source offset vượt record chưa được quarantine bền vững. | poison-ledger.jsonl, source offsets, DLQ tx/crash trace. |
| L08-A04Restart có data recovery | Ba subcase RabbitMQ leader/Kafka broker/consumer restart giữ đủ tập accepted ID, side effect một lần/ID; restore leadership/assignment/checkpoint trong deadline. Kafka controller quorum vẫn khỏe; RabbitMQ còn đa số replica. | restart-matrix.json, accepted-set diff, topology/group state trước-sau. |
| L08-A05Alert thật, có phục hồi | Lag fixture có 200 record chờ xử lý, alert lag>100 for 10s firing trong 15 giây từ mẫu đầu đủ điều kiện; sau release drain đủ 200 ID và resolved trong thời hạn alert đã chốt. Disk alarm RabbitMQ firing thật, probe publish không bị đánh dấu success khi chưa confirm; restore ngưỡng và đối soát probe đầy đủ. | alert-events.jsonl, raw lag/disk samples, policy before/after, probe ledger. |
| L08-A06Security: 8 deny + 4 allow | Mỗi broker có 4 deny: CA không tin cậy, credential cũ đã thu hồi, publish sai resource, consume sai queue/group. Thêm 2 allow/broker bằng credential mới: publish đúng và consume đúng. Tổng 12 probe/run có lỗi/receipt xác định; TLS vẫn bật, không dùng superuser cho app. | security-matrix.csv, redacted ACL/permission exports, rotation audit. |
| L08-A07Replay có scope, idempotency và rebuild | Replay hai lần cùng 20 ID vào mode dedupe không đổi 20 effect/tổng=2000. Mode rebuild mới tạo đúng 20 effect/tổng=2000 và canonical projection hash bằng baseline. Group/checkpoint live không đổi; poison chưa sửa vẫn quarantine. | replay-plan.json, before/after DB hashes, group offsets, replay audit. |
| L08-A08Runbook và capacity/security sign-off | Runbook có triệu chứng→probe phân biệt→hành động an toàn→recovery gate→rollback→owner; dùng raw evidence từ lag/disk/restart/security. Có budget/tải/catch-up và kết luận broker selection theo yêu cầu, không ký đạt chỉ dựa screenshot. | incident-runbook.md, readiness-review.md, evidence links. |
Deliverables. Schemas/reader fixtures, compatibility runner, raw-bytes quarantine handler, rolling/restart/alert scripts có cleanup, security probe matrix, replay/rebuild verifier, runbook và readiness-review.md. Kèm bộ evidence chuẩn theo giao thức chung.
Review quyết định. RabbitMQ authorization cần đúng vhost/exchange/queue; Kafka cần đúng topic/group và transactional ID nếu dùng transactions. Security probe phải phân biệt TLS failure, authentication failure và authorization deny. Application dùng quyền tối thiểu; replay, monitoring và management access có owner riêng. [R10] [K9]
↑ Mục lục 8 labRubric · Production-ready / Senior
Chấm trên evidence có thể chạy lại, không chấm theo số tool hoặc độ đẹp của dashboard. Mức cao không được bù một lỗi mất dữ liệu bằng điểm hiệu năng. Bộ lab này chứng minh hành vi trong failure model đã khai báo; không tự chứng minh DR liên vùng, mất cả datacenter hoặc mọi workload production.
| Mức | Bằng chứng cần có | Điểm chưa được phép bỏ qua |
|---|---|---|
| 1 · Happy path | Topology/partition rõ; publish, consume, transform hoạt động; fixture và ledger cơ bản có thể tái tạo. | Chưa đủ để kết luận về crash, duplicate, durability, security hoặc recovery. |
| 2 · Failure-aware | Có crash window, duplicate, retry/DLQ, raw metric; chỉ ra được expected và actual, tái hiện negative control rồi sửa. | Không gọi production-ready khi quorum/ISR, offset gap, replay hoặc least privilege còn thiếu evidence. |
| 3 · Production-ready / Senior | Đủ 64/64 gate ở cả ba lần chạy, tương đương 192 kết quả PASS; không có BLOCKED. Budget đã chốt đều được đối chiếu; chứng minh guarantee scope, HA/overload, schema, security và recovery an toàn. Người khác chạy lại bằng deliverables, không cần tác giả thao tác tay. | Phải giải thích giới hạn guarantee, unknown outcome, failure-domain assumptions và những gì chưa được thử. “Production-ready” ở đây là mức hồ sơ/thực hành, không phải phê duyệt phát hành production. |
Buổi bảo vệ senior: quyết định từ evidence
| Nội dung | Câu hỏi phải trả lời | Evidence liên quan |
|---|---|---|
| Guarantee & data safety | Crash ở đâu gây loss/duplicate? Phần nào atomic, phần nào eventual? Event có outcome chưa rõ được đối soát thế nào? Inbox retention có đủ cho replay không? | Lab 02, 04, 06, 07: timeline, ledger và set diff. |
| HA & capacity | Mất một node có còn đủ capacity? Khi mất đa số hoặc ISR dưới min, chọn dừng ghi hay hạ guarantee? Prefetch/key/batch nào đáp ứng age, RAM và catch-up budget? | Lab 02, 03, 05: quorum/ISR state, benchmark, backlog-age và recovery metrics. Nếu chưa đo tải khi degraded, phải ghi rõ và bổ sung trước cam kết capacity N−1. |
| Operations & security | Alert nào phát hiện trước khi khách hàng bị ảnh hưởng? Quyền nào cần cho live/replay/admin? Khi replay làm tăng lag hoặc DLQ, ai dừng và rollback thế nào? | Lab 04, 08: dry-run/audit, 12 security probes, alert firing/resolved, runbook có owner. |
| Broker selection | Workload cần per-message routing/ack hay log retention và independent replay? Ordering theo key có chấp nhận hotspot không? Chi phí vận hành nào khiến quyết định đổi? | Decision memo dùng kết quả Lab 01–08; nêu một workload phù hợp mỗi broker và một điều kiện làm đảo lựa chọn, không khẳng định broker thắng tuyệt đối. |
Hồ sơ bàn giao của người học: runner/adapter, fixtures, config export, 64 assertion definitions, kết quả ba lần chạy, evidence hash, decision memo và runbook. Reviewer chọn lại một crash window, một ca HA và một ca replay/security để chạy độc lập. 8.3A tiếp tục phần execution labs; không thay thế bất kỳ lab nguồn nào ở trang này.
Tài liệu kỹ thuật đối chiếu
[R1] RabbitMQ · Alternate Exchanges
AE và mandatory routing.
[R2] RabbitMQ · Consumer Acknowledgements and Publisher Confirms
Confirm, consumer ack, return và channel-scoped delivery.
[R3] RabbitMQ · Queues
Declaration equivalence, durability và queue properties.
[R4] RabbitMQ · Quorum Queues
Replica majority, HA và at-least-once dead lettering.
[R5] RabbitMQ · Consumer Prefetch
Scope của prefetch và số delivery chưa ack.
[R6] RabbitMQ · Consumers
Consumer lifecycle, capacity và connection recovery.
[R7] RabbitMQ · Dead Letter Exchanges
Dead-letter routing, safety, cycle và death metadata.
[R8] RabbitMQ · Time-To-Live and Expiration
Queue/message TTL và thời điểm loại bỏ message hết hạn.
[R9] RabbitMQ · Memory and Disk Alarms
Resource alarm và publisher blocking.
[R10] RabbitMQ · Authentication, Authorisation, Access Control
TLS/authentication/authorization và resource permissions.
[R11] RabbitMQ · Exchanges
Direct, topic, fanout và routing.
[K1] Apache Kafka 4.2 · Basic Kafka Operations
Tăng partitions, consumer groups và thao tác vận hành.
[K2] Apache Kafka 4.2 · KafkaConsumer API
Manual commit, next offset, rebalance, pause/resume.
[K3] Apache Kafka 4.2 · Producer Configs
Acks, idempotence, partitioner và producer buffers.
[K4] Apache Kafka 4.2 · Topic Configs
Min ISR, replication-related topic settings.
[K5] Apache Kafka 4.2 · Consumer Configs
Isolation, poll interval, group protocol và fetch limits.
[K6] Apache Kafka 4.2 · Design
Delivery semantics và phạm vi transaction/EOS.
[K7] Apache Kafka 4.2 · KafkaProducer API
Transaction APIs và fencing.
[K8] Apache Kafka 4.2 · Monitoring
Broker/client metrics và operational evidence.
[K9] Apache Kafka 4.2 · Authorization and ACLs
Topic/group/transactional-ID permissions.
[S1] Apache Avro 1.12 · Specification
Ví dụ chính thức về writer/reader schema resolution; không bắt buộc dùng Avro.