Flink 1 — Tổng quan xử lý luồng thời gian thực

13 thg 7, 2026 4 lượt xem
#data-engineering
#real-time
#flink
#stream-processing

Từ dữ liệu "đã xong" sang dữ liệu "đang chảy"

Phần lớn hệ thống dữ liệu truyền thống làm việc theo lối batch (theo lô): gom dữ liệu suốt một khoảng thời gian — một giờ, một ngày — rồi chạy một job xử lý trọn gói. Bạn ngủ dậy, báo cáo đêm qua đã sẵn. Mô hình này đơn giản, dễ suy luận, và đúng cho rất nhiều bài toán (đối soát cuối ngày, tính lãi, dựng data warehouse). Nhưng nó có một giả định ngầm: dữ liệu đứng yên khi bạn xử lý nó, và độ trễ hàng giờ là chấp nhận được.

Giả định đó vỡ vụn ở nhiều nghiệp vụ ngân hàng hiện đại. Một giao dịch thẻ đáng ngờ cần bị chặn trong lúc nó đang diễn ra, không phải phát hiện vào sáng hôm sau khi tiền đã ra khỏi hệ thống. Một cảnh báo hạn mức, một dashboard giám sát hệ thống thanh toán, một tín hiệu rủi ro thanh khoản — tất cả mất phần lớn giá trị nếu trễ vài giờ. Đây là địa hạt của stream processing (xử lý luồng): xử lý dữ liệu liên tục, ngay khi nó phát sinh, với độ trễ tính bằng mili giây đến giây.

Bài này mở đầu series về Apache Flink — một trong những engine xử lý luồng mạnh nhất hiện nay. Ta sẽ làm rõ stream processing thực chất là gì, phân biệt nó với batch và với "micro-batch", định vị Flink giữa các đối thủ, rồi mổ xẻ kiến trúc runtime để bạn có bản đồ tổng thể trước khi đi sâu vào từng chủ đề ở các bài sau.

Stream processing là gì

Bản chất của stream processing rất gọn: thay vì đợi tích đủ một lô rồi xử lý một lần, bạn xử lý từng bản ghi (hoặc từng nhóm nhỏ) ngay khi nó đến, và duy trì kết quả cập nhật liên tục. Nguồn dữ liệu ở đây là một luồng sự kiện (event stream) không có điểm kết thúc: mỗi lần quẹt thẻ, mỗi lần đăng nhập, mỗi lệnh chuyển khoản là một sự kiện được đẩy vào hệ thống theo thời gian.

Cách phân biệt gốc rễ nhất giữa hai thế giới nằm ở tính chất của dữ liệu:

  • Bounded data (dữ liệu có biên): một tập dữ liệu hữu hạn, biết trước điểm đầu và điểm cuối — ví dụ toàn bộ giao dịch của tháng 5. Xử lý bounded data chính là batch processing.
  • Unbounded data (dữ liệu vô biên): một dòng sự kiện không bao giờ kết thúc — giao dịch cứ tiếp tục phát sinh mãi mãi. Không có "bản ghi cuối cùng" để chờ. Xử lý unbounded data chính là stream processing.

Điểm tinh tế: unbounded không có nghĩa "vô hạn tức thời". Tại bất kỳ thời điểm nào, bạn chỉ thấy các sự kiện đã đến cho tới lúc này. Điều này sinh ra những câu hỏi mà batch không phải trả lời: khi nào thì một "cửa sổ 1 phút" được coi là đóng? Xử lý ra sao khi một sự kiện đến trễ so với thứ tự? Làm sao đảm bảo mỗi sự kiện được tính đúng một lần dù máy chủ có thể sập giữa chừng? Đây chính là những vấn đề cốt lõi mà Flink giải quyết, và ta sẽ đào sâu ở Flink 3 — Event-time & windows, Flink 4 — Quản lý stateFlink 6 — Fault tolerance.

Micro-batch vs true streaming

Không phải mọi thứ tự nhận là "streaming" đều xử lý theo từng bản ghi. Có hai kiến trúc rất khác nhau ẩn dưới cùng một cái tên.

Micro-batch. Đây là cách Spark Structured Streaming vận hành (ở chế độ mặc định). Về bản chất, nó vẫn là batch, chỉ là các lô cực nhỏ và chạy liên tục. Engine gom sự kiện đến trong một khoảng ngắn — ví dụ 500 mili giây hoặc vài giây — rồi chạy một mini-job Spark trên lô đó, rồi lặp lại. Ưu điểm: tái dùng toàn bộ engine batch đã trưởng thành, thông lượng rất cao, mô hình lập trình quen thuộc với người đã dùng Spark. Nhược điểm cố hữu: độ trễ bị chặn dưới bởi độ dài lô — nếu lô 1 giây thì không sự kiện nào được xử lý nhanh hơn ~1 giây.

True streaming (per-record). Đây là cách Flink vận hành nguyên bản. Mỗi bản ghi được đẩy qua đồ thị xử lý ngay khi đến, không chờ gom lô. Kết quả là độ trễ có thể xuống mili-giây. Đây là mô hình phù hợp khi từng mili-giây có giá — chặn giao dịch gian lận, phản hồi rủi ro tức thời.

Tiêu chíMicro-batchTrue streaming (per-record)
Đơn vị xử lýLô nhỏ theo chu kỳTừng bản ghi
Độ trễ điển hìnhTrăm mili-giây đến giâyMili-giây
Thông lượngRất caoCao
Đại diệnSpark Structured StreamingApache Flink, Kafka Streams
Điểm mạnhTái dùng engine batch, đơn giảnĐộ trễ thấp, xử lý event-time tinh vi

Lưu ý công bằng: Spark có chế độ "continuous processing" độ trễ thấp hơn nhưng còn nhiều giới hạn; và trong rất nhiều bài toán, micro-batch vài trăm mili-giây là quá đủ. Chọn công cụ theo yêu cầu độ trễ thực tế, đừng theo khẩu hiệu.

Apache Flink là một engine xử lý luồng có trạng thái (stateful), phân tán, được thiết kế cho thông lượng cao và độ trễ thấp, là dự án cấp cao (top-level project) của Apache Software Foundation. Ta bóc tách từng vế:

  • Stateful (có trạng thái): Flink cho phép mỗi phép toán ghi nhớ thông tin xuyên qua nhiều sự kiện — ví dụ "tổng số tiền giao dịch của thẻ này trong 10 phút qua". State này được Flink quản lý, phân mảnh theo khóa, và — quan trọng — được checkpoint định kỳ để khôi phục sau sự cố mà không mất mát. Đây là điều làm Flink vượt xa một bộ lọc sự kiện đơn thuần; xem Flink 4 — Quản lý state.
  • Phân tán (distributed): công việc được chia thành nhiều task chạy song song trên nhiều máy, mở rộng theo chiều ngang khi tải tăng.
  • Thông lượng cao / độ trễ thấp: nhờ mô hình per-record với pipeline hóa và cơ chế checkpoint không chặn (asynchronous barrier snapshotting), Flink đạt đồng thời cả hai điều thường bị coi là đánh đổi.

Một điểm định danh quan trọng: Flink hỗ trợ exactly-once semantics (mỗi sự kiện tác động lên kết quả đúng một lần) đối với state nội bộ — nền tảng cho tính đúng đắn trong nghiệp vụ tài chính, chi tiết ở Flink 6.

Ba cái tên hay bị đặt cạnh nhau, nhưng chúng khác nhau ngay từ bản chất là gì.

Apache Flink — một engine/cluster xử lý luồng đầy đủ. Bạn viết job và submit nó lên một cụm Flink (JobManager + TaskManager). Mạnh nhất ở: true streaming độ trễ thấp, xử lý event-time tinh vi, quản lý state lớn, exactly-once. Yếu ở: phải vận hành một cụm riêng (dù có thể chạy trên Kubernetes/YARN), đường học dốc hơn.

Spark Structured Streaming — chế độ streaming của Apache Spark. Là engine nhưng vận hành theo micro-batch. Mạnh nhất khi: bạn đã dùng Spark cho batch và muốn dùng lại kỹ năng, code, hạ tầng; thông lượng cực cao; bài toán chấp nhận độ trễ giây. Yếu ở: độ trễ bị micro-batch chặn dưới; xử lý event-time/state kém uyển chuyển hơn Flink.

Kafka Streamskhông phải cụm, mà là một thư viện (library) Java/Scala. Bạn nhúng nó vào ứng dụng của mình; ứng dụng đó chính là worker, không có JobManager điều phối tách rời. Gắn chặt với Apache Kafka: đọc từ topic, xử lý, ghi ra topic. Mạnh nhất khi: pipeline đã sống trong hệ sinh thái Kafka, muốn triển khai gọn như một microservice, không muốn nuôi thêm cụm. Yếu ở: khả năng mở rộng và xử lý state/event-time không sâu bằng Flink; ràng buộc vào Kafka.

FlinkSpark Structured StreamingKafka Streams
DạngEngine + clusterEngine + clusterThư viện nhúng
Mô hìnhTrue streamingMicro-batchTrue streaming (per-record)
Độ trễMili-giây~giâyMili-giây
State & event-timeRất mạnhTrung bìnhKhá
Nguồn dữ liệuĐa dạng (Kafka, file, DB CDC...)Đa dạngChỉ Kafka
Vận hànhCụm riêngCụm SparkKhông cụm, nhúng vào app
Chọn khiĐộ trễ thấp, state phức tạp, exactly-onceĐã có Spark, chấp nhận trễ giâyĐã dùng Kafka, muốn nhẹ

Nguyên tắc chọn nhanh: đã sống trong Kafka và muốn nhẹ → Kafka Streams; đã đầu tư Spark và trễ giây là ổn → Spark Streaming; cần độ trễ mili-giây với state/event-time phức tạp và exactly-once → Flink. So sánh sâu hơn về Kafka Streams ở data-engineering-05 — Kafka streaming.

Một job Flink chạy trên một cụm gồm hai loại tiến trình chính, cộng với client submit.

JobManager (bộ điều phối). Là "bộ não" của cụm. Nó nhận job từ client, biến logic của bạn thành một dataflow graph (đồ thị luồng dữ liệu), lập lịch từng task lên các TaskManager, và — cực kỳ quan trọng — điều phối checkpoint (chụp ảnh nhất quán state toàn hệ thống để khôi phục sau sự cố). Nếu một task fail, JobManager chịu trách nhiệm khởi động lại và phục hồi từ checkpoint gần nhất. Trong cụm production thường có nhiều JobManager ở chế độ high-availability để tránh điểm chết đơn.

TaskManager (worker thực thi). Là các tiến trình thực sự chạy công việc. Mỗi TaskManager cung cấp một số task slot — đơn vị tài nguyên cố định; mỗi slot chạy một phần của pipeline. Song song hóa (parallelism) của job chính là tổng số slot mà các task của nó chiếm. TaskManager cũng là nơi state được lưu giữ (trong bộ nhớ hoặc trên đĩa qua RocksDB) và nơi dữ liệu chảy giữa các phép toán.

Client. Không thuộc runtime của cụm. Client là nơi bạn submit job: nó dịch chương trình của bạn thành dataflow graph rồi gửi lên JobManager, sau đó có thể ngắt kết nối (detached mode) hoặc chờ theo dõi.

Dataflow graph. Flink diễn giải chương trình của bạn thành một đồ thị: node là các operator (source → transform → sink), cạnh là dòng dữ liệu. Các operator liền kề có thể được gộp chuỗi (operator chaining) vào cùng một slot để giảm chi phí truyền dữ liệu. Đây là mô hình lập trình cốt lõi, sẽ được đào ở Flink 2 — Dataflow model.

Các tầng API

Flink cung cấp nhiều tầng API, đánh đổi giữa mức trừu tượng và mức kiểm soát. Bạn có thể trộn lẫn chúng trong cùng một chương trình.

  • Table API / Flink SQL — tầng cao nhất, khai báo (declarative). Bạn mô tả cái gì muốn tính bằng SQL hoặc API dạng bảng; Flink lo làm thế nào. Tuyệt vời cho phân tích, aggregation, join theo cửa sổ, và cho người quen SQL. Đây là điểm vào dễ nhất; chi tiết ở Flink 5 — Flink SQL.
  • DataStream API — tầng giữa, mệnh lệnh (imperative) trên luồng. Bạn thao tác trực tiếp với các phép map, filter, keyBy, window, process. Đây là tầng "chủ lực" cho phần lớn ứng dụng streaming thực tế; xem Flink 2.
  • ProcessFunction — tầng thấp nhất trong tập API xử lý luồng. Cho bạn quyền truy cập trực tiếp vào state, timer (đặt hẹn giờ theo event-time/processing-time), và từng sự kiện thô. Dùng khi logic quá đặc thù để diễn đạt bằng các toán tử sẵn có — ví dụ máy trạng thái phát hiện chuỗi hành vi gian lận.

Minh họa (KHÔNG chạy được — chỉ để hình dung dạng code DataStream):

// Minh hoạ — không phải code chạy trong sandbox
DataStream<Transaction> txns = env.addSource(new FlinkKafkaConsumer<>(...));
txns
  .keyBy(Transaction::getCardId)
  .window(TumblingEventTimeWindows.of(Time.minutes(10)))
  .aggregate(new SumAmount())     // tổng tiền/thẻ trong 10 phút
  .filter(sum -> sum > THRESHOLD) // vượt ngưỡng → nghi vấn
  .addSink(new AlertSink());

Và cùng ý tưởng bằng Flink SQL (minh họa, KHÔNG chạy được trong sandbox — đây là phương ngữ Flink, không phải PostgreSQL):

-- Minh hoạ Flink SQL — KHÔNG chạy được trong sandbox
SELECT card_id,
       TUMBLE_START(event_time, INTERVAL '10' MINUTE) AS win_start,
       SUM(amount) AS total_10m
FROM transactions
GROUP BY card_id, TUMBLE(event_time, INTERVAL '10' MINUTE)
HAVING SUM(amount) > 100000000;

Triết lý "streaming-first"

Điểm triết học phân biệt Flink với các engine gốc-batch: Flink coi stream là mô hình gốc, còn batch chỉ là một trường hợp đặc biệt của stream — cụ thể là stream trên dữ liệu có biên (bounded). Ngược lại, Spark khởi thủy là engine batch rồi mới thêm streaming lên trên (dưới dạng micro-batch).

Hệ quả không nhỏ: cùng một engine, cùng một tập API xử lý được cả hai chế độ; khi bạn cho Flink một nguồn hữu hạn, nó tự chạy ở chế độ batch tối ưu (không cần checkpoint liên tục, có thể xử lý theo giai đoạn). Nhờ đó bạn không phải bảo trì hai codebase batch/stream tách biệt cho cùng một logic nghiệp vụ — một lợi thế vận hành lớn khi đội cần vừa xử lý dòng real-time vừa chạy backfill lịch sử bằng cùng một logic.

Ba trụ cột dưới đây là lý do người ta chọn Flink cho các bài toán khó, và mỗi trụ cột có một bài riêng trong series:

  • Stateful cấp production: state được phân mảnh theo khóa, lưu cục bộ ở TaskManager (có thể dùng RocksDB cho state vượt RAM), và checkpoint bất đồng bộ để không chặn luồng — cho phép giữ state cực lớn mà vẫn khôi phục được. Xem Flink 4.
  • Event-time: Flink xử lý theo thời điểm sự kiện thực sự xảy ra, không phải thời điểm nó đến hệ thống, dùng watermark để xử lý dữ liệu đến trễ và lệch thứ tự một cách đúng đắn. Đây là điều tối quan trọng khi mạng trễ hay thiết bị offline gửi dồn. Xem Flink 3.
  • Exactly-once: nhờ checkpoint nhất quán toàn cục, mỗi sự kiện tác động lên state đúng một lần dù có sự cố — nền tảng cho tính đúng đắn tài chính. Xem Flink 6.

Về triển khai và vận hành thực chiến (Kubernetes, HA, autoscaling, giám sát), xem Flink 7 — Deployment & Ops; còn tập hợp các bài toán ngân hàng cụ thể ở Flink 8 — Use case ngân hàng.

Use case thực tế

Bối cảnh (NCB). Đội rủi ro cần một hệ thống giám sát giao dịch thẻ real-time để chặn gian lận. Yêu cầu nghiệp vụ: nếu một thẻ phát sinh ≥ 5 giao dịch trong 2 phút với tổng số tiền ≥ 50 triệu VND, hoặc phát sinh giao dịch ở ≥ 3 quốc gia khác nhau trong 10 phút, thì phát cảnh báo trong dưới 1 giây kể từ giao dịch cuối để có thể chặn kịp.

Vì sao batch/micro-batch không đủ. Job batch cuối ngày phát hiện gian lận thì tiền đã ra khỏi hệ thống. Micro-batch 5 giây vẫn là 5 giây trễ trên đường ra tiền — trong tấn công thẻ tự động, hàng chục giao dịch có thể trôi qua trong khoảng đó.

Thiết kế bằng Flink (mô tả, không phải cấu hình chạy được):

  1. Nguồn: giao dịch thẻ được đẩy vào một topic Kafka card-transactions ngay khi core banking ghi nhận; Flink đọc trực tiếp topic này.
  2. Khóa theo thẻ: keyBy(card_id) — Flink phân mảnh state theo từng thẻ, mỗi thẻ có bộ đếm và tổng riêng.
  3. Cửa sổ event-time: đếm giao dịch và cộng dồn số tiền trong cửa sổ trượt 2 phút và 10 phút, dùng event-time + watermark để xử lý đúng cả khi giao dịch đến lệch thứ tự do trễ mạng.
  4. State + ProcessFunction: giữ tập quốc gia đã xuất hiện của mỗi thẻ trong 10 phút; khi vượt ngưỡng thì phát sự kiện cảnh báo.
  5. Sink: cảnh báo đẩy vào topic fraud-alerts để hệ thống chặn thẻ tiêu thụ, đồng thời ghi vào kho phục vụ điều tra.
  6. Exactly-once: nhờ checkpoint, khi một TaskManager sập giữa cao điểm, bộ đếm không bị tính lặp hay bỏ sót — tránh cả báo động giả lẫn bỏ lọt.

Kết quả kỳ vọng (minh họa): độ trễ cảnh báo p99 dưới 1 giây, xử lý được hàng chục nghìn giao dịch/giây trên một cụm vài TaskManager, và khôi phục sau sự cố node mà không sai lệch bộ đếm. Toàn bộ chuỗi cửa sổ — event-time — state — exactly-once này sẽ được dựng từng bước qua các bài tiếp theo.

Ghi nhớ

  • Batch xử lý dữ liệu bounded (có biên), stream xử lý dữ liệu unbounded (vô biên) — khác biệt gốc rễ nằm ở tính chất dữ liệu, không chỉ ở tốc độ.
  • Micro-batch (Spark Structured Streaming) vẫn là batch lô nhỏ — độ trễ bị chặn dưới bởi độ dài lô (giây); true streaming (Flink) xử lý từng bản ghi — độ trễ mili-giây.
  • Flink là engine xử lý luồng có trạng thái, phân tán, thông lượng cao / độ trễ thấp, dự án top-level của Apache, hỗ trợ exactly-once.
  • Định vị: Flink = engine mạnh nhất cho state/event-time độ trễ thấp; Spark Streaming = tái dùng Spark, chấp nhận trễ giây; Kafka Streams = thư viện nhúng gọn, gắn Kafka.
  • Runtime: JobManager điều phối + checkpoint; TaskManager thực thi task trong các slot; client submit job dưới dạng dataflow graph.
  • Ba tầng API: Table/SQL (khai báo) → DataStream (mệnh lệnh) → ProcessFunction (state + timer, mức thấp nhất).
  • Streaming-first: Flink coi batch là trường hợp đặc biệt của stream (bounded), dùng chung engine và API cho cả hai.
  • Ba trụ cột làm nên sức mạnh Flink: stateful cấp production, xử lý event-time bằng watermark, và exactly-once — nền tảng cho giám sát giao dịch ngân hàng real-time.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5