Realtime và fan-out

Chủ đề Dành cho thành viên
Bài đọc thử: 3/3 (đây là bài miễn phí cuối của bạn) · Xem gói thành viên

Bài toán

Một sự kiện (tin nhắn, ảnh, tweet, vote) phải tới rất nhiều người, rất nhanh, qua rất nhiều kết nối mở cùng lúc.

Diễn tiến theo thời gian

  • 2011 · Instagram: fan-out feed chạy bất đồng bộ trong Gearman (~200 worker Python), nhờ vậy đăng ảnh nhanh như nhau với user ít hay nhiều follower. Feed lưu trong Redis. Instagram 2011
  • 2011→2014 · WhatsApp (qua HighScalability): tin nhắn chỉ xếp hàng trên server tới khi client kết nối lại và xác nhận, không lưu lịch sử; media đi qua HTTP server riêng. Chỉ số sức khoẻ chính là độ dài hàng đợi message của từng process; kiểm thử bằng traffic production (rẽ nhánh, dồn DNS) vì tải tổng hợp không mô phỏng được. WhatsApp 2014
  • 2012 · WhatsApp: giữ kết nối. Một server Erlang giữ 2,28 triệu kết nối TCP. WhatsApp 2012
  • 2013 · Twitter: chịu đỉnh 143.199 tweet/giây (~25 lần bình thường) sau khi tách service "timeline" riêng và đổi storage tweet sang hash. Lưu ý: bài này không mô tả cơ chế fan-out timeline; phần đó cần nguồn khác. Twitter 2013
  • 2013 · Twitter (qua HighScalability): 300K QPS đọc so với khoảng 6.000 ghi/giây; home timeline fan-out lúc ghi vào cụm Redis (mỗi timeline 3 bản, tối đa 800 phần tử, chỉ user hoạt động 30 ngày), search thì ngược lại (ghi một máy, đọc scatter-gather). Tài khoản hàng chục triệu follower làm p99 giao hàng tới 5 phút, reply tới trước tweet gốc; hướng đi: không fan-out cho tài khoản rất lớn mà trộn lúc đọc. Twitter 2013
  • 2014 · SoundCloud: stream từ fan-out on write trên Cassandra (lưu trữ tăng theo bậc hai, đổi graph gần như không được) sang fan-in on read: mỗi hành động một lần append vào outbox, đọc thì merge outbox của người mình follow. Dữ liệu cỡ trăm GB nên để hết trong RAM; Roshi dùng CRDT kiểu LWW-element-set trên Redis ZSET. SoundCloud 2014
  • 2014 · WhatsApp (qua HighScalability): 147 triệu kết nối đồng thời trên ~150 chat server; hạ từ 2 triệu xuống 1 triệu kết nối/máy để có chỗ hấp thụ đỉnh khi user nhắn nhiều hơn. Mỗi node đích một process gửi riêng để node chậm chỉ làm nghẽn tin tới nó; wandist + định tuyến một hop trên pg2 nối các cluster ở nhiều data center. WhatsApp 2014
  • 2014 · Twitter: Timeline dùng Redis vì ghi tăng dần nhỏ trên list lớn; ziplist lớn làm latency ghi khi fan-out khó đoán, Hybrid List giữ kích thước khối cố định; tail latency của một shard chậm kéo cả query fan-out. Twitter 2014
  • 2014 · LinkedIn: bài viết của member toả ra ba kênh với ba độ trễ: feed (Feed-Mixer đọc Activities Index lúc request), thông báo bất đồng bộ chỉ cho kết nối mạnh và gộp theo tác giả, email Pulse bằng job Hadoop. LinkedIn 2014
  • 2014→2016 · LinkedIn: FollowFeed chọn fan-out-on-read: thử nghiệm cho thấy fan-out-on-write lớn gấp 62 lần, và feed dựng sẵn phải sắp lại toàn bộ mỗi lần đổi model relevance. Broker scatter-gather tới index node giữ timeline theo entity, relevance chạy ngay trên node; p99 nhanh 5 lần Sensei. Feed nhiều tầng: first pass ranker lấy top-k rẻ, second pass ranker chấm ít ứng viên. LinkedIn 2016 LinkedIn 2015
  • 2015→2018 · Pinterest: following feed từ fan-out lúc ghi (job chấm điểm Pin cho mọi follower, ghi vào HBase) sang sinh lúc đọc (Apiary, Polaris, Aperture, Scorpion). Lý do: tiết kiệm lưu trữ, tín hiệu tươi hơn, thử nghiệm dễ hơn. Pinterest 2018
  • 2015→2016 · Ele.me: message broadcast cho đơn hàng (RabbitMQ): phát sự kiện, không phát trạng thái; message chỉ để báo, consumer cần thêm thì gọi API tra ngược; consumer phải idempotent. Polling của merchant được thay bằng long connection + push/pull, nhưng push qua mạng public có lúc nhanh hơn replication, nên client đọc replica không thấy đơn mới. Ele.me 2020 Ele.me 2020
  • 2015 · Riot Games: chat LoL khoảng 1 tỷ event/ngày trên Erlang: mỗi player một process, tra bảng định tuyến nhân bản trong RAM rồi gửi bằng message passing; viết lại 10% phần xử lý chuỗi bằng C giảm CPU hơn 60% và bộ nhớ mỗi session từ 150 kB còn 25 kB. Riot Games 2015
  • 2016 · Stack Overflow: ~500.000 websocket đồng thời cho thông báo, vote, câu trả lời mới; chạy ngay trên web tier, thay polling; dùng Redis pub/sub. Vấn đề gặp phải: cạn ephemeral port và file handle ở load balancer. Stack Overflow 2016
  • 2016→2025 · Zalando: chiều ngược lại: kiến trúc "chỉ có event, không có read API" (2016) buộc mỗi team replay stream và tự dựng kho, sinh nhiều nguồn sự thật; event Offer gộp product không đổi nên trễ tới 30 phút dịp Cyber Week. Năm 2022 họ dựng Product Read API (p99 dưới 10 ms), vẫn phục vụ định dạng cũ trong thời hạn sunset. Zalando 2025
  • 2016 · Zomato: Food Feed (review, ảnh của bạn bè, ưu đãi) từ Redis sang Cassandra như dữ liệu chuỗi thời gian; schema thiết kế để đọc feed trong một lần thay vì đọc hàng trăm key. Zomato 2016
  • 2016→2023 · LinkedIn: mạng cấp 2 (thường ≥50.000 entity) không materialize được vì mỗi lần ghi khuếch đại khoảng 250 lần; LIquid làm cho việc tính lúc đọc đủ rẻ (index đồ thị trong RAM, mỗi cạnh vài L3 miss). PYMK chuyển từ GAIA sang LIquid: 120 → 18.000 QPS, dưới 50 ms (2023). LinkedIn 2020 LinkedIn 2023
  • 2017 · Discord: mỗi user một process session, mỗi guild một process; sự kiện trong guild fan-out tới mọi session. Với guild 30.000 người online, mỗi lần publish mất 0,9–2,1 giây. Manifold gom theo node đích (một message cho mỗi node) và phân phối theo core, vẫn giữ thứ tự. Thêm FastGlobal và Semaphore cho các đường nóng khác. Discord 2017
  • 2017 · Slack: team hàng chục nghìn người làm snapshot rtm.start quá nặng; Flannel ở edge phục vụ 4 triệu kết nối đồng thời, kế hoạch chuyển fan-out vào Flannel (một bản cho mỗi server thay vì mỗi người dùng) và chuyển client sang pub/sub theo màn hình (presence: số event giảm 5 lần). Slack 2017
  • 2017 · LinkedIn: dữ liệu chuẩn hoá tham chiếu nhau bằng URN nên frontend phải "decorate" đệ quy qua nhiều vòng gọi; dùng đường tổ tiên đã lưu sẵn (PathsToRoots) để lấy mọi URN rồi giải song song. LinkedIn 2017
  • 2018 · LinkedIn (presence): chấm online cho khoảng 500 triệu member trên pub/sub sẵn có của tin nhắn: heartbeat mỗi d giây + entry TTL d + ε chống chập chờn; offline phát hiện bằng delayed trigger (một Akka actor mỗi member online); D2 băm member ID để heartbeat dính một node; p99 dưới 200 ms. LinkedIn 2018
  • 2018 · LinkedIn (Concourse): fan-out thông báo nội dung: gắn feature của người tạo trước khi nhân bản (ít message), feature người nhận tra local sau khi đổi khoá theo người nhận; đọc topic Kafka local thay vì aggregate tăng năng lực 4 lần; đẩy delta feature hằng ngày, đẩy đủ hằng tuần. LinkedIn 2018
  • 2019 · Figma: multiplayer gán mỗi file thiết kế cho đúng một instance; hơn 80% user ở ngoài Mỹ nên muốn đặt Multiplayer gần user (chọn data center cho từng file). LiveGraph: client đăng ký query và nhận push khi kết quả đổi. Figma 2019
  • 2019 · Atlassian: board Jira poll mỗi phút → push qua JiRT đọc event bus sẵn có (StreamHub) và đẩy qua FPS (pub/sub frontend trên PubNub). Event chỉ mang ID (ARI/AVI), client tự gọi lại backend để lấy dữ liệu có kiểm tra quyền; client đệm event và xử lý từng cái để tránh bão refetch. Atlassian 2019
  • 2019 · Disney Hotstar: Social Feed IPL đẩy tới 50 triệu kết nối bằng MQTT (EMQX). Một node 250.000 client; cụm EMQX chỉ tới khoảng 8 node vì Mnesia; tách node publish riêng để latency không tăng theo số node; ELB phải pre-warm, mỗi cái khoảng 4 triệu kết nối. Cuối cùng bỏ cụm: mỗi node subscribe chạy một reverse bridge tự kéo tin từ node publish, đặt trong Auto Scaling Group. Disney Hotstar 2019
  • 2019 · Postman: service Sync ôm cả WebSocket lẫn giao dịch DB, gần 1 triệu kết nối đồng thời; mỗi lần deploy ngắt mọi client, đợt kết nối lại mất 6–8 giờ mới hồi. Tách gateway Bifrost (giữ kết nối, ít deploy, Redis làm broker) khỏi API (logic, deploy thường xuyên); chuyển dần qua service bootstrap, scale bằng nhiều máy nhỏ với metric autoscale riêng. Postman 2020
  • 2019→2021 · Slack: lớp kết thúc websocket chuyển HAProxy → Envoy vì reload HAProxy sinh tiến trình mới phải giữ hàng giờ cho websocket dài sống rút dần; Envoy cập nhật endpoint động và hot restart không rớt kết nối; panic routing giúp trong sự cố 4/1/2021. Slack 2021
  • 2019 · Zillow: chat giữa chủ nhà và người thuê: Satellite API chạy websocket phát cả tin người dùng lẫn tin hệ thống, thay cho trao đổi qua email. Zillow 2019
  • 2019 · Discord: Member list chỉ gửi thay đổi của phần đang hiển thị; phía server cần sorted set chịu nhiều thay đổi và trả index, làm bằng Rust NIF trong Elixir. Discord 2019
  • 2020 · eBay: Kafka chỉ bảo đảm thứ tự trong một partition của một cluster. Nhân bản sang cluster khác là mất thứ tự, trừ khi chỉ ghi ở một nơi hoặc gắn số version vào header để sắp lại. eBay 2020
  • 2021 · Zerodha: ticker đẩy hàng chục triệu gói dữ liệu thị trường mỗi giây, viết lại ít nhất 5 lần trong 6 năm. Giữa các phòng ban, mạng API đồng bộ điểm-điểm được thay bằng PubSub để các hệ không còn biết nhau. Zerodha 2021
  • 2021 · WhatsApp: multi-device với E2EE: server không đọc được tin nên fan-out dời về client, mã hoá và gửi N lần cho N thiết bị của cả hai bên; group vẫn dùng Sender Key; cuộc gọi gửi SRTP secret riêng cho từng thiết bị. WhatsApp 2021
  • 2022 · Railway: trang log poll mỗi 2 giây được thay bằng GraphQL subscription → gRPC stream đẩy log thẳng về frontend. Railway 2022
  • 2023 · WhatsApp: cuộc gọi: tắt chuông người lạ bằng giao thức do server thực thi để máy nhận không xử lý dữ liệu lạ (chống zero-click), server quyết bằng privacy token do client phát; tuỳ chọn đi qua relay thay cho peer-to-peer để giấu IP. WhatsApp 2023
  • 2023 · Discord: Một guild hơn 1 triệu người online (Midjourney): công việc fan-out tăng bình phương theo số người. Passive session cắt 90% việc (chỉ nới khoảng 3 lần kích thước tối đa); relay chia fan-out ra nhiều process/máy, sau phải bỏ việc chép toàn bộ danh sách thành viên vào mỗi relay; thao tác duyệt mọi thành viên chạy ở worker đọc ETS để guild không đứng. Discord 2023
  • 2024 · Meta/WhatsApp: codec MLow cho cuộc gọi trên mạng kém và máy yếu (hơn 20% cuộc gọi trên ARMv7, hàng chục triệu cuộc gọi WhatsApp/ngày trên máy hơn 10 năm): gấp đôi chất lượng Opus ở 6 kbps, tính toán ít hơn 10%, đủ bitrate để nhét FEC. WhatsApp 2024
  • 2024 · Discord: Gateway đổi zlib sang zstandard streaming (fork ezstd thêm streaming; nén từng message nhỏ thì tệ hơn zlib), bỏ dictionary và buffer động vì lợi ít; passive update gửi delta thay vì snapshot (từ 35% xuống 5% băng thông gateway). Tổng giảm gần 40% băng thông. Discord 2024
  • 2025 · Trigger.dev: realtime cho tiến độ task không dùng WebSocket mà dùng replication slot của Postgres qua ElectricSQL: client gửi shape, nhận dòng đầu kèm transaction ID làm con trỏ, rồi long poll; subscribe được cả run đã bắt đầu trước. 20.000 cập nhật/giây, dưới 100 ms tới trình duyệt (theo Trigger.dev). Trigger.dev 2025
  • 2025 · HRT: fan-out phía client: nhiều tab của cùng một người dùng nạp và xử lý cùng dữ liệu realtime; gom về một SharedWorker dùng chung (HRTWorker). Hudson River Trading 2025
  • 2026 · Discord: mặt trái của mô hình monitor: 17% session chết thì mỗi process đang theo dõi nhận một :DOWN. Kết quả là bão kết nối lại ở gateway, và hàng loạt cuộc gọi bị dừng rồi tạo lại ở voice syncers (điều phối hơn 25.000 SFU). Discord 2026
  • 2026 · Discord: fan-out của guild tới hàng triệu session làm tracing nổ số span; lấy mẫu lớp hai theo số người nhận và cấm session mở trace mới sau fan-out. Trace định lượng được user mất gần 16 phút mới kết nối được tới một guild suy giảm. Discord 2026
  • 2026 · Stream (nhà cung cấp): "materialized feed": fan-out on read nhưng cache kết quả trong TiKV với cursor cho từng nguồn được theo dõi, mỗi lần đọc chỉ lấy phần mới rồi merge; ghi một lần như pull, đọc khoảng 10 ms gần như push (theo Stream). Benchmark tự chạy: 4,5 triệu user đồng thời, đọc p50 khoảng 15 ms; một kênh chat 5 triệu người, khoảng 15 triệu lượt giao/giây. Cuckoo filter bỏ qua user không nối vào server trước khi push. Stream (GetStream) 2026
  • 2026 · Optiver: fan-out thay đổi của bảng Postgres nhỏ tới hàng nghìn subscriber: không mỗi người một replication slot mà một PG feed đọc một slot và lọc theo bảng; client lấy snapshot rồi nhận stream, phần khâu nối là chỗ khó nhất. Optiver 2026

Các lựa chọn và đánh đổi

Bài toán con Ví dụ Kỹ thuật
Giữ nhiều kết nối dài WhatsApp, Stack Overflow, Discord Runtime nhẹ (BEAM), nâng giới hạn kernel, websocket thay polling
Fan-out một → nhiều Instagram, Discord Đẩy ra hàng đợi; gom theo node đích
Đỉnh đột biến Twitter Thiết kế cho đỉnh, cô lập lỗi
Bật lại hàng loạt (reconnect storm) Discord Lookup không copy, semaphore

Quy tắc rút ra

  1. Có hai bài toán khác nhau: giữ nhiều kết nối (bài toán của runtime/OS) và fan-out một → nhiều (bài toán phân phối). Đừng gộp làm một.
  2. Fan-out nên tách khỏi request ghi (Instagram dùng hàng đợi).
  3. Nhóm cực lớn (guild, tài khoản nhiều follower) là trường hợp riêng, cần đường xử lý riêng. Cùng gốc với Hot partition và thundering herd.
  4. Runtime actor model (Erlang/Elixir) lặp lại ở bài toán này. Xem Đổi ngôn ngữ và runtime.
  5. Giữ thứ tự sự kiện là ràng buộc thật; Discord loại phương án "mỗi publish một process" chính vì lý do này.
  6. Sinh sẵn hay sinh lúc đọc là đánh đổi giữa chi phí lưu trữ/độ tươi và latency đọc (Pinterest 2015 → 2018).
  7. Tách lớp giữ kết nối khỏi lớp logic: gateway giữ socket mỏng và ít deploy, logic deploy tự do; không thì mỗi lần deploy là một đợt kết nối lại hàng loạt (Postman 2019). Với broadcast một nguồn, để node lá tự kéo tin thay vì cụm đồng bộ trạng thái (Hotstar 2019). Postman 2020 Disney Hotstar 2019
  8. Luồng thay đổi có con trỏ thay cho hệ thông báo riêng: khi dữ liệu đã bền trong DB, đọc WAL (replication slot) và long poll theo transaction ID cho phép subscribe sau sự kiện và tự bắt kịp (Trigger.dev 2025). Trigger.dev 2025

Câu hỏi mở

  • Fan-out-on-write hay fan-out-on-read? SoundCloud 2014 lật từ inbox trên Cassandra sang fan-in on read vì dữ liệu outbox đủ nhỏ để nằm trong RAM (SoundCloud 2014). Pinterest trả lời một phần: chuyển từ sinh sẵn (rẻ khi đọc nhưng tốn lưu trữ cho cả user không quay lại, khó thử nghiệm) sang sinh lúc đọc, với hạ tầng latency cực thấp (Pinterest 2018). Twitter 2013 (qua HighScalability) trả lời phần tài khoản cực nhiều follower: fan-out lúc ghi cho đa số, còn tài khoản rất lớn thì trộn lúc đọc, vì p99 giao hàng tới 5 phút và reply tới trước tweet gốc (Twitter 2013). Vẫn chưa có nguồn đo chi phí của cách lai này sau khi triển khai. Stream 2026 (nhà cung cấp, benchmark tự chạy) mô tả một điểm giữa: pull nhưng cache kết quả với cursor theo từng nguồn, mỗi lần đọc chỉ lấy phần chênh Stream (GetStream) 2026; chưa có nguồn production của khách.
  • WhatsApp: WhatsApp 2014 bổ sung phần lớn (ejabberd → giao thức riêng, tin xếp hàng tới khi client lấy, đo độ dài hàng đợi, vá BEAM), nhưng là nguồn thứ cấp và tác giả tự nói chưa biết kiến trúc tổng thể (lưu trữ, định tuyến giữa các máy).
  • Liên hệ: Event-driven, message queue và Kafka (luồng event giữa các service ở backend: thứ tự, dead letter, Kafka đa region).