<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://vile-blog.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://vile-blog.github.io/" rel="alternate" type="text/html" /><updated>2026-09-15T22:11:36+07:00</updated><id>https://vile-blog.github.io/feed.xml</id><title type="html">Vi Le</title><subtitle>Personal site and blog of Vi Le — software engineer, ex-Grab, founder of Hustly Space. Projects, writing, and notes on building software.</subtitle><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><entry><title type="html">DDIA Chapter 12: The Future of Data Systems</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-12-future-of-data-systems/" rel="alternate" type="text/html" title="DDIA Chapter 12: The Future of Data Systems" /><published>2026-09-15T15:00:00+07:00</published><updated>2026-09-15T15:00:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-12-future-of-data-systems</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-12-future-of-data-systems/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 12: The Future of Data Systems.</em></p>

  <p><a href="/blog/2026/09/15/ddia-chapter-11-stream-processing/">Chapter 11</a> closed with batch and stream processing being two answers to the same question. This last chapter takes a step back from any single technique and asks something bigger: now that we’ve seen replication, partitioning, transactions, consensus, batch and stream processing — how do all these pieces actually fit together in a real system, and what do we, as the people building them, owe the people who end up depending on them?</p>

  <h2 id="unbundling-the-database-small-tools-instead-of-one-big-one">Unbundling the database: small tools instead of one big one</h2>

  <p>The first big idea ties directly back to something I wrote about twice already. <a href="/blog/2026/09/15/ddia-chapter-10-batch-processing/">Chapter 10</a> opened with the Unix philosophy — small tools, each doing one thing, chained together with pipes. <a href="/blog/2026/09/15/ddia-chapter-11-stream-processing/">Chapter 11</a> showed change data capture turning every database write into an event that independent systems could subscribe to. This chapter puts the two together into a genuinely appealing idea: instead of one database trying to be good at <em>everything</em> — transactions, full-text search, caching, analytics, all bundled into a single product — you can <strong>unbundle</strong> it into several small, specialized tools, each excellent at one job, kept in sync by a shared stream of events.</p>

  <p><img src="/assets/images/ddia/ch12-unbundling.svg" alt="A traditional database bundles the query engine, index, cache, and replication into one product; an unbundled system instead feeds a shared event log to several small, specialized tools — the same Unix-pipe idea from Chapter 10, applied one level up" /></p>

  <p>A single “batteries included” database like Postgres has to make one set of tradeoffs and hope they’re good enough for every use case its users throw at it. An unbundled setup instead lets you pick the best specialized tool for each job — Elasticsearch for full-text search, Redis for a fast cache, a dedicated analytics database for reporting — and feed all of them from the same underlying stream of events, so they never drift out of sync with each other or with the source of truth. The tradeoff is real too: you’re now operating several systems instead of one, and you’ve traded a single vendor’s tested defaults for your own responsibility to wire them together correctly.</p>

  <h2 id="correctness-doesnt-have-to-mean-waiting-for-everyone-to-agree">Correctness doesn’t have to mean waiting for everyone to agree</h2>

  <p>The second idea revisits something <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">Chapter 9</a> already showed was expensive: getting every node to agree before proceeding. This chapter makes a subtler point — you don’t always need that level of agreement to be correct, if you design the <em>ends</em> of the system carefully instead of relying entirely on the <em>middle</em>. This is the <strong>end-to-end argument</strong>: a guarantee is only as good as the point where it’s actually checked, so instead of trying to make every layer of infrastructure in between perfectly reliable, you put the real correctness check at the boundary that actually matters — often right where a request enters or leaves your system.</p>

  <p>A concrete version of this: instead of relying purely on the network or the database to prevent a duplicate request (say, a user double-clicking “buy now”), the client can generate a unique request ID up front and the server can simply refuse to process the same ID twice — a small, cheap check at the true edge of the system, rather than an expensive guarantee threaded through every layer underneath it. It’s a genuinely freeing idea after a whole book about how unreliable networks and clocks are: you don’t have to fix every layer, you just have to make sure the one check that actually matters is in the right place.</p>

  <h2 id="the-part-of-the-chapter-that-isnt-really-about-engineering-at-all">The part of the chapter that isn’t really about engineering at all</h2>

  <p>The last section is the one I found most unexpected, because it isn’t about mechanics at all — it’s about responsibility. The book is blunt that the same techniques covered in this whole book — collecting data, deriving insights from it, predicting behavior from it — can be used to build a genuinely useful recommendation system, or to build a surveillance tool, or a system that quietly discriminates against people based on patterns in data that reflect historical unfairness rather than anything about a specific individual. A credit-scoring or hiring algorithm trained on biased historical data doesn’t magically become neutral just because it’s “just following the data” — it can bake in and even amplify exactly the same bias, at a scale and speed a human process never could.</p>

  <p>The book’s phrase for this that stuck with me: data, once collected, doesn’t just sit there neutrally — it becomes a liability, something that can be subpoenaed, breached, repurposed for things the people it describes never agreed to. The practical takeaway isn’t “don’t build data systems,” it’s “the engineer who decides what gets logged, retained, and fed into a model is making a real decision with real consequences for real people, whether or not they think of it that way.”</p>

  <h2 id="closing-the-loop-on-the-whole-book">Closing the loop on the whole book</h2>

  <p>Looking back across everything from <a href="/blog/2026/09/15/ddia-chapter-1-reliable-scalable-maintainable/">reliability and scalability</a> through this final chapter, the shape of the whole book finally makes sense as one connected argument rather than twelve separate topics: <a href="/blog/2026/09/15/ddia-chapter-2-data-models/">data models</a> and <a href="/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/">storage engines</a> decide how a single machine holds data honestly; <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> and <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning</a> spread that honesty across many machines; <a href="/blog/2026/09/15/ddia-chapter-7-transactions/">transactions</a>, <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">distributed-systems trouble</a>, and <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consensus</a> keep that honesty intact when things fail; and <a href="/blog/2026/09/15/ddia-chapter-10-batch-processing/">batch</a> and <a href="/blog/2026/09/15/ddia-chapter-11-stream-processing/">stream processing</a> turn that honest data into something useful. This chapter’s real closing point is that getting all of that technically right is necessary, but it was never sufficient on its own — the last question always has to be what you’re actually building it for, and who has to live with the answer.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 12: The Future of Data Systems.</em></p>

  <p><a href="/blog/2026/09/15/ddia-chapter-11-stream-processing/">Chương 11</a> kết thúc với việc batch và stream processing là hai câu trả lời cho cùng một câu hỏi. Chương cuối này lùi lại khỏi bất kỳ kỹ thuật đơn lẻ nào và hỏi một điều lớn hơn: giờ đã thấy replication, partitioning, transaction, consensus, batch và stream processing — tất cả những mảnh này thực sự khớp với nhau ra sao trong một hệ thống thật, và chúng ta, những người xây dựng chúng, nợ những người cuối cùng phụ thuộc vào chúng điều gì?</p>

  <h2 id="unbundling-database-nhiu-cng-c-nh-thay-v-mt-cng-c-ln">Unbundling database: nhiều công cụ nhỏ thay vì một công cụ lớn</h2>

  <p>Ý tưởng lớn đầu tiên nối thẳng về thứ tôi đã viết hai lần rồi. <a href="/blog/2026/09/15/ddia-chapter-10-batch-processing/">Chương 10</a> mở đầu bằng triết lý Unix — công cụ nhỏ, mỗi cái làm một việc, nối lại bằng pipe. <a href="/blog/2026/09/15/ddia-chapter-11-stream-processing/">Chương 11</a> cho thấy change data capture biến mỗi lượt ghi database thành một sự kiện mà các hệ thống độc lập có thể subscribe vào. Chương này gộp hai thứ lại thành một ý tưởng thực sự hấp dẫn: thay vì một database cố gắng giỏi mọi thứ — transaction, tìm kiếm full-text, cache, phân tích, tất cả gộp vào một sản phẩm duy nhất — bạn có thể <strong>unbundle</strong> (tháo rời) nó thành vài công cụ nhỏ, chuyên biệt, mỗi cái xuất sắc ở một việc, được giữ đồng bộ bởi một luồng sự kiện chung.</p>

  <p><img src="/assets/images/ddia/ch12-unbundling.svg" alt="Một database truyền thống gộp query engine, index, cache, và replication vào một sản phẩm; một hệ thống unbundled thay vào đó cho vài công cụ nhỏ, chuyên biệt cùng đọc một event log chung — đúng ý tưởng Unix-pipe từ Chương 10, áp dụng lên một tầng cao hơn" /></p>

  <p>Một database “trọn gói” như Postgres phải đưa ra một bộ đánh đổi duy nhất và hy vọng nó đủ tốt cho mọi use case người dùng ném vào nó. Một hệ thống unbundled thay vào đó cho phép bạn chọn công cụ chuyên biệt tốt nhất cho từng việc — Elasticsearch cho tìm kiếm full-text, Redis cho cache nhanh, một database phân tích riêng cho báo cáo — và cho tất cả chúng ăn từ cùng một luồng sự kiện nền, để chúng không bao giờ lệch pha với nhau hay với nguồn sự thật gốc. Đánh đổi cũng có thật: giờ bạn đang vận hành nhiều hệ thống thay vì một, và bạn đã đổi các mặc định đã được một nhà cung cấp kiểm chứng lấy trách nhiệm của chính mình trong việc nối chúng lại đúng cách.</p>

  <h2 id="ng-n-khng-nht-thit-phi-c-ngha-l-ch-mi-ngi-ng-">Đúng đắn không nhất thiết phải có nghĩa là chờ mọi người đồng ý</h2>

  <p>Ý tưởng thứ hai quay lại một điều <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">Chương 9</a> đã cho thấy là tốn kém: khiến mọi node đồng ý trước khi tiếp tục. Chương này đưa ra một điểm tinh vi hơn — bạn không phải lúc nào cũng cần mức độ đồng thuận đó để đúng đắn, nếu bạn thiết kế cẩn thận <em>hai đầu</em> của hệ thống thay vì hoàn toàn dựa vào <em>phần giữa</em>. Đây là <strong>end-to-end argument</strong> (luận điểm đầu-cuối): một đảm bảo chỉ tốt bằng đúng điểm nó thực sự được kiểm tra, nên thay vì cố làm cho mọi tầng hạ tầng ở giữa hoàn toàn đáng tin cậy, bạn đặt lần kiểm tra đúng đắn thật sự ở ranh giới thực sự quan trọng — thường chính là nơi một request đi vào hoặc rời khỏi hệ thống của bạn.</p>

  <p>Một phiên bản cụ thể của điều này: thay vì hoàn toàn dựa vào mạng hay database để ngăn một request bị trùng (ví dụ, người dùng bấm “mua ngay” hai lần), client có thể tạo sẵn một request ID duy nhất từ đầu, và server chỉ đơn giản từ chối xử lý cùng một ID hai lần — một lần kiểm tra nhỏ, rẻ, ở đúng ranh giới thật sự của hệ thống, thay vì một đảm bảo tốn kém được luồn qua mọi tầng bên dưới nó. Đây là một ý tưởng thực sự giải phóng sau cả một cuốn sách nói về việc mạng và đồng hồ không đáng tin cậy tới mức nào: bạn không cần sửa mọi tầng, bạn chỉ cần đảm bảo đúng một lần kiểm tra thực sự quan trọng nằm ở đúng chỗ.</p>

  <h2 id="phn-ca-chng-khng-h-ni-v-k-thut-cht-no">Phần của chương không hề nói về kỹ thuật chút nào</h2>

  <p>Phần cuối là phần tôi thấy bất ngờ nhất, vì nó hoàn toàn không nói về cơ chế — nó nói về trách nhiệm. Cuốn sách nói thẳng rằng đúng những kỹ thuật được nói tới trong cả cuốn sách này — thu thập dữ liệu, rút ra insight từ nó, dự đoán hành vi từ nó — có thể được dùng để xây một hệ thống gợi ý thực sự hữu ích, hoặc để xây một công cụ giám sát, hoặc một hệ thống âm thầm phân biệt đối xử với con người dựa trên các pattern trong dữ liệu phản ánh sự bất công lịch sử hơn là bất cứ điều gì về một cá nhân cụ thể. Một thuật toán chấm điểm tín dụng hay tuyển dụng được huấn luyện trên dữ liệu lịch sử thiên lệch không tự nhiên trở nên trung lập chỉ vì nó “chỉ đang theo dữ liệu” — nó có thể đóng khung và thậm chí khuếch đại đúng sự thiên lệch đó, ở quy mô và tốc độ mà một quy trình con người chưa bao giờ có thể đạt tới.</p>

  <p>Câu cuốn sách dùng cho điều này khiến tôi nhớ mãi: dữ liệu, một khi đã được thu thập, không chỉ nằm đó trung lập — nó trở thành một gánh nặng trách nhiệm, thứ có thể bị triệu tập ra tòa, bị rò rỉ, bị tái sử dụng cho những việc mà những người nó mô tả chưa bao giờ đồng ý. Bài học thực tế không phải là “đừng xây hệ thống dữ liệu,” mà là “kỹ sư quyết định cái gì được log lại, được giữ lại, và được đưa vào một model đang đưa ra một quyết định thật, với hậu quả thật cho những con người thật, dù họ có nghĩ theo cách đó hay không.”</p>

  <h2 id="khp-li-ton-b-cun-sch">Khép lại toàn bộ cuốn sách</h2>

  <p>Nhìn lại tất cả từ <a href="/blog/2026/09/15/ddia-chapter-1-reliable-scalable-maintainable/">reliability và scalability</a> cho tới chương cuối này, hình dạng của cả cuốn sách cuối cùng cũng hợp lý như một luận điểm liền mạch thay vì mười hai chủ đề rời rạc: <a href="/blog/2026/09/15/ddia-chapter-2-data-models/">data model</a> và <a href="/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/">storage engine</a> quyết định cách một máy đơn lẻ giữ dữ liệu một cách trung thực; <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> và <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning</a> trải sự trung thực đó ra nhiều máy; <a href="/blog/2026/09/15/ddia-chapter-7-transactions/">transaction</a>, <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">rắc rối hệ phân tán</a>, và <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consensus</a> giữ sự trung thực đó nguyên vẹn khi có sự cố; và <a href="/blog/2026/09/15/ddia-chapter-10-batch-processing/">batch</a> cùng <a href="/blog/2026/09/15/ddia-chapter-11-stream-processing/">stream processing</a> biến dữ liệu trung thực đó thành thứ hữu ích. Điểm khép lại thực sự của chương này là làm đúng tất cả những điều đó về mặt kỹ thuật là cần thiết, nhưng chưa bao giờ là đủ tự thân — câu hỏi cuối cùng luôn phải là bạn thực sự đang xây nó để làm gì, và ai sẽ phải sống chung với câu trả lời đó.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 12" /><category term="System Design" /><category term="Ethics" /><summary type="html"><![CDATA[The book's last chapter isn't about a new technique — it's about stepping back and asking what all of this replication, partitioning, and stream processing is actually for, and who it's responsible to.]]></summary></entry><entry><title type="html">DDIA Chapter 11: Stream Processing</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-11-stream-processing/" rel="alternate" type="text/html" title="DDIA Chapter 11: Stream Processing" /><published>2026-09-15T14:30:00+07:00</published><updated>2026-09-15T14:30:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-11-stream-processing</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-11-stream-processing/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 11: Stream Processing.</em></p>

  <p><a href="/blog/2026/09/15/ddia-chapter-10-batch-processing/">Chapter 10</a> ended on a cliffhanger of sorts: batch processing is great when you’re happy to wait for a full day’s (or year’s) data to pile up before computing anything from it. This chapter is about the other half of that pair — what happens when you genuinely can’t wait, because the whole point is reacting the instant something happens: flagging a fraudulent card swipe before it clears, updating a live dashboard, refreshing a search index the moment new content is published.</p>

  <h2 id="an-event-is-just-a-fact-that-a-record-used-to-be--but-now-it-never-stops-arriving">An event is just a fact that a record used to be — but now it never stops arriving</h2>

  <p>The mental shift the book asks for is small but important: a <strong>stream</strong> is just an unbounded, never-ending sequence of <strong>events</strong>, where an event is a small, immutable record of something that happened, tagged with when it happened. The unbounded part is the whole difference from batch processing — a batch job reads a file that has a definite end; a stream processor reads something that, by design, is never “done.”</p>

  <p>Where do these events actually come from? The book’s answer connects directly back to two things I’d already run into: <strong>message queues</strong> and <strong>databases</strong>. A message broker like <strong>Kafka</strong> is built specifically to carry a continuous flow of events from producers to consumers, durably and in order — but the more interesting source, and the one that genuinely surprised me, is the database itself, through something called <strong>change data capture</strong>.</p>

  <h2 id="change-data-capture-turning-the-database-changed-into-an-event">Change data capture: turning “the database changed” into an event</h2>

  <p><strong>Change data capture (CDC)</strong> takes every insert, update, and delete happening inside a database and turns each one into an event on a stream, in the exact order they happened. Instead of every other part of the system polling the database or querying it directly to notice changes, they simply subscribe to this stream of “here’s what changed” events.</p>

  <p><img src="/assets/images/ddia/ch11-cdc-stream.svg" alt="Change data capture turns every database write into an event on a durable, ordered log; independent consumers — a search index, a cache, a fraud detector — each read that same log at their own pace instead of talking to the database directly" /></p>

  <p>What clicked for me here is how naturally this solves a problem I’d actually run into without having a name for it: keeping a search index, a cache, and a database all in sync with each other, without wiring every single one of them to talk directly to every other one. With CDC, the database is the single source of truth, its changes flow out as one ordered stream, and anything that needs to stay in sync — a search index at one company, a fraud-detection system at another, a cache at a third — just subscribes independently, reading at its own pace, without ever needing to know the others exist. This is exactly the role Kafka plays at companies like LinkedIn (where it originated) — one durable, replayable log that many independent systems can read from concurrently.</p>

  <h2 id="windows-how-do-you-add-up-something-that-never-ends">Windows: how do you “add up” something that never ends?</h2>

  <p>A batch job can compute “total sales for the day” by reading the whole day’s data and summing it, because the input has a clear boundary. A stream never has a clear boundary, so the book introduces <strong>windows</strong> — an artificial boundary you draw yourself, like “the last 5 minutes” or “this specific hour” — to make aggregate questions (“how many logins in the last 5 minutes?”) answerable at all over an endless stream.</p>

  <p>The genuinely tricky part, which I hadn’t appreciated before, is that events don’t always arrive in the order they actually happened. A phone can lose signal and send a batch of delayed events all at once; a network hiccup can make one event arrive seconds after events that happened after it. So a stream processor has to make a real decision: does it wait a little while for stragglers before finalizing a window’s answer (more correct, but slower to produce a result), or does it finalize quickly and risk being wrong when a late event shows up afterward? There’s no universally correct answer — it’s a real tradeoff between latency and completeness that every stream processing system has to pick a stance on.</p>

  <h2 id="exactly-once-the-promise-thats-harder-than-it-sounds">Exactly-once: the promise that’s harder than it sounds</h2>

  <p>The last idea that stuck with me connects straight back to <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">Chapter 8’s honest inventory of distributed-systems trouble</a>: what happens if a stream processor crashes partway through handling an event, and has to restart? If it simply reprocesses everything it’s unsure about, some events might get counted twice. The guarantee everyone actually wants is <strong>exactly-once semantics</strong> — each event affects the final result exactly once, no matter how many times the underlying machinery has to retry after a crash. Real systems (Kafka’s own transactional features, Flink’s checkpointing) achieve this not by making failures impossible, but by making the <em>effect</em> of retried work idempotent — safe to apply more than once without changing the outcome — which is a much more achievable goal than pretending crashes won’t happen.</p>

  <p>Looking at chapters 10 and 11 together, the real lesson is that batch and stream processing aren’t competitors — they’re the same underlying question (“how do I compute derived data from a big pile of events?”) answered under two different constraints: how much you’re willing to wait, and whether you can define a clean beginning and end to your input at all.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 11: Stream Processing.</em></p>

  <p><a href="/blog/2026/09/15/ddia-chapter-10-batch-processing/">Chương 10</a> kết thúc bằng một kiểu “để lửng”: batch processing tuyệt vời khi bạn sẵn sàng chờ đủ dữ liệu của cả một ngày (hay cả năm) rồi mới tính toán gì đó từ nó. Chương này nói về nửa còn lại của cặp đó — chuyện gì xảy ra khi bạn thực sự không thể chờ, vì cả vấn đề nằm ở việc phản ứng ngay khi có gì đó xảy ra: chặn một giao dịch thẻ gian lận trước khi nó được xử lý xong, cập nhật một dashboard trực tiếp, làm mới search index ngay khi nội dung mới được đăng.</p>

  <h2 id="mt-s-kin-ch-l-mt-s-tht-tng-l-mt-bn-ghi--nhng-gi-n-khng-bao-gi-ngng-n">Một sự kiện chỉ là một sự thật từng là một bản ghi — nhưng giờ nó không bao giờ ngừng đến</h2>

  <p>Sự chuyển đổi tư duy cuốn sách yêu cầu nhỏ nhưng quan trọng: một <strong>stream</strong> chỉ là một chuỗi <strong>sự kiện (event)</strong> không giới hạn, không bao giờ kết thúc, nơi một sự kiện là một bản ghi nhỏ, bất biến, về việc gì đó đã xảy ra, gắn kèm thời điểm nó xảy ra. Phần “không giới hạn” chính là toàn bộ khác biệt so với batch processing — một batch job đọc một file có điểm kết thúc rõ ràng; một stream processor đọc thứ mà, theo thiết kế, không bao giờ “xong.”</p>

  <p>Những sự kiện này thực sự tới từ đâu? Câu trả lời của cuốn sách nối thẳng về hai thứ tôi đã từng gặp: <strong>message queue</strong> và <strong>database</strong>. Một message broker như <strong>Kafka</strong> được xây riêng để mang một luồng sự kiện liên tục từ producer tới consumer, bền vững và đúng thứ tự — nhưng nguồn thú vị hơn, và cũng là nguồn thực sự khiến tôi bất ngờ, chính là bản thân database, thông qua thứ gọi là <strong>change data capture</strong>.</p>

  <h2 id="change-data-capture-bin-database-va-thay-i-thnh-mt-s-kin">Change data capture: biến “database vừa thay đổi” thành một sự kiện</h2>

  <p><strong>Change data capture (CDC)</strong> lấy mọi lượt insert, update, delete xảy ra bên trong database và biến mỗi cái thành một sự kiện trên một stream, đúng theo thứ tự chúng xảy ra. Thay vì mọi phần khác của hệ thống phải polling database hoặc query trực tiếp để nhận ra thay đổi, chúng chỉ cần subscribe vào stream “đây là những gì vừa thay đổi” này.</p>

  <p><img src="/assets/images/ddia/ch11-cdc-stream.svg" alt="Change data capture biến mỗi lượt ghi database thành một sự kiện trên một log bền vững, có thứ tự; các consumer độc lập — search index, cache, hệ thống chống gian lận — mỗi cái đọc cùng log đó theo nhịp độ riêng thay vì nói chuyện trực tiếp với database" /></p>

  <p>Điều thực sự sáng tỏ với tôi ở đây là cách này giải quyết tự nhiên một vấn đề tôi từng gặp phải mà chưa biết gọi tên là gì: giữ một search index, một cache, và một database đồng bộ với nhau, mà không phải nối dây để từng cái nói chuyện trực tiếp với từng cái khác. Với CDC, database là nguồn sự thật duy nhất, thay đổi của nó chảy ra thành một stream có thứ tự, và bất cứ thứ gì cần giữ đồng bộ — một search index ở công ty này, một hệ thống chống gian lận ở công ty khác, một cache ở công ty thứ ba — chỉ cần subscribe độc lập, đọc theo nhịp độ riêng, không bao giờ cần biết những cái khác tồn tại. Đây chính xác là vai trò Kafka đóng tại các công ty như LinkedIn (nơi nó ra đời) — một log bền vững, có thể đọc lại được, mà nhiều hệ thống độc lập có thể cùng đọc.</p>

  <h2 id="window-lm-sao-cng-dn-mt-th-khng-bao-gi-kt-thc">Window: làm sao “cộng dồn” một thứ không bao giờ kết thúc?</h2>

  <p>Một batch job có thể tính “tổng doanh số trong ngày” bằng cách đọc hết dữ liệu cả ngày rồi cộng lại, vì input có ranh giới rõ ràng. Một stream không bao giờ có ranh giới rõ ràng, nên cuốn sách giới thiệu <strong>window</strong> — một ranh giới nhân tạo bạn tự vẽ ra, kiểu “5 phút vừa qua” hay “đúng giờ này,” để những câu hỏi tổng hợp (“có bao nhiêu lượt đăng nhập trong 5 phút vừa qua?”) có thể trả lời được trên một stream vô tận.</p>

  <p>Phần thực sự hóc búa, điều tôi chưa từng để ý trước đây, là sự kiện không phải lúc nào cũng tới theo đúng thứ tự chúng thực sự xảy ra. Một chiếc điện thoại có thể mất sóng rồi gửi dồn một loạt sự kiện bị trễ cùng lúc; một trục trặc mạng có thể khiến một sự kiện tới sau vài giây so với những sự kiện xảy ra sau nó. Nên một stream processor phải đưa ra một quyết định thật sự: nó có chờ một chút cho những sự kiện tới trễ trước khi chốt câu trả lời của một window (đúng hơn, nhưng chậm cho ra kết quả), hay nó chốt nhanh và chấp nhận rủi ro sai khi một sự kiện trễ xuất hiện sau đó? Không có câu trả lời đúng phổ quát nào — đây là một đánh đổi thật sự giữa độ trễ và tính đầy đủ mà mọi hệ thống stream processing phải tự chọn lập trường.</p>

  <h2 id="exactly-once-li-ha-kh-hn-nghe-qua-rt-nhiu">Exactly-once: lời hứa khó hơn nghe qua rất nhiều</h2>

  <p>Ý tưởng cuối cùng đọng lại trong tôi nối thẳng về <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">bản kiểm kê thành thật về rắc rối hệ phân tán ở Chương 8</a>: chuyện gì xảy ra nếu một stream processor crash giữa chừng khi đang xử lý một sự kiện, và phải restart? Nếu nó đơn giản xử lý lại mọi thứ nó không chắc chắn, một số sự kiện có thể bị đếm hai lần. Đảm bảo mà mọi người thực sự muốn là <strong>exactly-once semantics</strong> — mỗi sự kiện ảnh hưởng tới kết quả cuối cùng đúng một lần, bất kể cỗ máy bên dưới phải thử lại bao nhiêu lần sau một lần crash. Các hệ thống thật (tính năng transactional của chính Kafka, checkpointing của Flink) đạt được điều này không phải bằng cách làm cho failure trở nên bất khả thi, mà bằng cách làm cho <em>hiệu ứng</em> của công việc bị thử lại trở nên idempotent — an toàn khi áp dụng nhiều hơn một lần mà không làm thay đổi kết quả — một mục tiêu khả thi hơn nhiều so với việc giả vờ crash sẽ không xảy ra.</p>

  <p>Nhìn chương 10 và 11 cùng nhau, bài học thực sự là batch và stream processing không phải đối thủ của nhau — chúng là cùng một câu hỏi nền tảng (“làm sao tính ra derived data từ một đống sự kiện khổng lồ?”) được trả lời dưới hai ràng buộc khác nhau: bạn sẵn sàng chờ bao lâu, và bạn có thể định nghĩa được một điểm đầu, điểm cuối rõ ràng cho input của mình hay không.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 11" /><category term="Stream Processing" /><category term="Kafka" /><summary type="html"><![CDATA[Batch processing waits for a full day's data before computing anything. This chapter asks: what if you can't wait — what if the answer needs to update the instant a new event happens?]]></summary></entry><entry><title type="html">DDIA Chapter 10: Batch Processing</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-10-batch-processing/" rel="alternate" type="text/html" title="DDIA Chapter 10: Batch Processing" /><published>2026-09-15T14:00:00+07:00</published><updated>2026-09-15T14:00:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-10-batch-processing</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-10-batch-processing/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 10: Batch Processing.</em></p>

  <p>Everything from <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> through <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consensus</a> was about keeping data safe and correct across multiple machines. This chapter starts a new part of the book, and the question changes: given a huge pile of data that’s already sitting there safely, how do you actually <em>compute something useful from it</em> — a search index, a set of recommendations, a report — without waiting forever? The book calls this kind of computed output <strong>derived data</strong>, and batch processing is the first of two ways it covers for producing it.</p>

  <h2 id="the-unix-philosophy-scaled-up">The Unix philosophy, scaled up</h2>

  <p>The chapter’s opening example is almost embarrassingly simple, and that’s exactly the point: counting how often each word appears in a log file using nothing but classic Unix command-line tools — <code class="language-plaintext highlighter-rouge">cat</code> to read the file, <code class="language-plaintext highlighter-rouge">sort</code> to group identical words next to each other, <code class="language-plaintext highlighter-rouge">uniq -c</code> to count how many times each one repeats. Each of these tools does exactly one small thing, and they’re chained together with pipes, where the output of one becomes the input of the next.</p>

  <p>What makes this example matter for the rest of the chapter is the <em>design principle</em> behind it: every one of these tools reads from a plain stream of text and writes to a plain stream of text, with no knowledge of what tool comes before or after it in the chain. Because the interface is so simple and uniform, you can rearrange, swap, or insert new tools into the pipeline freely. This turns out to be the exact same idea that lets modern batch processing systems combine dozens of small operations into one large computation without every piece needing to understand every other piece.</p>

  <h2 id="mapreduce-the-same-idea-spread-across-a-thousand-machines">MapReduce: the same idea, spread across a thousand machines</h2>

  <p>A single Unix pipeline runs on one machine. <strong>MapReduce</strong> — the framework Google introduced and popularized, and which Hadoop later made available outside Google — takes that same “read input, transform it, group it, write output” pattern and spreads it across a whole cluster of machines, so it can chew through datasets far too large for any single machine to hold.</p>

  <p><img src="/assets/images/ddia/ch10-batch-pipeline.svg" alt="Chaining batch jobs the MapReduce way writes every stage's output to disk before the next stage starts; a modern dataflow engine like Spark keeps data in memory between stages, closer to how Unix pipes avoid writing temp files" /></p>

  <p>The <strong>map</strong> step takes the input, split across many machines, and transforms each individual record independently — in the word-count example, turning each line of text into a series of <code class="language-plaintext highlighter-rouge">(word, 1)</code> pairs. Then comes the <strong>shuffle</strong>: all the pairs with the same word get routed to the same machine, so that machine sees every occurrence of that one word together. Finally the <strong>reduce</strong> step takes each group and combines it into a final answer — adding up all the <code class="language-plaintext highlighter-rouge">1</code>s for a given word to get its total count. Input and output at every stage lives on a distributed filesystem (Hadoop’s version is called <strong>HDFS</strong>), which is what lets the whole thing scale to datasets spread across hundreds of machines in the first place.</p>

  <h2 id="where-mapreduce-gets-genuinely-clever-batch-joins">Where MapReduce gets genuinely clever: batch joins</h2>

  <p>Beyond simple counting, the chapter spends real time on something I hadn’t thought about before: how do you <em>join</em> two enormous datasets — say, a log of user activity and a separate table of user profiles — when both are too big to fit on one machine and you can’t just run a database-style join? The book covers a couple of approaches, and the tradeoff between them was a genuinely useful thing to internalize:</p>

  <p>A <strong>sort-merge join</strong> sorts both datasets by the shared key (say, user ID) and streams through them together, matching up records as it goes — very similar in spirit to how <code class="language-plaintext highlighter-rouge">sort</code> groups identical words together in the Unix example, just done at a much larger scale across a cluster. A <strong>broadcast hash join</strong> instead recognizes that one of the two datasets (the smaller one, like the user-profile table) can fit entirely in memory on every machine, so it gets copied out to all of them, and each machine can then look up matches locally without needing to sort or shuffle the giant dataset at all. Which one is better depends entirely on the actual size difference between the two datasets — exactly the kind of “know your actual workload” lesson that showed up back in the <a href="/blog/2026/09/15/ddia-chapter-1-reliable-scalable-maintainable/">Twitter timeline example</a> too.</p>

  <h2 id="why-the-diagram-matters-not-every-batch-system-pays-the-same-disk-cost">Why the diagram matters: not every batch system pays the same disk cost</h2>

  <p>The one design choice that most shaped how batch processing evolved after the original MapReduce is exactly what the diagram above shows. Classic MapReduce chains jobs together by having each one write its complete output to the distributed filesystem before the next job is even allowed to start reading it — safe and simple, but it means a computation with five sequential steps pays for five full round-trips to disk, even though the earlier Unix pipe example never needed to touch disk at all between commands. Newer <strong>dataflow engines</strong> — Spark, Flink, Tez — fix exactly this: they keep intermediate results in memory and pipeline data directly from one processing stage to the next wherever possible, only touching disk when they genuinely have to (running out of memory, or writing the final result). This is a big part of why a Spark job doing the same work as an old-style MapReduce job can run dramatically faster: not a smarter algorithm, just far less unnecessary disk I/O between the steps.</p>

  <p>Looking ahead, this chapter is really the “first half” of a pair — batch processing works great when you’re happy to wait for all the input to be collected before computing anything (a nightly report, reprocessing a year of logs). The next chapter tackles the opposite case: producing derived data continuously, as new events arrive one at a time, instead of waiting for a full batch to accumulate.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 10: Batch Processing.</em></p>

  <p>Mọi thứ từ <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> tới <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consensus</a> đều nói về việc giữ dữ liệu an toàn và đúng đắn trên nhiều máy. Chương này mở ra một phần mới của cuốn sách, và câu hỏi thay đổi: với một đống dữ liệu khổng lồ đã nằm sẵn đó an toàn rồi, làm sao bạn thực sự <em>tính ra thứ gì đó hữu ích từ nó</em> — một search index, một danh sách gợi ý, một báo cáo — mà không phải chờ mãi mãi? Cuốn sách gọi loại kết quả được tính ra này là <strong>derived data</strong> (dữ liệu dẫn xuất), và batch processing là cách đầu tiên trong hai cách sách nói tới để tạo ra nó.</p>

  <h2 id="trit-l-unix-phng-to-quy-m">Triết lý Unix, phóng to quy mô</h2>

  <p>Ví dụ mở đầu chương đơn giản đến mức gần như buồn cười, và đó chính xác là chủ đích: đếm mỗi từ xuất hiện bao nhiêu lần trong một file log chỉ bằng các công cụ dòng lệnh Unix kinh điển — <code class="language-plaintext highlighter-rouge">cat</code> để đọc file, <code class="language-plaintext highlighter-rouge">sort</code> để gom các từ giống nhau đứng cạnh nhau, <code class="language-plaintext highlighter-rouge">uniq -c</code> để đếm mỗi từ lặp lại bao nhiêu lần. Mỗi công cụ này chỉ làm đúng một việc nhỏ, và chúng được nối lại bằng pipe, nơi đầu ra của công cụ này trở thành đầu vào của công cụ tiếp theo.</p>

  <p>Điều khiến ví dụ này quan trọng cho phần còn lại của chương là <em>nguyên tắc thiết kế</em> đằng sau nó: mỗi công cụ trong số này đọc từ một luồng text đơn giản và ghi ra một luồng text đơn giản, không biết gì về công cụ đứng trước hay sau nó trong chuỗi. Vì interface đơn giản và thống nhất đến vậy, bạn có thể sắp xếp lại, thay thế, hoặc chèn công cụ mới vào pipeline một cách tự do. Hóa ra đây chính xác là ý tưởng cho phép các hệ thống batch processing hiện đại kết hợp hàng chục thao tác nhỏ thành một phép tính lớn mà không mảnh nào cần hiểu hết mọi mảnh khác.</p>

  <h2 id="mapreduce-cng--tng--tri-trn-hng-ngn-my">MapReduce: cùng ý tưởng đó, trải trên hàng ngàn máy</h2>

  <p>Một pipeline Unix đơn lẻ chạy trên một máy. <strong>MapReduce</strong> — framework Google giới thiệu và phổ biến, và sau này Hadoop mang ra ngoài Google — lấy đúng pattern “đọc input, biến đổi nó, gom nhóm nó, ghi output” đó và trải nó ra trên cả một cluster máy, để có thể nhai được những tập dữ liệu quá lớn cho bất kỳ một máy đơn lẻ nào chứa nổi.</p>

  <p><img src="/assets/images/ddia/ch10-batch-pipeline.svg" alt="Nối các batch job kiểu MapReduce ghi output của mỗi giai đoạn xuống đĩa trước khi giai đoạn tiếp theo bắt đầu; một dataflow engine hiện đại như Spark giữ dữ liệu trong bộ nhớ giữa các giai đoạn, gần giống cách Unix pipe tránh ghi file tạm" /></p>

  <p>Bước <strong>map</strong> nhận input, được chia trên nhiều máy, và biến đổi từng bản ghi riêng lẻ một cách độc lập — trong ví dụ đếm từ, biến mỗi dòng text thành một chuỗi cặp <code class="language-plaintext highlighter-rouge">(từ, 1)</code>. Sau đó tới <strong>shuffle</strong>: mọi cặp có cùng một từ được định tuyến về cùng một máy, để máy đó thấy được mọi lần xuất hiện của đúng từ đó gộp lại với nhau. Cuối cùng bước <strong>reduce</strong> nhận từng nhóm và gộp nó thành câu trả lời cuối — cộng hết các số <code class="language-plaintext highlighter-rouge">1</code> của một từ để ra tổng số lần xuất hiện. Input và output ở mỗi giai đoạn đều nằm trên một hệ thống file phân tán (phiên bản của Hadoop gọi là <strong>HDFS</strong>), đó chính là thứ cho phép toàn bộ việc này mở rộng ra được tới dữ liệu trải trên hàng trăm máy ngay từ đầu.</p>

  <h2 id="ch-mapreduce-thc-s-kho-lo-join-theo-kiu-batch">Chỗ MapReduce thực sự khéo léo: join theo kiểu batch</h2>

  <p>Ngoài việc đếm đơn giản, chương dành thời gian đáng kể cho một thứ tôi chưa từng nghĩ tới trước đây: làm sao <em>join</em> hai tập dữ liệu khổng lồ — ví dụ, một log hoạt động người dùng và một bảng profile người dùng riêng — khi cả hai đều quá lớn để nằm gọn trên một máy và bạn không thể chỉ chạy một join kiểu database thông thường? Cuốn sách trình bày vài cách tiếp cận, và sự đánh đổi giữa chúng là thứ thực sự đáng để ghi nhớ:</p>

  <p>Một <strong>sort-merge join</strong> sắp xếp cả hai tập dữ liệu theo key chung (ví dụ, user ID) rồi chạy xuyên qua chúng cùng lúc, khớp các bản ghi khi đi tới — về tinh thần khá giống cách <code class="language-plaintext highlighter-rouge">sort</code> gom các từ giống nhau lại với nhau trong ví dụ Unix, chỉ là làm ở quy mô lớn hơn nhiều trên cả một cluster. Một <strong>broadcast hash join</strong> thay vào đó nhận ra rằng một trong hai tập dữ liệu (tập nhỏ hơn, như bảng profile người dùng) có thể nằm gọn hoàn toàn trong bộ nhớ trên mọi máy, nên nó được sao chép ra cho tất cả các máy, và mỗi máy sau đó có thể tự tra cứu khớp cục bộ mà không cần sắp xếp hay shuffle tập dữ liệu khổng lồ kia chút nào. Cái nào tốt hơn phụ thuộc hoàn toàn vào chênh lệch kích thước thực tế giữa hai tập dữ liệu — đúng kiểu bài học “hiểu rõ workload thực tế của mình” từng xuất hiện ở <a href="/blog/2026/09/15/ddia-chapter-1-reliable-scalable-maintainable/">ví dụ timeline Twitter</a> trước đây.</p>

  <h2 id="v-sao-s--ny-quan-trng-khng-phi-h-thng-batch-no-cng-tr-cng-mt-ci-gi-v-a">Vì sao sơ đồ này quan trọng: không phải hệ thống batch nào cũng trả cùng một cái giá về đĩa</h2>

  <p>Quyết định thiết kế định hình nhiều nhất cách batch processing tiến hóa sau MapReduce gốc chính xác là điều sơ đồ ở trên thể hiện. MapReduce cổ điển nối các job lại bằng cách để mỗi job ghi toàn bộ output của nó xuống hệ thống file phân tán trước khi job tiếp theo được phép bắt đầu đọc nó — an toàn và đơn giản, nhưng nghĩa là một phép tính có năm bước tuần tự phải trả giá cho năm lượt đi-về đầy đủ tới đĩa, dù ví dụ Unix pipe trước đó chưa bao giờ cần đụng tới đĩa giữa các lệnh. Các <strong>dataflow engine</strong> mới hơn — Spark, Flink, Tez — sửa đúng điều này: chúng giữ kết quả trung gian trong bộ nhớ và truyền dữ liệu trực tiếp từ giai đoạn xử lý này sang giai đoạn tiếp theo bất cứ khi nào có thể, chỉ đụng tới đĩa khi thực sự cần (hết bộ nhớ, hoặc ghi kết quả cuối cùng). Đây là một phần lớn lý do vì sao một job Spark làm cùng công việc với một job MapReduce kiểu cũ có thể chạy nhanh hơn đáng kể: không phải nhờ thuật toán thông minh hơn, chỉ đơn giản là ít I/O đĩa không cần thiết hơn nhiều giữa các bước.</p>

  <p>Nhìn về phía trước, chương này thực chất là “nửa đầu” của một cặp — batch processing hoạt động tuyệt vời khi bạn sẵn sàng chờ toàn bộ input được gom đủ rồi mới tính toán gì đó (một báo cáo hàng đêm, xử lý lại log của cả một năm). Chương tiếp theo giải quyết trường hợp ngược lại: tạo ra derived data liên tục, khi sự kiện mới tới từng cái một, thay vì chờ gom đủ một mẻ hoàn chỉnh.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 10" /><category term="Batch Processing" /><category term="MapReduce" /><summary type="html"><![CDATA[A search index, a recommendation list, an analytics dashboard — none of them are typed in by hand, they're all computed from other data. This chapter is about the first of two ways to do that computing: in one big batch.]]></summary></entry><entry><title type="html">DDIA Chapter 9: Consistency and Consensus</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/" rel="alternate" type="text/html" title="DDIA Chapter 9: Consistency and Consensus" /><published>2026-09-15T10:00:00+07:00</published><updated>2026-09-15T10:00:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 9: Consistency and Consensus.</em></p>

  <p>This is the chapter that <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> kept gesturing toward every time it needed to safely pick a new leader, and that <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">the previous chapter</a> set up by explaining exactly how unreliable the network and clocks really are. Given all that unreliability, this chapter asks the big question directly: how do you get a group of machines to agree on <em>one single thing</em> — one value, one leader, one order of events — when any of them might be slow, might have crashed, or might simply be unreachable for a while?</p>

  <h2 id="linearizability-the-illusion-of-just-one-copy">Linearizability: the illusion of “just one copy”</h2>

  <p>The chapter opens with a specific, very strong consistency guarantee called <strong>linearizability</strong>. The plain-language version: even though your data might actually be spread across several replicated copies, the system behaves <em>as if</em> there were only ever one single copy, and every operation happens at one specific, real instant in time, visible to everyone immediately. Once you write something, literally everyone who asks afterward sees that new value — no stale reads from a lagging replica, no confusing “it’s there for me but not for you” moment.</p>

  <p>This sounds like exactly what everyone would obviously want, all the time — so the natural question is why it isn’t just the universal default. The honest answer: linearizability is expensive, because guaranteeing it typically means every read and write has to coordinate with enough of the other replicas to be sure nothing has changed elsewhere first, which adds real latency, especially across long distances (a request that has to round-trip to a data center on another continent before it can complete is never going to be instant). Systems that need to survive a network partition without pausing everything have to give up linearizability to keep working at all — this tradeoff even has a name, the <strong>CAP theorem</strong>, though the book is careful to point out CAP is narrower and more specific than the internet’s popularized version of it.</p>

  <h2 id="ordering-events-without-a-trustworthy-clock">Ordering events without a trustworthy clock</h2>

  <p>Since the previous chapter already established that physical clocks can’t be trusted to put events in the correct order, this chapter introduces a different tool: instead of asking “what time did this happen,” you ask “what happened <em>before</em> what.” This is captured by something called a <strong>logical clock</strong> — instead of real time, you track cause-and-effect relationships directly (if event B was created in response to event A, B is recorded as happening “after” A, regardless of what either machine’s physical clock said).</p>

  <p>The strongest, most useful version of this ordering is <strong>total order broadcast</strong>: a guarantee that every node in the system delivers the exact same sequence of messages, in the exact same order, no exceptions. This sounds abstract until you realize it’s secretly the same problem as replication itself — if every replica applies the exact same writes in the exact same order, they’ll all end up in the same final state. Total order broadcast and single-leader replication are, in a very real sense, describing the same underlying problem from two different angles.</p>

  <h2 id="consensus-agreeing-on-one-thing-despite-failures">Consensus: agreeing on one thing despite failures</h2>

  <p>This is the chapter’s actual centerpiece, and the definition is precise: <strong>consensus</strong> means getting a set of nodes to agree on a single value, with a guarantee that once a decision is made, it can never be silently reversed or contradicted later — even if some nodes crash, restart, or temporarily can’t communicate with the rest.</p>

  <p><img src="/assets/images/ddia/ch9-consensus.svg" alt="A 5-node cluster elects a leader with only 3 of 5 nodes able to communicate — a majority is enough, even though one node is currently unreachable" /></p>

  <p>The mechanism nearly every real consensus algorithm relies on is the <strong>majority vote</strong>: as long as more than half the nodes agree, the decision is considered final and safe, even if the remaining nodes are down or unreachable. This is precisely why consensus systems are almost always deployed with an odd number of nodes (3, 5, 7) — it guarantees there’s always a clear majority possible, and it’s also why a majority-based system can keep working even while some minority of nodes has failed, which is the entire point of building it this way in the first place.</p>

  <p>The book names the well-known real implementations of this idea, and I found it genuinely satisfying to connect algorithm names to tools I already recognized: <strong>Paxos</strong> (the original, notoriously hard to fully understand), <strong>Raft</strong> (designed specifically to be more understandable, and the algorithm behind <strong>etcd</strong>, the coordination store at the heart of Kubernetes), and <strong>ZAB</strong> (the protocol underneath <strong>Apache ZooKeeper</strong>, itself referenced back in the <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning chapter</a> as the kind of coordination service that tracks which machine currently owns which piece of data). Every one of these is, underneath the specific implementation details, solving the exact same majority-agreement problem.</p>

  <h2 id="why-consensus-isnt-used-for-absolutely-everything">Why consensus isn’t used for absolutely everything</h2>

  <p>Given how powerful and safe consensus is, the natural question is why entire databases aren’t built as one giant consensus system for every single write. The honest answer, which echoes the linearizability tradeoff from earlier in the chapter: consensus requires real network round-trips between a majority of nodes for every single decision, which makes it noticeably slower than a system that doesn’t need everyone to agree before proceeding. In practice, consensus gets reserved for the decisions that are genuinely worth paying that cost for — electing a leader, agreeing on cluster membership, committing a distributed transaction — rather than for every single ordinary read and write flowing through a system.</p>

  <h2 id="closing-the-loop-on-the-whole-distributed-systems-arc">Closing the loop on the whole distributed-systems arc</h2>

  <p>Looking back across <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a>, <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning</a>, <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">the unreliability of networks and clocks</a>, and now this chapter, the shape of the whole argument finally clicks into place for me: distributed systems are hard specifically <em>because</em> the network and clocks can’t be trusted (Chapter 8), which makes safely picking a leader or ordering events genuinely difficult (Chapter 5’s failover problem), which is exactly the problem consensus algorithms exist to solve properly (this chapter) — using nothing more clever than getting a plain majority of honest nodes to agree. It’s a satisfying amount of the book to have now connected into one coherent picture, built from a handful of genuinely simple underlying ideas.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 9: Consistency and Consensus.</em></p>

  <p>Đây là chương mà <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> cứ liên tục chỉ về phía trước mỗi khi cần bầu leader mới một cách an toàn, và được <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">chương trước</a> dọn đường bằng cách giải thích chính xác mạng và đồng hồ thực sự không đáng tin cậy tới mức nào. Với tất cả sự không đáng tin cậy đó, chương này hỏi thẳng câu hỏi lớn: làm sao khiến một nhóm máy đồng thuận về <em>đúng một thứ duy nhất</em> — một giá trị, một leader, một thứ tự sự kiện — khi bất kỳ máy nào trong số đó có thể chậm, có thể đã crash, hoặc đơn giản là không tới được trong một khoảng thời gian?</p>

  <h2 id="linearizability-o-gic-v-ch-c-mt-bn-sao-duy-nht">Linearizability: ảo giác về “chỉ có một bản sao duy nhất”</h2>

  <p>Chương mở đầu bằng một đảm bảo consistency rất mạnh, cụ thể, gọi là <strong>linearizability</strong>. Nói theo cách bình dân: dù dữ liệu của bạn thực ra có thể trải trên nhiều bản sao được replicate, hệ thống hành xử <em>như thể</em> chỉ từng có đúng một bản sao duy nhất, và mỗi thao tác xảy ra tại đúng một khoảnh khắc cụ thể, có thật, mà mọi người thấy được ngay lập tức. Một khi bạn ghi thứ gì đó, theo đúng nghĩa đen mọi người hỏi sau đó đều thấy giá trị mới đó — không có lượt đọc cũ từ một replica bị trễ, không có khoảnh khắc gây bối rối kiểu “tôi thấy rồi nhưng bạn thì chưa.”</p>

  <p>Nghe qua thì đây rõ ràng là thứ ai cũng muốn, mọi lúc — nên câu hỏi tự nhiên là vì sao nó không phải mặc định phổ quát. Câu trả lời thành thật: linearizability tốn kém, vì đảm bảo nó thường nghĩa là mỗi lượt đọc và ghi phải phối hợp với đủ số replica khác để chắc chắn không có gì thay đổi ở nơi khác trước đó, điều này cộng thêm độ trễ thật, đặc biệt qua khoảng cách xa (một request phải đi vòng tới một trung tâm dữ liệu ở lục địa khác trước khi có thể hoàn tất sẽ không bao giờ tức thì được). Các hệ thống cần sống sót qua một network partition mà không dừng mọi thứ lại buộc phải từ bỏ linearizability để tiếp tục hoạt động chút nào — sự đánh đổi này thậm chí có tên riêng, <strong>CAP theorem</strong>, dù cuốn sách cẩn thận chỉ ra CAP hẹp và cụ thể hơn phiên bản đã được phổ biến hóa trên internet nhiều.</p>

  <h2 id="sp-xp-s-kin-m-khng-cn-mt-ng-h-ng-tin-cy">Sắp xếp sự kiện mà không cần một đồng hồ đáng tin cậy</h2>

  <p>Vì chương trước đã xác lập rằng đồng hồ vật lý không thể được tin tưởng để đặt sự kiện theo đúng thứ tự, chương này giới thiệu một công cụ khác: thay vì hỏi “chuyện này xảy ra lúc mấy giờ,” bạn hỏi “cái gì xảy ra <em>trước</em> cái gì.” Điều này được nắm bắt bởi thứ gọi là <strong>logical clock</strong> (đồng hồ logic) — thay vì thời gian thật, bạn theo dõi trực tiếp quan hệ nhân-quả (nếu sự kiện B được tạo ra để phản hồi sự kiện A, B được ghi nhận là xảy ra “sau” A, bất kể đồng hồ vật lý của máy nào nói gì).</p>

  <p>Phiên bản mạnh nhất, hữu ích nhất của cách sắp xếp này là <strong>total order broadcast</strong>: một đảm bảo rằng mọi node trong hệ thống nhận được đúng cùng một chuỗi message, theo đúng cùng một thứ tự, không ngoại lệ. Nghe có vẻ trừu tượng cho tới khi bạn nhận ra nó thực chất là cùng một vấn đề với chính replication — nếu mọi replica áp dụng đúng cùng những lượt ghi theo đúng cùng thứ tự, chúng sẽ đều kết thúc ở cùng một trạng thái cuối. Total order broadcast và single-leader replication, theo một nghĩa rất thật, đang mô tả cùng một vấn đề nền tảng từ hai góc nhìn khác nhau.</p>

  <h2 id="consensus-ng-thun-v-mt-th-bt-chp-s-c">Consensus: đồng thuận về một thứ bất chấp sự cố</h2>

  <p>Đây là trọng tâm thực sự của chương, và định nghĩa rất chính xác: <strong>consensus</strong> nghĩa là khiến một tập node đồng thuận về một giá trị duy nhất, với đảm bảo rằng một khi quyết định đã được đưa ra, nó không bao giờ có thể bị âm thầm đảo ngược hay mâu thuẫn sau đó — kể cả khi một số node crash, restart, hoặc tạm thời không liên lạc được với phần còn lại.</p>

  <p><img src="/assets/images/ddia/ch9-consensus.svg" alt="Một cluster 5 node bầu leader dù chỉ có 3 trong 5 node liên lạc được — đa số là đủ, kể cả khi một node hiện đang không tới được" /></p>

  <p>Cơ chế mà gần như mọi thuật toán consensus thật đều dựa vào là <strong>majority vote</strong> (biểu quyết theo đa số): miễn là hơn một nửa số node đồng ý, quyết định được coi là cuối cùng và an toàn, kể cả khi các node còn lại đang chết hoặc không tới được. Đây chính xác là lý do các hệ thống consensus hầu như luôn được triển khai với số node lẻ (3, 5, 7) — nó đảm bảo luôn có thể có một đa số rõ ràng, và cũng là lý do một hệ thống dựa trên đa số có thể tiếp tục hoạt động ngay cả khi một thiểu số node đã lỗi, đó chính là toàn bộ mục đích của việc xây dựng nó theo cách này ngay từ đầu.</p>

  <p>Cuốn sách nêu tên các triển khai thật, nổi tiếng của ý tưởng này, và tôi thấy thực sự thỏa mãn khi kết nối tên thuật toán với các công cụ mình đã biết: <strong>Paxos</strong> (bản gốc, nổi tiếng là khó hiểu trọn vẹn), <strong>Raft</strong> (được thiết kế đặc biệt để dễ hiểu hơn, và là thuật toán đứng sau <strong>etcd</strong>, kho lưu trữ điều phối nằm ở trung tâm của Kubernetes), và <strong>ZAB</strong> (giao thức bên dưới <strong>Apache ZooKeeper</strong>, chính cái được nhắc tới ở <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">chương partitioning</a> như một loại coordination service theo dõi máy nào hiện đang sở hữu mảnh dữ liệu nào). Mỗi cái trong số này, bên dưới các chi tiết triển khai cụ thể, đều đang giải quyết đúng cùng một vấn đề đồng thuận theo đa số.</p>

  <h2 id="v-sao-consensus-khng-c-dng-cho-tuyt-i-mi-th">Vì sao consensus không được dùng cho tuyệt đối mọi thứ</h2>

  <p>Với việc consensus mạnh mẽ và an toàn đến vậy, câu hỏi tự nhiên là vì sao cả database không được xây thành một hệ thống consensus khổng lồ cho từng lượt ghi. Câu trả lời thành thật, vọng lại đánh đổi về linearizability từ đầu chương: consensus đòi hỏi những vòng round-trip mạng thật giữa đa số node cho mỗi quyết định, khiến nó chậm hơn đáng kể so với một hệ thống không cần mọi người đồng ý trước khi tiếp tục. Trong thực tế, consensus được dành riêng cho những quyết định thực sự đáng trả cái giá đó — bầu leader, đồng thuận về thành viên cluster, commit một distributed transaction — thay vì cho từng lượt đọc, ghi bình thường chảy qua hệ thống.</p>

  <h2 id="khp-li-vng-cung-h-phn-tn">Khép lại vòng cung hệ phân tán</h2>

  <p>Nhìn lại <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a>, <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning</a>, <a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">sự không đáng tin cậy của mạng và đồng hồ</a>, và giờ là chương này, hình dạng của toàn bộ luận điểm cuối cùng cũng sáng tỏ với tôi: hệ phân tán khó chính xác <em>vì</em> mạng và đồng hồ không thể tin tưởng được (Chương 8), điều này khiến việc bầu leader an toàn hay sắp xếp sự kiện thực sự khó khăn (vấn đề failover của Chương 5), và đó chính xác là vấn đề mà các thuật toán consensus tồn tại để giải quyết một cách đúng đắn (chương này) — không cần gì khéo léo hơn việc để một đa số đơn giản gồm các node trung thực đồng thuận với nhau. Đây là một lượng đáng kể của cuốn sách mà giờ tôi đã kết nối được thành một bức tranh nhất quán, được xây từ một số ý tưởng nền tảng thực sự đơn giản.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 9" /><category term="Consensus" /><category term="Linearizability" /><category term="Distributed Systems" /><summary type="html"><![CDATA[The chapter every earlier chapter kept pointing forward to. It finally answers: how does a group of unreliable machines agree on one thing, when the network lies and the clock can't be trusted?]]></summary></entry><entry><title type="html">DDIA Chapter 8: The Trouble with Distributed Systems</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/" rel="alternate" type="text/html" title="DDIA Chapter 8: The Trouble with Distributed Systems" /><published>2026-09-15T09:45:00+07:00</published><updated>2026-09-15T09:45:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 8: The Trouble with Distributed Systems.</em></p>

  <p>After transactions, the book takes what felt like a step back — but is actually the most important chapter so far for understanding <em>why</em> everything before it (replication, partitioning, isolation) is as complicated as it is. This chapter doesn’t introduce a new technique. It’s an honest, occasionally unsettling inventory of everything that can quietly go wrong the moment your system spans more than one machine, and it left me with a healthy amount of paranoia about two things I used to take completely for granted: the network, and the clock.</p>

  <h2 id="the-network-lies-to-you-and-it-lies-by-omission">The network lies to you, and it lies by omission</h2>

  <p>On a single machine, if a function call fails, you usually get a clear error immediately. Across a network, the book’s central, uncomfortable point is that you often get <em>nothing</em> — no error, no confirmation, just silence — and silence is fundamentally ambiguous. Did the request never arrive? Did it arrive and get processed, but the <em>response</em> got lost on the way back? Is the other machine just extremely slow right now? From the sending machine’s point of view, all three of these look absolutely identical: you sent something, and you haven’t heard back.</p>

  <p><img src="/assets/images/ddia/ch8-partition-clocks.svg" alt="A dropped link between two data centers looks the same to a timeout whether the other side is dead or just briefly unreachable — and clocks on separate machines quietly drift apart, which is why you can't safely order events by timestamp alone" /></p>

  <p>This is called a <strong>network partition</strong>, and real ones happen at real companies running real infrastructure — a misconfigured router, a severed undersea cable, a firewall rule change that accidentally blocks the wrong traffic. The book’s blunt conclusion: the <em>only</em> tool you actually have to distinguish “slow” from “dead” is a <strong>timeout</strong>, and a timeout is fundamentally a guess dressed up as a decision. Set it too short, and you’ll declare healthy, slightly-slow machines dead constantly, triggering unnecessary and disruptive failovers. Set it too long, and a genuinely dead machine keeps everyone else waiting far longer than necessary. There is no timeout value that is simultaneously always correct and always fast — you’re just picking where on that spectrum you’d rather be wrong.</p>

  <h2 id="clocks-dont-tell-the-truth-either">Clocks don’t tell the truth either</h2>

  <p>This was the section that genuinely surprised me, because I’d always assumed “just check the timestamp” was a safe, boring way to figure out which of two events happened first. It isn’t. Every machine has its own physical clock, and these clocks <strong>drift</strong> — they run very slightly fast or slow compared to real time, and compared to each other, purely due to hardware imperfections. Systems try to correct for this using NTP (Network Time Protocol), periodically syncing to a reference clock over the network — but that correction itself travels over the same unreliable network described above, and can be delayed, or occasionally jump the clock backward or forward to fix a large drift.</p>

  <p>The practical consequence the book hammers on: if two different machines each timestamp an event locally, and you later try to sort those events by comparing timestamps, you can get the order <em>wrong</em>, because the two clocks were never perfectly in sync to begin with. This is why “last write wins,” a strategy that sounds perfectly reasonable at first, is quietly dangerous in a distributed system — “last” according to <em>whose</em> clock? This exact problem is serious enough that Google built a specialized piece of infrastructure, <strong>TrueTime</strong>, specifically for their Spanner database — instead of pretending clocks are perfectly accurate, TrueTime reports a time as an honest <em>range</em> (“the real time is somewhere between X and Y”), and the system is designed to wait out that uncertainty window rather than gamble on a single, possibly-wrong timestamp.</p>

  <h2 id="process-pauses-your-code-can-stop-without-you-knowing">Process pauses: your code can stop without you knowing</h2>

  <p>The third source of trouble is one I’d genuinely never considered: even a program that isn’t crashed and isn’t stuck in a network wait can simply <em>stop running</em> for an unpredictable stretch of time, for reasons entirely outside its own control. The book’s main example is <strong>garbage collection (GC) pauses</strong> — many programming languages periodically pause your entire program to clean up unused memory, and this pause can occasionally last much longer than expected under load. From every other machine’s perspective, a process that’s paused for a long GC cycle looks exactly like a process that’s crashed or a network that’s down — because, again, all they can observe is “I haven’t heard from it in a while.”</p>

  <p>The unsettling implication: a distributed system has to be designed to tolerate a node going silent for an unpredictable amount of time and then <em>coming back to life and resuming exactly where it left off</em>, possibly still believing it holds a lock or a leadership role it actually lost while it was paused. This is why real systems use techniques like <strong>fencing tokens</strong> — a monotonically increasing number handed out with each lease or lock, so that if a paused node wakes up and tries to act on stale authority, the system can recognize its token is outdated and reject the action.</p>

  <h2 id="the-honest-lesson-assume-nothing-verify-everything">The honest lesson: assume nothing, verify everything</h2>

  <p>The chapter’s real conclusion isn’t a specific fix — it’s a change in posture. In a single-machine program, you can generally trust that if a function returns, it actually ran, and it ran once. In a distributed system, none of that is safe to assume: messages can be delayed, duplicated, or lost; clocks can’t be fully trusted to order events; and a node that seems dead might just be paused, and might wake back up believing something that’s no longer true. Every protocol covered in the chapters that follow — <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consensus and consistency</a> especially — exists specifically to build reliable guarantees <em>on top of</em> this genuinely unreliable foundation, not to pretend the foundation is more solid than it is.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 8: The Trouble with Distributed Systems.</em></p>

  <p>Sau transaction, cuốn sách có một bước nghe như lùi lại — nhưng thực ra đây là chương quan trọng nhất từ đầu tới giờ để hiểu <em>vì sao</em> mọi thứ trước đó (replication, partitioning, isolation) lại phức tạp đến vậy. Chương này không giới thiệu kỹ thuật mới nào. Nó là một bản kiểm kê thành thật, đôi khi khiến người đọc hơi bất an, về mọi thứ có thể âm thầm trục trặc ngay khi hệ thống của bạn trải rộng ra nhiều hơn một máy, và nó khiến tôi có một chút hoang mang lành mạnh về hai thứ tôi từng coi là hiển nhiên: mạng, và đồng hồ.</p>

  <h2 id="mng-ni-di-bn-v-n-ni-di-bng-cch-im-lng">Mạng nói dối bạn, và nó nói dối bằng cách im lặng</h2>

  <p>Trên một máy đơn lẻ, nếu một hàm gọi thất bại, bạn thường nhận được lỗi rõ ràng ngay lập tức. Qua mạng, điểm trung tâm, hơi khó chịu mà cuốn sách nêu ra là bạn thường nhận được <em>không gì cả</em> — không lỗi, không xác nhận, chỉ có im lặng — và sự im lặng đó về bản chất là mơ hồ. Request có bao giờ tới nơi không? Nó có tới và được xử lý, nhưng <em>phản hồi</em> bị mất trên đường về? Hay máy kia chỉ đang cực kỳ chậm ngay lúc này? Từ góc nhìn của máy gửi, cả ba trường hợp này trông giống hệt nhau: bạn đã gửi thứ gì đó, và chưa nghe phản hồi.</p>

  <p><img src="/assets/images/ddia/ch8-partition-clocks.svg" alt="Một đường kết nối bị đứt giữa hai trung tâm dữ liệu trông giống hệt nhau với một timeout dù phía bên kia đã chết hay chỉ tạm thời không tới được — và đồng hồ trên các máy riêng biệt âm thầm lệch pha nhau, đó là lý do bạn không thể an toàn sắp xếp sự kiện chỉ dựa vào timestamp" /></p>

  <p>Đây gọi là <strong>network partition</strong> (phân mảnh mạng), và những vụ thật xảy ra tại các công ty thật, vận hành hạ tầng thật — một router cấu hình sai, một dây cáp ngầm dưới biển bị đứt, một thay đổi quy tắc firewall vô tình chặn nhầm traffic. Kết luận thẳng thắn của cuốn sách: công cụ <em>duy nhất</em> bạn thực sự có để phân biệt “chậm” với “chết” là một <strong>timeout</strong>, và timeout về bản chất là một phỏng đoán được khoác áo thành một quyết định. Đặt nó quá ngắn, bạn sẽ liên tục tuyên bố những máy khỏe mạnh, chỉ hơi chậm là đã chết, kích hoạt những lần failover không cần thiết và gây rối. Đặt nó quá dài, một máy thực sự đã chết sẽ khiến mọi người khác chờ lâu hơn nhiều mức cần thiết. Không có giá trị timeout nào vừa luôn đúng vừa luôn nhanh — bạn chỉ đang chọn mình muốn sai ở đâu trên dải đó.</p>

  <h2 id="ng-h-cng-khng-ni-tht">Đồng hồ cũng không nói thật</h2>

  <p>Đây là phần thực sự khiến tôi bất ngờ, vì tôi vẫn luôn cho rằng “cứ nhìn timestamp” là một cách an toàn, nhàm chán để biết sự kiện nào trong hai sự kiện xảy ra trước. Không phải vậy. Mỗi máy có đồng hồ vật lý riêng, và các đồng hồ này <strong>trôi</strong> — chúng chạy nhanh hơn hoặc chậm hơn một chút so với thời gian thật, và so với nhau, hoàn toàn do khiếm khuyết phần cứng. Hệ thống cố gắng sửa điều này bằng NTP (Network Time Protocol), định kỳ đồng bộ với một đồng hồ tham chiếu qua mạng — nhưng chính việc sửa đó cũng đi qua cùng mạng không đáng tin cậy đã nói ở trên, và có thể bị trễ, hoặc đôi khi làm đồng hồ nhảy lùi hoặc nhảy tiến để sửa một độ lệch lớn.</p>

  <p>Hệ quả thực tế mà cuốn sách nhấn mạnh: nếu hai máy khác nhau mỗi máy đóng dấu thời gian một sự kiện cục bộ, và sau đó bạn cố sắp xếp các sự kiện đó bằng cách so sánh timestamp, bạn có thể sắp xếp <em>sai</em> thứ tự, vì hai đồng hồ đó chưa bao giờ đồng bộ hoàn hảo với nhau ngay từ đầu. Đây là lý do vì sao “last write wins” (ghi sau cùng thắng), một chiến lược nghe có vẻ hợp lý ban đầu, lại âm thầm nguy hiểm trong một hệ phân tán — “sau cùng” theo đồng hồ <em>của ai</em>? Vấn đề này nghiêm trọng đến mức Google đã xây riêng một hạ tầng chuyên dụng, <strong>TrueTime</strong>, dành riêng cho database Spanner của họ — thay vì giả vờ đồng hồ chính xác tuyệt đối, TrueTime báo cáo thời gian như một <em>khoảng</em> thành thật (“thời gian thật nằm đâu đó giữa X và Y”), và hệ thống được thiết kế để chờ hết khoảng bất định đó thay vì đánh cược vào một timestamp đơn lẻ, có thể sai.</p>

  <h2 id="process-pause-code-ca-bn-c-th-dng-li-m-bn-khng-h-bit">Process pause: code của bạn có thể dừng lại mà bạn không hề biết</h2>

  <p>Nguồn rắc rối thứ ba là thứ tôi thực sự chưa bao giờ nghĩ tới: ngay cả một chương trình không crash và không bị kẹt chờ mạng vẫn có thể đơn giản là <em>ngừng chạy</em> trong một khoảng thời gian không đoán trước được, vì những lý do hoàn toàn nằm ngoài tầm kiểm soát của chính nó. Ví dụ chính trong sách là <strong>garbage collection (GC) pause</strong> — nhiều ngôn ngữ lập trình định kỳ tạm dừng toàn bộ chương trình của bạn để dọn dẹp bộ nhớ không dùng tới, và lần tạm dừng này đôi khi có thể kéo dài hơn nhiều so với dự kiến khi tải cao. Từ góc nhìn của mọi máy khác, một process đang tạm dừng vì một chu kỳ GC dài trông y hệt như một process đã crash hoặc một mạng đã sập — vì, một lần nữa, tất cả những gì họ quan sát được là “tôi chưa nghe được gì từ nó một lúc rồi.”</p>

  <p>Hệ quả gây bất an: một hệ phân tán phải được thiết kế để chịu đựng việc một node im lặng trong một khoảng thời gian không đoán trước rồi <em>sống lại và tiếp tục đúng chỗ nó đã dừng</em>, có thể vẫn tin rằng nó đang giữ một lock hay vai trò leader mà thực ra nó đã mất trong lúc tạm dừng. Đây là lý do các hệ thống thật dùng kỹ thuật như <strong>fencing token</strong> — một con số tăng dần đơn điệu được cấp kèm mỗi lease hay lock, để nếu một node tạm dừng thức dậy và cố hành động dựa trên quyền hạn đã cũ, hệ thống có thể nhận ra token của nó đã lỗi thời và từ chối hành động đó.</p>

  <h2 id="bi-hc-thnh-tht-ng-gi-nh-g-c-xc-minh-mi-th">Bài học thành thật: đừng giả định gì cả, xác minh mọi thứ</h2>

  <p>Kết luận thực sự của chương không phải một cách sửa cụ thể — nó là một sự thay đổi trong thái độ. Trong một chương trình chạy trên một máy đơn lẻ, bạn thường có thể tin rằng nếu một hàm trả về, nó thực sự đã chạy, và chạy đúng một lần. Trong một hệ phân tán, không điều nào trong số đó an toàn để giả định: message có thể bị trễ, bị nhân đôi, hoặc bị mất; đồng hồ không thể được tin tưởng hoàn toàn để sắp xếp sự kiện; và một node trông như đã chết có thể chỉ đang tạm dừng, và có thể sống lại với niềm tin vào điều gì đó không còn đúng nữa. Mọi giao thức được nói tới trong các chương tiếp theo — đặc biệt là <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consensus và consistency</a> — tồn tại chính xác để xây dựng những đảm bảo đáng tin cậy <em>trên nền</em> của cái nền tảng thực sự không đáng tin cậy này, chứ không phải để giả vờ rằng nền tảng đó vững chắc hơn thực tế.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 8" /><category term="Distributed Systems" /><category term="Network Faults" /><category term="Clocks" /><summary type="html"><![CDATA[The chapter that explains why almost every scary outage story from a big tech company traces back to one of two things not behaving the way engineers assumed: the network, or the clock.]]></summary></entry><entry><title type="html">DDIA Chapter 7: Transactions</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-7-transactions/" rel="alternate" type="text/html" title="DDIA Chapter 7: Transactions" /><published>2026-09-15T09:30:00+07:00</published><updated>2026-09-15T09:30:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-7-transactions</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-7-transactions/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 7: Transactions.</em></p>

  <p>This chapter opens Part II properly, and it’s about a word I’d been using casually for years without ever needing to define it precisely: <strong>transaction</strong>. Loosely, it means “a group of operations that the database treats as one single unit” — but the whole chapter is really about what that promise is actually worth, because it turns out different databases, and even the same database configured differently, promise wildly different things while all technically calling it a “transaction.”</p>

  <h2 id="acid-is-four-separate-promises-not-one-word">ACID is four separate promises, not one word</h2>

  <p>Most people (myself included, until reading this properly) treat “ACID” as one blob meaning “the database is safe.” The book insists on splitting it into four genuinely distinct guarantees, and I found it useful to think about which specific bug each one actually prevents:</p>

  <ul>
    <li><strong>Atomicity</strong> — a transaction either fully happens or fully doesn’t; there’s no “half-finished” state visible to anyone. If your code transfers money by subtracting from one account and adding to another, atomicity is what guarantees you never end up in a world where the money left one account but never arrived at the other, even if the server crashes halfway through.</li>
    <li><strong>Consistency</strong> — here the book makes an important, easy-to-miss point: this is actually an application-level property, not something the database can enforce all on its own. The database can enforce specific rules you define (like “balances can’t go negative”), but “consistency” itself just means your data always satisfies whatever invariants your application cares about — the database is a tool that helps you keep that promise, not something that magically knows what your invariants are.</li>
    <li><strong>Isolation</strong> — concurrent transactions shouldn’t be able to see each other’s half-finished work. This is the guarantee that gets the most attention in the chapter, because it’s the one with the most ways to go subtly, dangerously wrong.</li>
    <li><strong>Durability</strong> — once a transaction says “committed,” it stays committed, even if the machine loses power one second later.</li>
  </ul>

  <h2 id="where-isolation-actually-breaks-down">Where isolation actually breaks down</h2>

  <p>The chapter’s real substance is a tour of specific ways concurrent transactions can step on each other, and honestly, several of these were bugs I would not have predicted on my own:</p>

  <p><strong>Dirty reads</strong> — one transaction sees a change from another transaction that hasn’t been committed yet, and might get rolled back. Imagine seeing your bank balance already reflect a transfer that then gets cancelled a moment later — you briefly saw money that was never actually real.</p>

  <p><strong>Lost updates</strong> — two transactions read the same value, both decide what to write based on that value, and one write silently overwrites the other, throwing away one of the two updates. The classic version of this, which the book uses and which I found genuinely clarifying:</p>

  <p><img src="/assets/images/ddia/ch7-lost-update.svg" alt="Two shoppers both read &quot;1 item in stock,&quot; both decide to buy it, and both writes succeed — the store just oversold an item it never actually had two of" /></p>

  <p>Two customers both check a product page at nearly the same instant, both see “1 in stock,” both click buy. If the database naively lets each transaction read the stock count, decide to subtract one, and write the new value back — without anything stopping the second write from blindly overwriting the first — you end up having sold the same last unit twice. Nobody’s individual code was wrong; the <em>interleaving</em> was the bug.</p>

  <p><strong>Write skew</strong> — a subtler cousin of lost updates, where two transactions read overlapping data, each makes a decision that’s individually fine given what it read, but the combination violates a rule neither transaction technically broke on its own. The book’s example: a hospital requires at least one doctor on call at all times; two doctors, seeing “there are currently two of us on call,” each independently decide it’s safe for <em>them</em> to go off call — and now there are zero.</p>

  <h2 id="isolation-levels-how-much-protection-youre-actually-buying">Isolation levels: how much protection you’re actually buying</h2>

  <p>Because preventing every one of these perfectly, all the time, is expensive, real databases offer a menu of <strong>isolation levels</strong>, and the honest, slightly uncomfortable truth the book states plainly: many databases don’t default to the strongest one, because the strongest one is slower.</p>

  <ul>
    <li><strong>Read committed</strong> — the weakest level covered in depth; it just guarantees you’ll never see another transaction’s <em>uncommitted</em> changes (no dirty reads). It does nothing to stop lost updates or write skew.</li>
    <li><strong>Snapshot isolation</strong> — each transaction sees a consistent snapshot of the database as it looked at the moment the transaction started, so it’s immune to dirty reads and most read-related weirdness. It’s a very popular default (Postgres calls its version of this “repeatable read”) because it’s much faster than full serializability, but it still doesn’t fully protect against write skew.</li>
    <li><strong>Serializable</strong> — the strongest level: the database guarantees the result is <em>as if</em> every transaction ran one at a time, in some order, with zero overlap, even though under the hood they might actually run concurrently for performance. This is the only level that genuinely closes every one of the bugs above, and it’s also the most expensive, because achieving that guarantee under real concurrent load takes real work — either literally running things one at a time, or detecting and aborting/retrying transactions that would have conflicted.</li>
  </ul>

  <p>The lesson that stuck with me: “my database uses transactions” tells you almost nothing on its own. The actual question that matters is “which isolation level, specifically, and did anyone deliberately choose it, or is it just whatever the database defaulted to?”</p>

  <h2 id="why-this-chapter-sets-up-everything-after-it">Why this chapter sets up everything after it</h2>

  <p>Reading this right after <a href="/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/">storage engines</a> and right before the more distributed-systems-heavy chapters made the sequencing click for me: this chapter is entirely about correctness on a <em>single</em> machine (or a single database, at least) under concurrent access. The much harder version of this same problem — transactions that span <em>multiple</em> machines, where a network can fail in the middle — is exactly what makes the next couple of chapters (<a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">the trouble with distributed systems</a> and <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consistency and consensus</a>) so much harder than this one. You can’t really appreciate why distributed transactions are painful until you’ve seen how much careful engineering it already takes to get transactions right on one honest, reliable machine.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 7: Transactions.</em></p>

  <p>Chương này chính thức mở đầu Phần II, và nó nói về một từ tôi vẫn dùng một cách tùy tiện bao năm nay mà chưa bao giờ cần định nghĩa chính xác: <strong>transaction</strong> (giao dịch). Nói một cách lỏng lẻo, nó nghĩa là “một nhóm thao tác mà database coi là một khối duy nhất” — nhưng cả chương thực chất nói về việc lời hứa đó thực sự đáng giá bao nhiêu, vì hóa ra các database khác nhau, và ngay cả cùng một database được cấu hình khác nhau, hứa hẹn những thứ khác nhau rất nhiều trong khi về mặt kỹ thuật đều gọi đó là “transaction.”</p>

  <h2 id="acid-l-bn-li-ha-ring-bit-khng-phi-mt-t">ACID là bốn lời hứa riêng biệt, không phải một từ</h2>

  <p>Hầu hết mọi người (kể cả tôi, cho tới khi đọc kỹ chương này) coi “ACID” là một khối duy nhất nghĩa là “database an toàn.” Cuốn sách khăng khăng tách nó thành bốn đảm bảo thực sự khác nhau, và tôi thấy hữu ích khi nghĩ về việc mỗi cái thực sự ngăn con bug cụ thể nào:</p>

  <ul>
    <li><strong>Atomicity (tính nguyên tử)</strong> — một transaction hoặc xảy ra hoàn toàn, hoặc không xảy ra chút nào; không có trạng thái “làm dở dang” nào lộ ra cho ai thấy. Nếu code của bạn chuyển tiền bằng cách trừ từ một tài khoản và cộng vào tài khoản khác, atomicity là thứ đảm bảo bạn không bao giờ rơi vào tình huống tiền đã rời khỏi một tài khoản nhưng chưa bao giờ tới tài khoản kia, kể cả khi server crash giữa chừng.</li>
    <li><strong>Consistency (tính nhất quán)</strong> — đây là chỗ cuốn sách đưa ra một điểm quan trọng, dễ bỏ sót: đây thực ra là một tính chất ở tầng ứng dụng, không phải thứ database có thể tự mình đảm bảo hoàn toàn. Database có thể ép các quy tắc cụ thể bạn định nghĩa (như “số dư không được âm”), nhưng “consistency” tự nó chỉ nghĩa là dữ liệu của bạn luôn thỏa mãn bất kỳ bất biến nào ứng dụng của bạn quan tâm — database là công cụ giúp bạn giữ lời hứa đó, không phải thứ tự nhiên biết bất biến của bạn là gì.</li>
    <li><strong>Isolation (tính cô lập)</strong> — các transaction chạy đồng thời không nên thấy được công việc làm dở của nhau. Đây là đảm bảo được chương này chú ý nhiều nhất, vì nó có nhiều cách âm thầm, nguy hiểm để sai nhất.</li>
    <li><strong>Durability (tính bền vững)</strong> — một khi transaction báo “đã commit,” nó vẫn ở trạng thái đã commit, kể cả khi máy mất điện đúng một giây sau đó.</li>
  </ul>

  <h2 id="ch-isolation-thc-s-sp-">Chỗ isolation thực sự sụp đổ</h2>

  <p>Nội dung thực chất của chương là một chuyến tham quan các cách cụ thể mà các transaction đồng thời có thể giẫm chân lên nhau, và thành thật mà nói, vài cái trong số này là bug tôi sẽ không tự đoán ra được:</p>

  <p><strong>Dirty reads (đọc bẩn)</strong> — một transaction thấy được thay đổi từ một transaction khác chưa được commit, và thay đổi đó có thể bị rollback sau đó. Hãy tưởng tượng thấy số dư ngân hàng của bạn đã phản ánh một lượt chuyển tiền rồi bị hủy ngay sau đó — bạn đã thấy thoáng qua số tiền chưa bao giờ thực sự có thật.</p>

  <p><strong>Lost updates (mất cập nhật)</strong> — hai transaction đọc cùng một giá trị, cả hai đều quyết định ghi gì đó dựa trên giá trị đó, và một lượt ghi âm thầm ghi đè lên lượt kia, làm mất đi một trong hai cập nhật. Phiên bản kinh điển của việc này, mà cuốn sách dùng và tôi thấy thực sự làm sáng tỏ vấn đề:</p>

  <p><img src="/assets/images/ddia/ch7-lost-update.svg" alt="Hai người mua cùng đọc thấy &quot;còn 1 sản phẩm trong kho,&quot; cả hai đều quyết định mua nó, và cả hai lượt ghi đều thành công — cửa hàng vừa bán vượt một món hàng chưa bao giờ thực sự có hai cái" /></p>

  <p>Hai khách hàng cùng xem một trang sản phẩm gần như cùng lúc, cả hai thấy “còn 1 trong kho,” cả hai đều bấm mua. Nếu database ngây thơ để mỗi transaction đọc số lượng tồn kho, quyết định trừ đi một, rồi ghi giá trị mới trở lại — mà không có gì ngăn lượt ghi thứ hai mù quáng ghi đè lên lượt đầu — bạn sẽ kết thúc bằng việc bán đúng đơn vị cuối cùng đó hai lần. Không đoạn code riêng lẻ nào sai cả; chính <em>cách chúng xen kẽ nhau</em> mới là bug.</p>

  <p><strong>Write skew (lệch ghi)</strong> — một người anh em họ tinh vi hơn của lost update, nơi hai transaction đọc dữ liệu chồng lấn nhau, mỗi transaction đưa ra một quyết định riêng lẻ là ổn dựa trên những gì nó đọc được, nhưng kết hợp lại thì vi phạm một quy tắc mà không transaction nào về mặt kỹ thuật tự mình phá vỡ. Ví dụ trong sách: một bệnh viện yêu cầu luôn có ít nhất một bác sĩ trực; hai bác sĩ, thấy “hiện đang có hai người trong chúng ta trực,” mỗi người độc lập quyết định <em>mình</em> có thể an toàn rời ca trực — và giờ còn lại số không.</p>

  <h2 id="isolation-level-bn-thc-s-ang-mua-bao-nhiu-s-bo-v">Isolation level: bạn thực sự đang mua bao nhiêu sự bảo vệ</h2>

  <p>Vì ngăn chặn hoàn hảo mọi thứ trên, mọi lúc, là tốn kém, các database thật cung cấp một thực đơn <strong>isolation level</strong>, và sự thật thành thật, hơi khó chịu mà cuốn sách nói thẳng: nhiều database không mặc định dùng mức mạnh nhất, vì mức mạnh nhất chậm hơn.</p>

  <ul>
    <li><strong>Read committed</strong> — mức yếu nhất được nói kỹ; nó chỉ đảm bảo bạn sẽ không bao giờ thấy thay đổi <em>chưa commit</em> của transaction khác (không có dirty read). Nó không làm gì để ngăn lost update hay write skew.</li>
    <li><strong>Snapshot isolation</strong> — mỗi transaction thấy một bức ảnh chụp nhất quán của database đúng như nó trông vào thời điểm transaction bắt đầu, nên nó miễn nhiễm với dirty read và hầu hết sự kỳ lạ liên quan tới đọc. Đây là một lựa chọn mặc định rất phổ biến (Postgres gọi phiên bản của nó là “repeatable read”) vì nó nhanh hơn nhiều so với serializable đầy đủ, nhưng nó vẫn không bảo vệ hoàn toàn khỏi write skew.</li>
    <li><strong>Serializable</strong> — mức mạnh nhất: database đảm bảo kết quả <em>như thể</em> mọi transaction chạy từng cái một, theo một thứ tự nào đó, không hề chồng lấn, kể cả khi bên dưới chúng có thể thực sự chạy đồng thời để tăng hiệu năng. Đây là mức duy nhất thực sự đóng hết mọi con bug ở trên, và cũng là mức tốn kém nhất, vì đạt được đảm bảo đó dưới tải đồng thời thật đòi hỏi công sức thật — hoặc thực sự chạy từng cái một, hoặc phát hiện và hủy/thử lại các transaction lẽ ra sẽ xung đột.</li>
  </ul>

  <p>Bài học đọng lại trong tôi: “database của tôi dùng transaction” tự nó gần như không nói lên điều gì. Câu hỏi thực sự quan trọng là “isolation level cụ thể nào, và có ai chủ động chọn nó không, hay chỉ là bất kỳ thứ gì database mặc định?”</p>

  <h2 id="v-sao-chng-ny-thit-lp-nn-cho-mi-th-sau-n">Vì sao chương này thiết lập nền cho mọi thứ sau nó</h2>

  <p>Đọc chương này ngay sau <a href="/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/">storage engine</a> và ngay trước các chương thiên về hệ phân tán nhiều hơn khiến trình tự này sáng tỏ với tôi: chương này hoàn toàn nói về tính đúng đắn trên <em>một</em> máy đơn lẻ (hoặc ít nhất một database đơn lẻ) khi bị truy cập đồng thời. Phiên bản khó hơn nhiều của cùng vấn đề này — transaction trải trên <em>nhiều</em> máy, nơi mạng có thể chết giữa chừng — chính xác là thứ khiến vài chương tiếp theo (<a href="/blog/2026/09/15/ddia-chapter-8-distributed-systems-trouble/">rắc rối của hệ phân tán</a> và <a href="/blog/2026/09/15/ddia-chapter-9-consistency-and-consensus/">consistency và consensus</a>) khó hơn chương này rất nhiều. Bạn không thể thực sự thấm được vì sao distributed transaction lại đau đầu đến vậy cho tới khi thấy đã cần bao nhiêu công sức kỹ thuật cẩn thận chỉ để làm đúng transaction trên một máy trung thực, đáng tin cậy.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 7" /><category term="Transactions" /><category term="Isolation" /><summary type="html"><![CDATA[Two shoppers buy the last item in stock at the same instant. Whether your database lets that turn into a real bug depends entirely on the word 'transaction' actually meaning something.]]></summary></entry><entry><title type="html">DDIA Chapter 6: Partitioning</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-6-partitioning/" rel="alternate" type="text/html" title="DDIA Chapter 6: Partitioning" /><published>2026-09-15T09:15:00+07:00</published><updated>2026-09-15T09:15:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-6-partitioning</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-6-partitioning/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 6: Partitioning.</em></p>

  <p><a href="/blog/2026/09/15/ddia-chapter-5-replication/">Chapter 5</a> was about replication — keeping copies of the same data on multiple machines so the system survives one of them dying. This chapter is about something that sounds similar but is actually a completely separate problem: what do you do when your data is simply too big, or gets too much traffic, for <em>any single machine</em> to handle at all, even a perfectly healthy one? The answer is <strong>partitioning</strong> (also called sharding): splitting your data into smaller pieces and spreading those pieces across many machines.</p>

  <p>It’s worth sitting with why these are different problems before going further, because I originally assumed “replication” and “partitioning” were basically the same idea. They’re not. Replication answers “what if this machine dies?” Partitioning answers “what if this machine simply can’t fit or serve all the data by itself, dead or alive?” In a real system you almost always need both at once: split the data into partitions to spread the load, <em>and</em> replicate each partition so any one machine holding a piece of it can still die without losing data. Chapter 5 was about the replication half. This chapter is about the splitting half.</p>

  <h2 id="the-core-decision-how-do-you-decide-what-goes-where">The core decision: how do you decide what goes where?</h2>

  <p>Say you have a huge table of user data and ten machines to spread it across. The obvious first idea is “put user A’s data on machine 1, user B’s on machine 2,” and so on. But <em>how</em> do you decide, systematically, which machine any given piece of data belongs on? DDIA covers two main strategies, and the tradeoff between them is the whole chapter in miniature.</p>

  <p><img src="/assets/images/ddia/ch6-partition-strategies.svg" alt="Key range partitioning keeps data sorted but risks hot spots; hash partitioning spreads load evenly but loses cheap range queries" /></p>

  <p><strong>Partitioning by key range</strong> is like splitting a printed encyclopedia into volumes: Volume 1 covers entries A through D, Volume 2 covers E through H, and so on. Any entry has an obvious, predictable home based on where it falls alphabetically. The nice side effect: if you want to look up everything between “Da” and “De,” you know exactly which single volume to grab, and you can even scan through it in sorted order efficiently. The problem: if your encyclopedia happens to be about celebrities, and suddenly everyone is looking up entries starting with “S” this week for some reason, the poor volume covering “S” gets hammered with traffic while every other volume sits idle. This uneven load is called a <strong>hot spot</strong>, and it’s the classic weakness of range-based partitioning. A very real-world version of this: if you partition by timestamp (because timestamps happen to sort nicely), <em>all</em> new data being written right now lands on whichever single partition owns “today,” no matter how many machines you have.</p>

  <p><strong>Partitioning by hash of the key</strong> fixes the hot-spot problem by deliberately throwing away the nice ordering. Instead of “wherever this key alphabetically belongs,” you run the key through a hash function (a formula that turns any input into a number that looks essentially random and evenly spread out) and use that number to pick a machine. Now the data is scattered evenly and unpredictably across all your machines, so no single machine gets stuck holding “today’s” data or “everything starting with S.” The tradeoff: you’ve lost the ability to efficiently ask for a <em>range</em> of keys (like “everything from Monday to Friday”), because a range of real keys no longer corresponds to a nearby range of hash values — they’re scattered randomly across every machine, so a range query now has to ask <em>every single machine</em> instead of just one.</p>

  <p>Neither approach is “better” in general — it genuinely depends on whether your application needs efficient range queries more than it needs even load distribution, and that’s a real design decision, not something a database can automatically get right for you.</p>

  <h2 id="the-celebrity-problem">The celebrity problem</h2>

  <p>Even hash partitioning, which is supposed to spread load evenly, can still get ambushed by a single extremely popular piece of data — DDIA calls this a <strong>skewed workload</strong>. Imagine a social media platform where load is partitioned by user ID, which is normally a perfectly reasonable, evenly-distributed choice. Then a celebrity with a hundred million followers posts something, and suddenly one single user ID (the celebrity’s) is generating a wildly disproportionate amount of read and write traffic compared to literally every other user in the system — hashing didn’t help, because the <em>problem</em> was never that user IDs were unevenly spread out, it’s that a small number of specific keys are just genuinely, unavoidably way “hotter” than the rest. One practical trick the book mentions: for a small number of known hot keys, you can append a random suffix (splitting the celebrity’s data across, say, 20 sub-keys instead of one) purely to spread that one key’s load across multiple machines — at the cost of now having to gather and combine results from all 20 sub-keys whenever you need “the real” data for that key.</p>

  <h2 id="searching-by-something-other-than-the-partition-key-gets-messy">Searching by something other than the partition key gets messy</h2>

  <p>Partitioning works cleanly as long as you’re always looking things up by the exact key you partitioned on. Real applications also want to search by <em>other</em> attributes — “find all cars that are red,” when your data is partitioned by car ID, not by color. This is where secondary indexes (an index on a column other than the main key) run into trouble, and DDIA describes two approaches, each with a real cost:</p>

  <ul>
    <li><strong>Local indexes (partitioned by document):</strong> each machine keeps an index covering only the data that already physically lives on it — like each branch library keeping an index of only the books in that specific building. Writing new data is simple (you only ever touch the one machine that owns it), but a search like “find all red cars” now has to ask <em>every single machine</em> to check its own local index and then combine all the answers together — a pattern called <strong>scatter/gather</strong>. It works, but it’s slow and its cost grows with the number of machines you have.</li>
    <li><strong>Global indexes (partitioned by term):</strong> instead, you maintain one shared index — say, split up by color instead of by car ID — covering the <em>entire</em> dataset regardless of which machine each car physically lives on. Now “find all red cars” is fast, because you only have to check the one part of the index that covers “red.” The cost shows up on the write side instead: adding one new car might now require updating an index partition that lives on a completely different machine from where the car’s own data lives, and keeping that update honest and consistent across machines is genuinely harder to get right.</li>
  </ul>

  <p>I found this a really clean example of a pattern that shows up everywhere in system design: you can’t make both reads and writes maximally cheap and simple at the same time — you’re choosing which side absorbs the complexity, based on which one your application actually does more of.</p>

  <h2 id="rebalancing-without-a-truck-full-of-movers">Rebalancing without a truck full of movers</h2>

  <p>Systems grow. You’ll eventually add more machines, and existing data has to get reshuffled across the new, larger set of machines — this is called <strong>rebalancing</strong>. The naive approach — literally computing <code class="language-plaintext highlighter-rouge">hash(key) mod number_of_machines</code> to decide where something lives — sounds reasonable until you realize that adding just <em>one</em> extra machine changes the divisor, which changes the answer for almost <em>every single key in the entire database</em> simultaneously. That’s the data equivalent of renumbering every house on every street in a city just because one new street got added — a genuinely enormous, unnecessary amount of data movement for a small change in capacity.</p>

  <p>Real systems avoid this in one of a couple of ways:</p>

  <ul>
    <li><strong>Start with far more partitions than you currently have machines</strong> — say, always keep exactly 1,000 fixed partitions in existence, even on day one with just a handful of machines. Growing the cluster then just means moving some of those already-existing partitions onto the new machines, rather than recomputing where every single piece of data belongs from scratch.</li>
    <li><strong>Split partitions dynamically as they grow</strong>, the way a very popular Wikipedia article’s edit history might eventually get split into “part 1” and “part 2” once it gets unmanageably long — a partition that grows past a size threshold gets automatically split in two, and one of the two halves can then be handed off to a different, less busy machine.</li>
  </ul>

  <p>Either way, the goal is the same: change <em>where a chunk of data lives</em> without needing to change <em>how the data is chunked in the first place</em> every time the cluster’s size changes even slightly.</p>

  <h2 id="how-does-a-client-even-know-which-machine-to-ask">How does a client even know which machine to ask?</h2>

  <p>If data is scattered across dozens of machines and can be reshuffled at any time as the cluster grows, a client application obviously can’t just hardcode “user data lives on machine 7.” Something in the system has to track, in real time, the current answer to “which machine currently owns this piece of data,” and make that answer available to whoever’s asking. DDIA describes a few shapes this can take — a client might ask any machine and get redirected to the right one, or there might be a dedicated routing layer in front of everything, or the client itself might keep track directly — but in practice, most real systems solve this by leaning on a separate, small, extremely reliable <strong>coordination service</strong> (ZooKeeper is the most commonly cited example) that acts like a shared, trusted bulletin board: every machine posts “here’s what I currently own” to this bulletin board, and anyone who needs to route a request — the client, a routing tier, or another machine — just checks the board instead of needing to independently track every change themselves.</p>

  <h2 id="tying-this-back-to-chapter-5">Tying this back to Chapter 5</h2>

  <p>The thing I keep coming back to after finishing this chapter: replication and partitioning solve genuinely different problems, but almost no real system uses just one of them. You partition to spread out <em>load and size</em> across many machines, and you replicate <em>each partition</em> so that any one of those machines dying doesn’t cost you the data it was holding. Chapter 5 explained how to keep multiple copies of one dataset in sync. This chapter explained how to split one enormous dataset into pieces small enough for a single machine to reasonably own in the first place. Put together, that’s most of what “distributed database” actually means in practice — everything else is refinements on top of these two decisions.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 6: Partitioning.</em></p>

  <p><a href="/blog/2026/09/15/ddia-chapter-5-replication/">Chương 5</a> nói về replication — giữ nhiều bản sao của cùng dữ liệu trên nhiều máy để hệ thống sống sót khi một máy chết. Chương này nghe có vẻ tương tự nhưng thực ra là một vấn đề hoàn toàn khác: bạn làm gì khi dữ liệu của mình đơn giản là quá lớn, hoặc nhận quá nhiều traffic, đến mức <em>bất kỳ một máy đơn lẻ nào</em> cũng không thể xử lý nổi, kể cả khi máy đó hoàn toàn khỏe mạnh? Câu trả lời là <strong>partitioning</strong> (còn gọi là sharding): chia dữ liệu thành nhiều mảnh nhỏ hơn và trải chúng ra trên nhiều máy.</p>

  <p>Đáng để dừng lại suy nghĩ vì sao hai thứ này khác nhau trước khi đi tiếp, vì ban đầu tôi cứ nghĩ “replication” và “partitioning” về cơ bản là một ý tưởng. Không phải vậy. Replication trả lời câu hỏi “nếu máy này chết thì sao?” Partitioning trả lời câu hỏi “nếu máy này đơn giản là không thể chứa hoặc phục vụ nổi toàn bộ dữ liệu, dù sống hay chết, thì sao?” Trong một hệ thống thật, bạn hầu như luôn cần cả hai cùng lúc: chia dữ liệu thành các partition để trải tải ra, <em>và</em> replicate từng partition để bất kỳ máy nào đang giữ một mảnh trong đó chết đi cũng không làm mất dữ liệu. Chương 5 nói về nửa replication. Chương này nói về nửa chia nhỏ.</p>

  <h2 id="quyt-nh-ct-li-lm-sao-bit-ci-g-thuc-v-u">Quyết định cốt lõi: làm sao biết cái gì thuộc về đâu?</h2>

  <p>Giả sử bạn có một bảng dữ liệu người dùng khổng lồ và mười máy để trải nó ra. Ý tưởng đầu tiên hiển nhiên là “để dữ liệu người dùng A trên máy 1, người dùng B trên máy 2,” cứ thế. Nhưng bạn quyết định <em>như thế nào</em>, một cách có hệ thống, xem một mẩu dữ liệu bất kỳ thuộc về máy nào? DDIA trình bày hai chiến lược chính, và sự đánh đổi giữa chúng chính là cả chương này thu nhỏ lại.</p>

  <p><img src="/assets/images/ddia/ch6-partition-strategies.svg" alt="Partitioning theo dải key giữ dữ liệu có thứ tự nhưng dễ gặp hot spot; partitioning theo hash rải tải đều nhưng mất khả năng truy vấn theo dải rẻ tiền" /></p>

  <p><strong>Partitioning theo dải key (key range)</strong> giống như chia một bộ bách khoa toàn thư in giấy thành các tập: Tập 1 gồm các mục từ A đến D, Tập 2 từ E đến H, cứ thế. Bất kỳ mục từ nào cũng có một “nhà” rõ ràng, dự đoán được, dựa theo vị trí của nó trong bảng chữ cái. Hiệu ứng phụ hay ho: nếu bạn muốn tra mọi thứ từ “Da” đến “De,” bạn biết chính xác cần lấy đúng một tập nào, và thậm chí có thể lướt qua nó theo thứ tự đã sắp xếp một cách hiệu quả. Vấn đề: nếu bộ bách khoa toàn thư của bạn tình cờ là về người nổi tiếng, và tuần này vì lý do gì đó mọi người đều tra các mục bắt đầu bằng chữ “S,” thì tội nghiệp cái tập phủ chữ “S” bị dội bom traffic trong khi mọi tập khác nằm không. Tải không đồng đều này gọi là <strong>hot spot</strong>, và đó là điểm yếu kinh điển của partitioning theo dải. Một phiên bản rất thực tế của điều này: nếu bạn partition theo timestamp (vì timestamp tình cờ sắp xếp rất gọn gàng), <em>toàn bộ</em> dữ liệu mới đang được ghi ngay lúc này sẽ dồn vào đúng một partition đang sở hữu “hôm nay,” bất kể bạn có bao nhiêu máy đi nữa.</p>

  <p><strong>Partitioning theo hash của key</strong> giải quyết vấn đề hot spot bằng cách cố tình vứt bỏ luôn cái thứ tự gọn gàng đó. Thay vì “thuộc về đâu theo bảng chữ cái,” bạn chạy key qua một hàm hash (một công thức biến bất kỳ đầu vào nào thành một con số trông gần như ngẫu nhiên và trải đều) rồi dùng con số đó để chọn máy. Giờ dữ liệu được rải đều và không thể đoán trước trên tất cả các máy, nên không máy nào bị kẹt phải giữ “dữ liệu hôm nay” hay “mọi thứ bắt đầu bằng S.” Đánh đổi: bạn mất khả năng hỏi hiệu quả về một <em>dải</em> key (kiểu “mọi thứ từ thứ Hai đến thứ Sáu”), vì một dải key thật không còn tương ứng với một dải giá trị hash gần nhau nữa — chúng bị rải ngẫu nhiên trên mọi máy, nên giờ một truy vấn theo dải phải hỏi <em>từng máy một</em> thay vì chỉ một máy.</p>

  <p>Không cách nào “tốt hơn” nói chung — nó thực sự phụ thuộc vào việc ứng dụng của bạn cần truy vấn theo dải hiệu quả hơn hay cần tải phân bố đều hơn, và đó là một quyết định thiết kế thật sự, không phải thứ database có thể tự động làm đúng thay bạn.</p>

  <h2 id="vn--ngi-ni-ting">Vấn đề “người nổi tiếng”</h2>

  <p>Ngay cả hash partitioning, thứ vốn được kỳ vọng trải tải đều, vẫn có thể bị phục kích bởi đúng một mẩu dữ liệu cực kỳ nổi tiếng — DDIA gọi đây là <strong>skewed workload</strong> (tải lệch). Tưởng tượng một nền tảng mạng xã hội mà tải được partition theo user ID, vốn dĩ là một lựa chọn hoàn toàn hợp lý, phân bố đều. Rồi một người nổi tiếng với hàng trăm triệu follower đăng một bài, và đột nhiên đúng một user ID (của người nổi tiếng đó) tạo ra một lượng traffic đọc/ghi lệch hẳn so với mọi người dùng khác trong hệ thống — hashing không giúp được gì, vì <em>vấn đề</em> chưa bao giờ là user ID phân bố không đều, mà là một số ít key cụ thể đơn giản là “nóng” hơn hẳn phần còn lại một cách không thể tránh khỏi. Một mẹo thực tế cuốn sách nhắc tới: với một số ít key nóng đã biết trước, bạn có thể gắn thêm một hậu tố ngẫu nhiên (chia dữ liệu của người nổi tiếng ra thành, ví dụ, 20 sub-key thay vì một) chỉ để trải tải của đúng key đó ra nhiều máy — đổi lại là giờ bạn phải gom và kết hợp kết quả từ cả 20 sub-key mỗi khi cần “dữ liệu thật” của key đó.</p>

  <h2 id="tm-kim-theo-th-khc-ngoi-partition-key-th-rc-ri">Tìm kiếm theo thứ khác ngoài partition key thì rắc rối</h2>

  <p>Partitioning hoạt động gọn gàng miễn là bạn luôn tra cứu bằng đúng key mình đã dùng để partition. Ứng dụng thực tế cũng muốn tìm theo <em>thuộc tính khác</em> — “tìm mọi xe màu đỏ,” trong khi dữ liệu của bạn được partition theo ID xe, không phải theo màu. Đây là chỗ secondary index (index trên một cột khác ngoài key chính) gặp rắc rối, và DDIA mô tả hai cách tiếp cận, mỗi cách đều có cái giá thật:</p>

  <ul>
    <li><strong>Local index (partition theo document):</strong> mỗi máy giữ một index chỉ phủ dữ liệu đã nằm sẵn trên chính nó — giống như mỗi chi nhánh thư viện chỉ giữ index của những cuốn sách trong đúng tòa nhà đó. Ghi dữ liệu mới thì đơn giản (bạn chỉ đụng đúng một máy sở hữu nó), nhưng một truy vấn kiểu “tìm mọi xe đỏ” giờ phải hỏi <em>từng máy một</em> để kiểm tra index cục bộ của nó rồi gộp mọi câu trả lời lại — một pattern gọi là <strong>scatter/gather</strong>. Nó hoạt động, nhưng chậm và chi phí tăng theo số máy bạn có.</li>
    <li><strong>Global index (partition theo term):</strong> thay vào đó, bạn duy trì một index dùng chung — ví dụ, chia theo màu thay vì theo ID xe — phủ <em>toàn bộ</em> dữ liệu bất kể mỗi xe thực sự nằm trên máy nào. Giờ “tìm mọi xe đỏ” nhanh, vì bạn chỉ cần kiểm tra đúng phần index phủ “đỏ.” Cái giá lộ ra ở phía ghi thay vào đó: thêm một xe mới giờ có thể cần cập nhật một partition index nằm trên một máy hoàn toàn khác với máy chứa dữ liệu của chính chiếc xe đó, và giữ cho cập nhật đó trung thực, nhất quán qua nhiều máy thực sự khó làm đúng hơn.</li>
  </ul>

  <p>Tôi thấy đây là một ví dụ rất gọn gàng cho một pattern xuất hiện khắp nơi trong thiết kế hệ thống: bạn không thể làm cho cả đọc lẫn ghi đều rẻ và đơn giản tối đa cùng lúc — bạn đang chọn bên nào hấp thụ sự phức tạp, dựa trên việc ứng dụng của bạn thực sự làm nhiều thao tác nào hơn.</p>

  <h2 id="cn-bng-li-m-khng-cn-c-xe-ti-ch-">Cân bằng lại mà không cần cả xe tải chở đồ</h2>

  <p>Hệ thống rồi sẽ lớn lên. Bạn sẽ thêm máy mới vào lúc nào đó, và dữ liệu hiện có phải được xáo trộn lại trên tập máy mới, lớn hơn — gọi là <strong>rebalancing</strong>. Cách tiếp cận ngây thơ — tính thẳng <code class="language-plaintext highlighter-rouge">hash(key) mod số_lượng_máy</code> để quyết định dữ liệu nằm ở đâu — nghe có vẻ hợp lý cho đến khi bạn nhận ra chỉ cần thêm <em>một</em> máy thôi cũng đổi số chia, làm đổi luôn câu trả lời cho gần như <em>mọi key trong toàn bộ database</em> cùng lúc. Đó là phiên bản dữ liệu của việc đánh số lại mọi căn nhà trên mọi con phố trong thành phố chỉ vì có thêm một con phố mới — một lượng di chuyển dữ liệu khổng lồ, không cần thiết cho một thay đổi nhỏ về công suất.</p>

  <p>Hệ thống thật tránh điều này theo một trong vài cách:</p>

  <ul>
    <li><strong>Bắt đầu với số partition nhiều hơn hẳn số máy hiện có</strong> — ví dụ, luôn giữ đúng 1.000 partition cố định ngay từ ngày đầu, kể cả khi chỉ có vài máy. Mở rộng cluster khi đó chỉ đơn giản là chuyển một số partition đã tồn tại sẵn sang máy mới, thay vì tính lại từ đầu xem mỗi mẩu dữ liệu thuộc về đâu.</li>
    <li><strong>Tự động chia nhỏ partition khi chúng lớn lên</strong>, giống như lịch sử chỉnh sửa của một bài Wikipedia cực kỳ nổi tiếng cuối cùng có thể được chia thành “phần 1” và “phần 2” khi nó dài đến mức khó quản lý — một partition vượt quá một ngưỡng kích thước sẽ tự động bị chia đôi, và một trong hai nửa có thể được giao cho một máy khác, ít bận hơn.</li>
  </ul>

  <p>Dù theo cách nào, mục tiêu vẫn vậy: đổi <em>nơi một mẩu dữ liệu đang nằm</em> mà không cần đổi <em>cách dữ liệu được chia nhỏ ngay từ đầu</em> mỗi khi kích thước cluster thay đổi dù chỉ một chút.</p>

  <h2 id="vy-client-lm-sao-bit-phi-hi-my-no">Vậy client làm sao biết phải hỏi máy nào?</h2>

  <p>Nếu dữ liệu được rải trên hàng chục máy và có thể bị xáo trộn lại bất cứ lúc nào khi cluster lớn lên, một ứng dụng client rõ ràng không thể chỉ hardcode “dữ liệu người dùng nằm trên máy 7.” Phải có thứ gì đó trong hệ thống theo dõi, theo thời gian thực, câu trả lời hiện tại cho câu hỏi “mẩu dữ liệu này hiện đang thuộc về máy nào,” và cung cấp câu trả lời đó cho bất kỳ ai đang hỏi. DDIA mô tả vài hình dạng khác nhau cho việc này — client có thể hỏi bất kỳ máy nào rồi được chuyển hướng tới đúng máy, hoặc có thể có một tầng định tuyến riêng đứng trước mọi thứ, hoặc chính client có thể tự theo dõi trực tiếp — nhưng trong thực tế, hầu hết hệ thống thật giải quyết việc này bằng cách dựa vào một <strong>coordination service</strong> (dịch vụ điều phối) riêng, nhỏ, cực kỳ đáng tin cậy (ZooKeeper là ví dụ hay được nhắc tới nhất) hoạt động như một bảng thông báo chung, đáng tin: mỗi máy dán “đây là những gì tôi đang sở hữu” lên bảng thông báo này, và bất kỳ ai cần định tuyến một request — client, một tầng định tuyến, hay một máy khác — chỉ cần nhìn vào bảng thay vì phải tự theo dõi độc lập từng thay đổi.</p>

  <h2 id="ni-li-vi-chng-5">Nối lại với Chương 5</h2>

  <p>Điều tôi cứ nghĩ lại sau khi đọc xong chương này: replication và partitioning giải quyết những vấn đề thực sự khác nhau, nhưng gần như không hệ thống thật nào chỉ dùng một trong hai. Bạn partition để trải <em>tải và kích thước</em> ra nhiều máy, và bạn replicate <em>từng partition</em> để bất kỳ máy nào trong số đó chết đi cũng không làm mất dữ liệu nó đang giữ. Chương 5 giải thích cách giữ nhiều bản sao của một tập dữ liệu đồng bộ với nhau. Chương này giải thích cách chia một tập dữ liệu khổng lồ thành các mảnh đủ nhỏ để một máy đơn lẻ có thể sở hữu một cách hợp lý ngay từ đầu. Gộp lại, đó là phần lớn ý nghĩa thực sự của “distributed database” trong thực tế — mọi thứ còn lại chỉ là tinh chỉnh thêm trên hai quyết định này.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 6" /><category term="Partitioning" /><category term="Scalability" /><summary type="html"><![CDATA[Replication (Chapter 5) is about surviving a dead machine. This chapter is about a completely different problem: what happens when your data is just too big for one machine to hold in the first place.]]></summary></entry><entry><title type="html">DDIA Chapter 5: Replication</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-5-replication/" rel="alternate" type="text/html" title="DDIA Chapter 5: Replication" /><published>2026-09-15T09:00:00+07:00</published><updated>2026-09-15T09:00:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-5-replication</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-5-replication/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 5: Replication.</em></p>

  <p>This chapter answers a question I’d always taken for granted without really thinking through: how does a website or app keep working when one of the machines behind it just… dies? Physical machines fail — hard disks wear out, power supplies blow, someone in a data center trips over the wrong cable. The obvious fix is to keep more than one copy of your data on more than one machine, so if one dies, another one already has the data ready to go. That’s <strong>replication</strong>, and it sounds simple in one sentence, but the chapter spends its length on everything that makes it genuinely tricky to do safely — and once I worked through it, a bunch of things I used to lump together as “just high availability stuff” turned out to be distinct, specific problems.</p>

  <h2 id="failure-isnt-just-on-or-off">Failure isn’t just “on or off”</h2>

  <p>The first thing that reframed this chapter for me: I used to picture “a machine failing” as a light switch — it’s either fine or it’s dead. Real failures are messier than that. A machine can be <em>slow</em> instead of dead. A network cable can be flaky, dropping some messages but not all. A machine can be perfectly healthy but unreachable from certain other machines due to a network hiccup, while still being reachable from others — this is called a <strong>partial failure</strong>, and it’s genuinely confusing because from any single other machine’s point of view, “that server is slow to respond” and “that server is dead” look <em>identical</em> until you wait long enough to be sure.</p>

  <p>This matters because a system that only knows how to handle the clean, easy case — “the machine crashed and restarted” — falls apart the first time it hits one of these messier, more ambiguous situations. So the real goal of “high availability” isn’t “prevent every possible failure” (you can’t — hardware breaks, that’s just physics), it’s “design the system so that when something breaks, the rest of it can keep going anyway.” Which immediately implies two rules: no single machine should be the <em>only</em> copy of any piece of data, and no single machine should be the <em>only one allowed</em> to accept new writes forever.</p>

  <h2 id="three-ways-to-organize-your-copies">Three ways to organize your copies</h2>

  <p>Given that you’re going to keep several copies of the data, the next question is: who’s allowed to write to which copy, and how do the copies stay in sync? The book walks through three common setups, and I found it much easier to remember them by thinking about who’s “in charge” of writes in each one:</p>

  <p><img src="/assets/images/ddia/ch5-replication.svg" alt="Single-leader replication: one machine takes every write and streams it to the others, so if it dies, one of them can be promoted to take its place" /></p>

  <ul>
    <li><strong>Single-leader</strong> — think of a classroom with one teacher writing notes on the board, and every student just copying down whatever the teacher writes. One machine (the “leader”) is the only one allowed to accept new writes; every other machine (a “follower”) just receives a copy of everything the leader does, in order. This is easy to reason about — there’s never any doubt about which copy is the “real,” most current one — but it has an obvious weak point: if the teacher (the leader) suddenly leaves, someone has to step up and take over, and until that happens, nobody can write anything new.</li>
    <li><strong>Multi-leader</strong> — instead of one teacher, imagine several classrooms in different cities, each with its own teacher, and the teachers periodically compare notes with each other to stay in sync. Several machines can all accept writes at the same time (often used when you have offices or users in different countries, so each region can write to a nearby copy instead of one far away). The catch: if two teachers in two different cities write conflicting things onto their boards before comparing notes, <em>someone</em> has to decide which version wins, or how to merge them.</li>
    <li><strong>Leaderless</strong> — there’s no teacher at all; any student can just shout out an update, and everyone tries to make sure enough of the group has heard the latest version. Concretely, the client talks directly to several copies at once for both reads and writes, and as long as enough of them agree (a “quorum” — think of it as “enough votes to be confident”), the system considers the operation successful. This avoids ever having a single leader that can fail, but it pushes the hard problem onto figuring out, after the fact, when different copies have quietly drifted out of sync with each other.</li>
  </ul>

  <p>What I found genuinely useful here isn’t memorizing the three names — it’s noticing that none of them “solves” the hard problem, they just move it somewhere else. Single-leader moves it to “how do we safely hand off leadership when the leader dies.” Multi-leader moves it to “how do we merge two conflicting versions of the truth.” Leaderless moves it to “how does anyone know if they just read a stale copy.”</p>

  <h2 id="when-the-leader-dies-theory-meets-a-genuinely-messy-reality">When the leader dies, theory meets a genuinely messy reality</h2>

  <p>This was the part of the chapter that felt the least like abstract theory and the most like real, hard-won engineering experience. “The leader dies, so promote a follower” sounds like a one-line fix, but every step of actually doing that safely has a sharp edge hiding in it:</p>

  <ol>
    <li><strong>Figuring out the leader is actually dead</strong>, and not just briefly slow or temporarily unreachable — usually done by waiting for some timeout period with no response. Wait too short a time and you’ll trigger a false alarm every time the leader is just momentarily busy, kicking off an unnecessary and disruptive handoff. Wait too long and real outages last longer than they need to.</li>
    <li><strong>Picking which follower becomes the new leader</strong> — ideally the one with the most up-to-date copy of the data, not one that’s lagging behind and would silently lose recent writes. Choosing correctly, safely, with multiple machines possibly disagreeing about who should win, is exactly the kind of problem that needs a proper “everyone agrees on one answer” protocol — which is the entire subject of the consensus algorithms covered later, in Chapter 9.</li>
    <li><strong>Telling every client and every other follower</strong> about the new leader, so everyone starts sending writes to the right place going forward.</li>
  </ol>

  <p>The genuinely scary failure mode here is called <strong>split brain</strong>: the old leader comes back online after being unreachable for a bit, doesn’t realize a new leader has already been promoted in its absence, and keeps right on accepting writes as if nothing happened — meanwhile so does the <em>new</em> leader. Now you have two machines both convinced they’re in charge, both accepting different writes, and no clean way to know which version of events is the “real” one without manually untangling the mess afterward. Kleppmann’s blunt point here, which stuck with me: there is no timeout value that is simultaneously perfectly safe and perfectly fast. You’re always picking a point on that tradeoff, not making the tradeoff disappear.</p>

  <h2 id="even-when-nothing-fails-copies-can-still-lag-behind">Even when nothing “fails,” copies can still lag behind</h2>

  <p>Here’s a subtler problem that has nothing to do with anything actually breaking. Followers usually replicate <em>asynchronously</em> — meaning the leader doesn’t wait around for every follower to confirm before telling the original writer “done, saved” — because waiting for every single copy on every single write would make everything painfully slow. But this means followers are always at least a little bit behind the leader, and that small lag can cause genuinely confusing bugs. Picture posting a comment on a website, having it save successfully, and then immediately refreshing the page — only to see the comment has vanished, because your refresh happened to read from a follower that hadn’t caught up yet. Nothing broke. The comment is safe on the leader. But from the user’s point of view, it looks exactly like data loss.</p>

  <p>The book names a few specific promises a system can choose to make, to prevent exactly this kind of confusion:</p>

  <ul>
    <li><strong>Read-your-writes</strong> — you should always be able to see your <em>own</em> recent changes immediately, even if other people’s reads are still served from a slightly-behind copy.</li>
    <li><strong>Monotonic reads</strong> — once you’ve seen a piece of data, you should never see an <em>older</em> version of it later, which can otherwise happen if two of your reads happen to land on two different followers that aren’t equally caught up.</li>
    <li><strong>Consistent prefix reads</strong> — if one event genuinely happened before another in reality, nobody should ever see them in the reverse order.</li>
  </ul>

  <p>None of these come for free — each one is a specific thing an engineering team has to deliberately build (send a user’s own reads to the leader right after they write something, keep a user’s session pinned to the same follower, track which version of the data a client has already seen). Skip one, and you get a very specific, very reproducible bug that some real user will eventually stumble into.</p>

  <h2 id="why-this-keeps-pointing-at-chapter-9">Why this keeps pointing at Chapter 9</h2>

  <p>The thread running underneath all of this — choosing a new leader safely, avoiding split brain, agreeing on which of several conflicting writes actually “won” — is really the exact same underlying problem wearing different clothes: getting a group of machines to agree on one answer, even when some of them might be slow, crashed, or unable to talk to each other. That underlying problem has a name, <strong>consensus</strong>, and it’s serious enough to deserve its own full chapter later in the book (Chapter 9). So “high availability” isn’t really a separate topic from consensus at all — it’s consensus, applied to the one specific question that matters most when you’re keeping a system alive: <em>who is allowed to accept writes right now?</em></p>

  <h2 id="what-actually-changed-in-how-i-think-about-this">What actually changed in how I think about this</h2>

  <p>Before this chapter, “high availability” was a vague, feel-good phrase to me — something you got by “adding more servers.” After it, I see it as a set of very concrete, very deliberate tradeoffs: how long to wait before declaring a leader dead, which specific staleness guarantees you’re willing to promise your users, and how you’ll detect and untangle conflicting writes when they happen. None of that comes from the failure-free, everything-working case — it all comes from deliberately planning for the mess.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 5: Replication.</em></p>

  <p>Chương này trả lời một câu hỏi mà trước giờ tôi cứ mặc nhiên coi là hiển nhiên mà chưa thực sự nghĩ kỹ: làm sao một website hay app vẫn hoạt động khi một trong những máy đứng sau nó đột nhiên… chết? Máy móc vật lý thì sẽ hỏng — ổ cứng mòn, nguồn điện cháy, ai đó trong trung tâm dữ liệu vấp phải nhầm dây cáp. Cách sửa hiển nhiên là giữ nhiều hơn một bản sao dữ liệu trên nhiều hơn một máy, để nếu một máy chết, máy khác đã có sẵn dữ liệu, sẵn sàng phục vụ ngay. Đó là <strong>replication</strong>, nghe qua thì đơn giản trong đúng một câu, nhưng cả chương dành phần lớn nội dung cho mọi thứ khiến việc này thực sự khó làm cho an toàn — và sau khi đọc kỹ, một loạt thứ tôi từng gộp chung thành “mấy cái liên quan tới high availability” hóa ra lại là những vấn đề riêng biệt, rất cụ thể.</p>

  <h2 id="failure-khng-ch-n-gin-l-bt-hoc-tt">Failure không chỉ đơn giản là “bật hoặc tắt”</h2>

  <p>Điều đầu tiên khiến tôi nhìn lại cả chương: trước đây tôi hình dung “một máy bị lỗi” giống như một công tắc điện — hoặc là ổn, hoặc là chết hẳn. Failure thật thì lộn xộn hơn nhiều. Một máy có thể <em>chậm</em> thay vì chết hẳn. Một đường mạng có thể chập chờn, làm rớt vài message nhưng không phải tất cả. Một máy có thể hoàn toàn khỏe mạnh nhưng không tới được từ một số máy khác do trục trặc mạng, trong khi vẫn tới được từ những máy khác — gọi là <strong>partial failure</strong> (lỗi cục bộ), và nó thực sự gây rối vì từ góc nhìn của bất kỳ máy nào khác, “server đó phản hồi chậm” và “server đó đã chết” trông <em>giống hệt nhau</em> cho đến khi bạn chờ đủ lâu để chắc chắn.</p>

  <p>Điều này quan trọng vì một hệ thống chỉ biết xử lý kịch bản sạch sẽ, dễ dàng — “máy crash rồi restart lại” — sẽ sụp đổ ngay lần đầu gặp phải một trong những tình huống lộn xộn, mơ hồ hơn này. Vậy nên mục tiêu thực sự của “high availability” không phải là “ngăn chặn mọi failure có thể xảy ra” (bạn không thể — phần cứng sẽ hỏng, đó là vật lý), mà là “thiết kế hệ thống sao cho khi có gì đó hỏng, phần còn lại vẫn tiếp tục hoạt động được.” Điều này kéo theo ngay hai quy tắc: không một máy nào nên là bản sao <em>duy nhất</em> của bất kỳ mẩu dữ liệu nào, và không một máy nào nên là nơi <em>duy nhất được phép</em> nhận ghi mãi mãi.</p>

  <h2 id="ba-cch-t-chc-cc-bn-sao">Ba cách tổ chức các bản sao</h2>

  <p>Một khi đã quyết định giữ nhiều bản sao dữ liệu, câu hỏi tiếp theo là: ai được phép ghi vào bản sao nào, và các bản sao giữ đồng bộ với nhau ra sao? Cuốn sách trình bày ba cách sắp xếp phổ biến, và tôi thấy dễ nhớ hơn nhiều khi nghĩ về việc ai đang “cầm trịch” việc ghi trong từng cách:</p>

  <p><img src="/assets/images/ddia/ch5-replication.svg" alt="Single-leader replication: một máy nhận mọi lượt ghi rồi truyền lại cho các máy khác, nên nếu nó chết, một trong số các máy đó có thể được thăng cấp thay thế" /></p>

  <ul>
    <li><strong>Single-leader</strong> — hình dung một lớp học với một giáo viên duy nhất viết ghi chú lên bảng, và mọi học sinh chỉ việc chép lại những gì giáo viên viết. Một máy (gọi là “leader”) là máy duy nhất được phép nhận lượt ghi mới; mọi máy khác (gọi là “follower”) chỉ nhận một bản sao của mọi thứ leader làm, theo đúng thứ tự. Cách này dễ suy luận — không bao giờ có nghi ngờ về việc bản sao nào mới là “thật,” cập nhật nhất — nhưng có một điểm yếu hiển nhiên: nếu giáo viên (leader) đột nhiên rời đi, phải có ai đó đứng ra thay thế, và cho tới khi điều đó xảy ra, không ai ghi được gì mới cả.</li>
    <li><strong>Multi-leader</strong> — thay vì một giáo viên, hãy tưởng tượng nhiều lớp học ở nhiều thành phố khác nhau, mỗi lớp có giáo viên riêng, và các giáo viên định kỳ so sánh ghi chú với nhau để giữ đồng bộ. Nhiều máy có thể cùng nhận ghi cùng lúc (thường dùng khi bạn có văn phòng hoặc người dùng ở nhiều quốc gia, để mỗi vùng ghi vào một bản sao gần đó thay vì một bản sao ở xa). Vấn đề: nếu hai giáo viên ở hai thành phố khác nhau viết những thứ mâu thuẫn nhau lên bảng trước khi kịp so sánh ghi chú, <em>ai đó</em> phải quyết định phiên bản nào thắng, hoặc gộp chúng lại như thế nào.</li>
    <li><strong>Leaderless</strong> — chẳng có giáo viên nào cả; học sinh nào cũng có thể hét lên một cập nhật, và mọi người cố đảm bảo đủ số đông trong nhóm đã nghe được phiên bản mới nhất. Cụ thể, client nói chuyện trực tiếp với nhiều bản sao cùng lúc cho cả đọc và ghi, và miễn là đủ số bản sao đồng ý (gọi là “quorum” — cứ nghĩ như “đủ phiếu để tự tin”), hệ thống coi thao tác đó là thành công. Cách này tránh được việc có một leader duy nhất có thể chết, nhưng đẩy vấn đề khó sang việc phải tìm ra, sau khi sự việc đã xảy ra, khi nào các bản sao khác nhau đã âm thầm lệch pha nhau.</li>
  </ul>

  <p>Điều tôi thấy thực sự hữu ích ở đây không phải là học thuộc ba cái tên — mà là nhận ra không cái nào trong số đó thực sự “giải quyết” được vấn đề khó, chúng chỉ dời nó sang chỗ khác. Single-leader dời nó thành “làm sao chuyển giao quyền lãnh đạo an toàn khi leader chết.” Multi-leader dời nó thành “làm sao gộp hai phiên bản mâu thuẫn của sự thật.” Leaderless dời nó thành “làm sao ai đó biết được mình vừa đọc phải một bản sao đã cũ.”</p>

  <h2 id="khi-leader-cht-l-thuyt-ng-phi-mt-thc-t-thc-s-ln-xn">Khi leader chết, lý thuyết đụng phải một thực tế thực sự lộn xộn</h2>

  <p>Đây là phần khiến tôi cảm thấy ít giống lý thuyết trừu tượng nhất và giống kinh nghiệm kỹ thuật thật, gian nan nhất. “Leader chết, thì thăng cấp một follower” nghe như một câu sửa đơn giản, nhưng mỗi bước để thực sự làm điều đó an toàn đều ẩn chứa một góc khuất:</p>

  <ol>
    <li><strong>Xác định leader thực sự đã chết</strong>, chứ không chỉ chậm tạm thời hay tạm thời không tới được — thường làm bằng cách chờ một khoảng timeout không có phản hồi. Chờ quá ngắn thì bạn sẽ kích hoạt báo động giả mỗi khi leader chỉ đang bận tạm thời, gây ra một lần chuyển giao không cần thiết và gây rối. Chờ quá lâu thì sự cố thật kéo dài hơn mức cần thiết.</li>
    <li><strong>Chọn follower nào trở thành leader mới</strong> — lý tưởng là follower có bản sao dữ liệu mới nhất, không phải một follower đang bị trễ và sẽ âm thầm làm mất các lượt ghi gần đây. Chọn đúng, an toàn, khi nhiều máy có thể bất đồng về việc ai nên thắng, chính xác là loại vấn đề cần một giao thức “mọi người đồng thuận về một câu trả lời” đúng nghĩa — đó chính là toàn bộ chủ đề của các thuật toán consensus được nói tới sau này, ở Chương 9.</li>
    <li><strong>Báo cho mọi client và mọi follower khác</strong> biết về leader mới, để mọi người bắt đầu gửi lượt ghi tới đúng nơi kể từ đó.</li>
  </ol>

  <p>Failure mode thực sự đáng sợ ở đây gọi là <strong>split brain</strong>: leader cũ quay lại online sau một thời gian không tới được, không nhận ra đã có leader mới được thăng cấp trong lúc nó vắng mặt, và cứ tiếp tục nhận ghi như chưa có chuyện gì xảy ra — trong khi leader <em>mới</em> cũng đang làm y hệt vậy. Giờ bạn có hai máy đều tin chắc mình đang cầm quyền, cả hai đều nhận các lượt ghi khác nhau, và không có cách nào sạch sẽ để biết phiên bản nào của sự việc mới là “thật” mà không phải gỡ rối thủ công sau đó. Quan điểm thẳng thắn của Kleppmann ở đây, điều đọng lại trong tôi: không có giá trị timeout nào vừa an toàn tuyệt đối vừa nhanh tuyệt đối cùng lúc. Bạn luôn đang chọn một điểm trên đường đánh đổi đó, chứ không phải làm cho đánh đổi đó biến mất.</p>

  <h2 id="ngay-c-khi-khng-c-g-hng-cc-bn-sao-vn-c-th-b-tr">Ngay cả khi không có gì “hỏng,” các bản sao vẫn có thể bị trễ</h2>

  <p>Đây là một vấn đề tinh vi hơn, chẳng liên quan gì tới việc có thứ gì thực sự bị hỏng. Follower thường replicate theo kiểu <em>bất đồng bộ</em> — nghĩa là leader không ngồi chờ mọi follower xác nhận trước khi báo cho người ghi ban đầu là “xong, đã lưu” — vì chờ mọi bản sao xác nhận ở mỗi lượt ghi sẽ khiến mọi thứ chậm đến khó chịu. Nhưng điều này nghĩa là follower luôn trễ hơn leader ít nhất một chút, và độ trễ nhỏ đó có thể gây ra những con bug thực sự gây bối rối. Hãy tưởng tượng bạn đăng một bình luận trên một website, nó lưu thành công, rồi bạn lập tức tải lại trang — chỉ để thấy bình luận đó biến mất, vì lần tải lại đó tình cờ đọc từ một follower chưa kịp bắt kịp. Không có gì hỏng cả. Bình luận vẫn an toàn trên leader. Nhưng từ góc nhìn của người dùng, nó trông y hệt như mất dữ liệu.</p>

  <p>Cuốn sách gọi tên vài cam kết cụ thể một hệ thống có thể chọn để đưa ra, nhằm ngăn đúng loại bối rối này:</p>

  <ul>
    <li><strong>Read-your-writes</strong> — bạn phải luôn thấy được thay đổi <em>của chính mình</em> ngay lập tức, ngay cả khi lượt đọc của người khác vẫn đang được phục vụ từ một bản sao hơi trễ.</li>
    <li><strong>Monotonic reads</strong> — một khi đã thấy một mẩu dữ liệu, bạn không bao giờ nên thấy một phiên bản <em>cũ hơn</em> của nó sau đó, điều có thể xảy ra nếu hai lượt đọc của bạn tình cờ rơi vào hai follower không cập nhật ngang nhau.</li>
    <li><strong>Consistent prefix reads</strong> — nếu một sự kiện thực sự xảy ra trước một sự kiện khác trong thực tế, không ai nên thấy chúng theo thứ tự ngược lại.</li>
  </ul>

  <p>Không cái nào trong số này miễn phí — mỗi cái là một thứ cụ thể mà một team kỹ thuật phải chủ động xây dựng (gửi lượt đọc của chính người dùng về leader ngay sau khi họ ghi gì đó, giữ session của một người dùng gắn cố định với cùng một follower, theo dõi phiên bản dữ liệu nào một client đã từng thấy). Bỏ qua một cái, bạn sẽ có một con bug rất cụ thể, rất dễ tái hiện, đang chờ một người dùng thật nào đó vô tình vấp phải.</p>

  <h2 id="v-sao-chng-ny-c-lin-tc-hng-v-chng-9">Vì sao chương này cứ liên tục hướng về Chương 9</h2>

  <p>Sợi chỉ chạy xuyên suốt bên dưới tất cả những điều này — chọn leader mới an toàn, tránh split brain, đồng thuận xem lượt ghi mâu thuẫn nào trong số nhiều lượt thực sự “thắng” — thực chất là đúng một vấn đề nền tảng khoác những bộ áo khác nhau: làm cho một nhóm máy đồng thuận về một câu trả lời, ngay cả khi một số máy có thể chậm, đã chết, hoặc không nói chuyện được với nhau. Vấn đề nền tảng đó có tên riêng, <strong>consensus</strong>, và nó nghiêm trọng đến mức xứng đáng có cả một chương riêng sau này trong sách (Chương 9). Vậy nên “high availability” thực ra không phải một chủ đề tách biệt khỏi consensus chút nào — nó chính là consensus, được áp dụng vào đúng một câu hỏi quan trọng nhất khi bạn cố giữ cho hệ thống sống: <em>ai được phép nhận ghi ngay lúc này?</em></p>

  <h2 id="iu-thc-s-thay-i-trong-cch-ti-ngh-v-chuyn-ny">Điều thực sự thay đổi trong cách tôi nghĩ về chuyện này</h2>

  <p>Trước chương này, “high availability” với tôi là một cụm từ mơ hồ, nghe có vẻ hay ho — thứ bạn có được bằng cách “thêm nhiều server hơn.” Sau chương này, tôi nhìn nó như một tập các đánh đổi rất cụ thể, rất chủ đích: chờ bao lâu trước khi tuyên bố leader đã chết, những đảm bảo về độ “cũ” của dữ liệu nào bạn sẵn sàng hứa với người dùng, và bạn sẽ phát hiện, gỡ rối các lượt ghi mâu thuẫn ra sao khi chúng xảy ra. Không điều nào trong số đó đến từ trường hợp mọi thứ đều hoạt động trơn tru, không có failure — tất cả đều đến từ việc chủ động lên kế hoạch cho sự lộn xộn.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 5" /><category term="Replication" /><category term="High Availability" /><summary type="html"><![CDATA[The chapter that actually explains how a database keeps working when a machine dies — which is what most people really mean by 'high availability.']]></summary></entry><entry><title type="html">DDIA Chapter 4: Encoding and Evolution</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-4-encoding-and-evolution/" rel="alternate" type="text/html" title="DDIA Chapter 4: Encoding and Evolution" /><published>2026-09-15T08:45:00+07:00</published><updated>2026-09-15T08:45:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-4-encoding-and-evolution</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-4-encoding-and-evolution/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 4: Encoding and Evolution.</em></p>

  <p>This chapter closes out Part I of the book, and it’s about a problem that sounds almost too obvious to write a whole chapter on: how do you turn an in-memory object — a struct, a class instance, whatever your programming language calls it — into bytes you can send over a network or write to disk, and then turn it back into an object later? That’s called <strong>encoding</strong> (or serialization). The reason it deserves a full chapter is the second half of the title: <strong>evolution</strong> — what happens when the <em>shape</em> of that data needs to change, but you can’t update every single reader and writer of it at exactly the same instant.</p>

  <h2 id="why-just-update-everything-at-once-is-never-actually-an-option">Why “just update everything at once” is never actually an option</h2>

  <p>This is the part of the chapter that reframed the whole topic for me. In any system beyond a toy project, you basically never get to update all your code and all your data at the same instant:</p>

  <ul>
    <li><strong>Server-side rolling deploys</strong> — when you deploy new code to a fleet of, say, a hundred servers, you don’t take them all down at once (that would mean an outage). You update them a few at a time, which means for some window of time, old code and new code are <em>both running simultaneously</em>, and they might both be reading and writing the exact same data or exchanging messages with each other.</li>
    <li><strong>Client-side apps</strong> — a user might not update their mobile app for months. The new server has to keep working correctly with old app versions still in the wild, sometimes for years.</li>
    <li><strong>Data outlives code</strong> — a row written to a database five years ago, by code that no longer exists, still has to be readable by whatever code is running today.</li>
  </ul>

  <p><img src="/assets/images/ddia/ch4-schema-evolution.svg" alt="During a rolling deploy, old and new server versions run at the same time and must speak a message format both understand — new fields stay optional so nothing breaks in either direction" /></p>

  <p>Once I saw it framed this way, I realized “backward compatibility” isn’t some extra nice-to-have feature — it’s a mechanical necessity of how real deployments physically work. You can’t will your way out of the fact that old and new code overlap in time; you can only design your encoding so that the overlap doesn’t break anything.</p>

  <h2 id="two-compatibility-directions-and-why-you-need-both">Two compatibility directions, and why you need both</h2>

  <p>The book defines two distinct kinds of compatibility, and I found it useful to think of them as two separate questions rather than one blurry concept:</p>

  <ul>
    <li><strong>Backward compatibility</strong> — can <em>newer</em> code read data that was written by <em>older</em> code? This is usually the easier one: newer code typically knows about old fields and can just keep supporting them.</li>
    <li><strong>Forward compatibility</strong> — can <em>older</em> code read data written by <em>newer</em> code? This is the trickier direction, because old code, by definition, doesn’t know about fields that didn’t exist yet when it was written. The old code has to be written defensively enough to just <em>ignore</em> fields it doesn’t recognize, rather than crashing on them.</li>
  </ul>

  <p>During a rolling deploy, you genuinely need both at once, in both directions, because at any given moment some machines are old and some are new, and they’re all talking to each other simultaneously.</p>

  <h2 id="text-formats-vs-binary-formats">Text formats vs. binary formats</h2>

  <p>The chapter walks through a few families of encoding formats, and the tradeoff pattern is one I now recognize everywhere:</p>

  <p><strong>Text-based formats</strong> — JSON, XML, CSV. Their big advantage is that they’re human-readable — you can open one in a plain text editor and understand it, which is genuinely valuable for debugging. Their downside is that they’re verbose (field names get repeated in every single record) and somewhat ambiguous about types (JSON, for instance, famously doesn’t distinguish integers from floating-point numbers, and has no native way to represent large numbers precisely — a real, documented source of bugs when large IDs get silently rounded).</p>

  <p><strong>Binary formats with a schema</strong> — Protocol Buffers (Google), Thrift (originally Facebook), Avro (Apache, heavily used inside the Kafka/LinkedIn ecosystem). Instead of repeating field names in every message, you define a schema once — “field 1 is <code class="language-plaintext highlighter-rouge">name</code>, a string; field 2 is <code class="language-plaintext highlighter-rouge">age</code>, an integer” — and the encoded bytes just contain the values in a known order, referencing fields by a short numeric tag instead of spelling out the name every time. This is dramatically smaller on the wire and faster to parse, at the cost of needing that schema definition to make sense of the raw bytes at all.</p>

  <p>What made schemas click for me as more than “a way to save bytes”: a schema is also a <em>contract</em>. It’s the thing that lets Protobuf and Avro define precise, checkable rules for what counts as a backward- or forward-compatible change — for example, you’re generally allowed to <em>add</em> a new optional field (old readers just skip it, satisfying forward compatibility) but you’re generally <em>not</em> allowed to change a field’s type or reuse someone else’s old field number, because that silently corrupts data for whichever version doesn’t expect it.</p>

  <h2 id="dataflow-how-encoded-data-actually-travels">Dataflow: how encoded data actually travels</h2>

  <p>The last part of the chapter looks at the different paths encoded data takes through a real system, and each path has its own compatibility expectations:</p>

  <ul>
    <li><strong>Through a database</strong> — you write a row today, and it might be read years from now by a completely different version of your application. This is backward compatibility stretched out over a very long timescale, which is why database schema migrations are treated so carefully in real teams — you’re not just changing today’s code, you’re committing to how every future version of your code will need to interpret today’s write.</li>
    <li><strong>Through service calls (REST, RPC)</strong> — one service calls another over the network, and the two services are very likely running different code versions at any given time, especially in a company running dozens of independently-deployed microservices. Both directions of compatibility matter constantly, because you rarely control exactly when every other team deploys their side.</li>
    <li><strong>Through a message queue (like Kafka)</strong> — a message gets written once by a producer and might be read later by several different consumers, each potentially running different code versions, and the message itself might sit in the queue for a while before anyone reads it. This is arguably the trickiest case, because the producer often doesn’t even know who all the eventual readers are, or what version of the code they’ll be running when they finally read it.</li>
  </ul>

  <h2 id="why-this-closes-out-part-i-so-well">Why this closes out Part I so well</h2>

  <p>Looking back at the four chapters so far — <a href="/blog/2026/09/15/ddia-chapter-1-reliable-scalable-maintainable/">reliability and scalability</a>, <a href="/blog/2026/09/15/ddia-chapter-2-data-models/">data models</a>, <a href="/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/">storage engines</a>, and now encoding — I can see why the book groups them as “foundations.” They’re all about a single machine’s honest relationship with its own data: how to survive faults, how to shape data, how to store it, and now, how to let that shape safely change over time. Everything from here on — replication, partitioning, transactions, consensus — is about coordinating <em>multiple</em> machines, and none of that works if a single machine can’t even reliably read data that a slightly different version of itself wrote yesterday.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 4: Encoding and Evolution.</em></p>

  <p>Chương này khép lại Phần I của cuốn sách, và nó nói về một vấn đề nghe có vẻ hiển nhiên đến mức khó tin lại cần cả một chương: làm sao biến một object trong bộ nhớ — một struct, một instance của class, tùy ngôn ngữ lập trình bạn gọi nó là gì — thành byte để gửi qua mạng hoặc ghi xuống đĩa, rồi sau đó biến ngược lại thành object? Đó gọi là <strong>encoding</strong> (hay serialization). Lý do nó xứng đáng cả một chương nằm ở nửa sau của tiêu đề: <strong>evolution</strong> (tiến hóa) — chuyện gì xảy ra khi <em>hình dạng</em> của dữ liệu đó cần thay đổi, nhưng bạn không thể cập nhật mọi reader và writer của nó đúng cùng một khoảnh khắc.</p>

  <h2 id="v-sao-c-cp-nht-ht-mi-th-cng-lc-khng-bao-gi-thc-s-l-mt-la-chn">Vì sao “cứ cập nhật hết mọi thứ cùng lúc” không bao giờ thực sự là một lựa chọn</h2>

  <p>Đây là phần khiến tôi nhìn lại toàn bộ chủ đề này theo cách khác. Trong bất kỳ hệ thống nào vượt ra khỏi một dự án đồ chơi, bạn về cơ bản không bao giờ được cập nhật toàn bộ code và toàn bộ dữ liệu đúng cùng một khoảnh khắc:</p>

  <ul>
    <li><strong>Rolling deploy phía server</strong> — khi bạn deploy code mới lên một hạm đội, giả sử một trăm server, bạn không tắt hết chúng cùng lúc (như vậy sẽ gây downtime). Bạn cập nhật từng ít một, nghĩa là trong một khoảng thời gian nào đó, code cũ và code mới <em>cùng chạy song song</em>, và chúng có thể cùng đọc, ghi đúng một dữ liệu hoặc trao đổi tin nhắn với nhau.</li>
    <li><strong>Ứng dụng phía client</strong> — một người dùng có thể không cập nhật app di động của họ trong nhiều tháng. Server mới phải tiếp tục hoạt động đúng với các phiên bản app cũ vẫn đang tồn tại ngoài kia, đôi khi hàng năm trời.</li>
    <li><strong>Dữ liệu sống lâu hơn code</strong> — một dòng được ghi vào database năm năm trước, bởi code giờ không còn tồn tại, vẫn phải đọc được bởi bất kỳ code nào đang chạy hôm nay.</li>
  </ul>

  <p><img src="/assets/images/ddia/ch4-schema-evolution.svg" alt="Trong lúc rolling deploy, phiên bản server cũ và mới cùng chạy song song và phải nói cùng một định dạng message mà cả hai đều hiểu — field mới luôn để tùy chọn để không phá vỡ gì theo cả hai chiều" /></p>

  <p>Khi nhìn theo cách đóng khung này, tôi nhận ra “backward compatibility” (tương thích ngược) không phải một tính năng phụ hay ho nào đó — nó là một yêu cầu cơ học bắt buộc từ cách các đợt deploy thật vận hành về mặt vật lý. Bạn không thể ước cho việc code cũ và mới chồng lấn nhau về thời gian biến mất; bạn chỉ có thể thiết kế encoding của mình sao cho sự chồng lấn đó không phá vỡ điều gì.</p>

  <h2 id="hai-chiu-tng-thch-v-v-sao-bn-cn-c-hai">Hai chiều tương thích, và vì sao bạn cần cả hai</h2>

  <p>Cuốn sách định nghĩa hai loại tương thích riêng biệt, và tôi thấy hữu ích khi nghĩ về chúng như hai câu hỏi tách biệt thay vì một khái niệm mơ hồ:</p>

  <ul>
    <li><strong>Backward compatibility (tương thích ngược)</strong> — code <em>mới hơn</em> có đọc được dữ liệu do code <em>cũ hơn</em> ghi ra không? Đây thường là chiều dễ hơn: code mới thường biết về các field cũ và có thể cứ tiếp tục hỗ trợ chúng.</li>
    <li><strong>Forward compatibility (tương thích xuôi)</strong> — code <em>cũ hơn</em> có đọc được dữ liệu do code <em>mới hơn</em> ghi ra không? Đây là chiều hóc búa hơn, vì theo định nghĩa, code cũ không biết về những field chưa tồn tại vào lúc nó được viết. Code cũ phải được viết đủ phòng thủ để chỉ <em>bỏ qua</em> các field nó không nhận ra, thay vì bị crash vì chúng.</li>
  </ul>

  <p>Trong một đợt rolling deploy, bạn thực sự cần cả hai chiều cùng lúc, vì tại bất kỳ thời điểm nào cũng có máy cũ và máy mới, và tất cả chúng đều đang nói chuyện với nhau đồng thời.</p>

  <h2 id="nh-dng-text-vs-nh-dng-binary">Định dạng text vs. định dạng binary</h2>

  <p>Chương này đi qua vài họ định dạng encoding, và pattern đánh đổi ở đây là thứ giờ tôi nhận ra ở khắp mọi nơi:</p>

  <p><strong>Định dạng dựa trên text</strong> — JSON, XML, CSV. Ưu điểm lớn của chúng là con người đọc được — bạn có thể mở nó bằng một trình soạn thảo text đơn giản và hiểu được, điều này thực sự quý giá khi debug. Nhược điểm là chúng dài dòng (tên field bị lặp lại trong mỗi bản ghi) và có phần mơ hồ về kiểu dữ liệu (JSON, chẳng hạn, nổi tiếng là không phân biệt số nguyên với số thực, và không có cách nào native để biểu diễn số lớn một cách chính xác — một nguồn bug thật, đã được ghi nhận, khi ID lớn bị âm thầm làm tròn).</p>

  <p><strong>Định dạng binary có schema</strong> — Protocol Buffers (Google), Thrift (ban đầu từ Facebook), Avro (Apache, được dùng rất nhiều trong hệ sinh thái Kafka/LinkedIn). Thay vì lặp lại tên field trong mỗi message, bạn định nghĩa một schema đúng một lần — “field 1 là <code class="language-plaintext highlighter-rouge">name</code>, kiểu string; field 2 là <code class="language-plaintext highlighter-rouge">age</code>, kiểu integer” — và byte được encode chỉ chứa giá trị theo một thứ tự đã biết, tham chiếu field bằng một tag số ngắn thay vì viết ra tên đầy đủ mỗi lần. Cách này nhỏ hơn đáng kể khi truyền đi và parse nhanh hơn, đổi lại là cần có định nghĩa schema đó thì mới hiểu được byte thô là gì.</p>

  <p>Điều khiến schema thực sự “sáng” ra với tôi, hơn cả “một cách tiết kiệm byte”: schema còn là một <em>hợp đồng</em>. Đó là thứ cho phép Protobuf và Avro định nghĩa các quy tắc chính xác, kiểm chứng được về việc thay đổi nào được tính là tương thích ngược hay tương thích xuôi — ví dụ, bạn thường được phép <em>thêm</em> một field tùy chọn mới (reader cũ chỉ bỏ qua nó, thỏa mãn tương thích xuôi) nhưng bạn thường <em>không được phép</em> đổi kiểu của một field hoặc tái sử dụng số field cũ của ai đó khác, vì điều đó âm thầm làm hỏng dữ liệu cho bất kỳ phiên bản nào không lường trước điều đó.</p>

  <h2 id="dataflow-d-liu--encode-thc-s-di-chuyn-nh-th-no">Dataflow: dữ liệu đã encode thực sự di chuyển như thế nào</h2>

  <p>Phần cuối chương nhìn vào các con đường khác nhau mà dữ liệu đã encode đi qua trong một hệ thống thật, và mỗi con đường có kỳ vọng tương thích riêng:</p>

  <ul>
    <li><strong>Qua database</strong> — bạn ghi một dòng hôm nay, và nó có thể được đọc nhiều năm sau bởi một phiên bản ứng dụng hoàn toàn khác. Đây là tương thích ngược kéo dài trên một khung thời gian rất dài, đó là lý do vì sao migration schema database được các team thật xử lý cẩn thận đến vậy — bạn không chỉ đang đổi code hôm nay, bạn đang cam kết cách mọi phiên bản code tương lai sẽ cần diễn giải lượt ghi hôm nay.</li>
    <li><strong>Qua service call (REST, RPC)</strong> — một service gọi một service khác qua mạng, và hai service đó rất có thể đang chạy các phiên bản code khác nhau tại bất kỳ thời điểm nào, đặc biệt trong một công ty vận hành hàng chục microservice deploy độc lập. Cả hai chiều tương thích đều quan trọng liên tục, vì bạn hiếm khi kiểm soát chính xác lúc nào team khác deploy phía của họ.</li>
    <li><strong>Qua message queue (như Kafka)</strong> — một message được ghi đúng một lần bởi producer và có thể được đọc sau đó bởi nhiều consumer khác nhau, mỗi consumer có thể chạy phiên bản code khác nhau, và bản thân message có thể nằm trong queue một thời gian trước khi ai đó đọc nó. Đây có lẽ là trường hợp hóc búa nhất, vì producer thường thậm chí không biết hết ai sẽ là người đọc cuối cùng, hay họ sẽ chạy phiên bản code nào khi cuối cùng đọc nó.</li>
  </ul>

  <h2 id="v-sao-chng-ny-khp-li-phn-i-mt-cch-trn-vn">Vì sao chương này khép lại Phần I một cách trọn vẹn</h2>

  <p>Nhìn lại bốn chương đã qua — <a href="/blog/2026/09/15/ddia-chapter-1-reliable-scalable-maintainable/">reliability và scalability</a>, <a href="/blog/2026/09/15/ddia-chapter-2-data-models/">data model</a>, <a href="/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/">storage engine</a>, và giờ là encoding — tôi hiểu vì sao cuốn sách nhóm chúng lại thành “nền tảng.” Tất cả đều nói về mối quan hệ trung thực của một máy đơn lẻ với chính dữ liệu của nó: cách sống sót qua sự cố, cách định hình dữ liệu, cách lưu trữ nó, và giờ là cách để hình dạng đó thay đổi an toàn theo thời gian. Mọi thứ từ đây trở đi — replication, partitioning, transaction, consensus — đều nói về việc phối hợp <em>nhiều</em> máy, và không điều nào trong số đó hoạt động được nếu một máy đơn lẻ còn không thể đọc đáng tin cậy dữ liệu mà một phiên bản hơi khác của chính nó đã ghi hôm qua.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 4" /><category term="Encoding" /><category term="Schema Evolution" /><summary type="html"><![CDATA[You can't deploy new code to a thousand servers all at once, and users won't update their app the instant you ship it. So old and new versions of your system are always talking to each other — this chapter is about making sure they can.]]></summary></entry><entry><title type="html">DDIA Chapter 3: Storage and Retrieval</title><link href="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/" rel="alternate" type="text/html" title="DDIA Chapter 3: Storage and Retrieval" /><published>2026-09-15T08:30:00+07:00</published><updated>2026-09-15T08:30:00+07:00</updated><id>https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval</id><content type="html" xml:base="https://vile-blog.github.io/blog/2026/09/15/ddia-chapter-3-storage-and-retrieval/"><![CDATA[<div data-lang-content="en">

  <p><em>Book: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> by Martin Kleppmann — Chapter 3: Storage and Retrieval.</em></p>

  <p>After <a href="/blog/2026/09/15/ddia-chapter-2-data-models/">data models</a>, the book goes one level deeper: forget how your data <em>looks</em> to your application for a moment — how does the database actually store it on an actual physical disk, in a way that lets it find any given piece of it back quickly? This is the chapter where I finally understood <em>why</em> Postgres and Cassandra genuinely behave like different animals under load, instead of just “both being databases.”</p>

  <h2 id="the-simplest-possible-database-and-why-its-bad">The simplest possible database, and why it’s bad</h2>

  <p>The book opens with a deliberately silly toy database: a script that appends every write as a new line to a text file, and looks things up by scanning the <em>entire file</em> from the top every single time. Writes are blazing fast (just append a line — nothing to search for, nothing to reorganize). Reads are painfully slow once the file gets big, because every single lookup means scanning potentially millions of lines. This toy example matters because it sets up the entire chapter’s central tension: <strong>fast writes and fast reads pull the storage engine’s design in opposite directions</strong>, and every real storage engine is really just a specific, clever compromise between the two.</p>

  <p>The first real improvement the book introduces is an <strong>index</strong> — a separate, smaller structure that tells you exactly where to look, so reads don’t need a full scan. But an index isn’t free: every index you add makes writes slightly slower, because now every write has to update the index too, not just append the raw data. This is a genuinely useful mental model I didn’t have before: <em>every index is a bet that you’ll read that field often enough to be worth paying a small write-cost on every single write, forever.</em></p>

  <h2 id="the-two-families-b-trees-and-lsm-trees">The two families: B-trees and LSM-trees</h2>

  <p>The chapter’s real payoff is a head-to-head comparison of the two dominant storage engine designs used in real production databases today, and once I saw them side by side, a lot of “why does this database behave this way” questions I’d had for years just resolved themselves:</p>

  <p><img src="/assets/images/ddia/ch3-btree-lsm.svg" alt="Two storage engine designs: a B-tree that updates fixed-size pages in place, versus an LSM-tree that appends to an in-memory table and periodically merges files on disk" /></p>

  <p><strong>B-trees</strong> — used by PostgreSQL, MySQL/InnoDB, and Oracle, among many others — organize data on disk into fixed-size pages arranged in a tree, where each write finds the <em>exact</em> page a key belongs to and updates it directly in place. This is the same basic idea as an old-school library card catalog: everything is kept in strict, browsable order, so both “find this one book” and “find every book between these two call numbers” are fast and predictable. The cost is that every single write means finding a specific spot on the physical disk and modifying it there, which — especially on spinning disks, and even on SSDs to a lesser degree — is a comparatively expensive operation called a <em>seek</em>, and it happens on every write.</p>

  <p><strong>LSM-trees</strong> (log-structured merge-trees) — used by Cassandra, RocksDB, LevelDB, and HBase — take the toy append-only log idea from the start of the chapter and make it genuinely production-grade. New writes first land in a small, fast, in-memory structure (a <em>memtable</em>). Once that fills up, it gets flushed to disk as an immutable file (an <em>SSTable</em>), and a background process periodically merges older SSTables together, discarding overwritten or deleted values along the way — a process called <em>compaction</em>. Because writes never modify anything in place — they only ever append — write throughput can be dramatically higher than a B-tree’s. The cost shows up on reads: a single lookup might, in the worst case, have to check the memtable <em>and</em> several SSTables before it finds the answer (or confirms it doesn’t exist), though clever tricks like <strong>Bloom filters</strong> — a small, probabilistic structure that can quickly say “this key is <em>definitely not</em> in this file” without actually reading the file — claw a lot of that read cost back.</p>

  <p>The everyday analogy that stuck with me: a B-tree is like keeping one perfectly alphabetized filing cabinet, where every new document gets carefully inserted into its exact correct spot right away — precise, but slow to file each new piece of paper. An LSM-tree is like tossing every new document onto an always-growing “inbox” pile, and only periodically, in the background, sorting and merging those piles into neater ones — fast to receive new paper, but you might have to check a few piles before you’re sure you’ve found (or ruled out) a given document.</p>

  <p>Neither is “better” — it comes back to the same tension from the very start of the chapter. Workloads that write constantly and read less often (logging systems, time-series data, write-heavy analytics pipelines) tend to love LSM-trees. Workloads that need consistently fast, predictable reads and can tolerate somewhat slower writes tend to reach for B-trees. This is exactly why Cassandra (LSM-tree) is the go-to choice for something like storing a constant stream of IoT sensor data, while Postgres (B-tree) is the go-to choice for a banking system’s account balances, where you’re reading a specific account far more often than you’re bulk-writing.</p>

  <h2 id="oltp-vs-olap-two-completely-different-jobs-wearing-the-same-word-database">OLTP vs OLAP: two completely different jobs wearing the same word, “database”</h2>

  <p>The last part of the chapter draws a line I’d always sort of sensed but never had clean vocabulary for: <strong>OLTP</strong> (online transaction processing) versus <strong>OLAP</strong> (online analytic processing). They’re both “databases,” but they’re optimized for almost opposite access patterns:</p>

  <ul>
    <li><strong>OLTP</strong> is what runs your actual application in real time — a user checking their bank balance, an app adding an item to a cart. Each query touches a small number of rows, but there are a <em>huge</em> number of concurrent queries happening every second, from many different users, each looking at their own tiny slice of the data.</li>
    <li><strong>OLAP</strong> is what a business analyst runs at the end of the quarter to answer something like “what was our total revenue by region, across the last two years?” Each query might scan <em>millions</em> of rows, but there are relatively few such queries, usually run by internal analysts rather than end users in real time.</li>
  </ul>

  <p>This split explains a design choice I’d seen without understanding why: <strong>column-oriented storage</strong>, used by systems like ClickHouse (which I wrote a <a href="/blog/2026/09/14/clickhouse-lightning-fast-analytics/">separate paper note on</a> — its whole architecture is basically a deep dive into exactly this idea). A normal row-oriented database stores an entire row together on disk, which is efficient for OLTP (“give me everything about this one order”) but wasteful for OLAP (“give me just the <code class="language-plaintext highlighter-rouge">revenue</code> column, summed, across 50 million orders”) — you’d end up reading every other column of every row just to throw it away. Column-oriented storage flips this: it stores each <em>column</em> together on disk instead, so an analytical query that only cares about <code class="language-plaintext highlighter-rouge">revenue</code> and <code class="language-plaintext highlighter-rouge">region</code> can skip reading every other column entirely, often making a scan over millions of rows dramatically faster.</p>

  <h2 id="why-this-chapter-earns-its-place-before-replication-and-partitioning">Why this chapter earns its place before replication and partitioning</h2>

  <p>Reading this chapter clarified something for me about the <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> and <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning</a> chapters I’d already written notes on: those chapters are about <em>where</em> copies of your data live and <em>how</em> it’s split across machines, but this chapter is about what’s actually happening on a <em>single</em> machine’s disk underneath all of that. A partitioned, replicated cluster of machines each running a badly-suited storage engine for the workload is still going to be slow — partitioning and replication solve “too much data/traffic for one machine,” not “the wrong data structure for this access pattern.” Both problems are real, and both need solving, but they’re genuinely separate layers of the same system.</p>

</div>
<div data-lang-content="vi">

  <p><em>Sách: <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> của Martin Kleppmann — Chương 3: Storage and Retrieval.</em></p>

  <p>Sau <a href="/blog/2026/09/15/ddia-chapter-2-data-models/">data model</a>, cuốn sách đi sâu thêm một tầng nữa: tạm quên đi việc dữ liệu của bạn <em>trông như thế nào</em> với ứng dụng, vậy database thực sự lưu nó xuống một ổ đĩa vật lý thật như thế nào, theo cách cho phép nó tìm lại bất kỳ mẩu dữ liệu nào thật nhanh? Đây là chương mà cuối cùng tôi mới hiểu <em>vì sao</em> Postgres và Cassandra thực sự hành xử như hai loài khác nhau khi chịu tải, chứ không chỉ đơn giản là “cả hai đều là database.”</p>

  <h2 id="database-n-gin-nht-c-th-v-v-sao-n-t">Database đơn giản nhất có thể, và vì sao nó tệ</h2>

  <p>Cuốn sách mở đầu bằng một database đồ chơi cố tình làm cho ngớ ngẩn: một script ghi mỗi lượt write thành một dòng mới nối vào cuối một file text, và tra cứu bằng cách quét <em>toàn bộ file</em> từ đầu mỗi lần. Ghi thì cực nhanh (chỉ nối thêm một dòng — chẳng cần tìm gì, chẳng cần sắp xếp lại gì). Đọc thì chậm khủng khiếp một khi file lớn lên, vì mỗi lượt tra cứu nghĩa là quét qua có thể hàng triệu dòng. Ví dụ đồ chơi này quan trọng vì nó thiết lập căng thẳng trung tâm của cả chương: <strong>ghi nhanh và đọc nhanh kéo thiết kế storage engine về hai hướng ngược nhau</strong>, và mọi storage engine thật ngoài đời chỉ là một sự thỏa hiệp khéo léo, cụ thể giữa hai thứ đó.</p>

  <p>Cải tiến thật đầu tiên cuốn sách giới thiệu là <strong>index</strong> — một cấu trúc riêng, nhỏ hơn, cho bạn biết chính xác chỗ nào cần nhìn vào, để đọc không cần quét toàn bộ. Nhưng index không miễn phí: mỗi index bạn thêm vào làm việc ghi chậm đi một chút, vì giờ mỗi lượt ghi phải cập nhật cả index đó nữa, không chỉ nối thêm dữ liệu thô. Đây là một mô hình tư duy thực sự hữu ích mà trước đây tôi chưa có: <em>mỗi index là một cược rằng bạn sẽ đọc field đó đủ thường xuyên để đáng trả một chút chi phí ghi trên mỗi lượt ghi, mãi mãi.</em></p>

  <h2 id="hai-h-storage-engine-b-tree-v-lsm-tree">Hai họ storage engine: B-tree và LSM-tree</h2>

  <p>Phần thưởng thực sự của chương là so sánh trực diện hai thiết kế storage engine thống trị được dùng trong các database production thật ngày nay, và một khi thấy chúng cạnh nhau, rất nhiều câu hỏi “vì sao database này lại hành xử kiểu này” tôi giữ trong đầu bao năm nay tự nhiên được giải đáp:</p>

  <p><img src="/assets/images/ddia/ch3-btree-lsm.svg" alt="Hai thiết kế storage engine: B-tree cập nhật các page kích thước cố định tại chỗ, so với LSM-tree nối vào một bảng trong bộ nhớ rồi định kỳ gộp các file trên đĩa" /></p>

  <p><strong>B-tree</strong> — được dùng bởi PostgreSQL, MySQL/InnoDB, và Oracle, cùng nhiều cái khác — tổ chức dữ liệu trên đĩa thành các page kích thước cố định sắp xếp theo dạng cây, nơi mỗi lượt ghi tìm ra <em>đúng</em> page mà key đó thuộc về và cập nhật trực tiếp tại chỗ. Đây cùng ý tưởng cơ bản với tủ mục lục thư viện kiểu cũ: mọi thứ được giữ theo thứ tự nghiêm ngặt, dễ duyệt, nên cả “tìm đúng một cuốn sách này” lẫn “tìm mọi cuốn sách giữa hai mã số này” đều nhanh và dự đoán được. Cái giá là mỗi lượt ghi đều nghĩa là tìm một vị trí cụ thể trên đĩa vật lý và sửa nó ngay tại đó, mà — đặc biệt với ổ đĩa quay, và ở mức độ nhẹ hơn ngay cả với SSD — là một thao tác tương đối tốn kém gọi là <em>seek</em>, và nó xảy ra ở mỗi lượt ghi.</p>

  <p><strong>LSM-tree</strong> (log-structured merge-tree) — được dùng bởi Cassandra, RocksDB, LevelDB, và HBase — lấy ý tưởng log chỉ-nối-thêm từ đầu chương và biến nó thành thứ thực sự dùng được ở production. Lượt ghi mới đầu tiên đi vào một cấu trúc nhỏ, nhanh, trong bộ nhớ (<em>memtable</em>). Khi cái đó đầy, nó được flush xuống đĩa thành một file bất biến (<em>SSTable</em>), và một tiến trình chạy nền định kỳ gộp các SSTable cũ lại với nhau, loại bỏ những giá trị đã bị ghi đè hoặc xóa trong quá trình đó — gọi là <em>compaction</em>. Vì lượt ghi không bao giờ sửa gì tại chỗ — chúng chỉ nối thêm — thông lượng ghi có thể cao hơn B-tree rất nhiều. Cái giá lộ ra ở phía đọc: một lượt tra cứu, trong trường hợp xấu nhất, có thể phải kiểm tra memtable <em>và</em> vài SSTable trước khi tìm ra câu trả lời (hoặc xác nhận nó không tồn tại), dù các mẹo khéo léo như <strong>Bloom filter</strong> — một cấu trúc nhỏ, mang tính xác suất, có thể nhanh chóng nói “key này <em>chắc chắn không có</em> trong file này” mà không cần thực sự đọc file — lấy lại được kha khá chi phí đọc đó.</p>

  <p>Phép so sánh đời thường đọng lại trong đầu tôi: B-tree giống như giữ đúng một tủ hồ sơ được xếp theo bảng chữ cái hoàn hảo, nơi mỗi tài liệu mới được cẩn thận chèn vào đúng vị trí của nó ngay lập tức — chính xác, nhưng chậm khi xếp từng tờ giấy mới. LSM-tree giống như ném mỗi tài liệu mới vào một chồng “hộp thư đến” luôn phình to, và chỉ định kỳ, ở hậu trường, mới sắp xếp và gộp các chồng đó thành những chồng gọn gàng hơn — nhận giấy mới thì nhanh, nhưng bạn có thể phải kiểm tra vài chồng trước khi chắc chắn đã tìm thấy (hoặc loại trừ) một tài liệu nào đó.</p>

  <p>Không cái nào “tốt hơn” — nó quay lại đúng căng thẳng từ đầu chương. Các workload ghi liên tục và ít đọc hơn (hệ thống logging, dữ liệu time-series, pipeline phân tích nặng về ghi) thường thích LSM-tree. Các workload cần đọc nhanh, dự đoán được một cách nhất quán và chấp nhận ghi chậm hơn một chút thường tìm tới B-tree. Đây chính xác là lý do Cassandra (LSM-tree) là lựa chọn hàng đầu cho thứ như lưu một dòng dữ liệu cảm biến IoT liên tục, trong khi Postgres (B-tree) là lựa chọn hàng đầu cho số dư tài khoản trong hệ thống ngân hàng, nơi bạn đọc một tài khoản cụ thể thường xuyên hơn nhiều so với việc ghi hàng loạt.</p>

  <h2 id="oltp-vs-olap-hai-cng-vic-hon-ton-khc-nhau-mang-chung-mt-ci-tn-database">OLTP vs OLAP: hai công việc hoàn toàn khác nhau mang chung một cái tên “database”</h2>

  <p>Phần cuối chương vẽ ra một ranh giới mà tôi vốn đã lờ mờ cảm nhận nhưng chưa bao giờ có từ vựng rõ ràng: <strong>OLTP</strong> (online transaction processing) so với <strong>OLAP</strong> (online analytic processing). Cả hai đều là “database,” nhưng chúng được tối ưu cho các pattern truy cập gần như ngược nhau:</p>

  <ul>
    <li><strong>OLTP</strong> là thứ chạy ứng dụng thật của bạn theo thời gian thực — một người dùng kiểm tra số dư ngân hàng, một app thêm một món hàng vào giỏ. Mỗi query đụng vào một số ít dòng, nhưng có một số lượng <em>khổng lồ</em> query đồng thời diễn ra mỗi giây, từ nhiều người dùng khác nhau, mỗi người nhìn vào một lát cắt nhỏ xíu của dữ liệu.</li>
    <li><strong>OLAP</strong> là thứ một chuyên viên phân tích kinh doanh chạy vào cuối quý để trả lời câu hỏi kiểu “tổng doanh thu của chúng ta theo từng khu vực trong hai năm qua là bao nhiêu?” Mỗi query có thể quét <em>hàng triệu</em> dòng, nhưng có tương đối ít query như vậy, thường được chạy bởi các nhà phân tích nội bộ chứ không phải người dùng cuối theo thời gian thực.</li>
  </ul>

  <p>Sự phân chia này giải thích một lựa chọn thiết kế tôi từng thấy mà không hiểu vì sao: <strong>column-oriented storage</strong> (lưu theo cột), được dùng bởi các hệ thống như ClickHouse (tôi có viết <a href="/blog/2026/09/14/clickhouse-lightning-fast-analytics/">một bài note riêng về paper này</a> — toàn bộ kiến trúc của nó về cơ bản là đào sâu đúng ý tưởng này). Một database lưu theo dòng (row-oriented) thông thường lưu cả một dòng cùng nhau trên đĩa, hiệu quả cho OLTP (“cho tôi mọi thứ về đúng đơn hàng này”) nhưng lãng phí cho OLAP (“cho tôi chỉ mỗi cột <code class="language-plaintext highlighter-rouge">revenue</code>, cộng tổng lại, qua 50 triệu đơn hàng”) — bạn sẽ phải đọc mọi cột khác của mọi dòng chỉ để rồi vứt đi. Lưu theo cột đảo ngược điều này: nó lưu từng <em>cột</em> cùng nhau trên đĩa thay vào đó, nên một query phân tích chỉ quan tâm tới <code class="language-plaintext highlighter-rouge">revenue</code> và <code class="language-plaintext highlighter-rouge">region</code> có thể bỏ qua hoàn toàn việc đọc mọi cột khác, thường làm cho việc quét qua hàng triệu dòng nhanh hơn đáng kể.</p>

  <h2 id="v-sao-chng-ny-xng-ng-ng-trc-replication-v-partitioning">Vì sao chương này xứng đáng đứng trước replication và partitioning</h2>

  <p>Đọc chương này làm rõ cho tôi một điều về các chương <a href="/blog/2026/09/15/ddia-chapter-5-replication/">replication</a> và <a href="/blog/2026/09/15/ddia-chapter-6-partitioning/">partitioning</a> mà tôi đã viết note trước đó: những chương đó nói về <em>nơi</em> các bản sao dữ liệu của bạn nằm và <em>cách</em> nó được chia ra trên nhiều máy, nhưng chương này nói về chuyện gì thực sự đang diễn ra trên đĩa của <em>một</em> máy đơn lẻ bên dưới tất cả những thứ đó. Một cluster máy đã được partition, replicate đầy đủ nhưng mỗi máy lại chạy một storage engine không hợp với workload vẫn sẽ chậm — partitioning và replication giải quyết “quá nhiều dữ liệu/traffic cho một máy,” không phải “sai cấu trúc dữ liệu cho pattern truy cập này.” Cả hai vấn đề đều có thật, và cả hai đều cần giải quyết, nhưng chúng thực sự là hai tầng riêng biệt của cùng một hệ thống.</p>

</div>]]></content><author><name>Vi Le</name><email>vikimmich6@gmail.com</email></author><category term="book-notes" /><category term="Designing Data-Intensive Applications" /><category term="Chapter 3" /><category term="Storage Engines" /><category term="B-trees" /><category term="LSM-trees" /><summary type="html"><![CDATA[Every database eventually has to answer the same boring-sounding question: how do you actually write bytes to a disk so you can find them again fast? The answer splits the entire database world in two.]]></summary></entry></feed>