How WhatsApp Grew to Nearly 500 Million Users, 11,000 cores, and 70 Million Messages a Second

Bài tóm tắt WhatsApp 2014 3. Quy mô lớn Độ khó: Khó

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

Nguồn thứ cấp: Todd Hoff (HighScalability) tóm tắt talk của Rick Reed tại Erlang Factory 2014; nhận định ghi "theo WhatsApp qua lời kể của HighScalability". Phần tiếp của WhatsApp 2014 (bài đó chủ yếu kể talk 2012). Khác bài kia, bài này mô tả kiến trúc backend sau khi lượng traffic tăng 10 lần.

Bối cảnh (đầu 2014)

  • 465 triệu user/tháng; 19 tỉ tin vào và 40 tỉ tin ra mỗi ngày; 600 triệu ảnh, 200 triệu voice, 100 triệu video mỗi ngày.
  • Đỉnh: 147 triệu kết nối đồng thời, 230K login/giây, 342K tin vào/giây và 712K tin ra/giây. Đêm Giáng sinh 146 Gb/s ra ngoài; đêm Giao thừa 2 tỉ ảnh được tải (46K/giây), một ảnh bị tải 32 triệu lần.
  • Khoảng 10 người làm phần Erlang, kiêm cả dev lẫn ops; theo Rick Reed là 40 triệu user mỗi kỹ sư.
  • Stack: Erlang R16B01 có patch riêng, FreeBSD 9.2, Mnesia, Yaws; bare metal thuê ở SoftLayer, cấu hình hai data center.
  • Khoảng 550 server cộng máy dự phòng: ~150 chat server (mỗi máy ~1 triệu điện thoại), ~250 server multimedia; node DB 512 GB RAM, node thường 64 GB; hơn 11.000 core chạy Erlang.

Thay đổi chính so với 2012

  • Bỏ mục tiêu tối đa kết nối mỗi máy: chạy 1 triệu kết nối/máy thay vì 2 triệu như 2012, vì cần chỗ trống hấp thụ đỉnh, user hoạt động nhiều hơn, và nhiều chức năng trước nằm ngoài nay chạy trên chính các máy đó. Vẫn giữ chiến lược máy rất to, ít node: theo Rick Reed, độ phức tạp vận hành tăng theo số node chứ không theo số core.
  • Kiến trúc: điện thoại nối tới chat và MMS; chat nối tới kho offline tạm thời (giữ tin trong lúc đang chuyển) và các DB Account, Profile, Push, Group. Tin nhắn và media chỉ lưu trong lúc giao; metadata media nằm trong Mnesia in-memory khoảng 2 TB RAM, chia 16 partition, khoảng 18 tỉ record.
  • Tách để lỗi không lan: mọi thứ được partition; hàng đợi đọc và ghi riêng; mỗi node đích một process gửi riêng để node hỏng chỉ làm nghẽn tin tới nó (họ patch Mnesia để làm vậy khi replicate async_dirty); dùng cast thay call trừ khi cần kết quả từ DB; call dùng timeout thay vì monitor; nosuspend cho cast best-effort để node không bị scheduler treo khi buffer phân tán đầy; buffer phân tán lớn.
  • Meta-cluster: tự viết wandist (transport kiểu dist trên gen_tcp, dạng mesh giữa các node cần nói chuyện) cộng lớp định tuyến một hop trên pg2, để giới hạn kích thước một cluster và cho cluster trải dài nhiều data center.
  • Song song hoá: gen_server một luồng → gen_factory (nhiều worker) → gen_industry (nhiều tiến trình dispatch) khi chính tiến trình dispatch thành nút thắt do fan-in từ nhiều node. Worker chọn theo key cho thao tác DB; I/O có độ trễ khó đoán thì dùng FIFO để tránh head-of-line blocking.
  • Partition dịch vụ: 2 đến 32 phần, đa số 32; node chạy theo cặp primary/secondary; giới hạn tối đa 8 tiến trình truy cập một bảng ETS hay một fragment Mnesia để kiểm soát tranh chấp khoá.
  • Mnesia không dùng transaction: băm record về một partition → một fragment → một worker, nên mọi truy cập một record đi qua một process; mỗi fragment chỉ được ghi trên một node nên replication một chiều. Patch OTP cho nhiều transaction manager async_dirty, cho thư mục Mnesia trải nhiều ổ đĩa; thu "đảo" Mnesia về 2 node để thao tác schema chạy được khi có tải. Network partition: báo động nhanh, chạy tiếp, rồi gộp lại bằng tay.

Tối ưu và patch

  • Kho offline từng nghẽn khi tải tăng đột ngột. Khoảng 50% tin được đọc trong 60 giây, nên họ thêm write-back cache: giao tin trước khi ghi xuống file system, tỉ lệ hit 98%; khi I/O nghẽn, cache làm bộ đệm.
  • Mailbox quá lớn (user ở rất nhiều group, hàng nghìn tin/giờ) bị đẩy khỏi cache vì làm bẩn cache.
  • Bảng account 512 fragment: tăng gấp đôi số host lại làm throughput giảm, vì cách băm tạo chuỗi băm dài hơn 2K (mục tiêu 7). Sửa hai dòng, nhanh gấp 4.
  • Patch BEAM: nhiều timer wheel thay cho một (hàng trăm nghìn lần đặt/xoá timer mỗi giây); round-robin file I/O bất đồng bộ qua các luồng; seed hash ETS; Mnesia nạp từ node gần thay vì bên kia lục địa; không xếp hàng dump khi đang dump.

Sự cố 22/2/2014

  • 210 phút downtime ngay sau thương vụ Facebook, không do tải: một router backend bỏ một VLAN, cả cluster mất kết nối rồi nối lại hàng loạt, rơi vào trạng thái chưa từng gặp.
  • Phải dừng toàn bộ rồi khởi động lại, việc nhiều năm không làm. Phát hiện pg2 khi mất/nối kết nối có thể nhắn tin theo bậc n³: hàng đợi tăng từ 0 lên 4 triệu trong vài giây.

Phát hành

  • Không mô phỏng được tải ở quy mô này (nhất là đỉnh nửa đêm Giao thừa). Thay đổi rủi ro chạy trên một phần nhỏ traffic, sửa nhanh tới khi ổn rồi mới mở rộng.
  • Nâng cấp kiểu rolling, mọi thứ có dư thừa; hot patch không cần restart là hiếm.
  • Việc còn treo (2014): nạp lại DB rất lâu và hay lỗi khi dữ liệu lớn; công cụ xem trạng thái và điều khiển cluster thời gian thực; partition theo luỹ thừa 2 (32 → 64 được, 128 thì không thực tế).

Vì sao

  • Theo WhatsApp: Erlang cho phép ít kỹ sư phục vụ nhiều user, SMP scale tốt nên giữ được ít node, đổi code khi đang chạy. Theo HighScalability, Mnesia có vẻ là nguồn vấn đề lớn ở quy mô này.
  • Không lưu trữ lâu dài (tin và media chỉ tồn tại khi đang giao) giúp kiến trúc chỉ xoay quanh throughput, cache và partition.

Bài học cho SWE

  • Khi máy bận hơn, giảm mật độ kết nối để có chỗ trống hấp thụ đỉnh; kỷ lục kết nối/máy không phải mục tiêu vận hành.
  • Cô lập theo đích (mỗi node một hàng đợi, đọc/ghi tách) để một chỗ chậm không chặn cả hệ.
  • Gửi mọi truy cập một record qua một process thay cho transaction là cách đổi tính nhất quán lấy throughput, đi kèm việc phải tự gộp khi network partition.
  • Tầng membership (pg2) có thể thành nguồn sự cố lớn nhất khi cả cluster mất kết nối cùng lúc; thứ hiếm khi làm (dừng và khởi động lại toàn bộ) nên được tập.

Liên quan