From Event-Driven Chaos to a Blazingly Fast Serving API
Bài tóm tắt Zalando 2025 4. Điều chỉnh Độ khó: Nâng cao
Zalando · 2025 · 4. Điều chỉnh · Độ khó: Nâng cao — bài gốc
Bối cảnh
-
2016: Zalando dựng microservices với các CRUD API riêng cho từng phần dữ liệu sản phẩm. Mỗi sản phẩm hoàn chỉnh được phát thành event; ai cần dữ liệu thì phải "subscribe stream, replay từ đầu, tự dựng kho riêng". Không có read API.
-
Hệ quả:
- team có nhân lực thì tự biến đổi dữ liệu rồi phát lại;
- team không có thì dựa vào bản của người khác (ví dụ Presentation API).
Kết quả là nhiều nguồn sự thật cạnh tranh nhau, giống trò "tam sao thất bản", và không truy được nguồn gốc thuộc tính.
-
Offer (merchant bán một product với giá và tồn kho):
- một pipeline nhiều tầng gộp event Product, Price, Stock;
- hơn 90% payload là dữ liệu product không đổi, đi kèm mỗi lần giá hay tồn kho đổi;
- dịp Cyber Week, event giá và tồn kho trễ tới 30 phút;
- các team khác bám vào định dạng ở các tầng trung gian (Alpha, Beta, Gamma).
Thay đổi chính
- 2022: dự án PODS tách dữ liệu Product khỏi Offer, bỏ product nặng khỏi event stream. Product Read API (PRAPI) phục vụ
/products/{id},/products/{id}/offersvà bản gộp cho Presentation. - Kiến trúc PRAPI:
- 4 component, mỗi cái một Deployment Kubernetes với luật scale riêng;
- I/O non-blocking (Netty, epoll);
- DynamoDB phía sau.
- Cô lập theo quốc gia: nhiều Market Group, mỗi nhóm phục vụ một tập nước, đổi định tuyến được (ví dụ tách traffic canary khỏi nước giá trị cao). Updater scale theo lag, ghi DynamoDB theo batch song song.
- Cache trước DynamoDB:
- Caffeine async cache 60 giây, 15 giây cuối là cửa sổ stale-while-refresh;
- Consistent Hash Load Balancing theo product-id: mỗi pod giữ một phần catalogue, nên cache cục bộ nhỏ vẫn hiệu quả, và càng scale rộng thì càng cache được nhiều;
- batch request dùng power of two random choices.
- Đóng góp cho Skipper (ingress):
- mỗi pod 100 vị trí cố định trên vòng hash, nên khi scale chỉ mất 1/N cache;
- Bounded Load: mỗi pod tối đa 2 lần mức trung bình, vượt thì tràn sang pod kế. Nhờ đó sản phẩm "hot" (ví dụ giày Nike bản giới hạn) không đè sập một pod.
- Tail latency:
- cache product dưới dạng
ByteArrayđể giảm GC; - nối gzip trực tiếp, không giải nén rồi nén lại;
- LIFO thay FIFO khi xếp hàng;
- client DynamoDB chính timeout 10 ms, client dự phòng 100 ms.
- cache product dưới dạng
- Migration cho khoảng 350 team:
- PRAPI nhận luôn mọi định dạng cũ qua header
Accept; - component tạm phát lại định dạng Alpha/Beta lên stream cũ, nên gỡ được ứng dụng cũ ngay, rồi cho các team một hạn sunset cố định.
- PRAPI nhận luôn mọi định dạng cũ qua header
- Tổ chức theo CQRS và Conway: tách thành hai team, ingestion (command) và serving (query).
Kết quả
Theo Zalando (2025): hàng triệu request mỗi giây, p99 dưới 10 ms cho GET đơn (payload JSON khoảng 1.000 dòng). Càng tải cao thì càng nhanh, vì nhiều catalogue nằm trong cache hơn.
Bài học cho SWE
- "Chỉ có event, không có read API" đẩy chi phí phục vụ dữ liệu sang mọi team và sinh nhiều nguồn sự thật. Event để phát thay đổi; còn đọc thì cần một API chủ sở hữu.
- Gộp dữ liệu hay đổi (giá, tồn kho) với dữ liệu ít đổi (product) vào cùng một event là lãng phí và gây trễ.
- Consistent hashing kèm bounded load là cách cache hot key mà không tạo hot pod (Hot partition và thundering herd).
- Migration hàng trăm team: phục vụ cả định dạng cũ trong một thời hạn, thay vì bắt mọi team đổi trước.
Liên quan
Realtime và fan-outHot partition và thundering herdMonolith và microservices — đi và quay lạiCache và invalidationEvent-driven, message queue và Kafka
Được nhắc tới trong
Chủ đề (5)
Công ty (1)
Bài tóm tắt (3)