0

Giải mã RdKafka: "Trùm cuối" ẩn mình giúp PHP giao tiếp với Kafka siêu tốc

Chào anh em cộng đồng Viblo!

Nếu anh em làm Kafka với PHP hoặc Laravel, chắc chắn anh em đã từng phải chạy cái lệnh này trên server hoặc trong Dockerfile: pecl install rdkafka.

Thậm chí, các thư viện xịn xò nhất hiện nay như mateusjunges/laravel-kafka hay enqueue/rdkafka đều có chữ "rdkafka" chình ình trong đó.

Vậy RdKafka thực chất là cái quái gì? Chữ "Rd" có nghĩa là gì? Tại sao PHP thuần không tự nói chuyện được với Kafka mà cứ phải dựa dẫm vào thằng này?

Hôm nay, chúng ta sẽ bóc trần sự thật về kiến trúc bên dưới của PHP khi làm việc với Streaming Data. Đảm bảo đọc xong, anh em sẽ hiểu tại sao thỉnh thoảng code PHP đẩy message lên Kafka lại bị... bốc hơi mất! Lên xe!

1. Bản chất: Tại sao PHP thuần "sợ" Kafka?

Như bài viết trước chúng ta đã phân tích, Kafka sử dụng Giao thức Nhị phân (Binary Protocol) chạy trực tiếp trên nền TCP. Nó yêu cầu một kết nối mạng được giữ liên tục (Persistent Connection) để trao đổi các nhịp tim (Heartbeat) và luân chuyển hàng triệu byte mỗi giây.

Và đây chính là điểm yếu "chí mạng" của PHP truyền thống (FPM):

  • PHP được thiết kế theo kiểu "Sống vội - Chết trẻ" (Share-nothing architecture). Mỗi request HTTP sinh ra một tiến trình, chạy xong vài dòng code là bị "giết" ngay lập tức để giải phóng RAM.
  • PHP không giỏi trong việc duy trì kết nối mạng dài hạn (Long-lived TCP connections) hoặc chạy xử lý bất đồng bộ đa luồng (Multithreading) ở tầng thấp.

Nếu dùng PHP thuần để tự parse từng byte nhị phân của Kafka, CPU của máy chủ sẽ bốc cháy vì quá tải, và tốc độ sẽ chậm như rùa bò.

2. Vị cứu tinh mang tên librdkafka

Để giải quyết bài toán này, những kỹ sư hàng đầu (cụ thể là Magnus Edenhill - cha đẻ của thư viện) đã viết ra một thư viện bằng ngôn ngữ C/C++ có tên là librdkafka.

(Fun fact: Chữ "rd" xuất phát từ "RapidData", tên công ty cũ của tác giả).

Đây là một cỗ máy xử lý Kafka khủng khiếp nhất, nhẹ nhất và nhanh nhất thế giới. Nó được dùng làm "lõi" cho hầu hết các client Kafka của các ngôn ngữ khác như Python (confluent-kafka-python), Go (confluent-kafka-go), Node.js (node-rdkafka), và tất nhiên là cả PHP.

Cái extension php-rdkafka mà bạn cài qua PECL thực chất chỉ là một lớp Vỏ (Wrapper). Nó giúp mã PHP của bạn giao tiếp được với cái lõi C/C++ librdkafka siêu tốc nằm bên dưới hệ điều hành.

3. "Phép thuật" Đa luồng (Multithreading) bí mật trong PHP

Đây là phần "mind-blowing" (ảo diệu) nhất mà 90% anh em dùng PHP không hề hay biết!

Chúng ta luôn được dạy là: PHP đơn luồng (Single-threaded). Code chạy từ trên xuống dưới, dòng này xong mới tới dòng kia.

Nhưng khi bạn dùng RdKafka, ứng dụng của bạn đã biến thành Đa luồng (Multi-threaded) một cách âm thầm!

Khi bạn gọi hàm $producer->produce(...) trong PHP:

  1. PHP không hề mở kết nối mạng để gửi data lên Kafka.
  2. Nó chỉ ném cục data đó vào một hàng đợi trong RAM và giao cho librdkafka.
  3. Lúc này, librdkafka đã âm thầm tạo ra một Luồng chạy ngầm (Background Thread) bằng ngôn ngữ C nằm sâu bên trong tiến trình PHP.
  4. Cái Thread ngầm này mới là kẻ đứng ra mở TCP socket, gom nhóm các message (Batching), và lầm lũi gửi data lên Kafka Broker bất kể code PHP của bạn ở bên ngoài đang làm gì.

4. Giải mã các Lỗi "Kinh Điển" vì thiếu hiểu biết về RdKafka

Chính vì sự tồn tại của cái Thread ngầm này mà anh em PHP thường xuyên đạp phải 2 bãi mìn sau:

Bãi mìn 1: Producer gửi tin nhắn bị "Bốc hơi"

Bạn viết một file Script test:

$producer = new RdKafka\Producer($conf);
$topic = $producer->newTopic("my-topic");

// Đẩy message
$topic->produce(RD_KAFKA_PARTITION_UA, 0, "Hello Kafka");

echo "Gửi xong!";
// Kết thúc file PHP

Bạn chạy script, thấy in ra "Gửi xong!", nhưng lên Kafka check thì không có dòng data nào cả!

Giải thích: Khi gọi hàm produce(), data mới chỉ được ném vào RAM của cái Thread ngầm (C/C++). Ngay giây tiếp theo, tiến trình PHP chạy đến cuối file và... kết thúc (Die). Khi PHP chết, nó kéo theo cái Thread ngầm chết theo, trong khi Thread này chưa kịp gửi data qua mạng lưới!

Cách sửa (Lệnh Flush thần thánh): Bạn bắt buộc phải ép PHP đứng lại chờ cái Thread ngầm làm xong việc rồi mới được chết. Bằng hàm flush():

$topic->produce(RD_KAFKA_PARTITION_UA, 0, "Hello Kafka");

// Chờ tối đa 10000 mili-giây (10 giây) để cái Thread ngầm gửi nốt data đi
$producer->flush(10000); 

echo "Gửi xong THẬT SỰ!";

Bãi mìn 2: Consumer không chịu cập nhật trạng thái (Rebalance)

Khi viết Kafka Consumer, bạn phải đặt nó trong một vòng lặp vô hạn while(true). Nhưng thư viện C/C++ bên dưới cũng cần nói chuyện với Kafka Broker để giữ nhịp tim (Heartbeat) báo rằng "Tôi còn sống".

Ở các phiên bản cũ của extension, nếu vòng lặp của bạn chạy xử lý data quá lâu mà không gọi hàm giao tiếp nào với RdKafka, Kafka Broker sẽ tưởng Worker này đã chết (Timeout) và đá nó ra khỏi group, gây ra hiện tượng Rebalance liên tục. (Từ librdkafka v1.0 trở lên, heartbeat đã được tự động đẩy vào một background thread riêng để khắc phục lỗi này, nhưng việc hiểu vòng đời poll() vẫn rất quan trọng).

Lời kết

RdKafka (librdkafka) là một kiệt tác kỹ thuật. Bằng cách mượn sức mạnh của C/C++, nó đã "độ" cho PHP một đôi cánh để có thể gánh được hàng chục ngàn message mỗi giây mà CPU vẫn không hề hấn gì.

Khi sử dụng các package như laravel-kafka, bạn sẽ không phải gọi hàm flush() hay cấu hình C++ thủ công vì tác giả package đã bọc sẵn các hàm an toàn cho bạn rồi. Tuy nhiên, việc hiểu rõ bản chất "Đa luồng ẩn" và cơ chế bất đồng bộ bên dưới sẽ giúp bạn debug những lỗi cực dị liên quan đến timeout, rớt mạng hay mất data một cách dễ dàng.

Anh em khi cài đặt Docker cho dự án có hay bị vật vã với cái extension rdkafka này không (nhất là trên Mac M1/M2)? Cùng chia sẻ kinh nghiệm compile extension dưới bình luận nhé!

Chúc anh em code xịn và hệ thống chạy mượt!


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í