← Kỹ sư dữ liệu← Data Engineer
Kỹ sư dữ liệuData Engineer19 Th7, 2026Jul 19, 202628 phút đọc22 min read

Streaming & Dữ liệu Real-timeStreaming & Real-Time Data

Thuộc bộ kiến thức Data Engineer Roadmap.

Tổng quan

Phần lớn công việc của một data platform xoay quanh câu hỏi về thời gian: con số này cần “tươi” đến mức nào? Một báo cáo tài chính hàng tháng có thể chấp nhận dữ liệu cũ một ngày. Nhưng một mô hình fraud detection quyết định có chặn một giao dịch thẻ tín dụng hay không thì không thể chờ quá vài trăm mili-giây. Streaming và real-time data engineering là tập hợp các công cụ và mô hình để xây dựng loại hệ thống thứ hai — các pipeline phản ứng với event ngay khi chúng xảy ra, thay vì chờ một lần chạy theo lịch để gom chúng lại.

Note này xây dựng tiếp trên ./03-data-sources-and-ingestion.md, nơi đã giới thiệu batch, micro-batch và streaming như các mô hình thời gian ingestion, và đã đề cập Change Data Capture (CDC) như cầu nối giữa log replication nội bộ của database và một stream các event. Ở đây chúng ta đi sâu hơn: điều gì thực sự phân biệt một “stream” với một “message queue,” Apache Kafka — nền tảng streaming phổ biến nhất trong ngành — tổ chức và đảm bảo việc phân phối dữ liệu như thế nào, hệ sinh thái messaging xung quanh (RabbitMQ, SNS/SQS) trông ra sao và khi nào nên dùng công cụ nào, và dữ liệu streaming được xử lý ra sao một khi nó đã chảy vào hệ thống. Note kết thúc bằng các use case thực tế — clickstream analytics, fraud detection, IoT telemetry, và replication dựa trên CDC — để gắn các khái niệm vào những hệ thống cụ thể mà bạn nhiều khả năng sẽ xây dựng hoặc vận hành.

Kiến thức nền tảng

Batch vs. streaming vs. micro-batch

./03-data-sources-and-ingestion.md đã giới thiệu ba mô hình thời gian này ở tầng ingestion; cùng ba mô hình đó cũng áp dụng tương tự ở tầng xử lý dữ liệu sau khi nó đã được nạp vào, và việc nhắc lại chính xác các khác biệt là đáng làm vì phần lớn sự nhầm lẫn trong ngành đến từ việc coi “streaming” là một thứ duy nhất, định nghĩa rõ ràng, trong khi thực tế nó trải dài trên một phổ (spectrum).

Batch processing xử lý một tập dữ liệu bounded (có giới hạn) — một khối dữ liệu có điểm bắt đầu và kết thúc rõ ràng — thường được gom lại trong một khoảng thời gian và xử lý theo lịch. Một job chạy ban đêm đọc bảng đơn hàng của ngày hôm qua và tính doanh thu theo ngày là batch: tập input đã đóng và hữu hạn ngay cả trước khi job bắt đầu chạy. Batch đơn giản về mặt khái niệm (đọc hết, transform, ghi ra, xong), và chính sự đơn giản này là lý do các công cụ như Airflow, dbt, và Spark DataFrame API cổ điển được xây dựng xoay quanh nó một cách tự nhiên. Cái giá phải trả là độ trễ (latency): output của một batch pipeline chỉ có thể “tươi” tính đến “lần chạy gần nhất.”

Streaming processing xử lý một chuỗi event unbounded (không giới hạn) không có điểm kết thúc xác định — dữ liệu tiếp tục đến vô hạn, và hệ thống xử lý phải liên tục ingest, tính toán, và trả kết quả ngay khi mỗi event (hoặc một nhóm nhỏ event) đến, mà không bao giờ tuyên bố input đã “hoàn tất.” Một dịch vụ fraud detection chấm điểm mỗi lần quẹt thẻ ngay lập tức là streaming theo nghĩa chặt chẽ này: không có ranh giới batch nào, chỉ có một “hiện tại” liên tục. Mô hình này đòi hỏi trả lời những câu hỏi mà batch không bao giờ phải đặt ra — điều gì xảy ra khi một event đến muộn hoặc sai thứ tự, cần giữ bao nhiêu state trong bộ nhớ cho một phép aggregation không bao giờ đóng lại, phải làm gì khi node xử lý crash giữa chừng — đó là lý do các hệ thống streaming về cấu trúc phức tạp hơn nhiều để xây dựng và vận hành so với batch.

Micro-batching là điểm cân bằng thực dụng, và trên thực tế đã trở thành cách phổ biến nhất mà hầu hết tổ chức triển khai “streaming.” Thay vì xử lý từng event một, một engine micro-batch gom event lại trong một khoảng thời gian ngắn, cố định — vài giây, hoặc thấp hơn nếu engine cho phép — và xử lý mỗi cửa sổ nhỏ này như một batch job thu nhỏ. Chế độ trigger mặc định của Spark Structured Streaming hoạt động chính xác theo cách này: bên dưới, các query “streaming” thực chất là một chuỗi các batch job nhỏ được kích hoạt mỗi khoảng trigger. Điều này cố ý làm mờ ranh giới giữa hai mô hình: bạn có được phần lớn sự đơn giản về vận hành và tư duy của batch (mỗi micro-batch chỉ là một phép tính DataFrame có giới hạn) trong khi vẫn có được phần lớn lợi ích về latency của streaming (kết quả xuất hiện trong vài giây, không phải vài giờ).

Khía cạnhBatchMicro-batchStreaming
Mô hình inputDataset bounded, đã đóngCác khối nhỏ bounded theo khoảng thời gian ngắnChuỗi unbounded, không bao giờ đóng
Latency điển hìnhPhút đến giờ (hoặc ngày)Giây đến vài phútMili-giây đến vài giây
Độ phức tạp vận hànhThấpTrung bìnhCao
Quản lý stateTính lại mỗi lần chạy, ít state xuyên các lần chạyMột số state được mang qua giữa các micro-batch (ví dụ watermark)Quản lý state liên tục trong bộ nhớ/state-store xuyên suốt cả stream
Khôi phục lỗiChạy lại jobChạy lại micro-batch bị lỗi từ offset của nóCheckpoint/restore state đang xử lý, cần exactly-once semantics
Công cụ điển hìnhAirflow + Spark/dbt batch job, cron scriptSpark Structured Streaming (trigger mặc định), Structured Streaming trên DatabricksKafka Streams, Flink, Spark Structured Streaming (continuous trigger)
Use case điển hìnhBáo cáo hàng ngày/tuần, load data warehouse, backfillDashboard gần real-time, ETL incremental, cảnh báo với sai số chấp nhận vài giâyFraud detection, real-time bidding, gợi ý real-time, giám sát vận hành

Bài học thực tế: đừng mặc định chọn streaming “thực sự.” Hãy hỏi business thực sự cần độ tươi ở mức nào, và nhớ rằng micro-batch mang lại độ tươi ở mức giây với chỉ một phần nhỏ gánh nặng vận hành so với một stream processor chạy liên tục kèm quản lý state exactly-once.

Message vs. stream: một khác biệt định hình việc chọn công cụ

Kafka thường được giới thiệu là “chỉ là một message queue có khả năng scale khủng,” nhưng cách mô tả đó che giấu một khác biệt quyết định công cụ nào thực sự phù hợp với use case của bạn. Khác biệt cốt lõi nằm ở chuyện gì xảy ra với một mẩu dữ liệu sau khi nó được đọc.

Một message queue — RabbitMQ, AWS SQS, hầu hết các broker truyền thống — được xây dựng quanh ý tưởng rằng một message đại diện cho một đơn vị công việc cần được thực hiện đúng một lần. Khi một consumer đọc một message khỏi queue và acknowledge nó, broker sẽ xóa message đó đi. Nếu một consumer thứ hai cố đọc cùng queue, nó sẽ nhận được message tiếp theo, chứ không phải message đã được tiêu thụ — queue phân phối công việc giữa các consumer, và mỗi message chỉ dành cho đúng một người nhận. Mô hình này ánh xạ tự nhiên vào việc phân phối tác vụ: “xử lý đơn hàng này,” “resize ảnh này,” “gửi email này” — những công việc mà bạn muốn đúng một worker nhận mỗi đơn vị công việc, và khi xong thì nó biến mất.

Một stream — Kafka, Amazon Kinesis, Redpanda, Pulsar — được xây dựng quanh một ý tưởng khác: một log có thứ tự, chỉ ghi thêm (append-only), bền vững (durable) và có thể replay. Đọc từ một Kafka topic không xóa bất cứ thứ gì; mỗi consumer (hay mỗi consumer group độc lập) tự theo dõi vị trí (offset) của mình trong log và có thể đọc theo tốc độ riêng, replay từ một điểm trước đó, hoặc để nhiều ứng dụng hoàn toàn độc lập đọc đúng cùng một dữ liệu cho các mục đích khác nhau. Một topic clickstream event có thể đồng thời nuôi một mô hình fraud real-time, một batch ETL job load data warehouse mỗi đêm, và một dashboard giám sát — ba consumer không liên quan gì đến nhau, không ai xóa dữ liệu hay can thiệp vào công việc của người khác.

Message queue (vd: RabbitMQ, SQS)Stream (vd: Kafka)
Abstraction cốt lõiQueue chứa các message rời rạcLog có thứ tự, chỉ ghi thêm
Chuyện gì xảy ra khi đọcMessage (thường) bị xóa/ack và biến mấtDữ liệu vẫn còn; consumer chỉ tiến offset của riêng nó
Nhiều consumer độc lậpKhó — một message đi đến một consumer (trừ khi fanout trước)Tự nhiên — nhiều consumer group đọc cùng log độc lập
ReplayThường không — một khi đã tiêu thụ, nó biến mấtCó — tua lại về bất kỳ offset còn giữ và xử lý lại
Đảm bảo thứ tựThường theo từng queue, đôi khi best-effortĐảm bảo thứ tự mạnh trong từng partition
Phù hợp nhất vớiPhân phối tác vụ, work queue, decouple producer khỏi một jobEvent log, nhiều reader độc lập, lịch sử có thể replay, event sourcing

Khác biệt này là lý do hai loại công cụ không thể thay thế cho nhau, và tại sao câu hỏi “nên dùng Kafka hay RabbitMQ” thực chất là câu hỏi về cách dữ liệu sẽ được tiêu thụ, chứ không phải câu hỏi về throughput thô.

Khái niệm chính

Apache Kafka đi sâu

Apache Kafka là nền tảng streaming mã nguồn mở chiếm ưu thế, và hiểu các abstraction cốt lõi của nó gần như là điều kiện tiên quyết để làm việc với dữ liệu real-time ở bất kỳ đâu trong ngành.

Topic. Một topic là một stream các record có tên, mang tính logic — ví dụ orders.created hay clickstream.pageviews. Producer publish record vào một topic; consumer subscribe và đọc từ một hoặc nhiều topic. Topic là đơn vị tổ chức cơ bản, gần tương tự một bảng trong database hay một kênh có tên trong hệ thống pub/sub, chỉ khác ở chỗ những gì nó chứa là một chuỗi event bất biến (immutable) có thứ tự, chứ không phải các hàng có thể thay đổi.

Partition. Mỗi topic được chia thành một hoặc nhiều partition, và đây chính là cơ chế mang lại cho Kafka cả khả năng scale ngang lẫn đảm bảo về thứ tự. Mỗi partition tự nó là một log chỉ ghi thêm, có thứ tự chặt chẽ — các record trong cùng một partition được đảm bảo đọc theo đúng thứ tự đã ghi. Kafka không đảm bảo thứ tự giữa các partition, chỉ đảm bảo trong một partition. Đây là một sự đánh đổi có chủ đích: bằng cách chia một topic thành nhiều partition, Kafka có thể trải throughput của topic lên nhiều broker và nhiều consumer instance song song, với cái giá là chỉ đảm bảo thứ tự trong từng partition. Một producer thường gán một record vào một partition dựa trên key (ví dụ customer_id) — các record cùng key luôn rơi vào cùng một partition, đó là điều giúp bạn có được “thứ tự theo từng customer” ngay cả khi cả topic không có thứ tự toàn cục.

Producer và consumer. Producer là ứng dụng hoặc dịch vụ ghi record vào topic; consumer là ứng dụng đọc chúng. Producer chọn (trực tiếp hoặc qua một partitioner) record nào rơi vào partition nào; consumer đọc một partition tuần tự, theo dõi vị trí của mình bằng một offset — một số nguyên tăng dần đơn giản đánh dấu “tôi đã đọc đến đâu trong partition này.”

Consumer group. Một consumer instance đơn lẻ có thể tự đọc cả một topic, nhưng các hệ thống production thường muốn song song hóa việc tiêu thụ trên nhiều instance của cùng một ứng dụng — đó là mục đích của consumer group. Tất cả consumer instance chia sẻ cùng group.id tạo thành một consumer group logic, và Kafka tự động chia các partition của topic cho các thành viên trong group sao cho mỗi partition chỉ được đọc bởi đúng một consumer instance trong group đó tại một thời điểm. Đây là cách Kafka đạt được cả song song hóa (nhiều instance hơn, nhiều partition được đọc đồng thời) lẫn an toàn (không có hai instance nào trong cùng group xử lý trùng cùng một partition). Nếu một topic có 12 partition và một consumer group có 4 instance, mỗi instance được gán 3 partition; thêm instance (tối đa 12) và Kafka tự động rebalance việc gán. Quan trọng là, các consumer group khác nhau đọc cùng một topic hoàn toàn độc lập với nhau — đây chính xác là đặc tính “nhiều reader độc lập” phân biệt một stream với một queue.

Broker và replication. Một Kafka cluster được tạo thành từ các broker — các server thực sự lưu trữ dữ liệu partition và phục vụ request từ producer/consumer. Mỗi partition được replicate trên nhiều broker (mặc định phổ biến là replication factor bằng 3): một broker giữ replica leader, xử lý mọi read/write cho partition đó, còn các broker khác giữ replica follower, liên tục sao chép dữ liệu từ leader. Nếu broker của leader bị lỗi, một trong các follower đang in-sync sẽ tự động được thăng cấp thành leader, để partition vẫn khả dụng mà không mất dữ liệu. Replication này là điều làm Kafka fault-tolerant ở tầng lưu trữ, độc lập với fault tolerance phía consumer (offset, rebalancing consumer group).

Retention. Khác với message queue, Kafka không xóa một record ngay khi nó được tiêu thụ. Mỗi topic có một retention policy — cấu hình theo thời gian (ví dụ giữ 7 ngày dữ liệu) hoặc theo dung lượng (ví dụ giữ tối đa 100 GB mỗi partition) — và Kafka giữ record trong khoảng thời gian đó bất kể có consumer nào đã đọc chúng hay chưa. Đây là điều làm cho việc replay khả thi: một consumer group mới có thể khởi động và đọc toàn bộ lịch sử còn được giữ của một topic từ đầu, hoặc một consumer hiện có có thể tua ngược offset để xử lý lại một khoảng event trong quá khứ (ví dụ sau khi sửa một bug ở bộ xử lý downstream). Một số topic được cấu hình với log compaction thay vì (hoặc cùng với) retention theo thời gian/dung lượng, chỉ giữ lại record mới nhất cho mỗi key vô thời hạn — hữu ích cho các topic đại diện cho “trạng thái hiện tại” (ví dụ topic các event CDC đại diện giá trị hàng mới nhất) thay vì lịch sử event thuần túy.

Khái niệmNó làm gìVí von
TopicStream các record có tênMột bảng hoặc kênh có tên
PartitionShard có thứ tự, chỉ ghi thêm của một topicMột shard/partition của bảng đó
ProducerGhi record vào một topic (partition)Một người viết/chèn dữ liệu
ConsumerĐọc record từ một topic (partition), tự theo dõi offsetMột người đọc có bookmark
Consumer groupTập consumer chia sẻ công việc trên các partition của topicNhóm worker chia nhau xử lý task của một queue
BrokerServer lưu dữ liệu partition, phục vụ read/writeMột node database
Replication factorSố bản sao broker của mỗi partitionRAID / replica database
RetentionDữ liệu được giữ bao lâu bất kể đã tiêu thụ hay chưaMột file log không bị xóa khi đọc

Các hệ thống messaging khác ngoài Kafka

Kafka không phải công cụ duy nhất trong không gian này, và thường không phải công cụ đúng — lựa chọn phụ thuộc rất nhiều vào khác biệt message/stream đã nói ở trên.

RabbitMQ là một message broker truyền thống triển khai chuẩn AMQP (Advanced Message Queuing Protocol) (nó cũng hỗ trợ MQTT và STOMP). Nó nổi bật ở các mô hình task-queue và phân phối công việc cổ điển: producer publish một task, một exchange định tuyến nó đến một hoặc nhiều queue dựa trên quy tắc routing (exchange kiểu direct, fanout, topic, headers), và một worker tiêu thụ và acknowledge nó, sau đó nó biến mất. RabbitMQ hỗ trợ topology routing linh hoạt, độ ưu tiên theo message, delivery trì hoãn/lên lịch, và acknowledgment semantics chi tiết, nhưng nó không được thiết kế cho replay khối lượng lớn — một khi message đã ack, nó biến mất, và không có khái niệm sẵn có nào về “tua lại và xử lý lại lịch sử” như log được giữ lại của Kafka cung cấp.

AWS SNS + SQS, thường được triển khai theo cặp, là các thành phần pub/sub và queue được quản lý trên AWS. SNS (Simple Notification Service) là cơ chế fan-out pub/sub: một message được publish có thể được gửi tới nhiều subscriber (email, HTTP endpoint, Lambda function, hoặc SQS queue) đồng thời. SQS (Simple Queue Service) là một queue bền vững decouple producer khỏi consumer, đảm bảo delivery ít nhất một lần (at-least-once), và — ở dạng chuẩn (không phải FIFO) — không đảm bảo thứ tự (có biến thể queue FIFO cho trường hợp cần thứ tự chặt chẽ và xử lý exactly-once trong một message group, với throughput thấp hơn). Mô hình kết hợp kinh điển là SNS fan-out ra nhiều SQS queue: một event được publish vào một SNS topic, và nó tự động được sao chép vào nhiều SQS queue độc lập, mỗi queue được tiêu thụ bởi một dịch vụ downstream khác nhau — mang lại semantics “nhiều consumer độc lập” kiểu Kafka mà không cần chạy Kafka, đổi lại là không có mô hình replay/retention của Kafka (một SQS queue không giữ lịch sử dài như một Kafka partition; message thường bị xóa khi đã xử lý xong, tuân theo cửa sổ retention có thể cấu hình lên đến 14 ngày).

Apache KafkaRabbitMQAWS SNS + SQS
Mô hình deliveryPub/sub dựa trên log; nhiều consumer group độc lập đọc cùng dữ liệuDựa trên queue; thường một consumer mỗi message (fanout exchange cho phép pub/sub)SNS = fan-out pub/sub; SQS = queue point-to-point
Thứ tựMạnh trong một partition; không có giữa các partitionTheo từng queue, thường FIFO trong một queueSQS chuẩn: best-effort/không đảm bảo; SQS FIFO: chặt chẽ theo message-group
Retention / replayRetention cấu hình được (thời gian/dung lượng) độc lập với tiêu thụ; hỗ trợ replay đầy đủMessage biến mất khi đã ack; không có replay sẵn cóCửa sổ retention giới hạn (đến 14 ngày trên SQS); không replay theo offset
Đảm bảo deliveryMặc định at-least-once; có thể đạt exactly-once với producer idempotent/transactionalAt-least-once (với ack thủ công); hỗ trợ exactly-once trong một số cấu hình hạn chếAt-least-once
Hồ sơ về scaleThroughput rất cao, scale ngang qua partitionThroughput cao nhưng thường có trần thấp hơn Kafka; scale qua clustering/shardingĐược quản lý hoàn toàn, tự động scale, trả tiền theo mức dùng
Use case điển hìnhEvent streaming, pipeline CDC, tổng hợp log, nhiều consumer downstream độc lậpTask queue, phân phối công việc kiểu RPC, topology routing phức tạpDecouple kiểu serverless/AWS-native, thông báo fan-out, queue đơn giản không cần quản lý hạ tầng

Nguyên tắc chung: dùng queue (RabbitMQ hoặc SQS) khi bạn có các đơn vị công việc rời rạc cần được thực hiện đúng một lần bởi một worker; dùng stream (Kafka, Kinesis) khi nhiều hệ thống độc lập cần đọc cùng một lịch sử có thứ tự, có thể ở các thời điểm khác nhau và tốc độ khác nhau, hoặc khi cần replay.

Các use case streaming trong thực tế

Clickstream analytics. Mỗi lần xem trang, click, cuộn trang trên một website hay app là một event, và ở lưu lượng có ý nghĩa, các event này đến như một stream liên tục, unbounded, chứ không phải một batch gọn gàng. Dữ liệu clickstream thường chảy vào các topic Kafka (hoặc Kinesis/Pub/Sub), từ đó một stream processor tính các chỉ số cuộn liên tục — số user hoạt động trong 5 phút gần nhất, tỷ lệ rời funnel gần real-time — trong khi cùng topic đó được tiêu thụ độc lập bởi một batch job nạp toàn bộ lịch sử event vào warehouse để phân tích lịch sử. Đây là một ví dụ điển hình cho đặc tính “nhiều consumer độc lập đọc cùng một stream” phân biệt Kafka với queue.

Fraud detection. Một lần authorize thanh toán có ngân sách latency tính bằng hàng trăm mili-giây, không phải phút — đến lúc một batch job nhận ra một mẫu gian lận thì giao dịch đã hoàn tất rồi. Các hệ thống fraud detection thường tiêu thụ một stream các event giao dịch theo thời gian thực, chạy mỗi event qua một mô hình chấm điểm duy trì các feature stateful cuộn liên tục (ví dụ “số giao dịch của thẻ này trong 10 phút gần nhất”), và trả về quyết định chấp nhận/từ chối đồng bộ hoặc gần đồng bộ. Đây là một trong những trường hợp rõ ràng nhất mà streaming thực sự (không phải micro-batch) là hợp lý, vì yêu cầu về latency không còn chỗ cho dù chỉ vài giây trễ do batching.

IoT sensor telemetry. Các đội thiết bị — cảm biến công nghiệp, phương tiện, đồng hồ đo thông minh, thiết bị đeo — mỗi cái phát ra các reading nhỏ, thường xuyên, và khi cộng dồn lại tạo thành một stream có khối lượng và tốc độ cực cao. Vì thiết bị thường ở trên mạng có băng thông hạn chế, không ổn định, hoặc tính phí theo dung lượng, chúng thường publish qua một giao thức nhẹ như MQTT thay vì trực tiếp đến Kafka — MQTT được thiết kế cho mạng băng thông thấp, độ trễ cao, không ổn định, và dùng mô hình publish/subscribe nhẹ với broker có dấu chân nhỏ (ví dụ Mosquitto, AWS IoT Core, hoặc một cầu nối MQTT-to-Kafka được quản lý). Một mẫu phổ biến là edge pre-aggregation: thay vì truyền từng reading thô riêng lẻ, thiết bị hoặc một gateway cục bộ tổng hợp các reading trong một cửa sổ ngắn (ví dụ nhiệt độ trung bình mỗi phút) trước khi truyền, đánh đổi một chút độ chi tiết để giảm đáng kể băng thông và chi phí. Từ đó, một MQTT broker hoặc bridge (ví dụ một Kafka Connect MQTT source connector) đưa các reading đã tổng hợp vào topic Kafka để xử lý downstream.

Event CDC chảy vào các consumer real-time. Như đã đề cập trong ./03-data-sources-and-ingestion.md, các công cụ CDC dựa trên log như Debezium biến các thay đổi ở mức hàng của database thành một stream event được publish vào Kafka. Một khi các event đó nằm trong một topic, chúng hoạt động như bất kỳ stream nào khác: một materialized view hay search index có thể tiêu thụ chúng để đồng bộ gần real-time với database nguồn, một microservice downstream có thể phản ứng với các event nghiệp vụ cụ thể (ví dụ “trạng thái đơn hàng chuyển sang đã giao” kích hoạt một thông báo), và một batch ETL job có thể độc lập tiêu thụ cùng topic đó để nạp data warehouse — tất cả từ cùng một stream event CDC, không cần phối hợp gì giữa các consumer này vì mỗi cái tự theo dõi offset của mình trong consumer group riêng.

Xử lý dữ liệu streaming

Một khi event đang chảy qua một stream, logic xử lý áp dụng lên chúng chia thành hai nhóm lớn.

Stateless transformation xử lý mỗi event độc lập, không cần bất kỳ thông tin nào từ các event khác. Lọc bỏ record bị lỗi định dạng, map một payload JSON thô thành một cấu trúc có kiểu, hay che một trường nhạy cảm đều là stateless — việc transform một event không phụ thuộc vào bất kỳ event nào khác, điều này làm cho xử lý stateless dễ suy luận, dễ scale, và dễ khôi phục (một worker bị crash chỉ đơn giản tiếp tục từ offset đã commit gần nhất, không cần khôi phục state nào).

Stateful transformation đòi hỏi theo dõi thông tin xuyên qua nhiều event, và khó xây dựng đúng hơn đáng kể. Ví dụ kinh điển là windowed aggregation — “đếm bao nhiêu event xảy ra trong mỗi cửa sổ 5 phút,” “tính trung bình cuộn số tiền giao dịch mỗi customer trong 10 phút gần nhất.” Việc này đòi hỏi engine xử lý duy trì state (số đếm/tổng đang chạy cho mỗi window/key) xuyên suốt một stream unbounded, quyết định khi nào một window “hoàn tất” (điều này phức tạp hơn do event đến muộn hoặc sai thứ tự — được xử lý qua watermark, một cơ chế để tuyên bố “chúng ta không còn mong đợi event cũ hơn mốc thời gian này nữa”), và lưu state đó bền vững để một node xử lý bị crash có thể khôi phục mà không mất hay đếm trùng dữ liệu.

Hai processing engine chiếm ưu thế trong không gian này. Kafka Streams là một thư viện Java (không phải một cluster riêng cần vận hành) cho phép một ứng dụng tiêu thụ từ Kafka, thực hiện các transformation stateful hoặc stateless dùng mô hình stream-table duality, và ghi kết quả trở lại Kafka — nó gắn chặt với hệ sinh thái Kafka và phù hợp với các đội đã chuẩn hóa quanh Kafka. Spark Structured Streaming coi một stream như một bảng unbounded và cho phép bạn diễn đạt các phép tính streaming bằng cùng DataFrame API dùng cho batch job của Spark (đề cập thêm ở ./10-big-data-and-distributed-computing.md), chạy như một chuỗi micro-batch (hoặc, ở các phiên bản mới hơn, một chế độ xử lý liên tục với latency thấp hơn) — phù hợp tự nhiên với các đội đã chạy Spark cho batch ETL muốn mở rộng cùng bộ kỹ năng và codebase sang các workload streaming. Apache Flink, một engine streaming xử lý từng event thực sự, có hỗ trợ trưởng thành cho xử lý event-time và state exactly-once, là lựa chọn thứ ba phổ biến khi yêu cầu cốt lõi là xử lý stateful thông lượng cao, dưới giây, chứ không phải yêu cầu phụ.

Dù dùng engine nào, vận hành một pipeline streaming stateful trong production đòi hỏi cùng kỷ luật đã đề cập ở ./18-monitoring-and-observability.md: consumer lag (một consumer group đang tụt lại bao xa so với offset mới nhất) là chỉ số riêng của streaming quan trọng nhất cần cảnh báo, vì lag tăng dần nghĩa là một consumer không theo kịp input của nó và dữ liệu downstream đang âm thầm cũ dần theo từng giây.

Best Practices

Tài liệu tham khảo

Part of the Data Engineer Roadmap knowledge base.

Overview

Most of what a data platform does is fundamentally about time: how fresh does this number need to be? A monthly finance report can tolerate data that’s a day old. A fraud model deciding whether to block a credit card transaction cannot wait more than a few hundred milliseconds. Streaming and real-time data engineering is the set of tools and patterns for building the second kind of system — pipelines that react to events as they happen rather than waiting for a scheduled run to sweep them up.

This note builds on ./03-data-sources-and-ingestion.md, which introduced batch, micro-batch, and streaming as ingestion timing models and covered Change Data Capture (CDC) as the bridge between a database’s internal replication log and a stream of events. Here we go deeper: what actually distinguishes a “stream” from a “message queue,” how Apache Kafka — the de facto standard streaming platform — organizes and guarantees delivery of data, what the surrounding messaging ecosystem (RabbitMQ, SNS/SQS) looks like and when each tool fits, and how streaming data gets processed once it’s flowing. It closes with real-world use cases — clickstream analytics, fraud detection, IoT telemetry, and CDC-driven replication — that tie the concepts back to concrete systems you’re likely to build or operate.

Fundamentals

Batch vs. streaming vs. micro-batch

./03-data-sources-and-ingestion.md introduced these three timing models at the ingestion layer; the same three models apply just as much to processing data once it has landed, and the distinctions are worth restating precisely because so much confusion in the field comes from treating “streaming” as a single, well-defined thing when in practice it spans a spectrum.

Batch processing operates on a bounded collection of data — a chunk that has a definite beginning and end — usually gathered over some interval and processed on a schedule. A nightly job that reads yesterday’s orders table and computes daily revenue is batch: the input set is closed and finite before the job even starts. Batch is conceptually simple (read everything, transform, write, done) and this simplicity is why tools like Airflow, dbt, and Spark’s classic DataFrame API build around it so naturally. The cost is latency: the freshest a batch pipeline’s output can be is “as of the last run.”

Streaming processing operates on an unbounded sequence of events with no defined end — data keeps arriving indefinitely, and the processing system must continuously ingest, compute, and emit results as each event (or small group of events) arrives, without ever declaring the input “complete.” A fraud-detection service scoring every card swipe the instant it happens is streaming in this strict sense: there is no batch boundary, only a continuous now. This model demands answers to questions batch never has to ask — what happens when an event arrives late or out of order, how much state do you keep in memory for an aggregation that never closes, what do you do when the processing node crashes mid-stream — which is why streaming systems are structurally more complex to build and operate than batch ones.

Micro-batching is the pragmatic middle ground that has become the dominant way most organizations actually implement “streaming” in practice. Instead of processing one event at a time, a micro-batch engine collects events for a short, fixed interval — a few seconds, or as low as the engine allows — and processes each of these small windows as a miniature batch job. Spark Structured Streaming’s default trigger mode works exactly this way: under the hood, “streaming” queries are actually a sequence of small batch jobs fired every trigger interval. This blurs the line between the two paradigms deliberately: you get most of the operational and mental-model simplicity of batch (each micro-batch is just a bounded DataFrame computation) while getting most of the latency benefit of streaming (results land within seconds, not hours).

DimensionBatchMicro-batchStreaming
Input modelBounded, closed datasetSmall bounded chunks on a short intervalUnbounded, never-closing sequence
Typical latencyMinutes to hours (or days)Seconds to low minutesMilliseconds to low seconds
Operational complexityLowMediumHigh
State managementRecomputed each run, little cross-run stateSome carried state between micro-batches (e.g., watermarks)Continuous in-memory/state-store management across the whole stream
Failure recoveryRe-run the jobRe-run the failed micro-batch from its offsetCheckpoint/restore in-flight state, exactly-once semantics needed
Typical toolsAirflow + Spark/dbt batch jobs, cron scriptsSpark Structured Streaming (default trigger), Structured Streaming on DatabricksKafka Streams, Flink, Spark Structured Streaming (continuous trigger)
Typical use casesDaily/weekly reports, data warehouse loads, backfillsNear-real-time dashboards, incremental ETL, alerting with seconds-level toleranceFraud detection, real-time bidding, live recommendation, operational monitoring

The practical takeaway: don’t reach for “true” streaming by default. Ask what latency the business genuinely needs, and remember that micro-batch delivers seconds-level freshness with a fraction of the operational burden of a continuously-running stream processor with exactly-once state management.

Messages vs. streams: a distinction that shapes tool choice

Kafka is often introduced as “just a really scalable message queue,” but that description hides a distinction that determines which tool actually fits your use case. The core difference is what happens to a piece of data after it has been read.

A message queue — RabbitMQ, AWS SQS, most traditional brokers — is built around the idea that a message represents a unit of work to be done exactly once. When a consumer reads a message off the queue and acknowledges it, the broker removes it. If a second consumer tries to read the same queue, it gets the next message, not the one already consumed — the queue distributes work across consumers, and each message is meant for exactly one recipient. This model maps naturally onto task distribution: “process this order,” “resize this image,” “send this email” — jobs where you want exactly one worker to pick up each unit of work, and once it’s done, it’s gone.

A stream — Kafka, Amazon Kinesis, Redpanda, Pulsar — is built around a different idea: an ordered, append-only, durable and replayable log of records. Reading from a Kafka topic doesn’t remove anything; every consumer (or independent consumer group) tracks its own position (offset) in the log and can read at its own pace, replay from an earlier point, or have multiple entirely independent applications read the exact same data for different purposes. One topic of clickstream events can simultaneously feed a real-time fraud model, a batch ETL job that loads the data warehouse nightly, and a monitoring dashboard — three unrelated consumers, none of which removes the data or interferes with the others.

Message queue (e.g., RabbitMQ, SQS)Stream (e.g., Kafka)
Core abstractionQueue of discrete messagesOrdered, append-only log
What happens on readMessage is (typically) removed/acked and goneData stays; consumer just advances its own offset
Multiple independent consumersHard — a message goes to one consumer (unless fanned out first)Natural — many consumer groups read the same log independently
ReplayNot usually — once consumed, it’s goneYes — rewind to any retained offset and reprocess
Ordering guaranteeOften per-queue, sometimes best-effortStrong per-partition ordering
Best fitTask distribution, work queues, decoupling a producer from one jobEvent logs, multiple independent readers, replayable history, event sourcing

This distinction is why the two are not interchangeable, and why “should we use Kafka or RabbitMQ” is really a question about how the data will be consumed, not a question of raw throughput.

Key Concepts

Apache Kafka in depth

Apache Kafka is the dominant open-source streaming platform, and understanding its core abstractions is close to a prerequisite for working with real-time data anywhere in the industry.

Topics. A topic is a named, logical stream of records — e.g., orders.created or clickstream.pageviews. Producers publish records to a topic; consumers subscribe to and read from one or more topics. A topic is the fundamental organizing unit, roughly analogous to a table in a database or a named channel in a pub/sub system, except that what it holds is an ordered sequence of immutable events rather than mutable rows.

Partitions. Each topic is split into one or more partitions, and this is the mechanism that gives Kafka both its horizontal scalability and its ordering guarantees. Each partition is itself a strictly ordered, append-only log — records within a single partition are guaranteed to be read in the exact order they were written. Kafka provides no ordering guarantee across partitions, only within one. This is a deliberate tradeoff: by splitting a topic into partitions, Kafka can spread the topic’s throughput across many brokers and many consumer instances in parallel, at the cost of only guaranteeing order within each partition. A producer typically assigns a record to a partition based on a key (e.g., customer_id) — records with the same key always land in the same partition, which is what lets you get “ordering per customer” even though the topic as a whole has no total order.

Producers and consumers. Producers are the applications or services that write records to topics; consumers are the applications that read them. A producer chooses (directly or via a partitioner) which partition each record lands in; a consumer reads a partition sequentially, tracking its position with an offset — a simple monotonically increasing integer marking “how far into this partition have I read.”

Consumer groups. A single consumer instance can read a whole topic on its own, but production systems usually want to parallelize consumption across multiple instances of the same application — and that’s what a consumer group is for. All consumer instances that share the same group.id form one logical consumer group, and Kafka automatically divides the topic’s partitions among the group’s members so that each partition is read by exactly one consumer instance within that group at a time. This is how Kafka achieves both parallelism (more instances, more partitions read concurrently) and safety (no two instances in the same group double-process the same partition). If a topic has 12 partitions and a consumer group has 4 instances, each instance is assigned 3 partitions; add more instances (up to 12) and Kafka rebalances the assignment automatically. Crucially, different consumer groups reading the same topic are completely independent of each other — this is exactly the “multiple independent readers” property that distinguishes a stream from a queue.

Brokers and replication. A Kafka cluster is made of brokers — the servers that actually store partition data and serve producer/consumer requests. Each partition is replicated across multiple brokers (a common default is a replication factor of 3): one broker holds the leader replica, which handles all reads and writes for that partition, and the others hold follower replicas that continuously copy the leader’s data. If the leader’s broker fails, one of the in-sync followers is automatically promoted to leader, so the partition stays available without data loss. This replication is what makes Kafka fault-tolerant at the storage layer, independent of consumer-side fault tolerance (offsets, consumer group rebalancing).

Retention. Unlike a message queue, Kafka does not delete a record the moment it’s consumed. Every topic has a retention policy — configured by time (e.g., keep 7 days of data) or by size (e.g., keep up to 100 GB per partition) — and Kafka keeps records around for that period regardless of whether any consumer has read them yet. This is what makes replay possible: a new consumer group can start up and read a topic’s entire retained history from the beginning, or an existing consumer can rewind its offset to reprocess a window of past events (e.g., after fixing a bug in a downstream processor). Some topics are configured with log compaction instead of (or alongside) time/size retention, which keeps only the latest record for each key indefinitely — useful for topics that represent “current state” (e.g., a topic of CDC events representing the latest row values) rather than a pure event history.

ConceptWhat it doesAnalogy
TopicNamed stream of recordsA table or named channel
PartitionOrdered, append-only shard of a topicA shard/partition of that table
ProducerWrites records to a topic (partition)A writer/inserter
ConsumerReads records from a topic (partition), tracks its own offsetA reader with a bookmark
Consumer groupSet of consumers sharing work across a topic’s partitionsA pool of workers splitting a queue’s tasks
BrokerServer storing partition data, serving reads/writesA database node
Replication factorNumber of broker copies of each partitionRAID / database replicas
RetentionHow long data is kept regardless of consumptionA log file that isn’t truncated on read

Messaging systems beyond Kafka

Kafka is not the only tool in this space, and it is frequently not the right one — the choice depends heavily on the message/stream distinction covered above.

RabbitMQ is a traditional message broker implementing the AMQP (Advanced Message Queuing Protocol) standard (it also supports MQTT and STOMP). It excels at classic task-queue and work-distribution patterns: a producer publishes a task, an exchange routes it to one or more queues based on routing rules (direct, fanout, topic, headers exchanges), and a worker consumes and acknowledges it, after which it’s gone. RabbitMQ supports flexible routing topologies, per-message priority, delayed/scheduled delivery, and fine-grained acknowledgment semantics, but it is not designed for high-volume replay — once a message is acked, it’s gone, and there is no built-in notion of “rewind and reprocess history” the way Kafka’s retained log provides.

AWS SNS + SQS, usually deployed as a pair, are the managed pub/sub and queueing primitives in AWS. SNS (Simple Notification Service) is a pub/sub fan-out mechanism: a single published message can be delivered to many subscribers (email, HTTP endpoints, Lambda functions, or SQS queues) simultaneously. SQS (Simple Queue Service) is a durable queue that decouples a producer from a consumer, guarantees at-least-once delivery, and — in its standard (non-FIFO) form — does not guarantee ordering (a FIFO queue variant exists for when strict ordering and exactly-once processing within a message group are required, at lower throughput). The classic combined pattern is SNS fanning out to multiple SQS queues: one event gets published to an SNS topic, and it is automatically copied into several independent SQS queues, each consumed by a different downstream service — giving you Kafka-like “multiple independent consumers” semantics without running Kafka, at the cost of Kafka’s replay/retention model (an SQS queue doesn’t retain a long history the way a Kafka partition does; messages are typically deleted once fully processed, subject to a configurable retention window of up to 14 days).

Apache KafkaRabbitMQAWS SNS + SQS
Delivery modelLog-based pub/sub; many independent consumer groups read the same dataQueue-based; typically one consumer per message (fanout exchange enables pub/sub)SNS = pub/sub fan-out; SQS = point-to-point queue
OrderingStrong within a partition; none across partitionsPer-queue, generally FIFO within a queueSQS standard: best-effort/no guarantee; SQS FIFO: strict per message-group
Retention / replayConfigurable retention (time/size) independent of consumption; full replay supportedMessage is gone once acked; no built-in replayLimited retention window (up to 14 days on SQS); no offset-based replay
Delivery guaranteeAt-least-once by default; exactly-once achievable with idempotent/transactional producersAt-least-once (with manual ack); supports exactly-once in limited setupsAt-least-once
Scale profileVery high throughput, horizontally scalable via partitionsHigh throughput but generally lower ceiling than Kafka; scales via clustering/shardingFully managed, scales automatically, pay-per-use
Typical use caseEvent streaming, CDC pipelines, log aggregation, multiple independent downstream consumersTask queues, RPC-style work distribution, complex routing topologiesServerless/AWS-native decoupling, fan-out notifications, simple queues without infrastructure to manage

The rule of thumb: reach for a queue (RabbitMQ or SQS) when you have discrete units of work that should be done once by one worker; reach for a stream (Kafka, Kinesis) when multiple independent systems need to read the same ordered history, potentially at different times and different paces, or when you need replay.

Real-world streaming use cases

Clickstream analytics. Every page view, click, and scroll on a website or app is an event, and at meaningful traffic volume these events arrive as a continuous, unbounded stream rather than a neat batch. Clickstream data typically flows into Kafka (or Kinesis/Pub/Sub) topics, from which a stream processor computes rolling metrics — active users in the last 5 minutes, funnel drop-off in near-real-time — while the same topic is independently consumed by a batch job that lands the full event history in the warehouse for historical analysis. This is a textbook example of the “multiple independent consumers reading the same stream” property that distinguishes Kafka from a queue.

Fraud detection. A payment authorization has a latency budget measured in hundreds of milliseconds, not minutes — by the time a batch job would notice a fraudulent pattern, the transaction has already gone through. Fraud-detection systems typically consume a stream of transaction events in real time, run each event through a scoring model that maintains rolling, stateful features (e.g., “transaction count for this card in the last 10 minutes”), and emit an accept/decline decision back synchronously or near-synchronously. This is one of the clearest cases where true streaming (not micro-batch) is justified, because the latency requirement leaves no room for even a few seconds of batching delay.

IoT sensor telemetry. Fleets of devices — industrial sensors, vehicles, smart meters, wearables — each emit small, frequent readings, and in aggregate these form an extremely high-volume, high-velocity stream. Because devices are often on constrained, unreliable, or metered networks, they typically publish over a lightweight protocol like MQTT rather than directly to Kafka — MQTT is designed for low-bandwidth, high-latency, unreliable networks and uses a lightweight publish/subscribe model with a small broker footprint (e.g., Mosquitto, AWS IoT Core, or a managed MQTT-to-Kafka bridge). A common pattern is edge pre-aggregation: rather than transmitting every individual raw reading, the device or a local gateway aggregates readings over a short window (e.g., average temperature per minute) before transmitting, trading a small amount of granularity for a large reduction in bandwidth and cost. From there, an MQTT broker or bridge (such as a Kafka Connect MQTT source connector) feeds the aggregated readings into Kafka topics for downstream processing.

CDC events flowing into real-time consumers. As covered in ./03-data-sources-and-ingestion.md, log-based CDC tools like Debezium turn a database’s row-level changes into a stream of events published to Kafka. Once those events are in a topic, they behave like any other stream: a materialized view or search index can consume them to stay in near-real-time sync with the source database, a downstream microservice can react to specific business events (e.g., “order status changed to shipped” triggers a notification), and a batch ETL job can independently consume the same topic to load the data warehouse — all from the single CDC event stream, with no coordination required between these consumers because each tracks its own offset in its own consumer group.

Processing streaming data

Once events are flowing through a stream, the processing logic applied to them falls into two broad categories.

Stateless transformations operate on each event independently, without needing any information from other events. Filtering out malformed records, mapping a raw JSON payload into a typed structure, or masking a sensitive field are all stateless — the transformation of one event has no dependency on any other event, which makes stateless processing straightforward to reason about, scale, and recover (a crashed worker can simply resume from its last committed offset with no state to rebuild).

Stateful transformations require tracking information across multiple events, and are considerably harder to build correctly. The canonical example is a windowed aggregation — “count how many events occurred in each 5-minute window,” “compute the rolling average transaction amount per customer over the last 10 minutes.” This requires the processing engine to maintain state (the running count/sum per window/key) across an unbounded stream, decide when a window is “done” (which is complicated by events arriving late or out of order — handled via watermarks, a mechanism for declaring “we don’t expect events older than this timestamp anymore”), and persist that state durably so a crashed processing node can recover without losing or double-counting data.

Two processing engines dominate this space. Kafka Streams is a Java library (not a separate cluster to operate) that lets an application consume from Kafka, perform stateful or stateless transformations using a stream-table duality model, and produce results back to Kafka — it’s tightly coupled to the Kafka ecosystem and well suited to teams already standardized on Kafka. Spark Structured Streaming treats a stream as an unbounded table and lets you express streaming computations using the same DataFrame API used for Spark batch jobs (covered further in ./10-big-data-and-distributed-computing.md), running as a sequence of micro-batches (or, in newer versions, a lower-latency continuous processing mode) — a natural fit for teams already running Spark for batch ETL who want to extend the same skillset and codebase to streaming workloads. Apache Flink, a true event-at-a-time streaming engine with mature support for event-time processing and exactly-once state, is a common third option when sub-second, high-throughput stateful processing is the core requirement rather than a secondary concern.

Whichever engine is used, operating a stateful streaming pipeline in production requires the same discipline covered in ./18-monitoring-and-observability.md: consumer lag (how far behind a consumer group is from the latest offset) is the single most important streaming-specific metric to alert on, since a growing lag means a consumer can’t keep up with its input and downstream data is silently getting staler by the second.

Best Practices

References