Kafka Architecture and Design
Kiến trúc và Thiết kế của Apache Kafka
Apache Kafka là một nền tảng luồng dữ liệu phân tán, hoạt động như một hệ thống log phân tán (distributed log) để xử lý và lưu trữ các luồng sự kiện với thông lượng cao, độ trễ thấp và khả năng chịu lỗi. Dưới đây là phân tích chi tiết về kiến trúc và các nguyên tắc thiết kế, với trọng tâm là ứng dụng thực tế.
1. Tổng quan kiến trúc Kafka
Kafka được thiết kế để xử lý hàng triệu sự kiện mỗi giây trong môi trường phân tán. Kiến trúc của Kafka bao gồm các thành phần chính sau, phối hợp với nhau để đảm bảo tính mở rộng, chịu lỗi và hiệu năng cao:
Broker: Các máy chủ trong cụm Kafka, chịu trách nhiệm lưu trữ và quản lý dữ liệu (partition của topic).
Topic: Kênh logic để lưu trữ và phân loại sự kiện, được chia thành các partition.
Partition: Đơn vị nhỏ nhất của topic, là một log bất biến có thứ tự, được phân bố trên các broker.
Producer: Thành phần gửi sự kiện đến topic.
Consumer: Thành phần đọc sự kiện từ topic, thường thuộc về consumer group để xử lý song song.
ZooKeeper/KRaft: Quản lý metadata, bầu chọn leader, và điều phối cụm (KRaft thay thế ZooKeeper trong các phiên bản mới).
Cách Kafka hoạt động
Producer gửi sự kiện đến topic, được phân phối vào các partition dựa trên key (hoặc ngẫu nhiên nếu không có key).
Broker lưu trữ sự kiện trong partition và sao chép chúng đến các replica để đảm bảo chịu lỗi.
Consumer trong consumer group đọc từ các partition được gán, theo dõi offset để biết vị trí đã đọc.
ZooKeeper/KRaft quản lý trạng thái cụm, như vị trí partition, leader, và cấu hình topic.
Nguyên tắc thiết kế cốt lõi
Tính bất biến của log: Dữ liệu trong partition là bất biến, chỉ được thêm vào (append-only), đảm bảo tính nhất quán và đơn giản hóa việc phát lại.
Tách biệt lưu trữ và xử lý: Kafka lưu trữ dữ liệu độc lập với cách consumer xử lý, cho phép nhiều consumer đọc cùng dữ liệu theo cách khác nhau.
Khả năng mở rộng ngang: Thêm broker hoặc partition để tăng thông lượng.
Chịu lỗi: Sao chép partition và cơ chế leader-follower đảm bảo dữ liệu không bị mất khi có lỗi.
2. Thành phần kiến trúc chi tiết
Broker và Cụm
Mỗi broker là một máy chủ lưu trữ một số partition của topic. Một cụm Kafka gồm nhiều broker, phối hợp qua ZooKeeper hoặc KRaft.
Leader và Follower: Mỗi partition có một leader (xử lý đọc/ghi) và nhiều follower (bản sao, đồng bộ với leader). Nếu leader thất bại, một follower được bầu làm leader mới.
Cân nhắc thiết kế:
Đảm bảo số lượng broker đủ để phân phối partition và replica (thường
replication.factor=3
cho môi trường sản xuất).Sử dụng KRaft (Kafka Raft) trong phiên bản mới để loại bỏ phụ thuộc vào ZooKeeper, giảm độ phức tạp.
Topic và Partition
Topic là một danh mục logic, chia thành nhiều partition để song song hóa.
Partition là một log tuần tự, lưu trên đĩa, được sao chép trên nhiều broker.
Cân nhắc thiết kế:
Số partition quyết định mức độ song song: mỗi partition chỉ được đọc bởi một consumer trong một consumer group.
Chọn số partition dựa trên thông lượng (ước lượng 1-2 MB/s mỗi partition) và số lượng consumer.
Key của sự kiện phải được chọn cẩn thận để đảm bảo phân phối hợp lý (ví dụ: sử dụng ID đơn hàng để đảm bảo các sự kiện liên quan nằm cùng partition).
Offset và Lưu trữ
Mỗi sự kiện trong partition có một offset duy nhất, được consumer sử dụng để theo dõi tiến độ.
Kafka lưu trữ dữ liệu trên đĩa với chính sách giữ lại (
retention
) dựa trên thời gian (log.retention.hours
) hoặc kích thước (log.retention.bytes
).Cân nhắc thiết kế:
Cấu hình retention phù hợp với nhu cầu (ví dụ: 7 ngày cho phân tích lịch sử, hoặc vĩnh viễn cho event sourcing).
Sử dụng topic
__consumer_offsets
để lưu offset của consumer group, đảm bảo khôi phục trạng thái khi consumer khởi động lại.
Producer và Consumer
Producer: Gửi sự kiện đến topic, có thể cấu hình để ưu tiên độ bền (
acks=all
) hoặc độ trễ thấp (acks=0
).Consumer: Đọc sự kiện từ partition, hoạt động trong consumer group để cân bằng tải.
Cân nhắc thiết kế:
Producer nên sử dụng nén (
compression.type=snappy
) để giảm băng thông.Consumer cần quản lý offset cẩn thận (tự động hoặc thủ công) để tránh trùng lặp hoặc mất dữ liệu.
Ví dụ Golang (Producer):
package main import ( "github.com/Shopify/sarama" "log" ) func main() { config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll config.Producer.Compression = sarama.CompressionSnappy producer, err := sarama.NewSyncProducer([]string{"broker:9092"}, config) if err != nil { log.Fatal(err) } defer producer.Close() msg := &sarama.ProducerMessage{ Topic: "orders", Key: sarama.StringEncoder("order123"), Value: sarama.StringEncoder(`{"id":"order123","amount":100}`), } partition, offset, err := producer.SendMessage(msg) if err != nil { log.Fatal(err) } log.Printf("Gửi tin nhắn đến partition %d tại offset %d", partition, offset) }
Consumer Group
Consumer group cho phép nhiều consumer xử lý song song các partition của topic.
Kafka tự động cân bằng lại (rebalance) khi consumer tham gia/rời nhóm.
Cân nhắc thiết kế:
Giới hạn số consumer không vượt quá số partition để tránh consumer nhàn rỗi.
Điều chỉnh
session.timeout.ms
vàheartbeat.interval.ms
để giảm thiểu gián đoạn khi rebalance.
3. Các khía cạnh thiết kế quan trọng
Khả năng mở rộng
Mở rộng ngang: Thêm broker để tăng dung lượng lưu trữ và thông lượng. Thêm partition để tăng song song hóa.
Hạn chế: Tăng số partition có thể làm phức tạp quản lý offset và rebalancing. Tránh thay đổi số partition sau khi topic được tạo (cần tạo topic mới).
Mẹo: Sử dụng công cụ như
kafka-topics.sh
để phân tích thông lượng và điều chỉnh số partition.
Chịu lỗi
Sao chép: Sử dụng
replication.factor
(thường là 3) để đảm bảo dữ liệu được sao chép trên nhiều broker.Độ bền: Cấu hình
min.insync.replicas
để yêu cầu số lượng replica tối thiểu xác nhận ghi, tránh mất dữ liệu.Khôi phục: Kafka tự động bầu leader mới khi broker thất bại, nhưng cần giám sát để phát hiện sớm.
Hiệu năng
Producer: Tối ưu bằng cách sử dụng batch (
batch.size
), nén (compression.type
), và điều chỉnh thời gian chờ (linger.ms
).Consumer: Tăng
fetch.max.bytes
để cải thiện thông lượng, nhưng cần cân bằng với bộ nhớ.Lưu trữ: Kafka sử dụng I/O tuần tự và zero-copy để đạt hiệu năng cao. Đảm bảo đĩa SSD cho broker trong môi trường sản xuất.
Bảo mật
Xác thực: Sử dụng SASL/SSL để bảo mật kết nối giữa client và broker.
Phân quyền: Áp dụng ACL (Access Control Lists) để kiểm soát quyền truy cập topic.
Mã hóa: Bật SSL cho dữ liệu truyền tải và mã hóa tại chỗ (encryption at rest) nếu cần.
4. Mô hình thiết kế hệ thống với Kafka
Event Sourcing
Lưu mọi thay đổi trạng thái dưới dạng sự kiện trong Kafka.
Ví dụ: Topic
orders
lưu các sự kiện nhưOrderCreated
,OrderUpdated
. Tái tạo trạng thái bằng cách phát lại sự kiện.Cân nhắc: Đảm bảo retention đủ dài để phát lại toàn bộ lịch sử.
CQRS (Command Query Responsibility Segregation)
Tách biệt xử lý ghi (command) và đọc (query). Producer ghi sự kiện vào Kafka, consumer cập nhật mô hình đọc (read model).
Ví dụ: Một consumer group xử lý sự kiện
orders
để cập nhật cơ sở dữ liệu đọc cho tìm kiếm nhanh.
Xử lý luồng (Stream Processing)
Sử dụng Kafka Streams hoặc ksqlDB để xử lý thời gian thực, như tổng hợp, lọc, hoặc nối dữ liệu.
Ví dụ: Tính tổng doanh thu từ topic
orders
theo thời gian thực.
Tích hợp dữ liệu
Sử dụng Kafka Connect để nhập/xuất dữ liệu từ Kafka đến các hệ thống khác (như cơ sở dữ liệu, Elasticsearch).
Mẹo: Sử dụng connector có sẵn để giảm công sức phát triển.
5. Thực tiễn tốt khi thiết kế
Lựa chọn số partition: Bắt đầu với 10-50 partition mỗi topic, điều chỉnh dựa trên thông lượng thực tế. Tránh quá nhiều partition vì làm tăng chi phí quản lý.
Key của sự kiện: Sử dụng key để đảm bảo thứ tự (ví dụ: ID đơn hàng). Nếu không cần thứ tự, để key null để phân phối ngẫu nhiên.
Quản lý offset: Sử dụng commit thủ công trong các trường hợp yêu cầu xử lý chính xác một lần.
Giám sát: Tích hợp Prometheus/Grafana để theo dõi độ trễ consumer, thông lượng, và sức khỏe broker.
Golang: Sử dụng thư viện
sarama
cho kiểm soát chi tiết hoặcconfluent-kafka-go
cho tích hợp dễ dàng hơn.
6. Ví dụ thực tế: Hệ thống thương mại điện tử
Kiến trúc:
Topic
orders
với 12 partition,replication.factor=3
.Producer gửi sự kiện đơn hàng với key là ID đơn hàng.
Consumer group
order-processors
với 6 consumer xử lý đơn hàng (xác nhận, cập nhật kho).Consumer group
analytics
đọc topic để tính toán số liệu (ví dụ: doanh thu theo giờ).
Cân nhắc thiết kế:
Sử dụng nén
snappy
để giảm băng thông.Cấu hình
min.insync.replicas=2
để đảm bảo độ bền.Giám sát độ trễ consumer qua metrics để điều chỉnh số consumer/partitions.
Mã Golang (Consumer Group):
package main import ( "context" "log" "github.com/Shopify/sarama" ) func main() { config := sarama.NewConfig() config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRoundRobin config.Consumer.Offsets.AutoCommit.Enable = false consumerGroup, err := sarama.NewConsumerGroup([]string{"broker:9092"}, "order-processors", config) if err != nil { log.Fatal(err) } defer consumerGroup.Close() handler := &ConsumerGroupHandler{} for { consumerGroup.Consume(context.Background(), []string{"orders"}, handler) } } type ConsumerGroupHandler struct{} func (h *ConsumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { return nil } func (h *ConsumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil } func (h *ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { for msg := range claim.Messages() { log.Printf("Xử lý đơn hàng: %s, Partition: %d, Offset: %d", string(msg.Value), msg.Partition, msg.Offset) // Xử lý logic (ví dụ: cập nhật kho) session.MarkMessage(msg, "") // Commit offset thủ công } return nil }
Last updated