- vừa được xem lúc

Tìm hiểu cơ chế hoạt động của Producer: Bên trong Kafka Producer và Consumer – Phần 1

0 0 1

Người đăng: Tom Riu

Theo Viblo Asia

Tác giả: Danica Fine | Ngày đăng: 05/09/2024

Link bài viết gốc | Link series tiếng Việt

Có lẽ không cần tôi phải giải thích nhiều, mọi người cũng đều biết rằng Apache Kafka® là một công nghệ mạnh mẽ và hữu ích. Là một nền tảng truyền tải sự kiện phân tán (distributed event streaming platform), Kafka giỏi trong việc lưu trữ dữ liệu sự kiện và cung cấp chúng cho các ứng dụng ở phía sau để tiêu thụ để xử lý thông tin – theo thời gian thực hoặc gần sát với thời gian thực tùy thuộc vào trường hợp sử dụng. Điều tuyệt vời của Kafka là chính nó có thể thực hiện điều này mà bạn hầu như không cần tốn nhiều công sức. Nói một cách đơn giản, nó giống như một hộp đen.

Hãy nghĩ về nó. Chúng ta viết các Kafka Producer để gửi dữ liệu đến hộp đen, nơi chúng ta mong đợi rằng dữ liệu được lưu trữ đúng chỗ. Sau đó, chúng ta viết nhiều Kafka Consumer để lấy dữ liệu từ hộp đen này theo nhu cầu. Thông thường, mọi thứ diễn ra đúng như kỳ vọng.

Nhưng điều gì xảy ra khi hộp đen tiện lợi này không hoạt động như bạn mong muốn? Bạn làm gì khi Kafka Producer không hoạt động đúng? Điều gì xảy ra nếu Kafka Consumer không nhận được dữ liệu? Bạn nên bắt đầu từ đâu để tìm hiểu nguyên nhân?

Tôi sẽ là người đầu tiên thừa nhận rằng việc debug Kafka không hề dễ dàng; khi có sự cố, phần lớn khó khăn nằm ở việc dò tìm manh mối trong vô vàn dòng log và biết phải điều chỉnh thông số cấu hình nào (có thể là những thông số bạn chưa từng nghe đến). Điều đó thực sự là một rào cản.

Vì vậy, trong loạt bài này, chúng ta sẽ đi sâu vào hoạt động bên trong của Kafka và xem cách chúng ta tương tác với hộp đen thông qua các ProducerConsumer. Cụ thể, chúng ta sẽ xem cách các request từ client được xử lý bởi các broker. Trong quá trình đó, bạn sẽ học được các cấu hình ảnh hưởng đến từng bước xử lý như thế nào và các số liệu (metrics) bạn có thể sử dụng để giám sát hệ thống. Đến cuối series, bạn sẽ được trang bị đầy đủ kiến thức để có thể debug ứng dụng (hoặc cluster) của mình khi hộp đen không hoạt động như kỳ vọng.

Loạt bài gồm bốn phần bao gồm:

  1. Cách Producer Hoạt Động: Khám phá Producer đã làm gì phía sau hậu trường để chuẩn bị dữ liệu sự kiện thô (raw event data) để gửi đến Broker.
  2. Xử Lý Request Từ Producer: Tìm hiểu cách dữ liệu đi từ Producer Client đến ổ đĩa trên Broker.
  3. Chuẩn Bị Cho Yêu Cầu Truy Vấn Của Consumer: Xem cách Consumer gửi request lấy dữ liệu.
  4. Xử Lý Request Truy Vấn Của Consumer: Đi sâu vào hoạt động bên trong của Broker khi chúng cố gắng cung cấp dữ liệu cho Consumer.

Và dưới đây là bài đầu tiên trong bốn phần, bàn về:
Cách Producer Hoạt Động: Khám phá Producer đã làm gì phía sau hậu trường để chuẩn bị dữ liệu sự kiện thô (raw event data) để gửi đến Broker.

1. Cách Producer Hoạt Động

Hành trình từ Kafka Producer đến các broker là một quá trình dài và... đầy rẫy nguy hiểm. Nói nguy hiểm thì hơi quá, tôi "chém" vậy thôi. Nhưng thực tế, mỗi mẩu dữ liệu đều phải trải qua rất nhiều bước trước khi có thể “hạ cánh” an toàn xuống ổ đĩa.

producer.png

Để thực sự hiểu cách dữ liệu chuyển từ các Producer Client đến các Broker, tốt nhất là làm theo một ví dụ. Trong bài viết này, chúng ta sẽ thử tạo ra một vài dữ liệu và quan sát xem Producer đã làm những gì để chuẩn bị cho hành trình đó!

1.1. Chuẩn bị hành trang

Chúng ta là những lập trình có tâm, vì vậy trước tiên hãy bắt đầu với một schema. Bất kỳ schema nào cũng được, nhưng vì chúng ta sắp bắt đầu một hành trình của riêng mình, tại sao không chọn một schema thú vị? Có lẽ một schema liên quan đến các hobbit chẳng hạn? Họ thích phiêu lưu, bạn biết đấy.

Dưới đây là một schema đơn giản để theo dõi hành tung của các hobbit:

{ "doc": "Accounting for the whereabouts and current activities of hobbits.", "fields": [ { "doc": "Name of the hobbit in question.", "name": "hobbit_name", "type": "string" }, { "doc": "Current location of the hobbit.", "name": "location", "type": "string" }, { "doc": "Current status of the hobbit.", "name": "status", "type": { "name": "Status", "type": "enum", "symbols": ["EATING", "NAPPING", "SMOKING", "ADVENTURING", "THIEVING"] } } ], "name": "hobbitUpdate", "type": "record"
}

Bước tiếp theo hiển nhiên với bất kỳ ai muốn gửi dữ liệu đến Kafka là tạo một topic để lưu trữ dữ liệu đó. Giả sử chúng ta có một topic gọi là hobbit-updates – rất cơ bản, tôi biết. Chúng ta có thể truy cập vào Confluent Cloud và tạo topic đó với cấu hình mặc định: Partitions (phân vùng)6Cleanup policy (chính sách dọn dẹp)Delete, như sau:

New_Topic.png

Đừng lo nếu bạn chưa hiểu hết các cấu hình này, chúng sẽ được giải thích chi tiết hơn trong bài viết tiếp theo của series.

Với schema và topic đã sẵn sàng, chúng ta có thể bắt đầu gửi dữ liệu.

1.2. Gửi Dữ Liệu!

May mắn thay, việc thiết lập Kafka Producer và gửi sự kiện (event) đến Kafka hiện nay đã trở nên đơn giản hơn bao giờ hết. Trên trang Confluent Developer, có một phần Getting Started với các hướng dẫn cho nhiều ngôn ngữ lập trình phổ biến. Chọn ngôn ngữ yêu thích của bạn, viết một Producer, gửi vài sự kiện (event), và bam!, bạn đã có dữ liệu trong Kafka cluster. Đó là sức mạnh của hộp đen, nhớ chứ?

producer_send.png

Nhưng đó không phải là nội dung chính của bài viết này. Mục tiêu thực sự ở đây là đi sâu vào bên trong chiếc hộp đen - tìm hiểu xem điều gì xảy ra giữa lúc bạn gọi producer.send() và khi dữ liệu của bạn cuối cùng được ghi vào broker.

Vậy nên, hãy quay lại thời điểm chúng ta gọi producer.send() và quan sát từng bước diễn ra với sự kiện (event) đó.

1.3. Bắt Đầu Tại Producer Client

Vì Kafka được xem như một chiếc hộp đen, nên chúng ta rất dễ dàng bỏ qua hàng triệu chi tiết nhỏ diễn ra phía sau hậu trường để đảm bảo dữ liệu đến đúng nơi bạn muốn. Khi gửi dữ liệu tới broker, Kafka Producer đã lo liệu gần như tất cả mọi thứ mà bạn có thể không hề hay biết.

kafka_producer_client.png

1.3.1. Serializing

Chúng ta bắt đầu với một event (sự kiện) được gửi đến Producer. Bước đầu tiên là Producer chuyển đổi event thành dữ liệu dạng byte. Thường thì, event bạn cung cấp cho Producer được định dạng theo cách bạn muốn; ví dụ, một message object, JSON string, hoặc thứ gì đó dễ đọc với lập trình viên hoặc con người. Nhưng với các Kafka Broker, tất cả những thứ “đẹp đẽ” đó chẳng quan trọng. Broker chỉ hiểu ngôn ngữ duy nhất là byte. Vì vậy, Producer cần serialize dữ liệu.

Note từ dịch giả: Serialize nghĩa là chuyển từ một object thành một mảng byte

Cấu Hình serialization

Để thiết lập Producer serialize dữ liệu, bạn cần cung cấp các serializer phù hợp. Bạn có thể đã thấy một số cấu hình sau:

  • key.serializer: Cấu hình này cho phép bạn chỉ định một class chịu trách nhiệm serialize key của message bạn đang gửi, nhưng thường thì key chỉ là String thay vì một object phức tạp hơn.
  • value.serializer: Chỉ định một class dùng để serialize value của object bạn đang gửi.
  • schema.registry.url: Nếu serializer của bạn sử dụng Schema Registry, bạn cần cấu hình địa chỉ kết nối đến instance Schema Registry của mình.
  • schema.registry.basic.auth.user.info: Bạn cũng sẽ cần cấu hình thông tin xác thực để đăng nhập vào Schema Registry.

Vì chúng ta sử dụng Avro schema cho các đối tượng hobbit-updates, nên chúng ta cần triển khai một serializer phù hợp. Trong Python – ngôn ngữ tôi chọn – cần triển khai các hàm mô tả cách kết nối với Schema Registry, trả về schema string hobbit-updates, và mô tả cách chuyển đổi hobbit-updates thành từ điển (dictionary).

Đến cuối quá trình cấu hình này, Producer của chúng ta sẽ có thể lấy đối tượng message và chuyển đổi nó thành một khối dữ liệu thô dạng byte (chunk of raw bytes) để lưu trữ trên broker.

1.3.2. Phân Vùng (Partitioning)

Với các dữ liệu thô dạng byte trong tay, Producer cần phải quyết định sẽ gửi các byte này đi đâu. Để làm điều đó, nó cần xác định dữ liệu thuộc về phân vùng (partition) nào.

Cấu Hình Phân Vùng

Khi xây dựng Kafka message, bạn có thể nhận thấy rằng bạn có khả năng chỉ định phân vùng (partition) mà dữ liệu sẽ được gửi tới (ví dụ như Producer.produce() trong thư viện confluent-kafka-python). Nếu bạn tự chọn phân vùng (partition), Producer sẽ sử dụng phân vùng (partition) đó.

Nếu không chỉ định, Producer sẽ tính toán phân vùng (partition) bằng chiến lược phân vùng (partitioning strategy) được thiết lập sẵn, được kiểm soát bởi một số cấu hình sau:

  • partitioner.class: Mặc định (hoặc khi bạn chỉ định None), nếu message có key, Producer sẽ tính hash(key) modulo num_partitions. (Mặc định, không phải tất cả client đều sử dụng cùng thuật toán hash!) Nếu không có key, nó sẽ sử dụng Sticky Partitioning Strategy, nơi nó gửi một nhóm message đến một partition ngẫu nhiên, sau đó lại chọn một partition ngẫu nhiên khác, v.v. Mục tiêu là phân phối dữ liệu đồng đều trên các partition. Bạn cũng có thể tự viết logic phân vùng (partitioning) nếu muốn tối ưu tránh hiện tượng "hot partition".
  • partitioner.ignore.keys: Đúng như tên gọi. Nếu true, các key bị bỏ qua khi phân vùng (partitioning); nếu false, chúng được sử dụng để xác định phân vùng (partition). Vì vậy, nếu bạn muốn dữ liệu được phân phối đều nhưng vẫn muốn sử dụng key, bạn không nên cấu hình partitioner.class mà hãy thiết lập partitioner.ignore.keys=true.
  • partitioner.adaptive.partitioning.enablepartitioner.availability.timeout.ms: Nếu bạn muốn tối ưu hóa việc gửi dữ liệu đến broker bằng Sticky Partitioning Strategy, hai cấu hình này rất hữu ích. Khi partitioner.adaptive.partitioning.enable=true, Producer sẽ xem xét tốc độ phản hồi của các broker hiện tại và điều chỉnh để gửi nhiều dữ liệu hơn đến các broker phản hồi nhanh. Kết hợp với đó, partitioner.availability.timeout.ms mang ý nghĩa là nếu một request đi đến phân vùng (partition) mất nhiều thời gian hơn ngưỡng thời gian chờ này, partition đó sẽ bị bỏ qua và dữ liệu sẽ được gửi đến nơi khác.

Kết thúc bước này, producer đã biết được message sẽ thuộc về partition nào. Dựa trên metadata nhận từ cluster, producer có thể xác định broker nào đang giữ partition đó và sẵn sàng gửi message tới broker đó.

1.3.3. Gom Mẻ (Batching)

Tới bước này, Producer đang có một khối dữ liệu thô dạng byte (chunk of raw bytes), một phân vùng đã xác định, và một "ước mơ" – lưu trữ dữ liệu đó vào đúng broker. Nhưng khoan! Điều gì sẽ xảy ra nếu chúng ta lấy khối byte này và nhóm nó với các khối byte khác hướng tới cùng một broker? Chúng ta có thể nâng cao hiệu quả hơn trong các chu trình gửi – nhận (round-trip) với cluster, đúng không? Nghe hay đấy. Hãy gom nhóm dữ liệu!

Nếu ý nghĩ về việc gom nhóm dữ liệu khiến bạn cảm thấy khó chịu, bạn không hề đơn độc! Gom nhóm là một nhiệm vụ khó thực hiện đúng với Kafka. Có nhiều lợi ích từ việc gom nhóm đúng cách, nhưng đó là vấn đề… bạn phải làm nó đúng cách.

Note từ dịch giả: Round-trip (chu trình gửi – nhận) là toàn bộ chu trình mà một request đi từ producer đến broker, và sau đó phản hồi (acknowledgment) được gửi từ broker trở lại producer.

1.3.3.1. Cấu Hình Gom Nhóm

Có nhiều cấu hình khác nhau để kiểm soát cách gom nhóm hoạt động, nhưng ba cấu hình quan trọng nhất là:

  • batch.size: Tất nhiên bạn sẽ muốn kiểm soát kích thước batch; mặc định là khoảng 16 KB. Mỗi batch chỉ chứa các bản ghi (record) hướng đến cùng một phân vùng (partition). Một số lưu ý:

    • Nếu bản ghi lớn hơn batch.size, chúng sẽ không được gom nhóm. Vì vậy, điều quan trọng là bạn phải hiểu rõ dữ liệu của mình
    • Nếu giá trị batch.size được đặt là 0, gom nhóm sẽ bị vô hiệu hóa.
    • Nếu kích thước batch nhỏ, producer sẽ tạo ra nhiều batch hơn và gửi đến broker thường xuyên hơn. Điều này có thể làm giảm thông lượng (throughput), do mất thêm thời gian cho chu trình gửi – nhận (round-trip) để thực hiện các request với ít sự kiện (event) hơn. Mặt khác, nếu kích thước batch lớn hơn, bạn có thể tăng thông lượng, nhưng cũng có nguy cơ lãng phí bộ nhớ ở phía producer.
    • batch.size chỉ là giới hạn trên. Chúng ta cũng có một thành phần thời gian tác động đến việc ngắt quá trình thêm bản ghi vào batch, được xử lý bởi linger.ms.
  • linger.ms: Khoảng thời gian producer sẽ chờ để lấp đầy batch đến giới hạn batch.size. Một số điều cần ghi nhớ:

    • linger.ms được đặt là 0 theo mặc định, nghĩa là chúng ta không chờ để gom bản ghi trước khi gửi, do đó gom nhóm (batching) bị vô hiệu hóa. Nói cách khác, thay đổi batch.size không đủ để bật gom nhóm.
    • Nếu bạn muốn gom nhóm dữ liệu để có thông lượng tốt hơn, hãy biết rằng bạn đang đánh đổi bằng cách tăng độ trễ (latency). Giá trị bạn chọn cho linger.ms sẽ có thể thêm độ trễ tương ứng vào các request của bạn. Tuy nhiên, tăng linger.ms cũng có thể cải thiện độ trễ bằng cách giảm áp lực lên các broker.
  • buffer.memory: Đây là một cấu hình thường bị bỏ qua nhưng rất quan trọng. Trong khi producer chờ và gom nhóm bản ghi, dữ liệu phải được lưu trữ tạm thời ở đâu đó. buffer.memory cho producer biết nên dành bao nhiêu bộ nhớ đệm để lưu dữ liệu. Mặc định là khoảng 32 MB. Rõ ràng, buffer.memory nên lớn hơn batch.size hoặc bạn sẽ toang.

Bây giờ bạn đã có khả năng gom nhóm và có thể tối ưu bằng cách điều chỉnh các cấu hình trên. Cùng với đó, các giá trị mặc định cũng rất đáng chú ý. Nhiều thử nghiệm đã được thực hiện để chọn các giá trị mặc định cho từng cấu hình trên. Nếu bạn định thay đổi chúng, hãy kiểm thử diện rộng thật kỹ lưỡng – cả ở môi trường sản phẩm (production scale) – để đảm bảo bạn đã đánh giá đầy đủ các lợi ích và đánh đổi khi tinh chỉnh cơ chế gom nhóm.

Note từ dịch giả: Throughput (thông lượng) là tốc độ truyền tải dữ liệu thành công giữa producer – broker – consumer trong một khoảng thời gian nhất định.

1.3.3.2. Giám Sát Việc Gom Nhóm

Nếu bạn quyết định sử dụng gom nhóm (batching) và muốn đảm bảo mọi thứ diễn ra tốt đẹp, bạn phải theo dõi một số thông số (metrics). Dưới đây là những số liệu quan trọng nhất:

  • batch-size-avg: Số liệu này cho biết kích thước thực tế của batch. Nếu mọi thứ ổn, giá trị này sẽ gần bằng với batch.size. Nếu batch-size-avg liên tục thấp hơn kích thước batch đã đặt, thì linger.ms có thể không đủ cao. Đồng thời, nếu linger.ms cao mà batch vẫn nhỏ, có thể là các bản ghi không được tạo đủ nhanh và bạn đang thêm độ trễ không cần thiết.
  • records-per-request-avg: Đây là số lượng bản ghi trung bình trên các batch trong mỗi request. Nếu bạn muốn gom nhóm, đây là một chỉ số “kiểm tra sức khỏe” hữu ích.
  • record-size-avg: Đây là kích thước trung bình của các bản ghi trong batch; nó rất hữu ích để điều chỉnh tối ưu các cấu hình. Ví dụ, nếu giá trị này gần bằng hoặc cao hơn batch.size, thì producer của bạn thậm chí không thể gom nhóm.
  • buffer-available-bytes: Giúp bạn theo dõi lượng bộ nhớ đệm còn lại trên Producer.
  • record-queue-time-avg: Tôi chưa đề cập đến hàng đợi, nên số liệu này có thể làm bạn hơi bất ngờ. Nhưng trong khi chúng ta xây dựng batch, các bản ghi thực chất đang nằm trong một hàng đợi. Số liệu này cho thấy chúng ta mất bao lâu để lấp đầy batch trước khi gửi bản ghi.

1.3.4. Nén Dữ Liệu (Compressing)

Được rồi, bây giờ chúng ta có:
✅ Các raw bytes
Partition
✅ Nhiều raw bytes hơn

Có thể chúng ta có quá nhiều raw bytes. Bước tiếp theo (tùy chọn) cho producer là nén (compressing) các dữ liệu mà nó muốn gửi đến broker.

Mặc định, compression bị vô hiệu hóa, nhưng bạn hoàn toàn có thể lựa chọn một trong số các phương pháp nén (compression) sẵn có. Chọn phương pháp nén (compression) bạn muốn và sử dụng nén (compression) với cấu hình compression.type. Tôi khuyên dùng z-standard để bắt đầu, nhưng cũng khuyến khích bạn xem tài liệu để biết thêm thông tin nếu muốn sử dụng nén.

Nếu bạn cần tối ưu hơn, với sự ra đời của KIP-390 trong phiên bản Apache Kafka 3.8, bạn có khả năng chọn mức độ nén cho nhiều phương pháp này với compression.[type].level.

1.3.5. Gửi Request

Giờ thì chúng ta đã có đầy đủ mọi thứ cần thiết để gửi dữ liệu đến broker. Vậy Kafka producer thực hiện việc đó như thế nào?

Mỗi producer duy trì kết nối socket với một số broker Kafka. Sau đó, chúng gửi request bằng giao thức nhị phân (binary protocol) qua TCP.

Đây là mô hình request – response: producer gửi request đến broker để lưu trữ dữ liệu; và broker gửi response lại cho producer với kết quả (hy vọng là thành công) của request đó. Chính quá trình request này khởi động bước khởi đầu thực sự của hành trình hoành tráng của chúng ta.

1.3.5.1. Cấu Hình Gửi Request Của Producer

Trước khi đi xa hơn, có một số cấu hình cần lưu ý:

  • max.request.size: Kích thước tối đa mỗi request mặc định là khoảng 1 MB. Điều này sẽ giới hạn trực tiếp số lượng batch chúng ta có thể gửi trong một request. Các broker cũng có giới hạn về kích thước tối đa của request sau khi nén.
  • acks: Bạn chắc chắn đã thấy acks, hay "acknowledgements" trước đây. Để đảm bảo tính sẵn sàng cao (high availability) và khả năng chịu lỗi (fault tolerance) trong Kafka, bạn có thể cấu hình (tùy chọn) cho các topic-partitions có bản sao (replicas) – các bản sao được lưu trữ trên cluster. Dữ liệu ban đầu được gửi đến leader broker và sau đó được sao chép sang các broker chứa bản sao. Nói một cách đơn giản, acks trả lời câu hỏi: “Cần ghi thành công dữ liệu vào bao nhiêu bản sao đồng bộ (in-sync replicas) trước khi gửi phản hồi lại cho producer?” Mặc định là all, nhưng bạn có thể chọn acks=0 hoặc acks=1 nếu muốn mạo hiểm. (Số lượng bản sao đồng bộ (in-sync replicas) này có thể được cấu hình bằng min.insync.replicas – mặc định là 1, và bạn nên tăng lên nếu muốn nâng cao độ bền dữ liệu (durability))
  • max.in.flight.requests.per.connection: Các producer duy trì kết nối với nhiều broker khác nhau tùy thuộc vào nơi mà các topic-partition đang nằm. Với mỗi kết nối như vậy, không nên làm hàng đợi request trên broker bị "quá tải", do đó nên đặt ra giới hạn số lượng request có thể gửi mỗi kết nối. Mặc định là 5. (Tại sao là 5? Vì nó phù hợp với idempotence. ⤵️)
  • enable.idempotencetransactional.id: Nếu bạn muốn đảm bảo thứ tự dữ liệu, không bị mất mát, không bị ghi trùng, thì bạn nên bật tính năng idempotence. Khi enable.idempotence=true (mặc định), chúng ta đảm bảo rằng acks=all, producer bật retries, và max.in.flight.requests.per.connection=5. Bằng cách này, producer có thể đảm bảo tính bất biến khi lặp thao tác (idempotent) trong suốt phiên làm việc của producer. Nếu bạn muốn đảm bảo idempotent xuyên suốt nhiều phiên làm việc của producer khác nhau, thì bạn cần dùng transaction. Để bắt đầu sử dụng transaction, trước tiên bạn cần cấu hình transactional.id, sau đó bạn có thể bắt đầu và commit transaction ngay trong client code.
  • request.timeout.ms: Khi request được gửi đến broker, request.timeout.ms có hiệu lực. Đây là thời gian tối đa producer sẽ chờ response, trước khi thử lại (retry) hoặc ném ra exception; mặc định là 30 giây. Các lần thử lại (retry) có thể được cấu hình và tùy chỉnh với delivery.timeout.ms (đảm bảo lớn hơn request.timeout.ms cộng với linger.ms!), retries, và retry.backoff.ms.

Note từ dịch giả:
-Availability (khả năng sẵn sàng) là khả năng hệ thống duy trì hoạt động bình thường và tiếp nhận ghi dữ liệu ngay cả khi một vài thành phần gặp sự cố.
-Fault tolerance (khả năng chịu lỗi) là khả năng hệ thống vẫn hoạt động được ngay cả khi một vài thành phần gặp lỗi.
-Durability (độ bền dữ liệu) là khả năng đảm bảo rằng dữ liệu ghi vào hệ thống sẽ không bị mất, kể cả khi có sự cố.
-Idempotent (tính bất biến khi lặp thao tác) là một tính chất trong hệ thống máy tính, có nghĩa là: Một thao tác (hoặc một request) được thực hiện một hoặc nhiều lần, nhưng kết quả cuối cùng vẫn giống nhau.
-delivery.timeout.ms: tổng thời gian có thể retry
-retries: số lần retry
-retry.backoff.ms: khoảng nghỉ giữa các lần retry

1.3.5.2. Giám Sát Việc Gửi Request Của Producer

Trước khi tìm hiểu sâu hơn về vòng đời của request, bạn nên nắm rõ một số chỉ số (metrics) quan trọng liên quan đến cách producer xử lý request:

  • request-rate: Mô tả số lượng request được thực hiện mỗi giây (tốc độ gửi request) bởi producer.
  • requests-in-flight: Số liệu trên từng Producer mô tả số lượng request hiện đang chờ broker xử lý. Bạn có thể dùng nó để đánh giá xem các broker có đang bị quá tải hay không, hoặc producer của bạn có đang gửi số lượng request phù hợp không. Lý tưởng nhất, bạn sẽ muốn giá trị này thấp, điều đó cho thấy broker đang xử lý hiệu quả.
  • request-latency-avg: Khi request được gửi đến broker, một bộ đếm thời gian bắt đầu. Nó chỉ dừng lại cho đến khi producer nhận được response. Số liệu này đo thời gian trung bình để một request hoàn tất.

1.3.5.3. The Request

Vậy là chúng ta đã có trong tay những bản ghi đã được batch (gộp nhóm), thậm chí có thể đã được nén, và tất cả đều được gom theo topic-partition và broker tương ứng. Chúng ta có thể có nhiều batch như vậy. Bây giờ chúng ta có thể lấy toàn bộ các batch đó và gửi chúng trong một request duy nhất từ producer đến broker.

Việc hình dung cấu trúc lồng nhau (nested object) của request này có thể hơi khó. Vì vậy, cho những ai thích học qua hình ảnh, dưới đây là một ví dụ minh họa về request từ producer:

produce_request.png

Một lần nữa, cần nhấn mạnh rằng request này được gửi đến một broker duy nhất trong cụm (cluster). Tuy nhiên, request đó có thể chứa dữ liệu cho nhiều topic-partition khác nhau trên broker đó. Điều này phụ thuộc vào cách các producer hoạt động và loại dữ liệu mà chúng gửi đi. Kết quả có thể thay đổi tùy theo tình huống.

2. Tóm tắttt

Tác giả ví Kafka như một “hộp đen” giúp lưu trữ và phục vụ dữ liệu sự kiện gần như theo thời gian thực mà bạn không cần tốn nhiều công sức - nhưng khi có sự cố, việc gỡ lỗi có thể gây sự khó chịu. Để mở toang “hộp đen” này, loạt bài gồm bốn phần sẽ:

  1. Cách Producer Hoạt Động: Khám phá Producer đã làm gì phía sau hậu trường để chuẩn bị dữ liệu sự kiện thô (raw event data) để gửi đến Broker.
  2. Xử Lý Request Từ Producer: Tìm hiểu cách dữ liệu đi từ Producer Client đến ổ đĩa trên Broker.
  3. Chuẩn Bị Cho request Truy Vấn Của Consumer: Xem cách Consumer gửi request lấy dữ liệu.
  4. Xử Lý Request Truy Vấn Của Consumer: Đi sâu vào hoạt động bên trong của Broker khi chúng cố gắng cung cấp dữ liệu cho Consumer.

Trong suốt hành trình, bạn sẽ nắm được các tùy chọn cấu hình điều khiển từng giai đoạn và chỉ số (metrics) cần theo dõi - giúp bạn tự tin gỡ lỗi cả client và cluster khi “hộp đen” không hoạt động như mong đợi.

Và dưới đây là bài đầu tiên trong bốn phần, bàn về:
Cách Producer Hoạt Động: Khám phá Producer đã làm gì phía sau hậu trường để chuẩn bị dữ liệu sự kiện thô (raw event data) để gửi đến Broker.

2.1. Chuẩn bị hàng trang: Schema & Topic

  • Schema: Định nghĩa cấu trúc bản ghi (ví dụ Avro “hobbitUpdate” với các trường hobbit_name, location, status).
  • Topic: Tạo topic (ví dụ hobbit-updates) với số partition mong muốn (mặc định 6) và cleanup policy (ví dụ delete).

2.2. Gửi dữ liệu: producer.send()

Lệnh gọi này khởi động một chuỗi bước—serialization, partitioning, batching, compression, rồi gửi qua mạng.

2.3. Chuẩn hóa (Serialization)

Mục tiêu: Chuyển high-level object (JSON, POJO, etc.) thành mảng byte.
Cấu hình:

  • key.serializer / value.serializer

    • Trỏ tới lớp (ví dụ StringSerializer, AvroSerializer) triển khai serialize(topic, data) → byte[] cho key/value.
  • Schema Registry (nếu dùng Avro/JSON Schema/Protobuf):

    • schema.registry.url – endpoint của Registry
    • schema.registry.basic.auth.user.infoAPI_KEY:API_SECRET để xác thực

2.4. Phân vùng (Partitioning)

Mục tiêu: Lưu mỗi bản ghi (record) vào một phân vùng (partition) của topic.
Chiến lược phân vùng:

  • Thủ công: truyền trực tiếp tham số partition.
  • Tự động:
    • Key-hash strategy: partition = hash(key) % numPartitions.
    • Sticky strategy (mặc định khi không có key): gom liên tiếp nhiều message vào cùng partition cho tới khi đạt ngưỡng, sau đó chuyển đổi để cân bằng tải.

Tinh chỉnh:

  • partitioner.class – ghi đè hoàn toàn chiến lược phân vùng, có thể viết logic riêng (ví dụ round-robin, key-aware).
  • partitioner.ignore.keys (true/false) – nếu true, bỏ qua key dù có key, luôn dùng sticky.
  • Định tuyến phân vùng:
    • partitioner.adaptive.partitioning.enable=true – bật cơ chế định tuyến thích ứng: producer theo dõi độ trễ phản hồi của các broker và tự động điều chuyển tải sang các broker nhanh hơn.
    • partitioner.availability.timeout.ms – thời gian chờ trước khi loại bỏ phân vùng chậm: cấu hình khoảng thời gian tối đa producer sẽ đợi phản hồi từ một phân vùng (partition). Nếu quá thời gian này mà phân vùng vẫn chưa phản hồi, nó sẽ bị đánh dấu là không khả dụng và producer sẽ định tuyến dữ liệu sang phân vùng khác.

5. Gom nhóm (Batching)

Mục tiêu: Gộp nhiều bản ghi (record) cùng hướng đến một broker thành các gói dữ liệu (payload) lớn hơn để giảm số lần truyền dữ liệu qua mạng
Cấu hình chính:

  • batch.size (byte) – giới hạn kích thước batch cho mỗi partition (mặc định ~16 KB).
  • linger.ms (ms) – thời gian chờ tối đa để gom thêm bản ghi trước khi gửi batch (mặc định 0).
  • buffer.memory (byte) – tổng bộ nhớ đệm cho tất cả batch đang chờ gửi (mặc định ~32 MB).

Giám sát:

  • batch-size-avg – kích thước batch trung bình; gần bằng giá trị batch.size nghĩa là batch được sử dụng tốt.
  • records-per-request-avg – số bản ghi (record) trung bình mỗi request.
  • record-size-avg – kích thước trung bình của bản ghi (record); nếu ≥ batch.size nghĩa là không gom được thành nhóm.
  • buffer-available-bytes – lượng bộ nhớ đệm còn lại; giảm dần có thể báo hiệu hiện tượng nghẽn (backpressure).
  • record-queue-time-avg – thời gian trung bình bản ghi chờ trong hàng đợi trước khi gửi; tăng cao có thể do cấu hình linger.ms hoặc vấn đề về thông lượng (throughput) thấp.

6. Nén (Compression) (tuỳ chọn)

Mục tiêu: Giảm kích thước các gói dữ liệu (payload) và băng thông mạng.
Cấu hình:

  • compression.type – chọn phương pháp nén: none (mặc định), gzip, snappy, lz4, hoặc zstd.
  • compression.<type>.level (Kafka 3.8+) – tinh chỉnh mức độ nén (tỷ lệ nén so với mức sử dụng CPU) (ví dụ compression.zstd.level=3).
  • Lưu ý batch.size – sau nén batch vẫn phải nằm trong giới hạn batch.size và không vượt quá message.max.bytes của broker.

7. Gửi Request

Cơ chế: Kết nối TCP socket gửi request bằng giao thức nhị phân (binary protocol) tới leader broker cho từng partition.
Cấu hình quan trọng:

  • max.request.size (byte) – giới hạn tổng payload (đã serialize + nén) mỗi request (mặc định ~1 MB).
  • acks – số bản sao (replica) cần xác nhận (acknowledge) trước khi broker phản hồi:
    • 0 = không chờ xác nhận.
    • 1 = chỉ leader xác nhận.
    • all = tất cả các replica đồng bộ (in-sync replicas - ISR).
  • min.insync.replicas – kết hợp với acks=all để đảm bảo tối thiểu số lượng bản sao đồng bộ (in-sync replicas) cần có để lưu thành công.
  • max.in.flight.requests.per.connection – số request đồng thời trên mỗi kết nối broker (mặc định 5; cao hơn có thể tăng throughput nhưng rủi ro lỗi thứ tự khi retry).
  • Idempotence & Transactions:
    • enable.idempotence=true – cài đặt acks=all, bật retries, và max.in.flight.requests.per.connection=5 nhờ đó đảm bảo duy trì idempotence (tính bất biến khi lặp thao tác) trong session.
    • transactional.id – giúp đảm bảo tính atomic (tất cả hoặc không ghi gì) và idempotent (tính bất biến khi lặp thao tác) giữa nhiều phiên producer khác nhau.
  • Retry & timeout:
    • request.timeout.ms – thời gian chờ broker phản hồi trước khi retry (mặc định 30.000 ms = 30s).
    • delivery.timeout.ms – tổng thời gian cho retry (cần đảm bảo delivery.timeout.ms > request.timeout.ms + linger.ms).
    • retries / retry.backoff.ms – số lần retry và khoảng nghỉ giữa các lần.

Giám sát request:

  • request-rate – số request mà producer gửi đi mỗi giây.
  • requests-in-flight – số request đang chờ phản hồi; cao có thể cho thấy broker đang quá tải hoặc chậm.
  • request-latency-avg – độ trễ trung bình khứ hồi (round-trip) của mỗi request từ producer; tăng đột biến có thể do sự cố mạng hoặc broker.
  • failed-records-per-sec – số bản ghi bị lỗi hoặc bị loại bỏ mỗi giây.

Lời Kết

Mình xin gửi lời cảm ơn chân thành đến Danica Fine và đội ngũ Confluent đã mang đến loạt bài sâu sắc này - nhờ đó mà mình có cơ hội dịch thuật và chia sẻ kiến thức quý giá với mọi người.

Cảm ơn bạn đọc đã dành thời gian theo dõi blog, hy vọng phần dịch và ghi chú của mình sẽ giúp các bạn nắm bắt rõ hơn về cách Kafka vận hành “hộp đen” và tự tin hơn khi triển khai, vận hành hệ thống cũng như gỡ lỗi.

Nếu bạn có góp ý, câu hỏi hay kinh nghiệm thực tế muốn chia sẻ, đừng ngần ngại để lại bình luận hoặc liên hệ với mình. Mình rất mong được thảo luận và học hỏi thêm từ cộng đồng!

Chúc các bạn học tập và làm việc hiệu quả với Apache Kafka!

Link bài viết gốc | Link series tiếng Việt

Bình luận

Bài viết tương tự

- vừa được xem lúc

Kafka là gì?

Apache Kafka® là một nền tảng stream dữ liệu phân tán. . stream data: dòng dữ liệu, hãy tưởng tượng dữ liệu là nước trong 1 con suối. .

0 0 53

- vừa được xem lúc

001: Message-driven programming với Message broker và Apache Kafka

Bài viết nằm trong series Apache Kafka từ zero đến one. . . Asynchronous programming.

0 0 178

- vừa được xem lúc

002: Apache Kafka topic, partition, offset và broker

Bài viết nằm trong series Apache Kafka từ zero đến one. Nói qua về lịch sử, Kafka được phát triển bởi LinkedIn (các anh em dev chắc chẳng xa lạ gì) và viết bằng ngôn ngữ JVM, cụ thể là Java và Scala.

0 0 160

- vừa được xem lúc

003: Gửi và nhận message trong Apache Kafka

Bài viết nằm trong series Apache Kafka từ zero đến one. . . Nếu muốn các message được lưu trên cùng một partition để đảm bảo thứ tự thì làm cách nào.

0 0 236

- vừa được xem lúc

004: Apache Kafka consumer offset, Broker discovery và Zookeeper

Bài viết nằm trong series Apache Kafka từ zero đến one. 1) Consumer offset.

0 0 134

- vừa được xem lúc

Apache Kafka - Producer - Gửi message đến Kafka bằng kafka-python

Overview. Understand how to produce message and send to the Kafka topic. Architecture. .

0 0 77