+9

📨🧠 Kafka Internals – Log, Storage & Retention | Bí Mật Bên Trong Bộ Nhớ Của Kafka - Kafka 101 P7

Kafka Internals – Log, Storage & Retention | Bí Mật Bên Trong Bộ Nhớ Của Kafka

Hầu hết chúng ta đều quen thuộc với các cơ sở dữ liệu quan hệ (RDBMS) như PostgreSQL, MySQL hay các hệ thống NoSQL như MongoDB. Chúng sử dụng các cấu trúc dữ liệu phức tạp như B-Tree, LSM-Tree, đi kèm với cơ chế đánh chỉ mục (indexing) tinh vi để hỗ trợ các câu lệnh truy vấn dữ liệu ngẫu nhiên (random query) phức tạp.

Khi tiếp cận Apache Kafka, một câu hỏi tự nhiên xuất hiện: Làm thế nào một công nghệ lưu trữ có thể đạt được tốc độ ghi hàng triệu tin nhắn mỗi giây, xử lý băng thông hàng chục Gigabyte trên một giây mà vẫn đảm bảo độ bền vững dữ liệu trên đĩa cứng với chi phí cực thấp? Hệ cơ sở dữ liệu bí ẩn nào đang chạy bên dưới các Kafka Broker?

Câu trả lời sẽ khiến bạn ngạc nhiên: Không có cơ sở dữ liệu phức tạp nào cả.

Bên dưới lớp vỏ của Kafka là một thiết kế lưu trữ tối giản đến mức cực đoan: các tệp tin log ghi nối tiếp (append-only log) phẳng và thô sơ. Kafka không cố gắng thông minh hơn hệ điều hành; thay vào đó, nó tận dụng tối đa cơ chế vận hành của nhân Linux để tối ưu hóa I/O.

Bài viết này sẽ đưa bạn đi sâu vào bên trong "bộ não lưu trữ" của Kafka, phân tích cấu trúc vật lý của các tệp tin trên ổ đĩa, cách thức hoạt động của cơ chế dọn dẹp dữ liệu (retention), và những bài học xương máu giúp bạn bảo vệ ổ cứng hệ thống khỏi những pha sập nguồn đột ngột do đầy bộ nhớ.


Câu chuyện từ Production: Khi ổ cứng "bốc cháy" vì đặt niềm tin sai chỗ

Một đội ngũ kỹ sư vận hành hệ thống thu thập dữ liệu hành vi người dùng (Clickstream) trên một cụm Kafka. Để tối ưu hóa chi phí lưu trữ trên Cloud, họ tính toán rằng hệ thống chỉ cần lưu dữ liệu trong vòng 3 ngày để phục vụ cho các thuật toán phân tích. Cấu hình được đặt rất đơn giản ở mức cluster:

log.retention.hours=72

Mọi thứ vận hành trơn tru trong vài tháng. Nhưng vào ngày lễ độc thân 11/11, doanh nghiệp chạy chiến dịch khuyến mại lớn nhất năm. Lượng truy cập của người dùng tăng vọt gấp 15 lần ngày thường. Lượng tin nhắn đổ về Kafka tăng theo cấp số nhân.

Đúng 4 giờ sáng, hệ thống giám sát phát tín hiệu cảnh báo đỏ: Ổ cứng của Broker 2 đã đạt mức 99% dung lượng. Chỉ 10 phút sau, toàn bộ Broker 2 bị đóng băng (freezed), từ chối nhận mọi yêu cầu ghi mới vì phân vùng ổ đĩa chứa log đã bị đầy hoàn toàn. Hiệu ứng domino xảy ra khi các Broker còn lại cũng lần lượt cạn kiệt không gian đĩa do phải gánh tải thay.

                  [ Ổ CỨNG BROKER 2 ĐẦY 100% ]
+-------------------------------------------------------------+
| [Segment 1] (Cũ) | [Segment 2] | [Segment 3] | [Active Log] | -> [Hết chỗ!]
+-------------------------------------------------------------+
 * log.retention.hours = 72 (Chưa hết 72 giờ => Chưa được xóa)
 * Thiếu cấu hình khống chế dung lượng (log.retention.bytes)

Đội ngũ vận hành bối rối: "Chúng tôi cấu hình xóa dữ liệu sau 72 giờ, tại sao dữ liệu mới chỉ ghi được 18 giờ mà ổ cứng đã đầy? Tại sao Kafka không tự động xóa dữ liệu cũ đi để nhường chỗ cho dữ liệu mới?".

Họ đã quên mất một nguyên lý cơ bản: Kafka không thể xóa dữ liệu theo từng dòng (record) lẻ, và việc xóa dữ liệu chỉ có thể thực hiện trên các tệp tin phân đoạn (Segments) đã đóng.


Giải mã cấu trúc bộ nhớ vật lý của Kafka: Thư mục Partition

Nếu bạn SSH vào một máy chủ chạy Kafka Broker và tìm đến thư mục lưu trữ dữ liệu (định nghĩa bởi tham số log.dirs), bạn sẽ không tìm thấy bất kỳ file .db hay .sqlite nào. Thay vào đó, bạn sẽ thấy hàng loạt thư mục được đặt tên theo định dạng [TopicName]-[PartitionIndex].

Ví dụ, topic user-clicks với 3 partition sẽ tạo ra 3 thư mục:

  • user-clicks-0
  • user-clicks-1
  • user-clicks-2

Bên trong mỗi thư mục partition này là nơi phép thuật thực sự diễn ra. Bạn sẽ thấy các nhóm tệp tin có cùng tên nhưng mang các phần mở rộng khác nhau:

/var/lib/kafka/data/user-clicks-0/
├── 00000000000000000000.log
├── 00000000000000000000.index
├── 00000000000000000000.timeindex
├── 00000000000000532104.log
├── 00000000000000532104.index
└── 00000000000000532104.timeindex

Tên của các tệp tin này chính là Base Offset - offset của tin nhắn đầu tiên được lưu trữ trong phân đoạn (Segment) đó.

1. File .log (Phân đoạn dữ liệu)

Đây là nơi chứa dữ liệu tin nhắn thực tế được ghi dưới dạng nhị phân (binary). Cơ chế ghi ở đây là Append-only (chỉ ghi nối tiếp vào cuối file). Kafka không bao giờ cập nhật hoặc sửa đổi dữ liệu đã ghi. Thiết kế này giúp tốc độ ghi đạt mức tối đa vì ổ đĩa không phải di chuyển đầu đọc tìm kiếm vị trí (no disk seeks) - một thao tác cực kỳ đắt đỏ trên đĩa từ (HDD) và cả đĩa thể rắn (SSD).

2. File .index (Chỉ mục Offset vật lý)

Để Consumer có thể đọc dữ liệu nhanh chóng từ một offset bất kỳ (ví dụ: "Hãy cho tôi đọc từ offset 532150"), Kafka không thể quét tuần tự từ đầu file .log dung lượng hàng Gigabyte.

File .index là một chỉ mục thưa (sparse index). Nó ánh xạ từ một số offset nhất định sang vị trí byte vật lý (physical position) tương ứng trong file .log. Nhờ đó, Kafka có thể nhanh chóng định vị phân đoạn dữ liệu cần đọc bằng thuật toán tìm kiếm nhị phân (binary search) trên file index, sau đó chỉ cần thực hiện một thao tác nhảy (seek) trực tiếp đến vị trí byte trong file .log.

3. File .timeindex (Chỉ mục Thời gian)

Tương tự như file .index, nhưng thay vì ánh xạ offset, file .timeindex ánh xạ dấu thời gian (timestamp) của tin nhắn sang vị trí byte vật lý. File này phục vụ cho việc tìm kiếm dữ liệu theo thời gian (ví dụ: "Đọc dữ liệu từ 8 giờ sáng ngày hôm qua").


Cơ chế dọn dẹp dữ liệu (Retention Policies)

Dữ liệu không thể nằm trên ổ cứng mãi mãi. Kafka cung cấp 3 cơ chế dọn dẹp dữ liệu cũ để giải phóng không gian ổ đĩa:

                      [ CÁC CHÍNH SÁCH DỌN DẸP ]
       Time-based               Size-based             Log Compaction
  +-------------------+    +-------------------+    +--------------------+
  | Xóa khi vượt quá  |    | Xóa khi tổng kích  |    | Chỉ giữ lại bản tin|
  | thời gian cấu hình|    | thước vượt giới hạn|    | mới nhất của mỗi   |
  | (log.retention.ms)|    | (log.retention.b) |    | Key                |
  +-------------------+    +-------------------+    +--------------------+

1. Xóa theo thời gian (Time-based Retention)

Kafka kiểm tra thời gian sửa đổi cuối cùng của từng phân đoạn Segment. Nếu toàn bộ tin nhắn trong một Segment đã cũ hơn cấu hình (ví dụ: 72 giờ), Segment đó sẽ được đánh dấu để xóa bỏ.

2. Xóa theo kích thước (Size-based Retention)

Định nghĩa tổng dung lượng tối đa cho phép của một partition (ví dụ: 50 GB). Khi dung lượng thư mục partition vượt quá con số này, Kafka sẽ bắt đầu xóa các phân đoạn Segment cũ nhất cho đến khi dung lượng hạ xuống dưới ngưỡng an toàn.

3. Log Compaction (Nén Log theo Key)

Đối với một số bài toán như lưu trữ trạng thái tài khoản người dùng, chúng ta không quan tâm đến lịch sử thay đổi cũ, mà chỉ cần biết giá trị mới nhất của mỗi khóa (key).

Log Compaction là cơ chế định kỳ quét qua Segment cũ và chỉ giữ lại tin nhắn có offset lớn nhất cho mỗi key, loại bỏ hoàn toàn các bản ghi trung gian cũ hơn. Đây là nền tảng cho việc phục hồi trạng thái (state restoration) trong các ứng dụng Stream Processing và Change Data Capture (CDC).


Tại sao ổ cứng sụp đổ? Bí mật của "Active Segment"

Quay trở lại sự cố production ở đầu bài viết. Tại sao Kafka không xóa dữ liệu cũ khi ổ cứng bị đầy?

Lý do nằm ở nguyên lý: Kafka chỉ thực hiện dọn dẹp trên các Segment đã đóng (inactive segments). Đối với phân đoạn hiện tại đang được ghi dữ liệu (gọi là Active Segment), Kafka sẽ không bao giờ đụng vào nó cho dù tin nhắn bên trong có cũ đến đâu.

Mặc định, một Segment chỉ được đóng lại khi:

  • Kích thước của nó đạt mức giới hạn (segment.bytes, mặc định là 1 GB).
  • Hoặc thời gian mở của nó vượt quá giới hạn (segment.ms, mặc định là 7 ngày).

Trong sự cố trên, do lưu lượng tăng đột biến, ổ đĩa bị đầy trước khi các segment cũ được kích hoạt vòng quét dọn dẹp định kỳ (chạy mỗi 5 phút). Hơn nữa, nếu bạn chỉ cấu hình log.retention.hours mà không giới hạn dung lượng vật lý bằng log.retention.bytes, hệ thống của bạn hoàn toàn không có cơ chế tự vệ trước các đợt bùng nổ dữ liệu (traffic spikes).


Hướng dẫn thiết lập cấu hình lưu trữ tối ưu

Để thiết kế một hệ thống Kafka có độ bền vững cao nhưng không bao giờ làm tràn ổ đĩa, hãy áp dụng các nguyên tắc cấu hình sau:

1. Luôn sử dụng cấu hình "Hai tầng bảo vệ"

Đừng bao giờ tin tưởng hoàn toàn vào thời gian lưu trữ. Hãy thiết lập cả hai cấu hình thời gian và kích thước để tạo ra một chốt an toàn vật lý.

# Bảo vệ tầng 1: Xóa dữ liệu sau 7 ngày
log.retention.hours=168

# Bảo vệ tầng 2: Nếu dung lượng partition đạt 40GB, xóa ngay bất kể thời gian
log.retention.bytes=42949672960

Khi cấu hình cả hai tham số này, bất kỳ điều kiện nào đến trước cũng sẽ kích hoạt cơ chế xóa dữ liệu. Điều này giúp bạn sống sót qua những ngày lượng tải tăng đột biến.

2. Tối ưu kích thước phân đoạn (segment.bytes)

Kích thước segment mặc định là 1 GB phù hợp với hầu hết các hệ thống có lượng tải trung bình và lớn. Tuy nhiên:

  • Nếu dữ liệu quá ít: Bạn tạo ra một topic có lượng ghi cực kỳ thấp (ví dụ: vài Megabyte mỗi ngày). Với cấu hình 1 GB mặc định, phải mất hàng tháng hoặc hàng năm phân đoạn Segment đầu tiên mới đạt giới hạn và được đóng lại. Cho đến lúc đó, dữ liệu trong phân đoạn này sẽ không bao giờ bị xóa, bất chấp cấu hình log.retention.hours=24. Trong trường hợp này, hãy giảm segment.bytes xuống mức thấp hơn (ví dụ: 50 MB hoặc 100 MB).
  • Nếu dữ liệu quá nhiều: Đừng giảm segment size quá nhỏ. Việc có hàng vạn file segment nhỏ trên ổ đĩa sẽ ép hệ điều hành Linux phải mở hàng vạn File Descriptor, gây tràn bộ nhớ đệm index (PageCache) và suy giảm hiệu năng đọc ghi đáng kể.

Những đánh đổi sâu sắc trong tầng lưu trữ

Là một kiến trúc sư, bạn cần hiểu rõ những đánh đổi đằng sau mỗi cấu hình lưu trữ:

1. Tốc độ ghi vật lý vs Khả năng tái xử lý (Reprocessing Capability)

Nếu bạn giữ cấu hình retention quá ngắn (ví dụ: 12 giờ) để tiết kiệm tiền mua ổ cứng, bạn đang tự tước đi khả năng sửa lỗi của ứng dụng. Khi Consumer gặp bug nghiêm trọng trên production và đội ngũ phát triển phải mất 24 giờ để viết code sửa lỗi, bạn sẽ không thể "tua lại" (replay) offset để xử lý lại dữ liệu của ngày hôm qua vì toàn bộ dữ liệu đó đã bị Kafka Broker xóa mất. Hãy luôn cân đối dung lượng đĩa để giữ lại ít nhất 3 đến 7 ngày dữ liệu nhằm đảm bảo khả năng khắc phục sự cố.

2. Chi phí I/O của Log Compaction

Cơ chế Log Compaction không hề miễn phí. Nó đòi hỏi một luồng xử lý chạy ngầm (Log Cleaner threads) liên tục đọc các segment cũ, xây dựng bảng băm trong bộ nhớ để lọc trùng key, sau đó ghi các segment mới đã nén xuống đĩa cứng. Quy trình này ngốn rất nhiều CPU và tài nguyên đọc ghi ổ đĩa (Disk I/O). Nếu không giới hạn tài nguyên cho Log Cleaner, nó có thể tranh chấp băng thông ghi của các Producer chính, làm tăng latency của toàn bộ hệ thống.


Lời kết

Thiết kế lưu trữ của Apache Kafka là minh chứng cho thấy sự tối giản có thể mang lại hiệu năng phi thường. Bằng việc từ bỏ các cấu hình cơ sở dữ liệu quan hệ phức tạp và quay về với cấu trúc tệp tin log nối tiếp thô sơ, Kafka đã biến ổ đĩa cứng - vốn là điểm nghẽn cổ chai truyền thống - thành một lợi thế về mặt thông lượng.

Nhưng sự tối giản đó đòi hỏi kỹ sư phải nắm chắc luật chơi. Việc không hiểu rõ hành vi của Active Segment hay thiếu đi tham số khống chế dung lượng vật lý có thể biến một hệ thống ổn định thành một đống đổ nát chỉ sau vài giờ tải cao.

Khi đã làm chủ được cơ chế lưu trữ vật lý của Broker, làm thế nào để tối ưu hóa hiệu năng truyền tải dữ liệu từ phía Client? Làm sao để gửi và nhận tin nhắn với thông lượng lớn nhất mà không gây quá tải mạng hay nghẽn bộ nhớ của ứng dụng Backend? Bài viết tiếp theo sẽ đưa bạn đi sâu vào các kỹ thuật tối ưu hóa nâng cao dành cho cả Producer và Consumer.


🔍 Đi sâu hơn cùng TechCraft

Kafka chỉ thực sự phát huy giá trị khi bạn hiểu rõ data flow, delivery semantics, partitioning, replication và các trade-off vận hành phía sau nó.

Nếu bạn muốn tiếp tục rèn tư duy thiết kế hệ thống theo hướng thực chiến hơn, Dev Insider là nơi TechCraft tập trung các series đào sâu vào backend, database, distributed systems và production thinking.

🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113

📘 Facebook https://www.facebook.com/techcraft.official

🎥 YouTube https://www.youtube.com/@techcraft.official

🎵 TikTok https://www.tiktok.com/@techcraft.official

Hiểu trade-off. Thiết kế tốt hơn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí