Harbor

Apache Flink

Flink là engine xử lý stream có trạng thái, đọc event từ Kafka, gom theo cửa sổ thời gian và cho kết quả chính xác kể cả khi event đến muộn hoặc máy chết. Bài này giải thích event time, watermark, các loại window, state và checkpoint, exactly-once với Kafka, và so sánh với Kafka Streams, Spark.

15 phút đọcCông nghệ chủ chốt

Apache Flink là engine xử lý stream có trạng thái (stateful stream processing). Nó đọc một luồng event không có điểm kết thúc (thường từ Kafka), biến đổi, gom nhóm, join, rồi ghi kết quả ra nơi khác: database, Kafka topic, Elasticsearch, object storage.1

Hãy so với cách làm quen thuộc của dev Node.js:

Cách làmVí dụVấn đề khi tải lớn
Consumer tự viếtĐọc Kafka, INCR Redis cho mỗi clickMất hoặc đếm trùng khi crash, khó gom theo thời gian thật của event
Batch định kỳCron mỗi giờ chạy SQL trên bảng eventKết quả trễ cả giờ
Stream processorFlink giữ bộ đếm theo cửa sổ, checkpoint định kỳĐộ trễ thấp, kết quả chính xác, tự phục hồi

Lý do "mất hoặc đếm trùng khi crash" có nguồn: hàng đợi tin nhắn chỉ xoá message khi consumer xác nhận xong, nên message bị giao lại khi consumer lỗi (at-least-once), và message broker thường chỉ hứa giao ít nhất một lần chứ không đúng một lần.23 Hai cột còn lại của bảng là so sánh của khoá học.1

Mô hình cốt lõi của Flink (khái niệm cơ bản, chưa có sách):1

  • Một job là một đồ thị dataflow: source → các operator (map, filter, keyBy, window, join) → sink.
  • Mỗi operator chạy song song với parallelism N. Sau keyBy(adId), mọi event cùng adId đi tới cùng một subtask, nên subtask đó giữ trạng thái riêng cho key ấy. Ý tưởng chia theo key tương tự partition của hàng đợi: Grokking mô tả chia queue thành nhiều shard theo partition key, mỗi shard nằm trên một host.4
  • State (bộ đếm, danh sách, map) nằm ngay cạnh code xử lý, không cần gọi ra database cho mỗi event.
  • Checkpoint chụp lại state và vị trí đọc của source định kỳ, để khi có sự cố thì quay lại điểm đó và chạy tiếp. Grokking định nghĩa checkpointing là lưu trạng thái hệ thống vào nơi lưu bền khi trạng thái nhất quán, để khi lỗi thì lấy lại dữ liệu đã tính từ checkpoint trước và làm tiếp.5

Flink xử lý theo từng event (true streaming) nên độ trễ có thể thấp; batch được coi như một stream có điểm kết thúc.1

Kiến trúc cluster

Đang vẽ sơ đồ…
  • JobManager: nhận job, lập kế hoạch thực thi, phân task cho các TaskManager, điều phối checkpoint, khởi động lại khi có lỗi. Cần chạy chế độ HA để không có điểm chết duy nhất (có thể dựa vào ZooKeeper).
  • TaskManager: process thực thi operator, mỗi cái có một số task slot.
  • State backend: nơi giữ state (trong bộ nhớ hoặc trên đĩa local).
  • Checkpoint storage: object storage hoặc hệ file phân tán, nơi lưu bản chụp bền vững.

Toàn bộ mô tả thành phần ở trên là kiến thức về sản phẩm, thư viện chưa có sách kiểm chứng.1

Thời gian: event time, processing time và watermark

Hai loại thời gian

  • Processing time: đồng hồ của máy xử lý lúc nhận event. Đơn giản, nhưng kết quả phụ thuộc vào việc hệ thống chạy nhanh hay chậm.
  • Event time: thời điểm event thực sự xảy ra, ghi trong chính event (ví dụ clickedAt do client gửi). Kết quả đúng và lặp lại được, kể cả khi chạy lại dữ liệu cũ.

Tại sao quan trọng? Điện thoại mất sóng trong thang máy, vài phút sau mới gửi lô click. Nếu gom theo processing time, các click đó rơi vào phút sai. Báo cáo quảng cáo, gian lận, thanh toán đều cần event time.1

Watermark

Khi dùng event time, hệ thống cần biết "đến lúc nào thì có thể đóng cửa sổ 10:00–10:01". Watermark là một tín hiệu chạy kèm trong stream, nói rằng: "từ giờ trở đi, tôi không mong đợi event nào có thời gian nhỏ hơn T nữa".1

Đang vẽ sơ đồ…
  • Chiến lược phổ biến: watermark = thời gian event lớn nhất đã thấy trừ một khoảng trễ cho phép.
  • Khoảng trễ lớn: kết quả chính xác hơn nhưng ra chậm hơn. Khoảng trễ nhỏ: nhanh nhưng nhiều event bị coi là trễ.
  • Event đến sau watermark có thể được cập nhật lại vào kết quả cửa sổ, hoặc đưa ra một nhánh riêng để đối soát sau.
  • Nguồn không có dữ liệu có thể giữ watermark đứng yên và cần được cấu hình để bỏ qua.

Cả bốn ý trên là khái niệm của Flink, chưa có sách trong thư viện.1

Window, state và code mẫu

Ba loại window chính

WindowMô tảVí dụ
TumblingCửa sổ cố định, không chồng nhauSố click mỗi quảng cáo mỗi phút
Sliding (hop)Cửa sổ dài L, trượt mỗi S, có chồng nhauTop bài hát trong 1 giờ qua, cập nhật mỗi 5 phút
SessionGom event của một key, đóng khi im lặng quá khoảng gapPhiên xem của user, đóng sau một khoảng không hoạt động

Khái niệm cửa sổ cố định và cửa sổ trượt cũng có ở thuật toán rate limit của Grokking (fixed window counter, sliding window log, sliding window counter), nhưng đó là bài toán khác: đếm request để giới hạn tốc độ, không phải tổng hợp stream.6 Phân loại ba loại window ở trên là của Flink.1

Ngoài window, bạn có thể dùng state tự quản lý kèm timer: ví dụ ghi nhớ số giao dịch của một thẻ trong một khoảng thời gian gần nhất để phát hiện gian lận.1

Flink SQL là cách nhanh nhất để mô tả job, rất hợp để viết trên bảng trắng. Cú pháp dưới đây chỉ là minh hoạ khái niệm, chưa có sách kiểm chứng và có thể khác giữa các phiên bản.1

CREATE TABLE clicks (
  ad_id      STRING,
  user_id    STRING,
  click_ts   TIMESTAMP(3),
  WATERMARK FOR click_ts AS click_ts - INTERVAL '10' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'ad-clicks',
  'format' = 'json'
);

INSERT INTO ad_clicks_per_minute
SELECT ad_id, window_start, window_end, COUNT(*) AS clicks
FROM TABLE(
  TUMBLE(TABLE clicks, DESCRIPTOR(click_ts), INTERVAL '1' MINUTE)
)
GROUP BY ad_id, window_start, window_end;

Đổi TUMBLE thành HOP để có sliding window, hoặc SESSION để có session window.

Checkpoint và exactly-once với Kafka

Checkpoint hoạt động thế nào

Ý tưởng chung: chụp state và vị trí đọc tại cùng một thời điểm logic. Grokking lưu ý checkpoint chỉ có ý nghĩa nếu nó nhất quán: nếu một checkpoint ghi nhận đã nhận một message mà checkpoint của bên gửi chưa ghi nhận đã gửi nó thì đó là trạng thái không nhất quán.5

Flink đạt tính nhất quán này bằng thuật toán dùng các "barrier" chèn vào dòng dữ liệu:1

  1. JobManager chèn một barrier có số thứ tự vào các source.
  2. Source ghi lại offset đang đọc, rồi đẩy barrier xuống cùng dòng dữ liệu.
  3. Mỗi operator khi nhận đủ barrier từ mọi đầu vào sẽ chụp state của mình và chuyển barrier đi tiếp.
  4. Khi mọi sink xác nhận, checkpoint hoàn tất.

Khi một node chết, job được khởi động lại từ checkpoint gần nhất: khôi phục state, tua offset về đúng vị trí đã lưu, và xử lý lại. Vì state và offset được chụp cùng một thời điểm logic, mỗi event ảnh hưởng tới state đúng một lần.1

Savepoint là checkpoint do người dùng kích hoạt, dùng để nâng cấp code hoặc đổi parallelism mà không mất state.1

Exactly-once đầu cuối

State bên trong đúng một lần chưa đủ. Kết quả ghi ra ngoài cũng phải không trùng. Có hai cách:

  1. Sink transaction: ghi kết quả vào một transaction và chỉ commit khi checkpoint hoàn tất. Hệ quả: dữ liệu chỉ hiện ra sau mỗi checkpoint nên độ trễ đầu cuối bằng cỡ khoảng checkpoint, và consumer phía sau phải chỉ đọc dữ liệu đã commit.1
  2. Sink idempotent: ghi bằng upsert theo khoá tự nhiên, ví dụ (ad_id, window_start). Ghi lại lần hai cũng ra cùng kết quả. Microservices Patterns định nghĩa idempotent là gọi nhiều lần với cùng đầu vào không có tác dụng thêm, và nêu "tạo order với ID do client cung cấp" là ví dụ; đây là cách đơn giản để chịu được message trùng.3 Grokking cũng dùng khái niệm task idempotent để chịu việc chạy lại sau lỗi.7

Lưu ý: Exactly-once của stream processor nói về ảnh hưởng tới state và sink. Event vẫn có thể được xử lý nhiều lần bên trong sau khi phục hồi. Nếu operator gọi API bên ngoài (gửi email, trừ tiền), lời gọi đó có thể lặp. Cần idempotency key ở phía API, cùng nguyên tắc handler idempotent ở trên.3

Con số và sizing

Thư viện không có sách đo hiệu năng Flink, nên bài không đưa throughput, kích thước state hay cấu hình máy của sản phẩm; hãy đo bằng load test.1 Phần ví dụ dưới đây chỉ dùng phép tính ước lượng theo Grokking với đầu vào là giả định.

Ví dụ: ad click aggregator (giả định)

  • 100 triệu click/ngày → khoảng 1.160 click/giây trung bình (100.000.000 / 86.400). Peak ×10 cho chiến dịch lớn ≈ 12.000 click/giây (hệ số peak là giả định của khoá học).8
  • Chia topic thành nhiều partition (ví dụ 24); mỗi partition peak khoảng 500 event/giây, và mức song song của job nên khớp số partition (nguyên tắc chung của khoá học).4
  • Số TaskManager và slot cần tính theo throughput đo thực tế, không có con số trong sách.
  • State: số quảng cáo hoạt động × số cửa sổ đang mở × kích thước mỗi bản ghi (giả định), thường nhỏ so với RAM.
  • Đầu ra: ghi upsert theo (ad_id, minute) vào bảng tổng hợp.

Ở bài toán này, nút thắt hay gặp nhất là key nóng (một quảng cáo lớn dồn vào một subtask) và sink chậm. Cách xử lý key nóng: pre-aggregate theo (ad_id, random(0..9)) rồi gộp lần hai. Đây chính là ý tưởng sharded counter: chia một counter nóng thành nhiều shard ghi ngẫu nhiên rồi cộng lại khi đọc.9

Khi nào dùng: so với Kafka Streams và Spark

Phần so sánh sản phẩm này chưa có sách trong thư viện; bảng chỉ nêu khác biệt ở mức khái niệm.1

FlinkKafka StreamsSpark Structured Streaming
Dạng triển khaiCluster riêngThư viện nhúng trong service của bạnCluster Spark
Mô hìnhTừng eventTừng eventMicro-batch (mặc định)
Nguồn và đíchNhiều connectorTập trung vào KafkaNhiều connector
Hợp vớiPipeline lớn, nhiều nguồn, logic thời gian phức tạpTeam đã dùng Kafka, muốn deploy như microserviceTeam đã có Spark batch

Khi nào không cần stream processor (nhận định của khoá học):

  • Chỉ cần đếm đơn giản, chấp nhận sai số nhỏ: consumer + Redis INCR là đủ.
  • Tải nhỏ, kết quả trễ vài phút cũng được: cron chạy SQL trên Postgres hoặc warehouse.
  • Chỉ cần đẩy thay đổi DB sang hệ khác: Change Data Capture với Kafka Connect.

Use case điển hình

  • Ad click aggregation: tumbling window 1 phút theo ad_id, lọc click trùng bằng state theo click_id. Xem Ad click aggregator.
  • Top-K: đếm theo cửa sổ trượt cho từng item, rồi một operator gộp chọn top K. Grokking dùng sharded counter cho bài Top K (xu hướng hashtag trên Twitter).10 Xem Top K.
  • Phát hiện gian lận: state theo thẻ hoặc tài khoản, quy tắc như "nhiều giao dịch ở nhiều quốc gia trong một khoảng ngắn".
  • Metrics và alert: gom metrics theo cửa sổ, so ngưỡng. Grokking nhắc dùng hệ thống xử lý stream trực tuyến để làm giám sát gần thời gian thực.11 Xem Metrics monitoring.

Vận hành và điểm nói chuyện trong phỏng vấn

Quy mô nhỏ. Dùng dịch vụ managed thay vì tự vận hành cluster. Thư viện không có sách về các dịch vụ này hay giá của chúng nên bài không liệt kê.1

Quy mô lớn. Tự host cần theo dõi (danh sách theo kinh nghiệm của khoá học):1

  • Consumer lag của Kafka: lag tăng liên tục nghĩa là job không theo kịp.
  • Backpressure: operator nào đang làm chậm cả pipeline.
  • Thời gian và kích thước checkpoint: checkpoint lâu hơn khoảng checkpoint là dấu hiệu nguy hiểm.
  • Nâng cấp code qua savepoint, giữ định danh cố định cho operator để state ánh xạ đúng.

Sai lầm hay gặp: Gom theo processing time cho báo cáo tính tiền. Khi Kafka chậm hoặc job khởi động lại, event dồn vào cửa sổ sai và số liệu mỗi lần chạy lại một khác. Dùng event time cùng watermark.1

Trong phỏng vấn: Với bài toán aggregate realtime, vẽ Kafka → Flink → kho kết quả, rồi nói rõ bốn điều: gom theo event time với watermark trễ khoảng X giây, window loại gì, checkpoint định kỳ lên storage bền, và sink idempotent theo khoá (id, window) để có exactly-once. Bổ sung một job batch đối soát hằng ngày nếu bài toán dính tới tiền.35

Các bài toán hay dùng Flink:

Nguồn trích dẫn

  1. Grokking Modern System Design · ch.17 Distributed Messaging Queue · mục về consumer xoá message sau khi xử lý và visibility timeout (at-least-once delivery). ↩

  2. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages (tr. 95–96): broker thường chỉ hứa giao at-least-once; "Writing idempotent message handlers". ↩ ↩2 ↩3 ↩4

  3. Grokking Modern System Design · ch.17 Distributed Messaging Queue · phần chia queue thành các shard theo partition key, mỗi shard nằm trên một host. ↩ ↩2

  4. Grokking Modern System Design · ch.4 Non-functional System Characteristics · mục "Fault tolerance", "Checkpointing". ↩ ↩2 ↩3

  5. Grokking Modern System Design · ch.19 Rate Limiter · mục "Algorithms for rate limiting", "Sliding window log algorithm", "Sliding window counter algorithm". ↩

  6. Grokking Modern System Design · ch.23 Distributed Task Scheduler · mục "Task idempotency". ↩

  7. Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Requests estimation" và "Number of servers required". ↩

  8. Grokking Modern System Design · ch.24 Sharded Counters · mục "High-level solution sketch" và "Detailed design". ↩

  9. Grokking Modern System Design · ch.24 Sharded Counters · mục "Using sharded counters for the Top K problem". ↩

  10. Grokking Modern System Design · ch.13 Distributed Monitoring · bài "Focus on Client-side Errors in a Monitoring System", phần thiết kế collector dùng hệ thống xử lý stream trực tuyến. ↩