Trong các hệ thống phân tán, cân bằng tải tầng ứng dụng đóng vai trò then chốt, đặc biệt là cơ chế "sticky session" (phiên dính). Đối với giao thức MQTT, việc duy trì trạng thái kết nối và xử lý phiên làm việc hiệu quả là yếu tố quyết định đến hiệu năng của toàn bộ cụm broker.
Cơ chế Phiên MQTT và Thách thức trong Cụm phân tán
Các client MQTT thường thiết lập kết nối dài hạn để liên tục nhận dữ liệu. Khi mạng không ổn định hoặc client khởi động lại, kết nối có thể bị đứt quãng. Để đảm bảo không mất mát dữ liệu, client thường yêu cầu broker lưu trữ trạng thái phiên bằng cách thiết lập cờ Clean-Session thành false. Khi đó, broker sẽ lưu giữ các topic đã đăng ký và hàng đợi tin nhắn (QoS 1 và 2) trong thời gian client ngoại tuyến.
Khi mở rộng quy mô thành cụm (cluster), client kết nối thông qua một load balancer. Nếu client ngắt kết nối và sau đó được điều hướng đến một node khác khi kết nối lại, node mới phải thực hiện "session takeover" (tiếp quản phiên). Quá trình này đòi hỏi việc đồng bộ hóa và di chuyển dữ liệu trạng thái giữa các node, gây tiêu tốn tài nguyên mạng và CPU, đặc biệt khi số lượng tin nhắn tồn đọng lớn hoặc có nhiều client tái kết nối đồng loạt.
Giải pháp Sticky Session
Sticky session giải quyết vấn đề trên bằng cách đảm bảo load balancer luôn định tuyến một client cụ thể trở lại đúng node đã xử lý kết nối trước đó, giúp loại bỏ hoàn toàn chi phí di chuyển trạng thái. Để thực hiện điều này, load balancer cần phân tích gói tin MQTT ở tầng ứng dụng để trích xuất ClientID hoặc username. Thông tin này sau đó được sử dụng làm khóa để băm (hash) hoặc tra cứu bảng ánh xạ nhằm xác định node đích cố định cho client đó.
Thiết lập thực nghiệm với HAProxy và EMQX
Phần này sẽ hướng dẫn cách cấu hình HAProxy để thực hiện sticky session cho cụm EMQX sử dụng Docker.
Khởi tạo mạng và các node EMQX
Tạo một mạng ảo nội bộ để các container giao tiếp với nhau:
docker network create mqtt_internal_net
Triển khai hai node EMQX. Lưu ý biến môi trường PROXY_PROTOCOL được bật để broker có thể nhận diện IP thực của client thay vì IP của load balancer:
# Khởi tạo Node A
docker run -d --name emqx_node_a \
--network mqtt_internal_net \
-e EMQX_NODE_NAME=emqx@emqx_node_a \
-e EMQX_LISTENER__TCP__EXTERNAL__PROXY_PROTOCOL=on \
emqx/emqx:4.3.7
# Khởi tạo Node B
docker run -d --name emqx_node_b \
--network mqtt_internal_net \
-e EMQX_NODE_NAME=emqx@emqx_node_b \
-e EMQX_LISTENER__TCP__EXTERNAL__PROXY_PROTOCOL=on \
emqx/emqx:4.3.7
Liên kết Node B vào cụm do Node A làm master:
docker exec -it emqx_node_b emqx_ctl cluster join emqx@emqx_node_a
Cấu hình HAProxy 2.4
Tạo tệp cấu hình /opt/haproxy/haproxy.cfg với nội dung sau. Cấu hình này sử dụng stick-table để lưu trữ ánh xạ ClientID và node đích:
global
log stdout format raw daemon debug
nbproc 1
nbthread 4
stats socket :8888 level admin expose-fd listeners
defaults
log global
mode tcp
timeout connect 10s
timeout client 1m
timeout server 1m
frontend mqtt_frontend
bind *:1883
default_backend mqtt_backend_pool
backend mqtt_backend_pool
mode tcp
# Định nghĩa bảng sticky session dựa trên chuỗi ký tự
stick-table type string len 64 size 200k expire 1h
# Trích xuất ClientID từ payload MQTT CONNECT
stick on req.payload(0,0),mqtt_field_value(connect,client_identifier)
server broker_a emqx_node_a:1883 check-send-proxy send-proxy-v2
server broker_b emqx_node_b:1883 check-send-proxy send-proxy-v2
Khởi động container HAProxy:
docker run -d --network mqtt_internal_net --name haproxy_lb \
-p 1883:1883 -p 8888:8888 \
-v /opt/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg \
haproxy:2.4
Kiểm thử và Xác minh Sticky Table
Sử dụng Mosquitto client để kiểm tra luồng tin nhắn. Đầu tiên, tạo một subscriber với ClientID cố định:
docker run --rm -it --network mqtt_internal_net eclipse-mosquitto \
mosquitto_sub -h haproxy_lb -t 'sensor/telemetry' -I device_alpha
Từ một terminal khác, publish tin nhắn đến topic tương ứng:
docker run --rm -it --network mqtt_internal_net eclipse-mosquitto \
mosquitto_pub -h haproxy_lb -t 'sensor/telemetry' -m '{"temperature": 28.5}'
Để xác minh xem HAProxy có đang ghim device_alpha vào một node cụ thể hay không, truy vấn Runtime API của HAProxy thông qua socket thống kê:
echo "show table mqtt_backend_pool" | socat stdio tcp4-connect:127.0.0.1:8888
Kết quả trả về sẽ hiển thị bảng ánh xạ hiện tại, cho thấy ClientID device_alpha đã được gán cứng vào một server đích (ví dụ: broker_b), xác nhận cơ chế sticky session đang hoạt động chính xác ở tầng ứng dụng.