Using Rust to Scale Elixir for 11 Million Concurrent Users

Bài tóm tắt Discord 2019 3. Quy mô lớn Độ khó: Trung bình

Discord · 2019 · 3. Quy mô lớn · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Discord đổi cách cập nhật Member List: chỉ gửi thay đổi của phần danh sách đang hiển thị thay vì mọi thành viên, đỡ băng thông và pin cho client.
  • Phía server (Elixir) cần một sorted set giữ hàng trăm nghìn phần tử, chịu rất nhiều thay đổi và trả về vị trí (index) chỗ thêm/bớt.
  • Dữ liệu trong Elixir là bất biến: thêm một người vào guild 100.000 thành viên nghĩa là dựng danh sách mới 100.001 phần tử.

Thay đổi chính

  • Thử các cấu trúc có sẵn: MapSet không giữ thứ tự; List + sort mất 170.000 µs khi chèn cuối danh sách 250.000 phần tử; ordsets của Erlang còn 27.000 µs.
  • Tự viết bằng Elixir thuần: chuỗi các "cell" chứa ordset nhỏ (kiểu skip list) đưa chèn cuối xuống 5.000 µs nhưng chèn đầu lên 19.000 µs vì phần tử dồn dây chuyền qua các cell; cho cell phình rồi tách thì chèn đầu 4 µs, chèn cuối 640 µs. Giới hạn khoảng 250.000 thành viên.
  • Thử Rust qua NIF (Native Implemented Function) bằng thư viện Rustler, một tuần làm proof-of-concept: 0,4–2,85 µs với số nguyên; bản đầy đủ hỗ trợ Erlang term đo 0,61–3,68 µs với tập 5.000 đến 1.000.000 phần tử.
  • Mọi thao tác dưới 1 ms nên không cần lo chuyện yield/reduction của scheduler BEAM. Phía Elixir gọi SortedSet như một module bình thường. Mã nguồn mở sorted_set_nif.

Vì sao

  • Giữ logic realtime cốt lõi trong Elixir (đồng thời dễ, an toàn), chỉ hạ xuống Rust đúng cấu trúc dữ liệu nóng mà mô hình bất biến không làm nhanh được.

Bài học cho SWE

  • Benchmark theo vị trí xấu nhất (đầu, cuối, kích thước lớn), không chỉ trung bình: mỗi lần tối ưu một trường hợp lại sinh trường hợp xấu mới.
  • Native extension trong runtime có GC/scheduler riêng phải ngắn (dưới ngưỡng scheduler) và không được làm crash VM; Rustler đảm bảo phần sau.
  • Đổi ngôn ngữ từng điểm nóng thay vì viết lại cả service.

Liên quan