DDIA · Martin Kleppmann · Chương 11DDIA · Martin Kleppmann · Chương 11DDIA · Martin Kleppmann · Chapter 11
DDIA · Phần III · Chương 11 · Xử lý luồngDDIA · Phần III · Chương 11 · Xử lý luồngDDIA · Part III · Chapter 11 · Stream Processing

Theo lô, nhưng không có hồi kếtDữ liệu chảy mãi không hếtLike batch, but it never ends

Chương 10 dựng cả thế giới theo lô trên một giả định êm ái: đầu vào hữu hạn và đã biết, có điểm bắt đầu và điểm kết thúc, nên một job mới có lúc "chạy xong". Kleppmann mở Chương 11 bằng cách rút đúng giả định ấy đi12. Thực tế, dữ liệu đến dần và không bao giờ đủ: người dùng vẫn gõ, cảm biến vẫn gửi, giao dịch vẫn phát sinh. Chạy lô mỗi ngày một lần nghĩa là kết quả luôn cũ mất một ngày. Xử lý luồng là câu trả lời — bỏ ranh giới lô cố định, xử lý từng sự kiện ngay khi nó xuất hiện. Và cả chương là bản kiểm kê những thứ ta phải trả để có điều đó.

Ở chương trước, máy tính làm việc theo mẻ: gom đủ dữ liệu rồi mới bắt đầu, làm xong thì nghỉ. Nhưng dữ liệu thật thì chảy mãi — người ta cứ bấm, cứ mua, cứ gửi tin. Nếu mỗi ngày mới tính một lần thì kết quả luôn là chuyện của hôm qua. Chương này nói về cách làm việc với dòng dữ liệu không bao giờ hết: tới đâu tính tới đó. Nhớ nhé: luồng cũng giống mẻ thôi, chỉ là mẻ đó không có điểm dừng.

Chapter 10 built the whole batch world on one comfortable assumption: input that is bounded and known, with a beginning and an end, so a job can actually finish. Kleppmann opens Chapter 11 by pulling that assumption away12. In reality data arrives gradually and is never complete: users keep typing, sensors keep sending, transactions keep happening. Running a batch once a day means your results are always a day stale. Stream processing is the answer — drop the fixed batch boundary and process each event as it appears. The rest of the chapter is an inventory of what that costs.

∴
Luận đề của chương Điều quan trọng nhất The thesis of the chapter

Một "luồng" chỉ đơn giản là dữ liệu được cung cấp dần theo thời gian1. Nhưng khi ta nhận lấy một nhật ký nối đuôi bất biến làm trục chính, thì cơ sở dữ liệu, chỉ mục, cache và view vật chất hóa đều biến thành dẫn xuất của cùng một luồng sự kiện — và cái mà ta quen gọi là "trạng thái" chỉ là kết quả cộng dồn của luồng ấy. Cái giá phải trả gói trong ba câu hỏi khó: thời gian của ai, join vào lúc nào, và điều gì còn đúng khi có sự cố.

Hãy tưởng tượng một cuốn sổ chỉ được viết thêm, không bao giờ tẩy xóa. Mọi việc đã xảy ra đều ghi vào đó theo thứ tự. Muốn biết "bây giờ ra sao", cứ đọc lại cuốn sổ từ đầu và cộng dồn. Nhớ nhé: giữ cuốn sổ thì lúc nào cũng dựng lại được hiện tại, nhưng xóa cuốn sổ đi thì mất luôn quá khứ.

A "stream" simply means data made available incrementally over time1. But once you take an immutable append-only log as the spine, databases, indexes, caches and materialized views all become derivations of the same event stream — and what we call "state" is just that stream accumulated. The price comes as three hard questions: whose time, joined when, and what still holds when things fail.

Trục 1 · Transmitting event streamsAxis 1 · Transmitting event streams

Hàng đợi xóa tin, nhật ký thì nhớQueues forget; logs remember

Trước khi bàn xử lý, phải bàn vận chuyển. Kleppmann chia họ hàng công cụ truyền sự kiện làm hai nhánh khác nhau tận gốc1: message broker kiểu AMQP/JMS — xóa tin sau khi consumer báo đã nhận, và phân phối việc cho nhiều consumer ở mức từng tin; và nhật ký phân mảnh kiểu Kafka — chỉ nối đuôi vào đĩa, không xóa sau khi giao, và chỉ ghi nhớ offset mà mỗi consumer đã đọc tới. Sự khác biệt tưởng nhỏ ấy quyết định gần như mọi thứ còn lại trong chương.

Có hai kiểu "đưa tin" khác nhau. Kiểu thứ nhất giống hộp thư: đưa thư xong là bỏ thư đi, hết. Kiểu thứ hai giống cuốn sổ ghi chép: viết vào sổ rồi ai cũng đọc được, đọc tới đâu thì nhớ trang số mấy, muốn đọc lại từ đầu cũng được. Nhớ nhé: hộp thư thì gọn nhưng mất là mất, còn cuốn sổ thì tốn chỗ nhưng đọc lại được bao nhiêu lần tùy thích.

Before processing comes transport. Kleppmann splits the family of event-delivery tools into two branches that differ at the root1: AMQP/JMS-style message brokers — which delete a message once a consumer acknowledges it and dispatch work per message; and partitioned logs in the Kafka mould — append-only on disk, nothing deleted on delivery, with the broker merely recording each consumer's offset. That seemingly small difference decides almost everything else in the chapter.

An event is a small, self-contained, immutable object

Một sự kiện là một vật nhỏ, tự chứa, bất biến, ghi lại chi tiết của một việc đã xảy ra tại một thời điểm — thường kèm dấu thời gian theo đồng hồ thời-gian-trong-ngày1. Nó được producer sinh ra một lần, rồi có thể được nhiều consumer xử lý; các sự kiện cùng loại gom lại thành một topic hay stream.

Một "sự kiện" là mẩu ghi chú nhỏ: chuyện này đã xảy ra, lúc này. Viết xong là không sửa nữa. Một người viết ra, nhiều người có thể đọc. Nhớ nhé: sự kiện là chuyện đã rồi, nên nó không đổi.

An event is a small, self-contained, immutable object recording the details of something that happened at some point in time — usually with a time-of-day timestamp1. A producer generates it once, and multiple consumers may process it; related events are grouped into a topic or stream.

Về nguyên tắc, một tập tin hay một bảng CSDL cũng đủ để nối producer với consumer: producer ghi thêm, consumer định kỳ đọc xem có gì mới. Nhưng khi ta muốn độ trễ thấp, tần suất hỏi vòng phải dày lên — và chi phí hỏi vòng tăng theo, trong khi phần lớn lần hỏi trả về "chưa có gì". Càng hỏi dày thì tỉ lệ lần hỏi vô ích càng cao. Đó là lý do ta cần một cơ chế thông báo chủ động khi có sự kiện mới — và đó chính là công việc của hệ nhắn tin.

Log ≠ hàng đợi Broker xóa tin sau khi được ack; nhật ký giữ tin lại và chỉ nhớ offset của consumer.
Ba lối thoát Consumer không theo kịp thì chỉ có ba lựa chọn: bỏ tin, đệm vào hàng đợi, hoặc đẩy ngược áp lực.
Phát lại được Đọc nhật ký không làm đổi nhật ký, nên có thể tua offset về và chạy lại từ đầu.
Sổ và hộp thư Hộp thư đưa xong là bỏ; cuốn sổ thì vẫn còn đó.
Ba cách xử khi nghẽn Bỏ bớt tin, xếp hàng chờ, hoặc bảo người gửi chậm lại.
Đọc lại được Đọc sổ không làm mất chữ, nên muốn đọc lại lúc nào cũng được.
Log ≠ queue A broker deletes a message once acked; a log keeps it and only records the consumer's offset.
Three ways out When consumers fall behind there are only three options: drop, buffer in a queue, or apply backpressure.
Replayable Reading a log doesn't change it, so you can rewind the offset and run again.
  1. Producers, consumers, and the cost of polling

    Vì sao phải có hệ nhắn tinWhy a messaging system at all

    Một hệ nhắn tin tồn tại để producer không phải chờ consumer, và consumer không phải hỏi vòng. Kleppmann gợi ý phân loại mọi hệ như thế bằng hai câu hỏi1. Thứ nhất: nếu producer gửi nhanh hơn consumer xử lý thì sao? Chỉ có ba câu trả lời — bỏ tin, đệm tin trong hàng đợi, hoặc đẩy ngược áp lực (backpressure, cũng gọi là điều tiết luồng: chặn producer lại cho tới khi consumer bắt kịp). Thứ hai: nếu một nút sập hay ngoại tuyến thì có mất tin không? Muốn không mất thì phải ghi đĩa và/hoặc nhân bản — và cả hai đều có giá.

    Người gửi và người nhận không nên phải đợi nhau. Nhưng nếu người gửi gửi nhanh hơn người nhận kịp đọc thì sao? Chỉ có ba cách: bỏ bớt tin, xếp tin vào hàng chờ, hoặc bảo người gửi chậm lại. Rồi còn câu hỏi thứ hai: nếu máy mất điện thì tin có bay mất không? Muốn chắc thì phải chép tin xuống đĩa. Nhớ nhé: không có cách thứ tư đâu — chỉ ba cách ấy thôi.

    A messaging system exists so producers needn't wait for consumers and consumers needn't poll. Kleppmann suggests classifying any such system with two questions1. First: what if producers send faster than consumers can process? There are only three answers — drop messages, buffer them in a queue, or apply backpressure (flow control: block the producer until the consumer catches up). Second: what if nodes crash or go offline — are messages lost? Durability means writing to disk and/or replicating, and both cost something.

    Nhìn thấy ở đâuMỗi lần bạn chọn kích thước hàng đợi, thời gian giữ tin, hay bật tắt xác nhận ghi đĩa, bạn đang trả lời đúng hai câu hỏi này — dù có gọi tên chúng hay không.
    Ý chínhChỉnh hàng đợi to hay nhỏ chính là đang chọn: bỏ tin, chờ, hay bắt người gửi chậm lại.
    Where you see itEvery time you size a queue, set a retention window, or toggle durable acks, you are answering these two questions — named or not.

    Vì sao hỏi vòng là lối cụt

    Cơ sở dữ liệu vốn không được thiết kế để bị hỏi liên tục. Muốn độ trễ xuống thấp thì phải hỏi dày hơn, mà càng dày thì tỉ lệ lần hỏi trả về "chưa có gì" càng cao — chi phí tăng tuyến tính còn giá trị thu về thì không. Cơ chế thông báo đảo chiều: hệ báo cho ta, thay vì ta liên tục đi hỏi hệ.

    Ranh giới với xử lý theo lô

    Chương 10 có thể sắp xếp toàn bộ đầu vào vì đầu vào hữu hạn; chương 11 thì không. Mất khả năng sắp xếp toàn cục là mất luôn sort-merge join, mất luôn "chạy xong rồi vứt kết quả dở đi làm lại". Gần như mọi khác biệt kỹ thuật còn lại của chương này đều truy ngược về đúng chỗ đó.

  2. Direct messaging: fast, and unforgiving

    Nhắn tin trực tiếp: nhanh nhưng không tha thứDirect messaging: fast, and unforgiving

    Nhánh đơn giản nhất là bỏ hẳn trung gian: producer nói thẳng với consumer qua mạng. Kleppmann liệt kê những ví dụ thật1 — UDP multicast cho dữ liệu thị trường chứng khoán, các thư viện không-broker như ZeroMQ và nanomsg, StatsD và Brubeck dùng UDP không tin cậy để gom chỉ số, hay webhook: gọi ngược qua HTTP/RPC tới một điểm cuối do consumer đăng ký. Tất cả đều nhanh và gọn, nhưng đều dựa trên một giả định mong manh: producer và consumer đều đang online. Nếu consumer chết hay tạm ngoại tuyến, tin gửi trong quãng đó mất hẳn — và producer thì cũng chẳng nhớ gì để gửi lại.

    Cách đơn giản nhất là gọi thẳng: "ê, có chuyện này!" Nhanh, không cần ai đứng giữa. Nhưng nếu người kia đang ngủ thì lời gọi rơi vào khoảng không, và người gọi cũng không nhớ mình đã nói gì để nói lại. Nhớ nhé: gọi thẳng thì nhanh nhất, nhưng ai vắng mặt là người đó mất tin.

    The simplest branch drops the middleman: producers talk straight to consumers over the network. Kleppmann lists real examples1 — UDP multicast for stock-market feeds, brokerless libraries like ZeroMQ and nanomsg, StatsD and Brubeck collecting metrics over unreliable UDP, and webhooks: an HTTP/RPC callback to an endpoint the consumer registered. All are fast and lean, and all rest on one fragile assumption: producer and consumer are both online. If a consumer dies or goes briefly offline, messages sent meanwhile are simply gone — and the producer remembers nothing to resend.

    Dùng khi nàoKhi mất vài mẩu dữ liệu là chấp nhận được và độ trễ mới là thứ quý — ví dụ gom chỉ số đo đạc, nơi mất 0,1% mẫu không làm lệch kết luận.
    Dùng khi nàoKhi mất vài tin cũng không sao, mà nhanh mới quan trọng.
    When to useWhen losing a few data points is acceptable and latency is what matters — metrics collection, say, where dropping 0.1% of samples changes no conclusion.
  3. Message brokers, AMQP/JMS style

    Broker: một CSDL chuyên cho luồng tinA broker is a database for message streams

    Message broker (hàng đợi tin) về bản chất là một cơ sở dữ liệu được tối ưu cho luồng tin1: producer ghi vào broker, consumer đọc ra, và broker lo phần độ bền. Nhưng Kleppmann nhấn mấy khác biệt quan trọng so với một CSDL thật: broker xóa tin sau khi giao thành công, còn CSDL giữ dữ liệu tới khi bị xóa tường minh; broker giả định hàng đợi luôn ngắn — hàng dài lên thì thông lượng tụt; CSDL có chỉ mục phụ và truy vấn, còn broker chỉ có cơ chế khớp topic; và CSDL trả kết quả theo một ảnh chụp tại thời điểm truy vấn, trong khi broker thì báo khi dữ liệu đổi. Đây là dòng họ AMQP và JMS1011.

    Broker giống một bưu điện: người gửi bỏ thư vào, bưu điện giữ hộ, người nhận tới lấy. Lấy xong thì bưu điện bỏ thư đi — nó không giữ bản sao. Bưu điện cũng chỉ chịu được một đống thư vừa phải; thư chất cao quá thì chậm hẳn. Nhớ nhé: bưu điện giữ hộ giúp hai bên không phải đợi nhau, nhưng thư đã lấy rồi là không xin lại được.

    A message broker (message queue) is essentially a database optimized for handling message streams1: producers write to it, consumers read from it, and it handles durability. But Kleppmann stresses the differences from a real database: a broker deletes a message once delivery succeeds, whereas a database keeps data until explicitly deleted; a broker assumes queues stay short — throughput degrades as they grow; databases offer secondary indexes and queries, brokers only topic-subscription matching; and a database answers from a point-in-time snapshot, while a broker notifies when data changes. This is the AMQP and JMS family1011.

    Hai kiểu chia việcCân bằng tải: mỗi tin chỉ tới một consumer trong nhóm — dùng khi việc xử lý đắt và ta muốn song song hóa. Fan-out: mỗi tin tới mọi consumer — dùng khi nhiều hệ độc lập cùng cần nghe. Hai kiểu có thể kết hợp.
    Hai kiểu chia việcMột là chia nhau làm: mỗi thư một người nhận. Hai là phát cho tất cả: ai cũng nhận một bản.
    Two dispatch modesLoad balancing: each message goes to one consumer in the group — for parallelising expensive work. Fan-out: each message goes to every consumer — when several independent systems must all hear it. The two can be combined.

    Ack và gửi lại làm đảo thứ tự

    Consumer phải báo nhận tường minh; nếu kết nối đứt hoặc hết thời gian chờ mà chưa có ack, broker gửi lại tin đó. Kết hợp với cân bằng tải, hậu quả là thứ tự bị đảo: tin thứ hai có thể đã xử lý xong trong khi tin thứ nhất quay lại lượt gửi thứ hai và rơi vào một consumer khác. Muốn giữ thứ tự thì phải bỏ cân bằng tải và dùng một hàng đợi riêng cho mỗi consumer.

    Vì sao "hàng đợi ngắn" là giả định

    Broker thường giữ tin trong bộ nhớ (có thể tràn xuống đĩa) với kỳ vọng consumer tiêu thụ gần như tức thời. Khi consumer tụt lại, hàng đợi phình ra, dữ liệu bị đẩy khỏi bộ nhớ và mỗi tin lại thêm chi phí truy đĩa ngẫu nhiên — thông lượng xuống đúng lúc ta cần nó lên nhất.

  4. Partitioned logs: topics, partitions, offsets

    Nhật ký phân mảnh: đọc như tail -f, nhớ bằng offsetPartitioned logs: read like tail -f, remember by offset

    Nhánh thứ hai lấy chính nhật ký làm cấu trúc: một chuỗi bản ghi chỉ nối đuôi trên đĩa1. Producer nối vào cuối; consumer đọc tuần tự rồi chờ tin mới — hệt như tail -f một tập tin. Để vượt thông lượng của một ổ đĩa, nhật ký được phân mảnh, mỗi mảnh nằm trên một máy khác nhau; một topic là một nhóm mảnh chở cùng loại tin. Trong một mảnh, broker gán cho mỗi tin một offset tăng đơn điệu, và vì mảnh chỉ được nối đuôi nên thứ tự trong mảnh là tất định — nhưng không có bảo đảm thứ tự nào giữa các mảnh khác nhau. Apache Kafka, Amazon Kinesis Streams và DistributedLog của Twitter đều thuộc nhóm này45.

    Kiểu thứ hai không dùng hộp thư mà dùng cuốn sổ: ai có chuyện gì thì viết thêm một dòng vào cuối sổ, không bao giờ xóa dòng cũ. Người đọc cứ đọc từ trên xuống và nhớ mình đang ở dòng số mấy. Sổ dày quá thì chia ra nhiều quyển, mỗi quyển để một chỗ. Nhớ nhé: trong một quyển thì thứ tự luôn đúng, nhưng giữa các quyển thì không ai bảo đảm quyển nào trước quyển nào.

    The second branch takes the log itself as the structure: an append-only sequence of records on disk1. Producers append to the end; consumers read sequentially and then wait for new records — exactly like tail -f on a file. To exceed a single disk's throughput the log is partitioned, with different partitions on different machines; a topic is a group of partitions carrying the same kind of message. Within one partition the broker assigns each message a monotonically increasing offset, and because the partition is append-only its order is deterministic — but there is no ordering guarantee across different partitions. Apache Kafka, Amazon Kinesis Streams and Twitter's DistributedLog all belong here45.

    Được gì, mất gìĐược: fan-out gần như miễn phí — nhiều consumer đọc độc lập mà không ảnh hưởng nhau, vì đọc không làm đổi nhật ký. Mất: cân bằng tải trở nên thô — cả một mảnh được giao cho một nút trong nhóm consumer, nên số nút chia việc không vượt quá số mảnh; và một tin xử lý chậm sẽ chặn đầu hàng mọi tin sau nó trong cùng mảnh.
    Được gì, mất gìĐược: ai cũng đọc được cùng cuốn sổ mà không phiền nhau. Mất: một dòng khó nhằn nằm giữa sổ sẽ chắn luôn những dòng phía sau.
    What you gain and loseGain: fan-out is nearly free — many consumers read independently without affecting each other, since reading doesn't change the log. Lose: load balancing gets coarse — a whole partition is assigned to one node in the consumer group, so the number of nodes sharing the work cannot exceed the number of partitions; and one slow message head-of-line blocks everything behind it in that partition.

    Offset thay cho ack

    Broker kiểu nhật ký không theo dõi ack của từng tin — nó chỉ ghi lại offset mà consumer đã đọc tới5: mọi tin trước offset coi như đã xử lý, mọi tin sau thì chưa. Kleppmann chỉ ra đây đúng là số thứ tự nhật ký trong nhân bản một-leader ở Chương 5: broker đóng vai leader, consumer đóng vai follower. Nhờ vậy broker chỉ phải nhớ một con số cho mỗi nhóm consumer, thay vì trạng thái của từng tin.

    Cái giá của việc đơn giản hóa

    Vì offset là một con số cho cả mảnh, ta không thể nói "tin này xong rồi nhưng tin kia chưa". Một tin xử lý lâu bất thường buộc cả mảnh phải chờ. Với broker kiểu AMQP/JMS thì ngược lại: mỗi tin được ack riêng nên tin khó không chặn tin dễ — đổi lại là mất thứ tự và mất khả năng phát lại.

  5. Replay, retention, and consumer lag

    Phát lại được — nhưng đĩa vẫn hữu hạnReplayable — but the disk is still finite

    Hệ quả đắt giá nhất của nhật ký: tiêu thụ một nhật ký giống đọc một tập tin hơn là nhận thư1. Đọc là thao tác chỉ-đọc, không làm nhật ký đổi đi, nên ta có thể khởi động lại consumer ở một offset cũ hơn và chạy lại — cực kỳ quý khi thử nghiệm một phiên bản mới, hay khi cần khắc phục hậu quả của một lỗi vừa vá. Broker kiểu AMQP/JMS không làm được điều này, vì tin đã ack là đã biến mất. Cái giá là đĩa: nhật ký thực chất là một bộ đệm vòng có giới hạn, đoạn cũ bị xóa dần khi đầy. Nếu một consumer tụt xa tới mức những tin nó cần đã bị xóa, nó mất hẳn chúng — nên phải theo dõi liên tục khoảng cách giữa offset của consumer và đầu nhật ký.

    Vì đọc sổ không làm mất chữ, nên muốn đọc lại từ trang 10 lúc nào cũng được. Rất tiện khi ta lỡ tính sai và muốn tính lại. Nhưng sổ dày mãi thì hết chỗ để, nên người ta xé dần những trang cũ nhất. Ai đọc chậm quá, tới lúc quay lại thì trang mình cần đã bị xé mất rồi. Nhớ nhé: đọc lại được là món quà, nhưng chỉ trong khoảng thời gian mà sổ còn giữ.

    The log's most valuable consequence: consuming a log is more like reading a file than receiving mail1. Reading is a read-only operation that leaves the log unchanged, so you can restart a consumer at an older offset and replay — invaluable when testing a new version, or recovering from a bug you just fixed. AMQP/JMS-style brokers cannot do this, because an acked message is gone. The price is disk: the log is effectively a bounded ring buffer, discarding old segments as it fills. If a consumer falls so far behind that the messages it needs have been deleted, it loses them outright — so you must continuously monitor the gap between a consumer's offset and the head of the log.

    Nhìn thấy ở đâuMỗi chỉ số "consumer lag" trên bảng theo dõi chính là khoảng cách này. Nó không chỉ nói hệ đang chậm — nó nói bạn còn bao nhiêu thời gian trước khi sự chậm đó biến thành mất dữ liệu.
    Nhìn thấy ở đâuCon số "đang tụt lại bao xa" cho biết còn bao lâu nữa thì tụt thành mất luôn.
    Where you see itEvery "consumer lag" metric on a dashboard is exactly this gap. It doesn't just say the system is slow — it says how long you have before slow becomes lost.

    Chọn lối "đệm", với một cái đệm rất lớn

    Trở lại ba lựa chọn lúc nghẽn: broker kiểu nhật ký chọn dứt khoát lối đệm. Đệm ở đây lớn hơn nhiều so với bộ nhớ của một broker thường — nó là dung lượng đĩa dành cho nhật ký, thường đủ cho nhiều ngày. Nhưng "rất lớn" vẫn không phải "vô hạn", và ranh giới giữa chậm và mất chính là thời gian giữ tin.

    Lối thoát thứ hai cho dung lượng: nén nhật ký

    Ngoài việc xóa theo thời gian, còn một cách để nhật ký không phình mãi: nén nhật ký — quét các bản ghi có cùng khóa và chỉ giữ lại bản cập nhật mới nhất, đánh dấu việc xóa bằng một bản ghi tombstone. Tài liệu Kafka mô tả đúng cơ chế này6. Nhờ vậy nhật ký được bảo đảm chứa giá trị mới nhất cho mọi khóa — một tính chất sẽ thành nền móng cho toàn bộ trục tiếp theo.

Trục 2 · Databases and streamsAxis 2 · Databases and streams

Trạng thái là tích phân của luồngState is a stream, integrated

Đây là chỗ chương 11 chuyển từ kỹ thuật vận chuyển sang một luận điểm về bản thể. Kleppmann hỏi: nếu nhật ký nhân bản của cơ sở dữ liệu là một luồng sự kiện, thì cái gì mới là gốc — bảng dữ liệu hay cuốn nhật ký? Câu trả lời của ông đảo ngược trực giác thông thường: bảng là dẫn xuất, nhật ký mới là gốc. Từ đó mở ra hai kỹ thuật rất dễ bị gộp làm một nhưng thực ra khác hẳn — bắt thay đổi dữ liệu (CDC) và nguồn sự kiện (event sourcing)15.

Bình thường ta nghĩ: cái bảng dữ liệu mới là thật, còn cuốn sổ ghi chép chỉ là phụ. Chương này lật ngược lại: cuốn sổ mới là thật, còn cái bảng chỉ là kết quả cộng dồn cuốn sổ. Nhớ nhé: giữ cuốn sổ thì dựng lại bảng lúc nào cũng được, nhưng mất cuốn sổ thì không dựng lại được quá khứ.

Here Chapter 11 turns from transport engineering to a claim about what is primary. Kleppmann asks: if a database's replication log is a stream of events, which is the original — the table, or the log? His answer inverts the usual intuition: the table is the derivation; the log is the original. From there follow two techniques that are easily conflated but are genuinely different — change data capture (CDC) and event sourcing15.

Dual writes are the problem this solves

Bài toán thật sự là giữ nhiều hệ đồng bộ: cùng một dữ liệu phải xuất hiện trong CSDL, trong chỉ mục tìm kiếm, trong cache và trong kho phân tích. Cách ngây thơ — ghi song song vào từng nơi (dual writes) — hỏng theo hai kiểu. Lời giải là chọn một nguồn chân lý và để mọi hệ còn lại dẫn xuất từ nhật ký của nó.

Nếu cùng một thông tin phải có mặt ở bốn chỗ, đừng đi chép tay vào cả bốn. Hãy chọn một chỗ làm gốc, rồi để ba chỗ kia tự chép theo cuốn sổ của chỗ gốc. Nhớ nhé: một nguồn duy nhất thì mới khỏi lệch nhau.

The real problem is keeping systems in sync: the same data must appear in the database, the search index, the cache and the analytics warehouse. The naive approach — writing to each in parallel (dual writes) — fails in two ways. The fix is to pick one source of truth and derive everything else from its log.

Hai kiểu hỏng của dual write đáng gọi tên riêng1. Thứ nhất là tranh đua: hai client ghi đồng thời, client A tới CSDL trước còn client B tới chỉ mục trước — kết quả là CSDL giữ giá trị của B còn chỉ mục giữ giá trị của A, và hai kho lệch nhau vĩnh viễn dù không có lỗi nào xảy ra. Thứ hai là commit nguyên tử: một phép ghi thành công còn phép kia hỏng, và hai kho lại lệch nhau. Muốn gỡ đúng cách thì cần giao dịch phân tán kiểu hai pha — thứ mà Chương 9 đã cho thấy là đắt và giòn. Đơn giản hơn nhiều: đừng ghi song song, hãy ghi một chỗ rồi dẫn xuất.

Dual write hỏng Ghi song song nhiều kho sinh tranh đua và nửa-thành-công; không có cách sửa rẻ.
CDC ở tầng kho Trích changelog của CSDL; ứng dụng vẫn cập nhật và xóa tự do như thường.
Event sourcing ở tầng app Sự kiện mô tả ý định người dùng, và không bao giờ bị sự kiện sau đè lên.
Đừng chép tay bốn nơi Chọn một chỗ làm gốc, ba chỗ kia chép theo.
CDC: nghe lén cuốn sổ Cứ dùng kho như cũ, rồi đọc cuốn sổ nó tự ghi.
Nguồn sự kiện: viết sổ trước Ghi thẳng việc đã xảy ra, rồi mới dựng ra bảng.
Dual writes break Writing to several stores in parallel creates races and half-successes; there is no cheap fix.
CDC sits at the store Extract the database's changelog; the application keeps updating and deleting as usual.
Event sourcing sits at the app Events describe user intent, and later events never overwrite earlier ones.
  1. Change data capture (CDC)

    CDC: phong một kho làm leaderCDC: crowning one store the leader

    Bắt thay đổi dữ liệu là việc quan sát mọi thay đổi được ghi vào một cơ sở dữ liệu và trích chúng ra ở dạng có thể nhân bản sang hệ khác1. Về mặt kiến trúc, nó phong CSDL đó làm leader và biến mọi hệ dẫn xuất — chỉ mục tìm kiếm, kho dữ liệu, cache — thành follower; nhật ký phân mảnh là phương tiện chở changelog rất hợp vì nó giữ nguyên thứ tự. Kleppmann liệt kê một loạt hiện thực có thật: LinkedIn Databus, Facebook Wormhole, Yahoo! Sherpa, Bottled Water cho PostgreSQL, Maxwell và Debezium cho MySQL, Mongoriver, Oracle GoldenGate. CDC gần như luôn bất đồng bộ, nên nó thừa hưởng trọn vẹn các vấn đề trễ nhân bản của Chương 5.

    CDC là nghe lén cuốn sổ của kho dữ liệu. Ứng dụng vẫn ghi vào kho như bình thường, chẳng cần sửa gì; ở phía sau, ta đọc cuốn sổ mà kho tự ghi và chép lại vào những nơi khác. Nhớ nhé: một nơi làm chủ, những nơi còn lại chép theo — chứ đừng ai cũng tự viết.

    Change data capture means observing all data changes written to a database and extracting them in a form that can be replicated to other systems1. Architecturally it makes that database the leader and turns every derived system — search index, warehouse, cache — into a follower; a partitioned log is a natural transport because it preserves order. Kleppmann lists real implementations: LinkedIn's Databus, Facebook's Wormhole, Yahoo!'s Sherpa, Bottled Water for PostgreSQL, Maxwell and Debezium for MySQL, Mongoriver, Oracle GoldenGate. CDC is almost always asynchronous, so it inherits every replication-lag problem from Chapter 5.

    Hai chi tiết vận hành quyết địnhẢnh chụp ban đầu: muốn dựng một chỉ mục đầy đủ thì cần toàn bộ dữ liệu, mà nhật ký thì không giữ mãi từ đầu — nên phải chụp một ảnh và ảnh đó phải ứng với một vị trí đã biết trong changelog để nối tiếp không sót, không lặp. Nén nhật ký: giữ bản ghi mới nhất cho mỗi khóa và đánh dấu xóa bằng tombstone, nhờ đó nhật ký đủ để dựng lại toàn bộ nội dung mà không phình mãi6.
    Hai điều phải nhớMột: chụp một ảnh của kho lúc bắt đầu, và phải biết ảnh đó ứng với dòng số mấy trong sổ. Hai: mỗi khóa chỉ giữ dòng mới nhất, để sổ khỏi dày mãi.
    Two operational details that decide itInitial snapshot: building a full index needs the entire dataset, and the log doesn't stretch back forever — so take a snapshot, and it must correspond to a known position in the changelog to resume without gaps or duplicates. Log compaction: keep the most recent record per key and mark deletions with a tombstone, so the log can reconstruct the whole contents without growing forever6.

    Hiện thực đọc từ đâu

    Những công cụ CDC hiện đại thường đọc thẳng nhật ký ghi của chính CSDL — với MySQL là binlog, với PostgreSQL là luồng giải mã logic — thay vì dùng trigger hay cột updated_at12. Đọc nhật ký cho ta đúng thứ tự đã commit và không bỏ sót thay đổi trung gian, hai thứ mà cách quét-theo-chu-kỳ không bảo đảm được.

    Điều CDC không cho bạn

    Changelog của kho chỉ nói hàng dữ liệu đã đổi thành gì, không nói vì sao. Một dòng đổi từ "đang hoạt động" sang "đã hủy" có thể là người dùng tự hủy, là quản trị viên khóa tài khoản, hay là một tác vụ dọn dẹp — và CDC không phân biệt được. Ý định đã bị đánh rơi ngay tại tầng ứng dụng, trước khi changelog kịp nhìn thấy. Đây chính là chỗ event sourcing bước vào.

  2. Event sourcing

    Nguồn sự kiện: lưu cái đã xảy ra, không lưu cái đang làEvent sourcing: store what happened, not what is

    Nguồn sự kiện ra đời trong cộng đồng thiết kế hướng miền (DDD)113. Ý tưởng: lưu mọi thay đổi trạng thái của ứng dụng dưới dạng một nhật ký sự kiện. Khác biệt then chốt nằm ở tầng: sự kiện ở đây phản ánh việc xảy ra ở tầng ứng dụng — "sinh viên hủy đăng ký môn học", "đơn hàng bị trả lại" — chứ không phải "hàng số 4711 đổi cột status từ 1 thành 2". Sự kiện là bất biến, nhật ký chỉ được nối đuôi, và mã ứng dụng không được phép ghi đè hay viết lại quá khứ. Trạng thái hiện thời không được lưu trực tiếp — nó là thứ ta dựng lại bằng cách phát lại nhật ký.

    Thay vì ghi "trong tủ còn 3 quả táo", ta ghi từng việc đã xảy ra: "bỏ vào 5 quả", "lấy ra 1 quả", "lấy ra 1 quả". Muốn biết còn mấy quả thì cộng lại. Ghi kiểu này tốn công hơn, nhưng đổi lại ta biết vì sao lại còn 3 quả. Nhớ nhé: ghi việc đã xảy ra thì giữ được câu chuyện, ghi con số thì chỉ giữ được kết quả.

    Event sourcing came out of the domain-driven design community113. The idea: store all changes to application state as a log of events. The key difference is the level: these events reflect things that happened at the application level — "student cancelled course enrolment", "order returned" — not "row 4711 changed column status from 1 to 2". Events are immutable, the log is append-only, and application code must not overwrite or rewrite the past. Current state is not stored directly — it is something you reconstruct by replaying the log.

    Vì sao đáng côngSự kiện ở tầng ứng dụng mang theo ý định, nên về sau ta có thể dẫn ra những cách nhìn mà lúc viết chưa hề nghĩ tới. Một log ghi "đã hủy đăng ký vì trùng lịch" trả lời được vô số câu hỏi mà một log ghi "status: 1 → 2" không bao giờ trả lời nổi.
    Vì sao đáng côngVì sau này muốn hỏi gì cũng có câu trả lời, kể cả câu hỏi mà hôm nay chưa nghĩ ra.
    Why it's worth the workApplication-level events carry intent, so later you can derive views nobody imagined when the code was written. A log saying "cancelled enrolment due to timetable clash" answers questions that "status: 1 → 2" never can.

    Ảnh chụp chỉ là tối ưu

    Phát lại toàn bộ nhật ký mỗi lần khởi động là quá đắt, nên trong thực tế người ta chụp ảnh trạng thái định kỳ và chỉ phát lại phần đuôi. Nhưng Kleppmann phân biệt rõ: ảnh chụp là tối ưu hiệu năng, không phải nguồn chân lý. Xóa mọi ảnh chụp đi thì hệ vẫn đúng, chỉ chậm hơn; xóa nhật ký đi thì hệ mất luôn khả năng tồn tại.

    Công cụ chuyên dụng

    Có những kho được xây riêng cho lối này — Kleppmann nhắc Event Store và mô hình Chronicle trong nghiên cứu về duy trì view3. Nhưng lối dùng phổ biến hơn nhiều là ghép: một nhật ký phân mảnh làm kho sự kiện, cộng với các bộ xử lý luồng dựng ra view đọc.

  3. CDC ≠ event sourcing

    Hai thứ trông giống nhau, khác ở tầng và ở nguồn chân lýTwo lookalikes, split by level and by source of truth

    Kleppmann tách bạch hai khái niệm này rất dứt khoát, và đây là chỗ người đọc lần đầu hay trộn lẫn1. Với CDC, ứng dụng vẫn dùng cơ sở dữ liệu theo lối có thể sửa đổi — cập nhật và xóa tùy ý; changelog được trích ra ở tầng thấp, từ nhật ký ghi của kho, và mỗi sự kiện cập nhật thường chứa toàn bộ bản ghi mới. Vì sự kiện sau đè hẳn sự kiện trước cho cùng một khóa, nén nhật ký hoạt động tốt. Với event sourcing, logic ứng dụng được xây tường minh trên các sự kiện bất biến; kho sự kiện chỉ nối đuôi, và việc cập nhật hay xóa bị ngăn cản. Vì sự kiện mô tả ý định nên chúng không đè lên nhau — muốn dựng lại trạng thái phải có toàn bộ lịch sử, và nén nhật ký theo cùng kiểu ấy là bất khả.

    Hai cách này nhìn xa thì giống: cả hai đều cho ra một cuốn sổ. Nhưng khác nhau ở chỗ ai viết sổ. CDC: cái kho tự viết sổ, ghi lại "giờ hàng này thành thế này". Nguồn sự kiện: ứng dụng chủ động viết sổ, ghi lại "người dùng vừa làm việc này". Nhớ nhé: một bên chép kết quả, một bên chép việc làm — và chỉ bên sau mới giữ được lý do.

    Kleppmann separates these two firmly, and this is exactly where first-time readers blur them1. With CDC, the application still uses the database mutably — updating and deleting freely; the changelog is extracted at a low level, from the store's write log, and each update event typically carries the entire new record. Because a later event fully overwrites an earlier one for the same key, log compaction works well. With event sourcing, application logic is built explicitly on immutable events; the event store is append-only, and updates or deletes are discouraged. Because events describe intent they do not overwrite each other — reconstructing state requires the full history, and compaction in the same sense is impossible.

    Chọn cái nàoMuốn bổ sung hệ dẫn xuất cho một ứng dụng đang chạy mà không phải viết lại nó: chọn CDC. Muốn ý định của người dùng trở thành tài sản lâu dài và chấp nhận thiết kế ứng dụng quanh nhật ký ngay từ đầu: chọn event sourcing. Nguồn chân lý cũng khác — với CDC là bảng trong CSDL, với event sourcing là chính nhật ký sự kiện.
    Chọn cái nàoApp đã chạy rồi, không muốn sửa: chọn CDC. Làm mới từ đầu và muốn giữ lý do: chọn nguồn sự kiện.
    Which to chooseTo add derived systems to an application that is already running, without rewriting it: CDC. To make user intent a long-lived asset and design the application around the log from the start: event sourcing. The source of truth differs too — for CDC it's the table in the database; for event sourcing it's the event log itself.
  4. Commands and events

    Lệnh còn có thể bị từ chối; sự kiện thì đã rồiA command may be refused; an event already happened

    Một yêu cầu của người dùng đến hệ dưới dạng lệnh — và lệnh thì có thể thất bại, chẳng hạn vì vi phạm một ràng buộc1. Việc kiểm tra tính hợp lệ phải diễn ra đồng bộ, trước khi có bất kỳ sự kiện nào. Chỉ khi lệnh đã được chấp nhận, nó mới trở thành một sự kiện — một sự thật bền vững và bất biến. Ví dụ Kleppmann dùng là đặt chỗ ngồi: việc kiểm tra ghế còn trống và việc giữ ghế cho người này phải xảy ra trong một thao tác nguyên tử. Một cách khác là tách làm hai sự kiện — phát ra "đặt tạm" trước, rồi để một bộ xử lý riêng phát tiếp "xác nhận" hoặc "từ chối".

    "Cho tôi mượn quyển sách này" là một lời đề nghị — thủ thư có thể nói không, vì sách đã có người mượn. Nhưng khi thủ thư gật đầu và ghi vào sổ thì chuyện đã xong: không ai rút lại được nữa. Nhớ nhé: hỏi thì có thể bị từ chối, còn đã ghi vào sổ thì là chuyện đã rồi.

    A user's request arrives as a command — and a command may fail, for instance by violating a constraint1. Validation must happen synchronously, before any event exists. Only once the command is accepted does it become an event — a durable, immutable fact. Kleppmann's example is reserving a seat: checking availability and reserving the seat for this person must happen in one atomic operation. An alternative is to split it in two events — emit a "tentative reservation", then let a separate processor emit "confirmation" or "rejection".

    Vì sao phải táchNếu để lệnh chảy thẳng vào nhật ký rồi mới kiểm tra, ta đã ghi vào lịch sử một việc có thể chưa từng hợp lệ — và lịch sử thì theo định nghĩa không sửa được. Ranh giới đồng bộ/bất đồng bộ của cả hệ nằm đúng ở chỗ này.
    Vì sao phải táchVì sổ đã ghi là không tẩy được, nên phải hỏi cho chắc rồi mới ghi.
    Why the split mattersIf commands flow straight into the log and are validated afterwards, you have written into history something that may never have been valid — and history, by definition, cannot be edited. The system's synchronous/asynchronous boundary sits exactly here.

    Nơi ràng buộc phải sống

    Phần cần đồng bộ hóa thường rất nhỏ: một phép kiểm tra duy nhất trên một khóa (ghế này, tên đăng nhập này, số dư này). Mọi thứ còn lại — gửi email, cập nhật chỉ mục, dựng báo cáo — có thể bất đồng bộ hoàn toàn. Thiết kế tốt là co cái lõi đồng bộ lại nhỏ nhất có thể, chứ không phải bỏ nó đi.

    Khi phải phát ra một sự kiện "tạm"

    Lối hai-sự-kiện có ích khi việc kiểm tra cần tra cứu nhiều nơi và không thể gói trong một thao tác nguyên tử. Đổi lại, giao diện người dùng phải chịu được trạng thái "đang chờ", và phải có đường xử lý cho sự kiện từ chối — thứ mà thiết kế nào bỏ quên cũng để lại chỗ treo.

  5. State, streams, and immutability

    Sổ cái không dùng tẩyLedgers don't use erasers

    Kleppmann đóng khung quan hệ giữa trạng thái và luồng bằng một hình ảnh giải tích: trạng thái là cái ta có khi tích phân một luồng sự kiện theo thời gian, còn luồng thay đổi là cái ta có khi vi phân trạng thái theo thời gian115. Trạng thái khả biến và một nhật ký bất biến không mâu thuẫn nhau — chúng là hai mặt của cùng một dữ liệu. Ông mượn tiếp hình ảnh sổ cái kế toán: sổ chỉ được ghi thêm; ghi sai thì sửa bằng một bút toán bù, chứ không tẩy xóa dòng cũ; số dư luôn là kết quả cộng dồn16. Pat Helland đẩy cùng lập luận xa hơn trong một bài mà Kleppmann tham chiếu: khi lưu trữ đủ rẻ, bất biến trở thành lựa chọn mặc định hợp lý27.

    Kế toán không bao giờ dùng tẩy. Ghi nhầm thì viết thêm một dòng sửa lại, chứ không xóa dòng cũ đi. Nhờ vậy ai cũng thấy được chuyện gì đã xảy ra, kể cả cái nhầm và cách sửa nó. Nhớ nhé: viết thêm thì giữ được sự thật, tẩy xóa thì mất luôn.

    Kleppmann frames the state/stream relationship with an image from calculus: state is what you get when you integrate an event stream over time; a change stream is what you get when you differentiate state by time115. Mutable state and an immutable log do not contradict each other — they are two faces of the same data. He borrows the accountant's ledger: entries are only appended; a mistake is fixed with a compensating entry, never by erasing the old line; the balance is always an accumulation16. Pat Helland pushes the same argument further in a paper Kleppmann cites: once storage is cheap enough, immutability becomes the sensible default27.

    Ba món lợiMột: có dấu vết kiểm toán đầy đủ, vì quá khứ không bị ghi đè. Hai: sửa được hậu quả của mã lỗi — triển khai bản vá rồi chạy lại nhật ký để dựng lại view, thứ hoàn toàn bất khả nếu ta chỉ giữ trạng thái hiện thời. Ba: dẫn ra nhiều view khác nhau từ cùng một nhật ký — tách dạng dữ liệu được ghi khỏi dạng dữ liệu được đọc, đúng tinh thần CQRS14.
    Ba món lợiMột: lúc nào cũng tra lại được chuyện cũ. Hai: sửa lỗi xong thì tính lại từ đầu được. Ba: từ một cuốn sổ dựng ra nhiều kiểu bảng khác nhau.
    Three benefitsOne: a full audit trail, since the past is never overwritten. Two: recovery from buggy code — deploy the fix and reprocess the log to rebuild the view, which is simply impossible if you only keep current state. Three: derive several different views from the same log — separating the form data is written in from the form it is read in, which is the CQRS idea14.

    Cái giá lớn nhất: đọc-thấy-cái-mình-vừa-ghi

    Nhược điểm nặng nhất là các consumer của nhật ký thường bất đồng bộ, nên người vừa ghi có thể chưa thấy thay đổi của chính mình trong view đọc. Lối gỡ: thực hiện phép đọc và phép ghi view dẫn xuất trong cùng một đơn vị nguyên tử — điều này đòi giao dịch — hoặc chấp nhận và thiết kế giao diện quanh nó. Một lợi thế nhỏ đi kèm: nhật ký chỉ có một điểm nối đuôi, nên nhiều chỗ vốn cần khóa lại hóa ra không cần.

    Giới hạn của bất biến

    Bất biến không hợp với mọi workload: nơi tỉ lệ cập nhật và xóa rất cao, nhật ký phình nhanh còn việc nén thì thành nút thắt. Và có những lúc buộc phải xóa thật — vì quyền riêng tư hoặc vì luật. Kleppmann nhấn rằng việc này khó hơn nhiều so với vẻ ngoài của nó: bản sao nằm rải rác ở bản sao lưu, ở chỉ mục, ở cache, và "xóa" thường chỉ là đánh dấu chứ không phải làm cho dữ liệu thật sự biến mất khỏi đĩa3.

Trục 3 · Processing streams & reasoning about timeAxis 3 · Processing streams & reasoning about time

Truy vấn đứng yên, dữ liệu chạy quaThe query stands still; the data runs past

Có ba việc có thể làm với một luồng đã xử lý1: ghi vào một kho (cơ sở dữ liệu, cache, chỉ mục tìm kiếm); đẩy thẳng tới con người (thông báo, email, bảng điều khiển thời gian thực); hoặc sinh ra một hay nhiều luồng khác, nối thành đường ống. Nhưng khác biệt cốt tử với xử lý theo lô vẫn ở đó và không gỡ được: luồng không bao giờ kết thúc. Vì thế không thể sắp xếp toàn bộ đầu vào, sort-merge join của Chương 10 mất chỗ đứng, và mọi phép tính "trong khoảng thời gian X" buộc ta phải trả lời trước một câu hỏi tưởng đơn giản: thời gian nào?

Sau khi tính toán trên dòng dữ liệu, kết quả đi được ba nơi: cất vào kho, báo cho người biết, hoặc chảy tiếp thành một dòng mới. Nhưng có một chuyện khó: dòng dữ liệu không bao giờ hết, nên không thể xếp hết tất cả ra rồi mới tính. Nhớ nhé: khi không thể chờ "đủ dữ liệu", ta phải học cách tính với cái mình đang có.

There are three things you can do with a processed stream1: write it to a store (database, cache, search index); push it to people (notifications, emails, real-time dashboards); or produce one or more further streams, chained into a pipeline. But the crucial difference from batch remains and cannot be engineered away: a stream never ends. So you cannot sort the whole input, Chapter 10's sort-merge join loses its footing, and every "over the last X" computation forces you to answer a deceptively simple question first: which time?

The query/data relationship is inverted

Trong một cơ sở dữ liệu thường, dữ liệu nằm yên còn truy vấn chạy qua nó. Xử lý luồng đảo ngược điều đó: truy vấn được lưu lâu dài, còn sự kiện liên tục chảy ngang qua chúng1. Một khi đã quen với phép đảo này, cả một họ kỹ thuật hiện ra — tìm mẫu, tổng hợp thống kê, duy trì view, và cả tìm kiếm ngược chiều.

Trong tủ hồ sơ, giấy tờ nằm im còn ta đi tìm. Xử lý luồng thì ngược lại: câu hỏi nằm im, giấy tờ tự chạy qua và hễ cái nào khớp thì báo lên. Nhớ nhé: đổi chiều như vậy thì biết ngay khi có chuyện, khỏi phải đi hỏi lại mãi.

In an ordinary database, the data sits still and the query runs over it. Stream processing inverts that: queries are stored long-term while events flow continuously past them1. Once that inversion clicks, a whole family of techniques appears — pattern matching, statistical aggregation, view maintenance, and even search run backwards.

Kleppmann nhắc rằng ý này không mới: xử lý sự kiện phức hợp đã xuất hiện từ những năm 1990, với ngôn ngữ truy vấn khai báo kiểu SQL hoặc giao diện đồ họa, và nhiều hệ thương mại như Esper, IBM InfoSphere Streams, Apama, TIBCO StreamBase, SQLstream1. Cái mới của thập niên 2010 không phải bản thân phép đảo, mà là quy mô: khi nhật ký phân mảnh cho phép cùng một luồng được nhiều hệ đọc độc lập và phát lại được, phép đảo này thôi là một sản phẩm chuyên dụng và trở thành một lối kiến trúc chung.

Truy vấn đứng yên CEP lưu truy vấn lâu dài và cho sự kiện chảy qua — ngược hẳn cơ sở dữ liệu.
Hai đồng hồ Thời gian sự kiện và thời gian xử lý là hai thứ khác nhau; lẫn lộn là ra dữ liệu sai.
Join có thời điểm Kết quả join phụ thuộc thứ tự sự kiện đến, nên có thể phi tất định.
Câu hỏi nằm im Đặt sẵn câu hỏi, dữ liệu chạy qua thì báo.
Hai đồng hồ Lúc chuyện xảy ra và lúc máy tính biết là hai lúc khác nhau.
Ghép đôi theo giờ Ghép hai dòng lại thì kết quả phụ thuộc cái nào tới trước.
Standing queries CEP stores queries long-term and lets events flow past — the reverse of a database.
Two clocks Event time and processing time are different things; confusing them produces bad data.
Joins have a when A join's result depends on the order events arrive, so it can be nondeterministic.
  1. Complex event processing & stream analytics

    Tìm mẫu, và đếm thống kêMatching patterns, and counting statistics

    Xử lý sự kiện phức hợp (CEP) cho ta đặc tả các quy tắc để tìm những mẫu sự kiện nhất định — Kleppmann ví như một biểu thức chính quy chạy trên luồng1. Truy vấn được đăng ký một lần và nằm đó; khi một chuỗi sự kiện khớp mẫu, hệ phát ra một sự kiện phức hợp. Esper là hiện thực tiêu biểu của dòng này23. Phân tích luồng thì không tìm mẫu mà tính tổng hợp trên rất nhiều sự kiện: tần suất một loại sự kiện, trung bình trượt trong một khoảng, so sánh khoảng hiện tại với khoảng trước. Vì phải tính trên lượng dữ liệu lớn với bộ nhớ nhỏ, các hệ này hay dùng thuật toán xác suất — bộ lọc Bloom cho kiểm tra thành viên, HyperLogLog cho ước lượng lực lượng, các thuật toán ước lượng percentile.

    Có hai kiểu "tính trên dòng". Kiểu một là canh mẫu: dặn trước "hễ thấy ba lần rút tiền trong mười phút thì báo tôi", rồi cứ để đó. Kiểu hai là đếm: mỗi phút có bao nhiêu lượt bấm, trung bình năm phút qua là bao nhiêu. Nhớ nhé: một bên canh chuyện lạ, một bên đo chuyện thường.

    Complex event processing (CEP) lets you specify rules to search for certain patterns of events — Kleppmann likens it to a regular expression running over a stream1. The query is registered once and stays; when a sequence of events matches, the system emits a complex event. Esper is the canonical implementation23. Stream analytics looks for no patterns but computes aggregations over huge numbers of events: the rate of some event type, a rolling average over an interval, comparing the current interval with the previous one. Because they must compute over a lot of data with little memory, these systems often use probabilistic algorithms — Bloom filters for set membership, HyperLogLog for cardinality estimation, percentile estimation algorithms.

    Một hiểu lầm đáng sửaKleppmann nhấn: thuật toán xác suất không có nghĩa là kết quả gần đúng. Chúng là một tối ưu hóa — một thuật toán xử lý luồng hoàn toàn có thể cho kết quả chính xác nếu ta chịu trả chi phí bộ nhớ. Xấp xỉ là lựa chọn kỹ thuật, không phải giới hạn cố hữu của việc xử lý theo luồng.
    Một hiểu lầmDùng mẹo đếm nhanh không có nghĩa là kết quả sai. Muốn chính xác vẫn được, chỉ là tốn hơn.
    A misreading worth fixingKleppmann stresses that probabilistic algorithms do not mean approximate results. They are an optimization — a stream processing algorithm can be perfectly accurate if you are willing to pay in memory. Approximation is an engineering choice, not an inherent limit of streaming.

    Vì sao CEP cần ngôn ngữ riêng

    Mẫu mà CEP tìm thường mang tính thời gian: "A rồi B trong vòng 30 giây, mà không có C xen giữa". SQL thường không diễn đạt nổi loại điều kiện này một cách gọn gàng, nên các hệ CEP mở rộng SQL hoặc dựng ngôn ngữ khai báo riêng. Đây cũng là lý do Kleppmann xếp CEP thành một nhánh riêng thay vì gộp chung vào phân tích luồng.

    Ranh giới đang mờ dần

    Nhiều khung xử lý luồng ngày nay đồng thời làm cả hai việc, và cung cấp giao diện SQL để khai báo phép tổng hợp lẫn mẫu sự kiện9. Phân loại của Kleppmann vẫn hữu ích như một cách suy nghĩ — nó nói cho ta biết đang hỏi loại câu hỏi nào — chứ không còn là ranh giới giữa các sản phẩm.

  2. Materialized views & search on streams

    Duy trì view, và tìm kiếm ngược chiềuMaintaining views, and search run backwards

    Duy trì view vật chất hóa là dẫn ra một cách nhìn khác lên dữ liệu để truy vấn cho hiệu quả, rồi cập nhật cách nhìn đó mỗi khi dữ liệu nền đổi1. Điểm khác biệt so với phân tích luồng nằm ở cửa sổ: dựng lại trạng thái ứng dụng từ nhật ký sự kiện đòi mọi sự kiện trong khoảng thời gian bất kỳ, chứ không chỉ vài phút gần nhất — đây chính là event sourcing nhìn từ phía xử lý. Samza và Kafka Streams hỗ trợ lối dùng này, dựa trên cơ chế nén nhật ký của Kafka để nhật ký vẫn đủ dựng lại toàn bộ trạng thái mà không phình vô hạn9246. Tìm kiếm trên luồng thì lật ngược công cụ tìm kiếm quen thuộc: thay vì đánh chỉ mục tài liệu rồi chạy truy vấn, ta lưu truy vấn trước và cho tài liệu chạy ngang qua chúng — Elasticsearch gọi đây là percolate query22.

    Hai việc nữa làm được trên dòng. Một: giữ sẵn một cái bảng tóm tắt và cứ có tin mới thì sửa bảng ngay, để lúc cần xem là có liền. Hai: đăng ký trước điều mình quan tâm — "có tin nào nhắc tới đội bóng của tôi thì báo" — rồi tin tức chạy qua, cái nào khớp thì hệ báo. Nhớ nhé: tìm kiếm bình thường là mình đi tìm; tìm kiếm trên dòng là để nó tự tìm đến mình.

    Maintaining materialized views means deriving an alternative view of a dataset so you can query it efficiently, and updating that view whenever the underlying data changes1. What sets it apart from stream analytics is the window: rebuilding application state from an event log requires all events over an arbitrary time period, not just the last few minutes — this is event sourcing seen from the processing side. Samza and Kafka Streams support this, leaning on Kafka's log compaction so the log can still reconstruct the whole state without growing without bound9246. Searching on streams turns conventional search inside out: instead of indexing documents and then running queries, you store the queries and run documents past them — Elasticsearch calls this a percolate query22.

    Nhìn thấy ở đâuMọi cảnh báo dạng "báo cho tôi khi giá dưới X", mọi bộ đệm dòng thời gian, mọi bảng tổng hợp được cập nhật liên tục — đều là một trong hai lối này. Điều đáng nhớ: cả hai đều đổi chỗ chi phí, chuyển công việc từ lúc đọc sang lúc ghi.
    Nhìn thấy ở đâuMấy cái "báo tôi khi có tin mới" và mấy bảng thống kê luôn cập nhật — đều là hai lối này.
    Where you see itEvery "alert me when the price drops below X", every timeline cache, every continuously updated summary table is one of these two. Worth remembering: both move the cost, shifting work from read time to write time.
  3. Event time vs processing time

    "Phút vừa rồi" theo đồng hồ nào?"The last minute" — by whose clock?

    Nhiều khung xử lý luồng dùng đồng hồ hệ thống cục bộ của máy đang xử lý để chia cửa sổ — tức là dùng thời gian xử lý1. Cách này đơn giản và hoàn toàn hợp lý nếu độ trễ giữa lúc sự kiện sinh ra và lúc nó được xử lý là không đáng kể. Nó sụp đổ khi có trễ thật: hàng đợi, sự cố mạng, tranh chấp tài nguyên, consumer khởi động lại, hoặc chạy lại nhật ký sau khi vá lỗi. Kleppmann nêu một hiện vật kinh điển: khi bộ xử lý luồng được bật lại sau một quãng ngừng, nó ngốn rất nhanh phần tồn đọng — và nếu biểu đồ vẽ theo thời gian xử lý, ta thấy một đỉnh lưu lượng dựng đứng. Nhưng tốc độ sự kiện thật chưa hề đổi; cái đỉnh ấy là hiện vật của chính việc xử lý.

    Chuyện xảy ra lúc 9 giờ, nhưng máy tính mãi 10 giờ mới biết. Vậy khi hỏi "trong giờ vừa rồi có bao nhiêu chuyện", ta tính theo lúc chuyện xảy ra hay lúc máy biết? Nếu máy bị hỏng rồi bật lại, nó sẽ đọc dồn một lúc — nhìn biểu đồ tưởng có một trận bão, thật ra chỉ là máy đang đọc bù. Nhớ nhé: đừng lẫn lúc chuyện xảy ra với lúc mình biết chuyện.

    Many stream processing frameworks use the local system clock of the processing machine to cut windows — that is, processing time1. This is simple and entirely reasonable if the delay between an event occurring and being processed is negligible. It collapses when the delay is real: queueing, network faults, contention, consumer restarts, or replaying a log after a bug fix. Kleppmann gives a classic artifact: when a stream processor restarts after an outage, it chews through the backlog fast — and a chart plotted on processing time shows a vertical traffic spike. But the real event rate never changed; the spike is an artifact of the processing itself.

    Hệ quả thực tếBất kỳ chỉ số nào dùng để cảnh báo mà lại tính theo thời gian xử lý đều có thể báo động giả đúng vào lúc hệ vừa hồi phục — tức là đúng lúc bạn cần một tín hiệu đáng tin nhất.
    Hệ quảHệ vừa hồi phục thì biểu đồ dễ báo động giả, đúng lúc mình cần tin nó nhất.
    The practical upshotAny alerting metric computed on processing time can fire falsely exactly when the system has just recovered — precisely when you most need a trustworthy signal.

    Vì sao cả hai đồng hồ đều cần

    Thời gian sự kiện cho biết chuyện gì đã thật sự xảy ra; thời gian xử lý cho biết hệ của ta đang khỏe tới đâu. Chỉ số nghiệp vụ nên tính theo thời gian sự kiện, còn chỉ số vận hành — độ trễ, tồn đọng, thông lượng — thì chính là hiệu số giữa hai đồng hồ. Lẫn lộn chúng là lấy sức khỏe của hạ tầng làm tín hiệu kinh doanh.

    Mô hình Dataflow đặt lại câu hỏi

    Bài Dataflow của Google — mà Kleppmann tham chiếu — tách bài toán thành những câu hỏi độc lập: kết quả tính trên cái gì, ở đâu trong thời gian sự kiện, khi nào thì phát ra, và kết quả đã phát rồi thì chỉnh sửa ra sao17. Cách tách này giải thích vì sao "chọn cửa sổ" và "chọn lúc phát" là hai quyết định riêng biệt chứ không phải một.

  4. Knowing when you're ready

    Không bao giờ chắc là đã nhận đủYou can never be sure you have it all

    Khi cửa sổ được chia theo thời gian sự kiện, một vấn đề khó chịu hiện ra: ta không bao giờ biết chắc đã nhận đủ sự kiện cho một cửa sổ, hay vẫn còn những sự kiện đến muộn đang trên đường1. Kleppmann đưa ra hai lựa chọn, và cả hai đều là đánh đổi chứ không phải lời giải. Một: bỏ qua các sự kiện muộn — và theo dõi tỉ lệ bị bỏ như một chỉ số, để biết mình đang đánh mất bao nhiêu sự thật. Hai: phát một bản sửa — một giá trị cập nhật cho cửa sổ đã công bố, và có thể phải thu hồi kết quả cũ nếu nó đã đi tiếp xuống hạ nguồn.

    Muốn đếm "có bao nhiêu chuyện xảy ra lúc 9 giờ", ta phải đợi tới khi nào mới chắc là đã nhận đủ? Câu trả lời buồn: không bao giờ chắc cả — vẫn có thể còn một tin lạc đường về muộn. Nên hoặc là chốt sổ rồi thôi, hoặc là chốt tạm và sửa sau. Nhớ nhé: không có lúc nào gọi là "đủ", chỉ có lúc mình quyết định dừng.

    Once windows are cut by event time, an awkward problem appears: you can never be sure you have received all events for a window, or whether stragglers are still on the way1. Kleppmann offers two options, and both are trade-offs rather than solutions. One: ignore the late events — and track the proportion dropped as a metric, so you know how much truth you are losing. Two: publish a correction — an updated value for the window already published, possibly retracting the earlier output if it has flowed downstream.

    Vì sao không thể có "đủ"Không tồn tại một ranh giới nào chứng minh được rằng mạng sẽ không giao thêm một sự kiện cũ nữa. Đây không phải thiếu sót kỹ thuật có thể vá — nó là hệ quả trực tiếp của những giới hạn mà Chương 8 đã đặt ra về đồng hồ và mạng bất đồng bộ.
    Vì sao vậyVì không ai bảo đảm được là mạng sẽ không giao thêm một tin cũ nữa.
    Why "complete" is unreachableNo boundary can prove the network will not deliver one more old event. This is not a fixable engineering gap — it follows directly from the limits Chapter 8 established about clocks and asynchronous networks.
  5. Whose clock are you using?

    Đồng hồ của thiết bị có thể nói dốiThe device's clock may be lying

    Một sự kiện có thể bị đệm lại ở nhiều chặng trước khi tới nơi xử lý1. Ví dụ Kleppmann dùng rất sắc: một ứng dụng di động được dùng khi máy không có mạng sẽ ghi sự kiện vào bộ nhớ máy và chỉ gửi lên khi có kết nối trở lại — có thể hàng ngày sau. Tệ hơn, đồng hồ của thiết bị có thể sai, vô tình (cấu hình sai, pin cạn) hoặc cố ý (người dùng chỉnh giờ để qua mặt một giới hạn trong game). Cách hiệu chỉnh ông đề xuất là ghi ba dấu thời gian: (1) lúc sự kiện xảy ra, theo đồng hồ thiết bị; (2) lúc sự kiện được gửi lên máy chủ, theo đồng hồ thiết bị; (3) lúc máy chủ nhận được, theo đồng hồ máy chủ. Lấy (3) trừ (2) cho ta ước lượng độ lệch giữa hai đồng hồ — giả định độ trễ mạng không đáng kể — rồi áp độ lệch đó vào (1) để suy ra thời gian sự kiện thật.

    Điện thoại có thể ghi lại chuyện xảy ra lúc không có mạng, rồi mai mới gửi lên. Mà đồng hồ trong điện thoại thì có khi chạy sai, có khi bị chủ nhân chỉnh cho lệch. Cách sửa khéo: ghi ba mốc giờ — lúc chuyện xảy ra, lúc gửi đi (hai mốc này theo giờ máy), và lúc máy chủ nhận (theo giờ máy chủ). So hai mốc cuối là biết đồng hồ máy lệch bao nhiêu, rồi trừ đi. Nhớ nhé: không tin được một cái đồng hồ, thì so hai cái với nhau.

    An event may be buffered at several points before reaching the processor1. Kleppmann's example is sharp: a mobile app used offline records events on the device and only reports them when connectivity returns — possibly days later. Worse, the device clock may be wrong, accidentally (misconfiguration, dead battery) or deliberately (a user shifting the clock to bypass an in-game limit). His correction is to log three timestamps: (1) the time the event occurred, by the device clock; (2) the time it was sent to the server, by the device clock; (3) the time the server received it, by the server clock. Subtracting (2) from (3) estimates the offset between the two clocks — assuming network delay is negligible — and applying that offset to (1) recovers the true event time.

    Vì sao mẹo này đẹpNó không đòi thiết bị phải có đồng hồ đúng — chỉ đòi đồng hồ đó nhất quán với chính nó trong khoảng giữa (1) và (2). Đây đúng là tinh thần của Chương 8: đừng tin giá trị tuyệt đối của một đồng hồ, hãy dùng hiệu số giữa hai lần đọc cùng một đồng hồ.
    Vì sao khéoKhông cần đồng hồ đúng giờ, chỉ cần nó sai đều đều là đủ để tính bù.
    Why the trick is elegantIt doesn't require the device's clock to be correct — only that it be consistent with itself between (1) and (2). That is exactly Chapter 8's spirit: don't trust a clock's absolute value; use the difference between two readings of the same clock.
  6. Types of windows

    Bốn kiểu cửa sổFour kinds of window

    Khi đã chốt được dùng đồng hồ nào, mới tới việc chọn cửa sổ. Kleppmann liệt kê bốn kiểu, và chúng khác nhau ở hai câu hỏi: độ dài có cố định không, và các cửa sổ có chồng lấn nhau không117. Nhầm kiểu cửa sổ là ra một con số hoàn toàn hợp lý về mặt kỹ thuật nhưng trả lời sai câu hỏi nghiệp vụ.

    "Cửa sổ" là cách ta cắt dòng thời gian ra từng khúc để đếm. Có khúc cắt đều và không dính nhau, có khúc cắt đều nhưng chồng lên nhau, có khúc co giãn theo khoảng cách giữa các sự kiện, và có khúc tự kết thúc khi người ta ngừng hoạt động. Nhớ nhé: cắt kiểu nào thì ra con số kiểu đó — chọn sai kiểu là trả lời sai câu hỏi.

    Once you have settled which clock, you choose a window. Kleppmann lists four kinds, differing on two questions: is the length fixed, and do windows overlap117? Picking the wrong kind yields a number that is technically impeccable and answers the wrong business question.

    Nhảy · tumblingTumbling

    Độ dài cố định, không chồng lấn: mỗi sự kiện thuộc về đúng một cửa sổ. Ví dụ cửa sổ 1 phút: mọi sự kiện có dấu thời gian trong khoảng 10:03:00–10:03:59 rơi vào cùng một cửa sổ.

    Cắt đều từng phút một, không dính nhau. Mỗi chuyện thuộc đúng một phút.

    Fixed length, no overlap: every event belongs to exactly one window. With a 1-minute window, all events timestamped 10:03:00–10:03:59 fall in the same one.

    Trượt-bước · hoppingHopping

    Độ dài cố định nhưng chồng lấn theo một bước nhảy — ví dụ cửa sổ 5 phút, nhảy mỗi 1 phút, để làm mượt đường trung bình. Có thể hiện thực bằng cách tính cửa sổ nhảy 1 phút trước, rồi gộp năm cửa sổ liền kề.

    Cửa sổ 5 phút nhưng cứ 1 phút lại trượt tới một lần, nên các khúc đè lên nhau.

    Fixed length but overlapping by a hop size — e.g. 5-minute windows hopping every 1 minute, to smooth a moving average. It can be built by computing 1-minute tumbling windows first, then aggregating over five adjacent ones.

    Trượt · slidingSliding

    Chứa mọi sự kiện nằm trong một khoảng cách thời gian với nhau, không neo vào ranh giới cố định nào. Ví dụ của Kleppmann: một cửa sổ trượt 5 phút gộp hai sự kiện lúc 10:03:39 và 10:07:35 vì chúng cách nhau chưa tới 5 phút — dù chúng không nằm trong cùng một cửa sổ nhảy 5 phút nào cả. Hiện thực bằng một bộ đệm sắp theo thời gian, loại bỏ sự kiện đã hết hạn.

    Không cắt theo mốc cố định, mà hỏi: hai chuyện này có xảy ra gần nhau trong vòng 5 phút không?

    Contains all events that occur within some interval of each other, anchored to no fixed boundary. Kleppmann's example: a 5-minute sliding window groups events at 10:03:39 and 10:07:35 because they are less than 5 minutes apart — even though no 5-minute tumbling window contains both. Implemented with a time-sorted buffer that drops expired events.

    Phiên · sessionSession

    Không có độ dài cố định. Gom mọi sự kiện của cùng một người dùng xảy ra gần nhau về thời gian, và kết thúc khi người đó im lặng đủ lâu — ví dụ 30 phút không hoạt động. Đây chính là phép sessionization quen thuộc trong phân tích hành vi.

    Cửa sổ co giãn theo người: bắt đầu khi người ta vào, kết thúc khi người ta nghỉ đủ lâu.

    No fixed duration. Groups all events for the same user that occur closely together, ending once that user has been inactive long enough — say 30 minutes. This is the familiar sessionization of behavioural analytics.

  7. Stream joins

    Ba kiểu join, và cái bẫy thời điểmThree kinds of join, and the timing trap

    Kleppmann chia join trên luồng làm ba kiểu1. Luồng-luồng (join theo cửa sổ): cả hai đầu vào đều là sự kiện hoạt động; toán tử tìm những sự kiện liên quan xảy ra trong một cửa sổ thời gian. Ví dụ ông dùng: nối sự kiện tìm kiếm với sự kiện nhấp chuột theo mã phiên trong vòng một giờ, để đo tỉ lệ nhấp của mỗi truy vấn — bắt buộc phải có cửa sổ, vì người dùng có thể không bao giờ nhấp. Luồng-bảng (làm giàu luồng): một đầu vào là sự kiện hoạt động, đầu kia là một bảng cơ sở dữ liệu. Hỏi CSDL từ xa cho từng sự kiện thì chậm và dễ làm quá tải; cách tốt hơn là nạp một bản sao của bảng vào ngay trong bộ xử lý luồng và giữ nó cập nhật bằng changelog. Khác với xử lý theo lô — vốn dùng một ảnh chụp cố định tại một thời điểm — join luồng-bảng dùng một bảng liên tục đổi. Bảng-bảng (duy trì view vật chất hóa): cả hai đầu vào đều là changelog; mỗi thay đổi ở một bên được nối với trạng thái mới nhất của bên kia. Ví dụ: bộ đệm dòng thời gian mạng xã hội chính là kết quả join giữa bảng bài đăng và bảng theo dõi, được duy trì liên tục thay vì tính lại mỗi lần đọc.

    "Join" là ghép hai nguồn tin lại với nhau. Kiểu một: ghép hai dòng sự kiện, nhưng phải kèm điều kiện "trong vòng một giờ" — vì có khi vế kia không bao giờ tới. Kiểu hai: ghép một dòng sự kiện với một bảng tra cứu, và bảng đó thì luôn được cập nhật. Kiểu ba: ghép hai bảng, để giữ sẵn một kết quả cho khỏi tính lại. Nhớ nhé: ghép trên dòng thì luôn phải hỏi thêm "ghép theo lúc nào".

    Kleppmann splits stream joins into three kinds1. Stream-stream (window join): both inputs are activity events; the operator looks for related events occurring within a time window. His example: joining search events with click events by session ID within an hour, to measure each query's click-through rate — a window is unavoidable, because the user may never click. Stream-table (stream enrichment): one input is activity events, the other a database table. Querying the remote database for every event is slow and can overload it; better to load a copy of the table into the stream processor itself and keep it current from a changelog. Unlike batch — which uses a fixed point-in-time snapshot — a stream-table join uses a continuously updated table. Table-table (materialized view maintenance): both inputs are changelogs; each change on one side is joined with the latest state of the other. Example: a social timeline cache is the result of a join between the posts table and the follows table, maintained continuously rather than recomputed on every read.

    Điểm chung của cả baCả ba đều buộc bộ xử lý giữ trạng thái dựa trên một đầu vào rồi tra nó bằng đầu vào kia. Nghĩa là thứ tự sự kiện đến trực tiếp quyết định kết quả — và đó chính là chỗ cái bẫy nằm.
    Điểm chungCả ba đều phải nhớ một bên để chờ bên kia. Nên cái nào tới trước là kết quả đổi.
    What all three shareEach forces the processor to hold state derived from one input and query it from the other. So the order of arrival directly decides the result — and that is where the trap lies.

    Phi tất định: chạy lại ra kết quả khác

    Nếu hai sự kiện trên hai luồng xảy ra quanh cùng một thời điểm mà không có thứ tự xác định giữa chúng, join trở nên phi tất định: chạy lại cùng một job trên cùng một dữ liệu có thể ra kết quả khác. Ví dụ của Kleppmann: nối đơn hàng với thuế suất — nên dùng thuế suất hiện thời hay thuế suất lúc bán? Hai câu trả lời đều hợp lý, và chính vì thế mà "join" không phải một phép toán có nghĩa duy nhất.

    Chiều thay đổi chậm, và cái giá của nó

    Kho dữ liệu gọi hiện tượng này là slowly changing dimension (SCD), và xử lý bằng cách gán một định danh riêng cho mỗi phiên bản của bản ghi được join — join theo mã phiên bản thì tất định trở lại. Nhưng cái giá đi kèm rất cụ thể: vì phải giữ mọi phiên bản, ta không nén nhật ký được nữa. Đúng như mọi chỗ khác trong chương, tính tất định được mua bằng dung lượng.

Trục 4 · Fault toleranceAxis 4 · Fault tolerance

Hiệu ứng đúng một lần, không phải xử lý đúng một lầnEffectively once, not literally once

Xử lý theo lô có một tiện nghi mà luồng không bao giờ có: job kết thúc, nên hỏng thì cứ vứt kết quả dở đi rồi chạy lại từ đầu. Với một luồng vô hạn thì không có "lúc chạy xong" để chờ. Vì thế cả trục này là câu chuyện về việc chia cái vô hạn thành từng mẩu có thể thử lại — và về việc gọi đúng tên thứ mà ta thật sự đạt được ở cuối.

Làm theo mẻ thì dễ: mẻ nào hỏng thì bỏ mẻ đó, làm lại. Nhưng dòng dữ liệu không bao giờ hết, làm gì có "mẻ" nào để bỏ? Nên người ta phải tự cắt dòng ra từng mẩu nhỏ, và làm sao cho có làm lại mấy lần thì kết quả cuối cùng vẫn đúng một lần. Nhớ nhé: không phải là "chỉ làm một lần", mà là "làm mấy lần cũng ra như một lần".

Batch processing has a comfort streaming never gets: the job ends, so on failure you discard the partial output and rerun. With an infinite stream there is no "when it's done" to wait for. So this whole axis is about slicing the infinite into retryable pieces — and about naming correctly what you actually achieve at the end.

Exactly-once, said carefully

Điều thật sự đạt được không phải là mỗi sự kiện được xử lý đúng một lần theo nghĩa đen. Sự kiện vẫn có thể được thử lại nhiều lần; cái được bảo toàn là hiệu ứng cuối cùng — kết quả trông y như thể nó chỉ được xử lý một lần. Kleppmann gọi tính chất này là exactly-once semantics, đồng thời lưu ý effectively-once là tên mô tả đúng hơn1. Nó được mua bằng ba thứ cộng lại: bỏ phần việc dở của lần chạy hỏng, commit nguyên tử, và tính lũy đẳng cộng loại bỏ trùng lặp.

"Đúng một lần" nghe như là máy chỉ làm một lần duy nhất. Không phải vậy. Máy vẫn có thể làm đi làm lại — chỉ là kết quả cuối cùng trông giống như chỉ làm một lần. Giống việc bật công tắc đèn: bật mười lần thì đèn vẫn chỉ sáng, không sáng gấp mười. Nhớ nhé: mẹo ở chỗ làm sao cho lặp lại cũng vô hại.

What is actually achieved is not that each event is processed literally once. Events may still be retried many times; what is preserved is the final effect — the result looks as though it were processed once. Kleppmann calls this property exactly-once semantics, while noting that effectively-once is the more descriptive term1. It is bought with three things together: discarding the partial work of a failed attempt, atomic commit, and idempotence plus deduplication.

Cụm effectively-once mà Kleppmann dẫn lại không phải của ông: danh mục tham chiếu chính thức cho Chương 11 ghi rõ nó đến từ Viktor Klang, người đề xuất năm 2016 để gọi lối "at-least-once cộng thao tác lũy đẳng"3. Nói cách khác: hệ vẫn giao ít nhất một lần; còn "một lần" là tính chất của kết quả, không phải của việc thực thi. Hệ quả trực tiếp cho người đọc kỹ thuật: câu "Kafka bảo đảm exactly-once" nói trống không là sai. Tài liệu chính thức của Kafka trình bày ba mức ngữ nghĩa giao hàng và mô tả đúng một lần như thứ đạt được nhờ producer lũy đẳng và giao dịch, trong một phạm vi và với những giả định cụ thể — chứ không phải một lời bảo đảm phổ quát cho mọi đường đi của dữ liệu7.

Không có "chạy xong" Luồng vô hạn nên không thể đợi job kết thúc rồi mới thử lại như xử lý theo lô.
Hiệu ứng một lần Lũy đẳng cộng commit nguyên tử — chứ không phải mỗi sự kiện chạy đúng một lần.
Trạng thái phải dựng lại được Bằng ảnh chụp, bằng changelog, hoặc bằng cách phát lại chính đầu vào.
Không có lúc xong Dòng chảy mãi, nên phải tự cắt ra từng mẩu để làm lại được.
Làm lại vẫn đúng Như bật công tắc đèn: bấm mấy lần thì đèn vẫn chỉ sáng.
Nhớ được chỗ đang làm Hỏng rồi bật lại thì phải biết mình làm tới đâu.
No "done" An infinite stream means you can't wait for the job to end and then retry, as batch does.
Effectively once Idempotence plus atomic commit — not each event running literally once.
State must be rebuildable From snapshots, from a changelog, or by replaying the input itself.
  1. Microbatching and checkpointing

    Hai cách cắt cái vô hạn thành mẩuTwo ways to slice the infinite

    Vi-lô: Spark Streaming cắt luồng thành những khối nhỏ — ví dụ một giây — rồi xử lý mỗi khối như một job theo lô tí hon11921. Đánh đổi rất rõ: lô càng nhỏ thì chi phí lập lịch và phối hợp càng lớn; lô càng lớn thì độ trễ càng cao. Kleppmann còn chỉ ra một hệ quả ngầm đáng nhớ: vi-lô áp sẵn một cửa sổ nhảy đúng bằng kích thước lô, nên nếu bài toán cần cửa sổ khác thì phải xử lý thêm ở tầng trên. Điểm kiểm tra: Apache Flink đi lối khác — định kỳ sinh ảnh chụp cuộn của trạng thái và ghi vào lưu trữ bền, bằng cách chèn rào chắn vào luồng tin theo tinh thần thuật toán chụp ảnh phân tán của Chandy–Lamport1826. Khi một toán tử sập, nó khởi động lại từ điểm kiểm tra gần nhất và bỏ mọi kết quả sinh ra giữa điểm ấy với lúc sập.

    Không cắt được dòng thì ta tự cắt. Cách một: cứ mỗi giây gom lại thành một mẻ tí hon rồi làm như mẻ bình thường — hỏng mẻ nào làm lại mẻ đó. Cách hai: cứ một lúc lại chụp ảnh xem mình đang làm tới đâu; hỏng thì quay về tấm ảnh gần nhất rồi làm tiếp. Nhớ nhé: muốn làm lại được thì phải có chỗ để quay về.

    Microbatching: Spark Streaming breaks the stream into small blocks — one second, say — and processes each like a miniature batch job11921. The trade-off is plain: smaller batches mean more scheduling and coordination overhead; larger ones mean higher latency. Kleppmann notes an implicit consequence worth remembering: microbatching imposes a tumbling window equal to the batch size, so any other windowing must be built on top. Checkpointing: Apache Flink takes another route — periodically generating rolling snapshots of state written to durable storage, using barriers inserted into the message stream in the spirit of the Chandy–Lamport distributed snapshot algorithm1826. When an operator crashes it restarts from the most recent checkpoint and discards any output produced between that checkpoint and the crash.

    Điểm chungCả hai đều dựng lại đúng cái tiện nghi mà xử lý theo lô có sẵn: một ranh giới để vứt phần việc dở đi và làm lại. Khác nhau ở chỗ vi-lô cắt theo thời gian đến còn điểm kiểm tra cắt theo tiến độ của chính bộ xử lý.
    Điểm chungCả hai đều tạo ra một chỗ để quay về khi hỏng.
    What they shareBoth rebuild exactly the comfort batch had for free: a boundary at which partial work can be thrown away and redone. They differ in where the cut falls — microbatching cuts by arrival time, checkpointing by the processor's own progress.
  2. Where checkpointing stops working

    Ra khỏi khung là hết hủy đượcOnce it leaves the framework, you can't take it back

    Đây là chỗ nhiều người đọc lướt qua, và cũng là chỗ quyết định. Vi-lô và điểm kiểm tra chỉ cho hiệu ứng đúng một lần bên trong khung xử lý luồng1. Ngay khi kết quả rời khỏi bộ xử lý — ghi vào một cơ sở dữ liệu bên ngoài, gửi một email, gọi một dịch vụ thanh toán — khung không còn cách nào rút lại. Khởi động lại một tác vụ hỏng sẽ khiến tác dụng phụ ấy xảy ra hai lần: người dùng nhận hai email, tài khoản bị trừ tiền hai lượt. Riêng điểm kiểm tra không đủ để ngăn chuyện đó.

    Chụp ảnh để quay lại chỉ có tác dụng với những việc còn nằm trong máy. Nhưng nếu máy đã gửi email đi rồi thì không rút email về được nữa. Làm lại lần nữa thì người ta nhận hai email. Nhớ nhé: cái gì đã ra khỏi nhà thì không gọi về được, nên phải cẩn thận ngay lúc cho nó đi.

    This is the part readers skim, and it is the decisive one. Microbatching and checkpointing give effectively-once semantics only inside the stream framework1. As soon as output leaves the processor — written to an external database, an email sent, a payment service called — the framework can no longer discard it. Restarting a failed task makes that side effect happen twice: the user gets two emails, the account is charged twice. Checkpointing alone is not sufficient to prevent this.

    Vì sao chi tiết này quan trọngVì gần như mọi hệ có ích đều phải có tác dụng phụ ra bên ngoài. Nói cách khác, cái bảo đảm "đúng một lần" mà một khung quảng cáo luôn kèm theo một biên giới — và việc của người thiết kế là biết biên giới đó nằm ở đâu.
    Vì sao quan trọngVì hệ nào cũng phải làm gì đó ra bên ngoài, nên phải biết bảo đảm kia hết hiệu lực ở chỗ nào.
    Why this detail mattersBecause nearly every useful system must have external side effects. In other words, any framework's "exactly-once" guarantee comes with a boundary — and a designer's job is to know where that boundary is.
  3. Atomic commit and idempotence

    Hai mảnh ghép làm nên "hiệu ứng một lần"The two pieces that make "once" happen

    Mảnh thứ nhất là commit nguyên tử: mọi đầu ra và tác dụng phụ của việc xử lý một sự kiện phải có hiệu lực khi và chỉ khi việc xử lý thành công1 — gồm cả tin gửi xuống toán tử kế tiếp, phép ghi vào cơ sở dữ liệu, thay đổi trạng thái của toán tử, và cả việc ghi nhận đã tiêu thụ tin đầu vào. Hoặc tất cả cùng xảy ra, hoặc không cái nào. Google Cloud Dataflow, VoltDB và giao dịch của Kafka đều theo hướng này; khác với XA ở Chương 9, các hiện thực đó giữ giao dịch nội bộ trong một khung duy nhất nên hiệu quả hơn nhiều. Mảnh thứ hai là tính lũy đẳng: một thao tác lũy đẳng có thể thực hiện nhiều lần mà hiệu ứng vẫn như làm một lần. Thao tác không tự nhiên lũy đẳng thì có thể làm cho lũy đẳng bằng một chút siêu dữ liệu: mỗi tin trong Kafka có một offset bền và tăng đơn điệu, nên khi ghi một giá trị ra cơ sở dữ liệu ngoài, ta kèm theo offset của tin cuối đã kích hoạt phép ghi — nhờ đó biết được cập nhật này đã áp hay chưa và bỏ qua nếu rồi7. Kafka Streams xử lý trạng thái đúng theo lối này9.

    Hai mẹo cộng lại mới ra "làm lại vẫn đúng". Mẹo một: xong thì xong hết, hỏng thì hỏng hết — chứ không được nửa vời. Mẹo hai: làm sao cho làm lại cũng vô hại, bằng cách ghi kèm một con số thứ tự: "việc này tôi làm tới số 57 rồi". Lần sau thấy số 57 thì biết đã làm rồi, bỏ qua. Nhớ nhé: đánh số việc là cách đơn giản nhất để khỏi làm trùng.

    The first piece is atomic commit: all outputs and side effects of processing an event must take effect if and only if the processing succeeds1 — including messages sent to downstream operators, database writes, changes to operator state, and the acknowledgement of the input message. Either all of it happens or none of it. Google Cloud Dataflow, VoltDB and Kafka's transactions all work this way; unlike XA in Chapter 9, these implementations keep transactions internal to a single framework, which is far more efficient. The second piece is idempotence: an idempotent operation can be performed many times with the same effect as performing it once. An operation that isn't naturally idempotent can be made so with a little metadata: every Kafka message has a durable, monotonically increasing offset, so when writing a value to an external database you include the offset of the last message that triggered the write — letting you tell whether the update has already been applied and skip it if so7. Kafka Streams handles state in exactly this style9.

    Ba giả định đi kèmDựa vào lũy đẳng không miễn phí. Kleppmann nêu rõ nó đòi: khởi động lại phải phát lại đúng những tin đó theo đúng thứ tự đó; việc xử lý phải tất định; và không nút nào khác được cập nhật cùng giá trị song song — tức là phải có cơ chế rào chắn (fencing) như ở Chương 8. Phá vỡ bất kỳ giả định nào trong ba, và "một lần" tan biến trong im lặng.
    Ba điều kiệnLàm lại phải đúng thứ tự cũ, phải ra kết quả giống nhau, và không được có người thứ hai cùng sửa một chỗ.
    Three assumptions it rides onIdempotence isn't free. Kleppmann spells out its requirements: a restart must replay the same messages in the same order; processing must be deterministic; and no other node may concurrently update the same value — meaning you need fencing, as in Chapter 8. Break any one of the three and "once" quietly evaporates.

    Trident nói thẳng cái giá

    Tài liệu của Apache Storm là minh họa rất sạch cho sắc thái này. Storm gọi ngữ nghĩa của Trident là exactly-once, nhưng nói rõ cơ chế: mỗi lô được gán một mã giao dịch duy nhất và các cập nhật trạng thái được sắp thứ tự giữa các lô — tức là loại bỏ trùng lặp. Và tài liệu nói thẳng phần còn lại: nếu ta không chịu chi phí lưu mã giao dịch trong cơ sở dữ liệu thì ngữ nghĩa tụt về ít nhất một lần25. "Đúng một lần" ở đây là một tính chất mua được, không phải một thuộc tính sẵn có.

    Vì sao rẻ hơn XA

    Giao dịch nội bộ trong một khung không phải điều phối nhiều nhà cung cấp tài nguyên khác nhau qua giao thức hai pha. Nó tránh được phần lớn chi phí và gần như toàn bộ các chế độ hỏng khó chịu của XA — điều phối viên chết khi đang nghi vấn, giao dịch treo giữ khóa, phục hồi thủ công. Đổi lại, phạm vi bảo đảm co lại đúng bằng biên giới của khung đó.

  4. Rebuilding state after a failure

    Dựng lại trạng thái sau sự cốRebuilding state after a failure

    Mọi phép xử lý luồng cần trạng thái — join theo cửa sổ, bảng tra cứu, bộ đếm — đều phải bảo đảm trạng thái ấy dựng lại được sau sự cố1. Một lựa chọn là giữ trạng thái ở một kho từ xa và nhân bản ở đó, nhưng phải đi mạng cho từng tin nên chậm. Lựa chọn còn lại là giữ trạng thái cục bộ ngay tại bộ xử lý rồi nhân bản định kỳ, và mỗi khung làm một kiểu: Flink chụp ảnh trạng thái toán tử rồi ghi vào lưu trữ bền18; Samza và Kafka Streams nhân bản bằng cách gửi thay đổi trạng thái vào một topic Kafka có nén nhật ký2496; VoltDB nhân bản bằng cách xử lý dư thừa cùng một tin trên nhiều nút. Và đôi khi còn chẳng cần nhân bản: nếu trạng thái đủ nhỏ, phát lại chính đầu vào là dựng lại được — ví dụ trạng thái của một cửa sổ ngắn. Chọn cách nào phụ thuộc tương quan hiệu năng giữa mạng và đĩa, nên câu trả lời đổi theo thời gian và theo hạ tầng.

    Bộ xử lý luồng phải nhớ nhiều thứ: đang đếm tới đâu, đang chờ ghép với cái gì. Máy hỏng là mất trí nhớ. Nên phải có cách nhớ lại: hoặc thỉnh thoảng chép trí nhớ ra một chỗ an toàn, hoặc ghi mọi thay đổi vào một cuốn sổ riêng, hoặc — nếu trí nhớ ngắn — cứ đọc lại dữ liệu cũ là nhớ ra ngay. Nhớ nhé: cái gì nhớ trong đầu máy thì phải có bản sao ở ngoài.

    Any stream process that needs state — windowed joins, lookup tables, counters — must ensure that state can be recovered after a failure1. One option is to keep state in a remote datastore and replicate it there, but a network round trip per message is slow. The other is to keep state local to the processor and replicate it periodically, and each framework does it differently: Flink snapshots operator state to durable storage18; Samza and Kafka Streams replicate state changes by sending them to a log-compacted Kafka topic2496; VoltDB replicates by redundantly processing each message on several nodes. And sometimes replication isn't needed at all: if the state is small enough, replaying the input rebuilds it — the state of a short window, say. Which to choose depends on the relative performance of network and disk, so the answer shifts with time and infrastructure.

    Vì sao changelog thắng thếLối "gửi thay đổi trạng thái vào một topic có nén nhật ký" đóng vòng tròn của cả chương: trạng thái cục bộ trở thành một view vật chất hóa của một nhật ký sự kiện, và nhật ký ấy lại chính là thứ dựng lại nó sau sự cố. Cùng một cơ chế đã giải quyết việc giữ nhiều hệ đồng bộ, nay giải quyết luôn việc chịu lỗi.
    Điều hayCũng lại là cuốn sổ ấy: ghi mọi thay đổi vào sổ thì lúc nào cũng dựng lại được trí nhớ.
    Why the changelog approach winsSending state changes to a log-compacted topic closes the chapter's circle: local state becomes a materialized view of an event log, and that same log is what rebuilds it after a crash. The mechanism that solved keeping systems in sync now solves fault tolerance too.
Since 2017 · what the vendors changed

Chương 11 chụp ảnh hệ sinh thái ở năm 2017, và nó đã đổi khá nhiều. Khung khái niệm của Kleppmann vẫn đứng nguyên — nhưng hành vi sản phẩm thì phải tra tài liệu hiện thời, không tra trí nhớ.

Bốn thay đổi đáng để một người đọc hôm nay biết, và cả bốn đều nằm ở tầng hiện thực chứ không đụng tới lập luận của chương. Riêng điểm đầu tiên là chỗ rất dễ trích dẫn sai: Kafka có giao dịch và ngữ nghĩa đúng-một-lần, nhưng đó là một bảo đảm có phạm vi, không phải một lời hứa phổ quát.

Kafka: giao dịch Tài liệu hiện thời trình bày ba mức ngữ nghĩa giao hàng, producer lũy đẳng và giao dịch bắc qua nhiều phân mảnh7 — thứ chưa có trong bản tài liệu Kafka 0.9 mà danh mục tham chiếu của chương trỏ tới3.
Kafka: KRaft Siêu dữ liệu cụm nay do chính các controller của Kafka tự quản bằng một quorum kiểu Raft. Tài liệu vận hành ghi rõ bản cầu nối cuối cùng để di trú từ ZooKeeper sang KRaft là Kafka 3.98.
Kafka Streams Thư viện xử lý luồng chính thức của dự án, với bảng trạng thái và topic changelog — đúng lối mà chương mô tả, nay là một phần của bản phân phối9.
Spark: DStream đã lùi Tài liệu Spark nay gọi Spark Streaming (DStream) là thế hệ trước và không còn cập nhật, khuyến nghị dùng Structured Streaming2120. Ví dụ vi-lô trong sách vẫn đúng về mặt mô hình, chỉ là không còn là công cụ được khuyên dùng.

Ba ngộ nhận nguy hiểm nhất của chương nàyThe three most dangerous misreadings of this chapter

“Exactly-once nghĩa là mỗi sự kiện chạy đúng một lần”"Exactly-once means each event runs exactly once"

Sai, và sai theo hướng nguy hiểm. Sự kiện vẫn có thể được thử lại nhiều lần; cái được bảo toàn là hiệu ứng. Kleppmann ghi nhận effectively-once là tên mô tả đúng hơn3, và tính chất ấy chỉ có khi ba thứ cùng có mặt: bỏ phần việc dở, commit nguyên tử, lũy đẳng cộng loại trùng. Tài liệu Trident minh họa thẳng thừng: bỏ mã giao dịch đi thì ngữ nghĩa tụt về ít nhất một lần25.

Wrong, and dangerously so. Events may still be retried many times; what is preserved is the effect. Kleppmann notes effectively-once is the more descriptive term3, and the property holds only when three things are present together: discarding partial work, atomic commit, idempotence plus deduplication. Trident's documentation says it bluntly: drop the transaction id and the semantics fall back to at-least-once25.

“CDC và event sourcing là một”"CDC and event sourcing are the same thing"

Sai: chúng khác nhau ở tầng trừu tượng và ở nguồn chân lý13. CDC trích changelog ở tầng kho, ứng dụng vẫn ghi đè tự do, sự kiện chứa bản ghi mới đầy đủ nên nén nhật ký được — nguồn chân lý là cái bảng. Event sourcing dựng logic tường minh trên sự kiện bất biến mô tả ý định, không đè nhau, cần toàn bộ lịch sử — nguồn chân lý là chính nhật ký.

Wrong: they differ in level of abstraction and in source of truth13. CDC extracts a changelog at the storage layer, the application still overwrites freely, and events carry the full new record so compaction works — the table is the truth. Event sourcing builds logic explicitly on immutable events describing intent, which never overwrite each other and need the full history — the log is the truth.

“Đồng hồ hệ thống là đủ”"The system clock is good enough"

Sai: thời gian xử lý không phải thời gian sự kiện. Chạy lại nhật ký sau sự cố tạo ra một đỉnh lưu lượng hoàn toàn giả; một thiết bị ngoại tuyến có thể gửi sự kiện của hôm qua vào hôm nay; và đồng hồ thiết bị thì có khi sai hẳn. Ba dấu thời gian và cửa sổ theo thời gian sự kiện tồn tại chính vì lý do đó17.

Wrong: processing time is not event time. Replaying a log after an outage creates an entirely fictitious traffic spike; an offline device may deliver yesterday's events today; and device clocks are sometimes simply wrong. Three timestamps and event-time windows exist for exactly that reason17.

Tự soi hệ của mìnhAudit your own system

Tám câu hỏi soi đường ống luồng của bạnEight questions for your own streaming pipeline

Chương 11 không đưa ra một kiến trúc để sao chép — nó đưa ra một bộ từ vựng để hỏi cho đúng. Những câu dưới đây áp thẳng bộ từ vựng ấy vào một hệ bạn đang vận hành. Chúng không đo hệ của bạn "hiện đại" tới đâu, mà soi xem bạn đã nói được thành lời những đánh đổi mà mình đã ngầm chọn hay chưa.

Thử soi một hệ thật bằng mấy câu dưới. Không phải để khoe hệ xịn, mà để xem mình có thật sự hiểu nó đang đánh đổi những gì.

Chapter 11 offers no architecture to copy — it offers a vocabulary for asking the right questions. What follows applies that vocabulary to a system you actually run. These don't measure how "modern" your system is; they check whether you can state the trade-offs you have already silently made.

Đường ống của bạn là hàng đợi hay nhật ký — và bạn có phát lại được không?Đây là câu hỏi quyết định mọi thứ phía sau. Nếu tin bị xóa sau khi ack thì bạn không chạy lại được sau khi vá lỗi, và mọi lời bàn về "dựng lại view từ nhật ký" đều vô nghĩa với hệ của bạn.
Thời gian giữ tin của bạn là bao lâu, và consumer chậm nhất đang tụt bao xa?Hiệu số giữa hai con số này chính là khoảng thời gian bạn còn lại trước khi "chậm" biến thành "mất dữ liệu". Nếu chưa từng đặt hai con số ấy cạnh nhau, bạn đang có một hạn chót mà mình không biết.
Bạn có đang ghi song song vào nhiều kho không?Dual write sinh ra tranh đua và nửa-thành-công, và hậu quả là hai kho lệch nhau vĩnh viễn mà không hề có lỗi nào được ghi lại. Nếu có, hãy chỉ ra kho nào là nguồn chân lý, và những kho còn lại dẫn xuất từ nó bằng cơ chế gì.
Thứ bạn đang làm là CDC hay event sourcing — và bạn gọi tên nó đúng chưa?Nếu changelog được trích ở tầng kho và ứng dụng vẫn ghi đè tự do, đó là CDC: bạn có kết quả, không có ý định. Nếu vậy, đừng kỳ vọng về sau dẫn ra được những view cần biết vì sao dữ liệu đổi — thông tin đó đã mất từ lúc ghi.
Các phép tính theo thời gian của bạn dùng thời gian sự kiện hay thời gian xử lý?Thử một phép kiểm nhanh: nếu đường ống ngừng một tiếng rồi chạy lại, biểu đồ của bạn có hiện ra một đỉnh giả không? Nếu có, bạn đang dùng thời gian xử lý cho một câu hỏi nghiệp vụ.
Bạn xử lý sự kiện đến muộn bằng cách nào, và có đo tỉ lệ bị bỏ không?Chỉ có hai lối: bỏ qua, hoặc phát bản sửa. Không chọn tức là đã chọn lối thứ nhất một cách vô thức. Và nếu không đo tỉ lệ bị bỏ, bạn không biết mình đang đánh mất bao nhiêu sự thật.
Chạy lại cùng một job trên cùng dữ liệu có ra cùng kết quả không?Nếu có join giữa hai luồng mà thứ tự đến không xác định, câu trả lời thường là không. Hãy hỏi tiếp: bạn muốn join theo giá trị hiện thời hay giá trị tại thời điểm sự kiện — và bạn có đang giữ đủ phiên bản để làm được điều mình muốn không?
Bảo đảm "đúng một lần" của bạn hết hiệu lực ở đâu?Hãy chỉ tay vào đúng chỗ dữ liệu rời khỏi khung xử lý — phép ghi ra CSDL ngoài, email, lời gọi thanh toán. Ở ranh giới đó, điểm kiểm tra không còn giúp gì; chỉ còn commit nguyên tử hoặc lũy đẳng. Nếu không có cái nào, tác dụng phụ của bạn sẽ xảy ra hai lần vào một ngày nào đó.
Tin của bạn gửi xong là mất, hay vẫn còn đó để đọc lại?Đọc lại được thì lỡ tính sai còn tính lại được.
Cùng một thông tin bạn có đang chép tay vào nhiều chỗ không?Nên chọn một chỗ làm gốc, những chỗ kia chép theo.
Bạn tính "một giờ vừa rồi" theo lúc chuyện xảy ra hay lúc máy biết?Hai lúc đó khác nhau, và chọn sai là ra số sai.
Nếu máy làm lại một việc hai lần thì có sao không?Hệ tốt là làm lại mấy lần kết quả vẫn như một lần.
Is your pipeline a queue or a log — and can you replay?This decides everything downstream. If messages vanish on ack, you cannot reprocess after a fix, and "rebuild the view from the log" is meaningless for your system.
What is your retention, and how far behind is your slowest consumer?The gap between those two numbers is how long you have before "slow" becomes "lost". If you've never put them side by side, you have a deadline you don't know about.
Are you writing to several stores in parallel?Dual writes create races and half-successes, leaving stores permanently divergent with no error ever logged. If so, name the source of truth and the mechanism by which the rest derive from it.
Are you doing CDC or event sourcing — and do you name it correctly?If the changelog is extracted at the storage layer while the application overwrites freely, it's CDC: you have outcomes, not intent. Don't then expect to derive views that need to know why data changed — that was lost at write time.
Do your time-based computations use event time or processing time?A quick test: if the pipeline stalls for an hour and then catches up, does your chart show a fictitious spike? If so, you're using processing time for a business question.
How do you handle stragglers, and do you measure the drop rate?There are only two options: ignore, or publish a correction. Not choosing means you chose the first unconsciously. And without a drop-rate metric you don't know how much truth you're losing.
Does rerunning the same job on the same data give the same result?If you join two streams whose arrival order is undetermined, usually not. Then ask: do you want the current value or the value as of the event — and are you keeping enough versions to get what you want?
Where does your "exactly-once" guarantee stop applying?Point at the exact place data leaves the framework — the external database write, the email, the payment call. At that boundary checkpointing helps no more; only atomic commit or idempotence does. With neither, your side effect will happen twice one day.
Kết luận cô đọngThe conclusion, distilled
Xử lý luồng là xử lý theo lô sau khi rút mất điểm kết thúc — và mọi thứ khó khăn của chương này đều mọc ra từ đúng chỗ đó. Không còn "chạy xong" thì không còn sắp xếp toàn cục, không còn vứt kết quả dở đi làm lại, không còn cái lúc mà dữ liệu đã đủ. Đổi lại, ta nhận được một thứ mà xử lý theo lô không có: một nhật ký bất biến mà mọi bảng, mọi chỉ mục, mọi cache đều chỉ là dẫn xuất — nên sửa lỗi là chạy lại, và thêm một cách nhìn mới là thêm một consumer. Cái giá phải trả được gọi tên rất sòng phẳng: phải chọn đồng hồ, phải chọn cửa sổ, phải chấp nhận rằng "đủ dữ liệu" là một điều không bao giờ chứng minh được, và phải hiểu rằng thứ ta gọi là đúng một lần thật ra là hiệu ứng đúng một lần — mua bằng tính lũy đẳng và commit nguyên tử, chứ không phải bằng một lời hứa.Xử lý luồng chỉ là làm theo mẻ, nhưng mẻ không bao giờ hết. Vì thế không có lúc nào gọi là "đủ dữ liệu", và cũng không có lúc nào để bỏ đi làm lại từ đầu. Bù lại, ta có một cuốn sổ không tẩy xóa: mọi bảng biểu đều dựng được từ cuốn sổ đó, sai thì tính lại, muốn xem kiểu khác thì dựng thêm. Nhớ nhé: đừng tin ai nói "máy chỉ làm đúng một lần" — cái làm được là làm lại mấy lần cũng ra như một lần.Stream processing is batch processing with the ending removed — and every difficulty in this chapter grows from exactly that. No "done" means no global sort, no discarding partial output and rerunning, no moment at which the data is complete. In exchange you get something batch never had: an immutable log of which every table, index and cache is merely a derivation — so fixing a bug means reprocessing, and adding a new view means adding a consumer. The price is named honestly: choose a clock, choose a window, accept that "complete" can never be proven, and understand that what we call exactly-once is really an effectively-once result — bought with idempotence and atomic commit, not with a promise.