Building Pubsub for 50M concurrent socket connections

Bài tóm tắt Disney Hotstar 2019 3. Quy mô lớn Độ khó: Khó

Disney Hotstar · 2019 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Đọc toàn văn qua trình duyệt (blog.hotstar.com chặn crawler, Wayback CDX "Temporarily Offline"). Bài chỉ kể phần broadcast; phần unicast được hẹn ở bài sau.

Bối cảnh

  • IPL 2019, Hotstar ra Social Feed dưới video trên app mobile: nội dung gắn với diễn biến trận đấu nên phải đẩy realtime, không cache được, thời điểm không đoán trước (mỗi quả bóng dài ngắn khác nhau), và không được làm hao pin client.
  • Ba kiểu giao tiếp: fan-out (một nguồn, mọi user cùng nội dung, ưu tiên cao nhất), unicast (mỗi user một topic), fan-in (hành động của user lên backend, có thể thay bằng HTTP nên ưu tiên thấp nhất).

Thay đổi chính

  • Giao thức: so Socket.io, NATS, MQTT bằng prototype và benchmark, chọn MQTT. Bỏ gRPC streaming vì server push của HTTP/2 vốn để đẩy tài nguyên trước, và ELB của AWS (lúc đó) không hỗ trợ backend HTTP/2.
  • Broker mã nguồn mở (để tự tối ưu): VerneMQ và EMQX. Tiêu chí test fan-out: 1 publisher, tin 1 KB mỗi giây, N subscriber; số kết nối và subscription đủ, latency pub→sub trung bình khoảng 500 ms (dưới 1 giây) và không tăng dần, tin nhận = tin gửi × N.
    • VerneMQ ở 50.000 sub: thêm subscription không phải O(1), subscription tới trễ khoảng 20 giây.
    • EMQX một node c5.4xlarge: tới 250.000 sub trong ngưỡng; 500.000 vẫn giao đủ, chỉ chậm hơn. Có bug kẹt routing sau mỗi lần test, họ học Erlang và tự sửa.
  • Cụm: latency tăng tuyến tính theo số node (thêm bước nhảy giữa node). Tách node publish riêng (chỉ chuyển tiếp tới các node subscribe) thì latency không đổi theo số node: 2 triệu sub. Nhưng chỉ tới khoảng 8 node, do Mnesia (DB phân tán của Erlang) thiết kế cho ít node.
  • Load balancer: ELB cổ điển mặc định rớt kết nối ngẫu nhiên từ khoảng 500.000; phải pre-warm, mỗi ELB tới khoảng 4 triệu kết nối, nên dùng nhiều ELB. Đã thử NLB (gặp bug), HAProxy/Nginx, cân tải phía client; theo bài, cân tải kết nối TCP bền không đơn giản.
  • Vượt 2 triệu:
    • Nhiều cụm nối bằng bridge của EMQX, một node publish "master" chuyển xuống: phải đặt master là node ẩn trong Erlang distribution để các cụm thật sự độc lập, rồi qua mốc 10 triệu. Nhưng cứng nhắc: master phải biết mọi cụm, hai tham số (số cụm, số node mỗi cụm) khó scale.
    • Reverse bridge: một service Go chạy trên mỗi node subscribe, tự kéo mọi tin (wildcard topic) từ node publish rồi đẩy vào EMQX cục bộ. Node subscribe không cần biết nhau; đặt chúng trong Auto Scaling Group để co giãn và tự thay node hỏng.
  • Kết quả: một publisher phục vụ hàng nghìn node subscribe, mỗi node 250.000 client; 200 node là 50 triệu kết nối trở lên.

Vì sao

  • Broadcast không cần node subscribe biết nhau; bỏ phần đồng bộ trạng thái cụm (Mnesia) là bỏ trần scale (Realtime và fan-out).

Bài học cho SWE

  • Định nghĩa tiêu chí pass/fail của load test trước (số kết nối, latency không tăng dần, tin nhận đủ), rồi tìm giới hạn của một node trước khi lên cụm.
  • Với fan-out một nguồn, đổi push từ trung tâm thành pull từ mỗi node lá biến hệ cứng nhắc thành hệ co giãn được.
  • Load balancer cũng là một tầng có trần với kết nối bền: pre-warm, chia nhiều LB, hoặc cân tải phía client.

Liên quan