Case Study: Design Nearby Friends (Real-Time Location Sharing)
” tưởng tượng bạn đang đi chơi với nhóm bạn ở Sài Gòn. Ai cũng trên Zalo, nhưng không ai biết ai đang ở đâu. Bạn mở app — và trong vòng 1 giây, bản đồ hiện ra 5 người bạn đang ở gần bạn trong bán kính 5km. Phía sau sự ‘đơn giản’ đó là một hệ thống real-time phức tạp, nơi WebSocket gặp Pub/Sub gặp Redis ở quy mô 100 triệu người dùng.”
Tags: system-design nearby-friends real-time websocket pubsub redis alex-xu case-study vol2 Student: Hieu Prerequisite: Tuan-02-Back-of-the-envelope · Tuan-06-Cache-Strategy · Tuan-10-Consistent-Hashing · Tuan-17-Design-Chat-System Lien quan: Case-Design-Proximity-Service · Tuan-05-Load-Balancer · Tuan-08-Message-Queue · Tuan-13-Monitoring-Observability · Tuan-14-AuthN-AuthZ-Security Reference: Alex Xu, System Design Interview — An Insider’s Guide, Volume 2 — Chapter 2: Nearby Friends
1. Context & Why — Tại sao Nearby Friends quan trọng?
1.1 Analogy — Nhóm bạn đi chơi trong thành phố
hãy tưởng tượng bạn và 10 người bạn cùng đi chơi ở TP.HCM vào tối thứ 7. Mỗi người có kế hoạch riêng — có người ở quận 1 ăn uống, có người ở Bitexco mua sắm, có người ở Thủ Đức. Bạn muốn biết: ai đang ở gần mình để rủ nhau?
Cách truyền thống: Bạn nhắn tin hỏi từng người — “Ey, mày đang ở đâu?” — rồi đợi từng người trả lời. 10 người, 10 tin nhắn, 10 lần đợi. Có người không trả lời, có người trả lời trễ 30 phút. Lúc đó họ đã đi chỗ khác rồi.
Cách Nearby Friends: Bạn mở app, bật tính năng “Nearby Friends”. App tự động hiển thị trên bản đồ: “Minh đang ở Nguyễn Huệ, Tuấn đang ở Đồng Khởi cách 500m, Lan đang ở Bùi Viện cách 800m.” Cập nhật mỗi 30 giây. Bạn không cần hỏi ai cả — hệ thống làm mọi thứ tự động.
Đây chính là tính năng Nearby Friends — tương tự Snap Map (Snapchat), Zalo Nearby, Facebook Nearby Friends (đã discontinued 2022), hoặc WhatsApp Live Location.
1.2 Vấn đề kỹ thuật — Tại sao bài toán này khó?
| Vấn đề | Giải thích | Quy mô |
|---|---|---|
| Real-time location updates | Vị trí thay đổi liên tục, mỗi 30 giây phải cập nhật. Không phải query 1 lần như Proximity Service | 10M concurrent users x update mỗi 30s = ~333K updates/s |
| Bidirectional communication | Server cần push location của bạn bè đến client, không chỉ client pull | HTTP polling lãng phí, cần WebSocket |
| Fan-out problem | Mỗi user có trung bình 400 bạn. Mỗi update location phải notify tới 400 người | 333K updates/s x 400 friends = 133M messages/s |
| Stateful connections | WebSocket là stateful — khó scale hơn stateless HTTP | Cần connection management, reconnection, server affinity |
| Privacy sensitive | Location là personal data nhạy cảm nhất — cần opt-in, opt-out, precision control | GDPR, CCPA, luật bảo vệ dữ liệu cá nhân |
| Selective sharing | Chỉ hiển thị bạn bè đã opt-in và đang online. Không phải tất cả 400 bạn bè | Filtering logic phức tạp |
1.3 So sánh với Proximity Service
| Khía cạnh | Proximity Service (Chapter 1) | Nearby Friends (Chapter 2) |
|---|---|---|
| Đối tượng | Businesses (tĩnh) | Friends (động — di chuyển liên tục) |
| Update frequency | Businesses hiếm khi đổi vị trí | Users di chuyển mỗi giây |
| Communication | Request-Response (HTTP) | Bidirectional (WebSocket) |
| Data freshness | Chấp nhận stale vài giờ | Cần real-time (< 30s) |
| Indexing | Geohash/Quadtree cho millions of POIs | Không cần geospatial index — chỉ check khoảng cách giữa friends |
| Scale challenge | Read-heavy (60K QPS) | Write-heavy + fan-out (133M messages/s) |
| Reference | Case-Design-Proximity-Service | Bài này |
Insight quan trọng: Nearby Friends KHÔNG phải là Proximity Service cho người dùng. Proximity Service tìm “ai ở gần mình?” trong tất cả người lạ. Nearby Friends chỉ hiển thị bạn bè — bạn đã biết danh sách bạn bè, chỉ cần biết vị trí hiện tại của họ.
1.4 Real-World Applications
| App | Tính năng | Đặc điểm |
|---|---|---|
| Snap Map (Snapchat) | Hiển thị vị trí bạn bè trên bản đồ | Real-time, Bitmoji avatar, Ghost Mode để ẩn |
| Zalo Nearby | Tìm người dùng Zalo gần vị trí hiện tại | Khác — tìm người lạ, không chỉ bạn bè |
| WhatsApp Live Location | Chia sẻ vị trí real-time với contact/group | Có thời hạn (15min, 1h, 8h) |
| Find My Friends (Apple) | Xem vị trí gia đình/bạn bè | Tích hợp iOS, battery-efficient |
| Telegram Live Location | Chia sẻ vị trí trong chat | Tương tự WhatsApp |
2. Step 1 — Understand the Problem & Establish Design Scope
2.1 Clarifying Questions
| Câu hỏi | Trả lời | Ghi chú |
|---|---|---|
| Tính năng chính là gì? | Hiển thị danh sách bạn bè đang ở gần mình, trên bản đồ, cập nhật real-time | Core feature duy nhất |
| ”Gần” nghĩa là bao xa? | Trong bán kính có thể cấu hình, mặc định 5 miles (~8 km) | Configurable radius |
| Tần suất cập nhật? | Mỗi 30 giây | Không phải real-time từng giây — 30s là đủ |
| Quy mô bao lớn? | 100M DAU (Daily Active Users) | Scale của Facebook/Zalo |
| Bao nhiêu người online đồng thời? | ~10% của DAU ở peak = 10M concurrent | Peak hours: 7-10 PM |
| Trung bình mỗi user có bao nhiêu bạn? | 400 friends | Trung bình Facebook ~338, làm tròn 400 |
| User cần opt-in không? | Có — chỉ user bật tính năng mới chia sẻ vị trí | Privacy là số 1 |
| Có cần lưu location history? | Không — chỉ cần vị trí hiện tại | Giảm storage, tăng privacy |
| Hiển thị khoảng cách hay vị trí chính xác? | Cả hai — khoảng cách + vị trí trên bản đồ | Client render |
2.2 Functional Requirements
- FR1: User có thể bật/tắt tính năng Nearby Friends (opt-in/opt-out)
- FR2: Khi bật, app hiển thị danh sách bạn bè đang ở trong bán kính configurable (mặc định 5 miles)
- FR3: Danh sách cập nhật mỗi 30 giây mà không cần user refresh
- FR4: Mỗi friend entry hiển thị: tên, khoảng cách, thời gian cập nhật cuối cùng
- FR5: Chỉ hiển thị bạn bè cùng bật tính năng (mutual opt-in)
- FR6: Khi user tắt tính năng hoặc offline, biến mất khỏi danh sách của bạn bè
2.3 Non-Functional Requirements
| Yêu cầu | Mục tiêu | Giải thích |
|---|---|---|
| Availability | 99.9% | Tính năng social, không phải safety-critical như navigation |
| Latency | Location update propagation < 1 giây | Từ lúc friend update location đến lúc bạn nhận được |
| Scalability | 10M concurrent users, 333K location updates/s | Peak traffic |
| Consistency | Eventual consistency ok | Nhận vị trí trễ 1-2 giây là chấp nhận được |
| Battery efficiency | Không drain battery quá nhiều | GPS polling mỗi 30s, không phải liên tục |
| Privacy | Location chỉ chia sẻ với friends đã opt-in | GDPR compliant |
2.4 Estimation — Back-of-the-Envelope
Concurrent Users:
Location Update QPS:
Mỗi giây, hệ thống nhận ~333K location updates từ clients. Đây là write-heavy.
WebSocket Connections:
Nếu mỗi WebSocket server handle 50K connections (con số thực tế cho production):
Pub/Sub Fan-out Volume:
Nhưng không phải tất cả 400 friends đều online và opt-in. Giả sử 10% friends đang online và opt-in:
Đây là con số quan trọng: 13.3 triệu messages mỗi giây cần được deliver qua Pub/Sub system. Đây là thách thức lớn nhất của bài toán.
Redis Memory cho Location Cache:
Chỉ 1 GB Redis memory cho location cache của 10M users. Một Redis instance (64 GB RAM) dư sức.
Bandwidth cho Location Updates:
Outbound bandwidth gấp 40x inbound — đặc trưng của fan-out system.
Tóm tắt Estimation:
| Metric | Value |
|---|---|
| Concurrent users (peak) | 10M |
| WebSocket servers (50K conn/server) | 200 |
| Location update QPS | ~333K/s |
| Pub/Sub fan-out messages | ~13.3M/s |
| Redis memory (location cache) | ~1 GB |
| Inbound bandwidth | ~33 MB/s |
| Outbound bandwidth | ~1.33 GB/s |
3. Step 2 — High-Level Design
3.1 Lựa chọn Communication Protocol
Trước khi thiết kế architecture, phải chọn giao thức giao tiếp. Đây là quyết định quan trọng nhất của bài toán.
| Option | Mô tả | Ưu điểm | Nhược điểm | Phù hợp? |
|---|---|---|---|---|
| HTTP Polling | Client gửi GET request mỗi 30s | Đơn giản, stateless | Lãng phí bandwidth (mỗi request có HTTP headers ~500 bytes), 10M requests mỗi 30s = 333K QPS chỉ để poll | Không |
| HTTP Long Polling | Client gửi request, server giữ cho đến khi có data mới | Ít lãng phí hơn polling | Vẫn là 1 connection per pending request, không thật sự bidirectional | Không |
| Server-Sent Events (SSE) | Server push events qua HTTP | Đơn giản, built-in browser support | Chỉ server → client, không có client → server. Mà ta cần cả hai chiều | Không đủ |
| WebSocket | Full-duplex, bidirectional communication trên 1 TCP connection | Client gửi location, server push friend updates — trên cùng 1 connection. Lightweight (2-6 bytes overhead/message) | Stateful, khó scale hơn HTTP | Có — đây là lựa chọn |
Tại sao WebSocket? Vì ta cần bidirectional communication: client gửi location update (client → server) và server push friend location (server → client) trên cùng 1 connection. WebSocket là lựa chọn tự nhiên nhất. Tham chiếu Tuan-17-Design-Chat-System — chat system cũng dùng WebSocket vì lý do tương tự.
3.2 High-Level Architecture Overview
Hệ thống gồm 3 thành phần chính:
| Component | Chức năng | Technology |
|---|---|---|
| WebSocket Servers | Duy trì persistent connections với clients, nhận location updates, push friend locations | WebSocket (ws/wss) |
| Location Cache | Lưu vị trí hiện tại của mỗi user (latest location) | Redis |
| Pub/Sub | Propagate location updates đến tất cả friends đang online | Redis Pub/Sub |
Data Flow tóm tắt:
- User A bật tính năng → client mở WebSocket connection đến server
- Client gửi location update mỗi 30 giây qua WebSocket
- Server lưu location vào Redis (Location Cache)
- Server publish location update lên Pub/Sub channel của User A
- Tất cả friends của User A đang subscribe channel này → nhận được update
- Friends’ WebSocket servers tính khoảng cách → push đến friend’s client nếu trong radius
3.3 High-Level Architecture Diagram
flowchart TB subgraph Clients A["User A<br/>(Mobile App)"] B["User B<br/>(Mobile App)"] C["User C<br/>(Mobile App)"] end subgraph "API Gateway / Load Balancer" LB["Load Balancer<br/>→ [[Tuan-05-Load-Balancer]]"] end subgraph "WebSocket Server Fleet" WS1["WebSocket Server 1<br/>50K connections"] WS2["WebSocket Server 2<br/>50K connections"] WSN["WebSocket Server N<br/>50K connections"] end subgraph "Data Layer" Redis_Cache["Redis — Location Cache<br/>Key: user_id<br/>Value: {lat, lng, timestamp}"] Redis_PubSub["Redis Pub/Sub<br/>Channel per user<br/>Friends subscribe"] end subgraph "Supporting Services" UserSvc["User Service<br/>(Friends list, profile)"] DB[("Database<br/>User data, friend relationships")] end A -->|"WebSocket"| LB B -->|"WebSocket"| LB C -->|"WebSocket"| LB LB --> WS1 LB --> WS2 LB --> WSN WS1 & WS2 & WSN -->|"SET location"| Redis_Cache WS1 & WS2 & WSN -->|"PUBLISH update"| Redis_PubSub WS1 & WS2 & WSN -->|"SUBSCRIBE friend channels"| Redis_PubSub WS1 & WS2 & WSN -->|"Get friends list"| UserSvc UserSvc --> DB style LB fill:#42a5f5,color:#fff style Redis_Cache fill:#ef5350,color:#fff style Redis_PubSub fill:#ff7043,color:#fff style DB fill:#66bb6a,color:#fff
3.4 API Design (WebSocket Messages)
Vì dùng WebSocket, không có REST endpoints truyền thống. Thay vào đó, là các message types:
Client → Server Messages:
| Message Type | Payload | Mục đích |
|---|---|---|
location_update | {lat, lng, timestamp} | Gửi vị trí hiện tại mỗi 30s |
enable_nearby | {} | Bật tính năng Nearby Friends |
disable_nearby | {} | Tắt tính năng Nearby Friends |
update_radius | {radius_miles} | Thay đổi bán kính hiển thị |
Server → Client Messages:
| Message Type | Payload | Mục đích |
|---|---|---|
friend_location | {friend_id, lat, lng, timestamp, distance} | Vị trí mới của một friend |
friend_offline | {friend_id} | Friend vừa tắt tính năng hoặc offline |
nearby_friends_list | [{friend_id, lat, lng, distance}, ...] | Danh sách đầy đủ khi mới connect |
4. Step 3 — Deep Dive
4.1 Location Update Flow — Chi tiết từng bước
Đây là flow quan trọng nhất của hệ thống. Khi User A gửi location update, chuyện gì xảy ra?
sequenceDiagram participant A as User A (Client) participant WS_A as WebSocket Server<br/>(serving User A) participant Cache as Redis<br/>Location Cache participant PubSub as Redis<br/>Pub/Sub participant WS_B as WebSocket Server<br/>(serving User B) participant B as User B (Client) Note over A,B: User A và User B là bạn bè. Cả hai đều bật Nearby Friends. A->>WS_A: location_update {lat: 10.77, lng: 106.70, ts: ...} par Parallel Operations WS_A->>Cache: SET user:A:location {lat, lng, ts}<br/>TTL = 120s and WS_A->>PubSub: PUBLISH channel:user_A {lat, lng, ts} end Note over PubSub: User B đã SUBSCRIBE channel:user_A<br/>(vì B là bạn của A và đang online) PubSub->>WS_B: Message on channel:user_A {lat, lng, ts} Note over WS_B: WS_B tính khoảng cách giữa A và B.<br/>Nếu <= radius của B → push đến B. WS_B->>B: friend_location {friend_id: A, lat, lng, distance: 1.2km}
Chi tiết từng bước:
| Bước | Action | Component | Latency | Ghi chú |
|---|---|---|---|---|
| 1 | Client gửi location qua WebSocket | Client → WS Server | ~5ms (LAN) | Binary message, minimal overhead |
| 2a | Lưu location vào Redis cache | WS Server → Redis | ~1ms | SET với TTL 120s (2x update interval) |
| 2b | Publish lên user’s Pub/Sub channel | WS Server → Redis Pub/Sub | ~1ms | Song song với bước 2a |
| 3 | Redis fan-out đến subscribers | Redis Pub/Sub → WS Servers | ~1ms | Mỗi subscriber nhận được message |
| 4 | Tính khoảng cách | WS Server (receiver) | ~0.01ms | Haversine formula, CPU-bound |
| 5 | Push đến client nếu trong radius | WS Server → Client | ~5ms | Qua WebSocket connection |
| Total | ~10-15ms | Cực nhanh |
Aha Moment: Toàn bộ flow từ A update location đến B nhận được chỉ mất ~10-15ms. User cảm nhận là “real-time” mặc dù thực tế location chỉ cập nhật mỗi 30 giây. Bottleneck không phải latency — mà là fan-out volume.
4.2 Redis Pub/Sub — Trái tim của hệ thống
4.2.1 Thiết kế Channel
Core design decision: Mỗi user = 1 Pub/Sub channel.
| Thiết kế | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| 1 channel per user (chosen) | Channel user:A, friends subscribe | Đơn giản, granular control | Nhiều channels (10M) |
| 1 channel per geohash cell | Channel geo:w3gvk, users trong cell subscribe | Ít channels | Nhận updates từ người lạ (không chỉ friends), privacy issue |
| 1 global channel | Mỗi update broadcast đến tất cả | Đơn giản nhất | 333K updates/s x 10M subscribers = không khả thi |
Tại sao 1 channel per user? Vì Nearby Friends chỉ care về bạn bè, không phải tất cả người trong khu vực. Mỗi user chỉ cần subscribe channels của bạn bè — chính xác những người họ quan tâm.
4.2.2 Subscribe/Unsubscribe Flow
Khi User B lên online và bật Nearby Friends:
| Bước | Action | Chi tiết |
|---|---|---|
| 1 | B connect WebSocket | Mở persistent connection |
| 2 | Server lấy friends list của B | Query User Service → DB. Kết quả: [A, C, D, E, …] (400 friends) |
| 3 | Server check ai đang online và opt-in | Query Redis: EXISTS user:A:location, user:C:location, … |
| 4 | Server subscribe B vào channels của online friends | SUBSCRIBE channel:user_A, channel:user_C, … |
| 5 | Server lấy latest location của online friends | MGET user:A:location, user:C:location, … từ Redis cache |
| 6 | Server tính khoảng cách và gửi initial list | Push nearby_friends_list đến B qua WebSocket |
Khi User B offline hoặc tắt Nearby Friends:
| Bước | Action | Chi tiết |
|---|---|---|
| 1 | Server UNSUBSCRIBE B khỏi tất cả friend channels | UNSUBSCRIBE channel:user_A, channel:user_C, … |
| 2 | Server DELETE B’s location từ cache | DEL user:B:location |
| 3 | Server PUBLISH “offline” event lên B’s channel | PUBLISH channel:user_B {status: “offline”} |
| 4 | Friends của B nhận “offline” event | Push friend_offline {friend_id: B} đến friends’ clients |
4.2.3 Pub/Sub Fan-out Analysis
Đây là phần phức tạp nhất. Khi User A update location:
Nhưng không phải mỗi user có đúng 400 friends. Phân phối thực tế là long-tail:
| User type | Số friends | % users | Subscribers per channel |
|---|---|---|---|
| Light users | < 100 | 40% | ~10 |
| Average users | 100-500 | 45% | ~40 |
| Heavy users | 500-2000 | 14% | ~100-200 |
| Power users / celebrities | 2000-5000 | 1% | ~500+ |
Pitfall: Fan-out explosion với popular users. Một user có 5000 friends và 500 người đang online → mỗi 30 giây, 1 update của họ fan-out thành 500 messages. Nhân với 333K updates/s → có thể có những burst cực lớn.
Giải pháp cho popular users: Rate limit fan-out. Nếu user có > 500 subscribers, giảm tần suất update từ 30s xuống 60s hoặc 120s. User bình thường không bị ảnh hưởng.
4.2.4 Memory của Redis Pub/Sub Channels
7 GB cho Pub/Sub metadata — chấp nhận được cho một Redis cluster.
4.3 WebSocket Connection Management
4.3.1 Connection Lifecycle
stateDiagram-v2 [*] --> Connecting: User mở app,<br/>bật Nearby Friends Connecting --> Connected: WebSocket handshake OK Connected --> Subscribing: Server subscribe<br/>friend channels Subscribing --> Active: Subscriptions done,<br/>initial list sent Active --> Active: Location updates<br/>mỗi 30s Active --> Reconnecting: Network drop,<br/>server crash Reconnecting --> Connecting: Retry với<br/>exponential backoff Active --> Disconnecting: User tắt tính năng<br/>hoặc close app Disconnecting --> [*]: Cleanup subscriptions,<br/>delete location cache Reconnecting --> [*]: Max retries exceeded
4.3.2 Connection Parameters
| Parameter | Value | Lý do |
|---|---|---|
| Heartbeat interval | 30 giây | Trùng với location update interval — gửi heartbeat kèm location |
| Connection timeout | 10 giây | Nếu không connect được trong 10s → retry |
| Max reconnect attempts | 10 | Sau 10 lần → thông báo user “Không thể kết nối” |
| Reconnect backoff | Exponential: 1s, 2s, 4s, 8s, …, max 60s | Tránh thundering herd khi server restart |
| Idle timeout | 5 phút không nhận location update | Coi như user đã tắt GPS hoặc app bị kill → disconnect, cleanup |
| Max message size | 1 KB | Location update chỉ cần ~100 bytes. 1 KB là đủ margin |
4.3.3 Reconnection Strategy
Khi WebSocket connection bị mất (network issue, server crash, app bị background), client cần reconnect:
| Bước | Action | Chi tiết |
|---|---|---|
| 1 | Client detect disconnect | onclose hoặc onerror event |
| 2 | Wait theo backoff | Exponential: 1s, 2s, 4s, 8s, 16s, 32s, 60s (cap) |
| 3 | Reconnect đến Load Balancer | Có thể connect đến server khác (stateful concern!) |
| 4 | Re-authenticate | Gửi token qua WebSocket |
| 5 | Server re-subscribe friend channels | Giống flow ban đầu |
| 6 | Server gửi initial nearby friends list | Client refresh UI |
Quan trọng: Khi reconnect, client có thể được route đến server khác (vì Load Balancer). Server mới phải re-subscribe tất cả friend channels. Đây là chi phí của reconnection — nhưng vì subscribe là O(number_of_friends) và friends list được cache, chi phí này chấp nhận được.
4.4 Scaling WebSocket Servers
4.4.1 Thách thức — Stateful Connections
WebSocket connections là stateful: mỗi connection gắn với 1 server cụ thể. Không thể “load balance từng request” như HTTP. Sau khi connection được thiết lập, mọi message phải đi qua đúng server đó.
| Vấn đề | Giải thích | Giải pháp |
|---|---|---|
| Server failure | Server chết → 50K users mất connection | Client auto-reconnect, LB route đến server khác |
| Uneven distribution | Server A có 60K connections, server B có 20K | LB track connection count, route new connections đến server ít tải |
| Deployment | Rolling update → server restart → 50K users reconnect đồng thời | Graceful shutdown: server thông báo clients trước 30s, clients reconnect dần |
| Memory pressure | Mỗi connection ~ 10-50 KB memory. 50K connections = 0.5-2.5 GB/server | Monitor memory, set max connections per server |
4.4.2 Server Assignment Strategy
| Strategy | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Random (via LB) | LB random chọn server cho new connection | Đơn giản | Uneven distribution |
| Least connections | LB chọn server có ít connection nhất | Even distribution | Cần LB track state |
| Consistent hashing | Hash user_id → server | Reconnect lại cùng server (cache warm) | Khó handle server add/remove |
| Least connections (recommended) |
Dùng Least Connections strategy cho WebSocket LB. Tham chiếu Tuan-05-Load-Balancer. Consistent hashing (Tuan-10-Consistent-Hashing) có thể dùng nhưng complexity không đáng cho use case này — vì reconnection chỉ mất vài giây để re-subscribe.
4.4.3 Graceful Shutdown Flow
Khi cần restart WebSocket server (deploy, maintenance):
| Bước | Action | Duration |
|---|---|---|
| 1 | Mark server as “draining” | LB ngừng gửi new connections đến server này |
| 2 | Server gửi “reconnect” signal đến tất cả clients | Clients bắt đầu reconnect đến server khác |
| 3 | Đợi cho connections giảm về 0 (hoặc timeout) | Max 60 giây |
| 4 | Server unsubscribe tất cả Pub/Sub channels | Cleanup |
| 5 | Shutdown server | Safe |
4.5 Scaling Redis Pub/Sub
4.5.1 Vấn đề — Single Redis Pub/Sub Bottleneck
Một Redis instance có thể handle ~500K messages/s (Pub/Sub throughput). Nhưng ta cần deliver 13.3M messages/s. Cần nhiều Redis instances.
Làm tròn lên: 30 Redis Pub/Sub instances (với buffer).
4.5.2 Sharding Strategy cho Pub/Sub Channels
| Strategy | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Hash-based sharding | shard = hash(user_id) % N | Even distribution, deterministic | Resharding khi add/remove nodes |
| Consistent hashing | Dùng hash ring | Smooth resharding | Phức tạp hơn |
| Range-based | user_id 1-1M → shard 1, 1M-2M → shard 2 | Đơn giản | Uneven nếu user distribution không đều |
Chọn: Hash-based sharding (đơn giản, đủ tốt).
Mỗi WebSocket server cần biết: channel của user X nằm trên Redis shard nào. Vì hash function là deterministic → mỗi server tính được mà không cần lookup.
flowchart LR subgraph "WebSocket Servers" WS1["WS Server 1"] WS2["WS Server 2"] WS3["WS Server 3"] end subgraph "Redis Pub/Sub Cluster" R1["Redis Shard 1<br/>Channels: user_1, user_31, ..."] R2["Redis Shard 2<br/>Channels: user_2, user_32, ..."] R3["Redis Shard 3<br/>Channels: user_3, user_33, ..."] RN["Redis Shard N<br/>..."] end WS1 -->|"PUBLISH channel:user_1"| R1 WS1 -->|"SUBSCRIBE channel:user_2"| R2 WS2 -->|"PUBLISH channel:user_32"| R2 WS2 -->|"SUBSCRIBE channel:user_3"| R3 WS3 -->|"SUBSCRIBE channel:user_1"| R1 R1 -->|"Fan-out"| WS2 & WS3 R2 -->|"Fan-out"| WS1 & WS3 style R1 fill:#ef5350,color:#fff style R2 fill:#ef5350,color:#fff style R3 fill:#ef5350,color:#fff style RN fill:#ef5350,color:#fff
Lưu ý: Mỗi WebSocket server có thể connect đến nhiều Redis shards — vì friends của 1 user có thể nằm trên nhiều shards khác nhau. WS Server 1 serving User B phải subscribe channel:user_A trên shard 1, channel:user_C trên shard 3, v.v.
4.5.3 Redis Pub/Sub Connections Budget
Mỗi WebSocket server cần connect đến mỗi Redis shard (để subscribe/publish). Với 200 WS servers và 30 Redis shards:
Mỗi Redis instance có thể handle ~10K concurrent connections → đủ thoải mái.
4.5.4 Alternative — Dùng Message Queue thay Redis Pub/Sub?
| Khía cạnh | Redis Pub/Sub | Message Queue (Kafka, RabbitMQ) |
|---|---|---|
| Delivery guarantee | At-most-once (fire-and-forget) | At-least-once (Kafka), At-most-once (RabbitMQ) |
| Persistence | Không — message mất nếu subscriber offline | Có — Kafka lưu messages trên disk |
| Latency | Cực thấp (~1ms) | Cao hơn (~5-50ms tùy cấu hình) |
| Fan-out | Native — 1 publish, N subscribers nhận | Cần consumer groups hoặc topic-per-user |
| Memory | Chỉ lưu trong memory | Lưu trên disk (Kafka) |
| Ordering | Guaranteed per channel | Guaranteed per partition (Kafka) |
Tại sao chọn Redis Pub/Sub?
- At-most-once là đủ: Mất 1 location update không sao — 30 giây sau có update mới. Không cần durability.
- Latency cực thấp: Redis Pub/Sub ~1ms, Kafka ~5-50ms. Cho real-time feature, 1ms matters.
- Không cần persistence: Ta không care location 5 phút trước. Chỉ cần latest.
- Đơn giản: Redis Pub/Sub là built-in, không cần deploy/manage thêm Kafka cluster.
Trade-off: Nếu cần guaranteed delivery (ví dụ: notification system), dùng Kafka. Cho Nearby Friends, Redis Pub/Sub là perfect fit vì ta ưu tiên low latency và simplicity hơn durability.
4.6 Nearby Friend Calculation — Tính khoảng cách
4.6.1 Server-side vs Client-side Calculation
| Approach | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Server-side (chosen) | WS server tính khoảng cách trước khi push | Giảm bandwidth — chỉ push friends trong radius | Server cần biết vị trí của subscriber |
| Client-side | Server push tất cả friends’ locations, client tự filter | Server đơn giản hơn | Lãng phí bandwidth — push cả friends ở xa |
Chọn server-side calculation vì:
- Với 40 online friends, server chỉ push 5-10 friends trong radius thay vì 40 → tiết kiệm 75-87% bandwidth
- Client (mobile) không phải xử lý nhiều → tiết kiệm battery
- Server đã có vị trí của cả hai (sender và receiver) trong Redis cache
4.6.2 Haversine Formula
Khoảng cách giữa 2 điểm trên bề mặt Trái Đất:
Trong đó (bán kính Trái Đất), là latitude, là longitude.
Performance: Haversine là O(1) — chỉ là vài phép tính lượng giác. Tính 40 khoảng cách mất < 0.01ms. Không phải bottleneck.
4.6.3 Flow chi tiết khi nhận friend update
Khi WS Server (serving User B) nhận update từ channel:user_A:
| Bước | Action | Chi tiết |
|---|---|---|
| 1 | Nhận message từ Pub/Sub | {user_id: A, lat: 10.77, lng: 106.70, ts: ...} |
| 2 | Lấy vị trí hiện tại của User B | Từ local memory (cached khi B gửi location update) |
| 3 | Tính Haversine distance | d = haversine(B.lat, B.lng, A.lat, A.lng) |
| 4 | So sánh với radius của B | if d <= B.radius |
| 5a | Nếu trong radius → Push đến B | friend_location {friend_id: A, distance: d} |
| 5b | Nếu ngoài radius → bỏ qua | Không push, tiết kiệm bandwidth |
Optimization: WS Server serving User B lưu vị trí của B trong local memory (không phải query Redis mỗi lần). Vị trí được cập nhật mỗi 30 giây khi B gửi location update. Đây là “cache tại chỗ” — zero latency.
4.7 Adding/Removing Friends — Dynamic Subscription
Khi friend relationship thay đổi (thêm bạn mới, hủy kết bạn), subscriptions phải cập nhật:
4.7.1 Thêm bạn mới
| Bước | Action | Trigger |
|---|---|---|
| 1 | User A và User B trở thành bạn | Friend request accepted |
| 2 | Notification gửi đến WS Server của A và B | Qua internal message queue |
| 3 | WS Server của A subscribe channel:user_B | Nếu B đang online và opt-in |
| 4 | WS Server của B subscribe channel:user_A | Nếu A đang online và opt-in |
| 5 | Tính khoảng cách và push nếu cần | Cả A và B nhận vị trí của nhau |
4.7.2 Hủy kết bạn
| Bước | Action | Trigger |
|---|---|---|
| 1 | User A unfriend User B | UI action |
| 2 | Notification gửi đến WS Server của A và B | Qua internal message queue |
| 3 | WS Server của A unsubscribe channel:user_B | |
| 4 | WS Server của B unsubscribe channel:user_A | |
| 5 | Push friend_offline đến cả A và B | Xóa khỏi nearby list |
Lưu ý: Subscribe/unsubscribe trên Redis Pub/Sub là O(1) — cực nhanh. Không ảnh hưởng performance.
4.8 Handling Inactive Users — TTL và Cleanup
4.8.1 Vấn đề
User có thể inactive vì nhiều lý do:
| Tình huống | Hệ thống nhìn thấy | Cần xử lý |
|---|---|---|
| User tắt app | WebSocket disconnect event | Cleanup ngay |
| App bị kill bởi OS | WebSocket disconnect event (có thể trễ) | Cleanup ngay |
| User đi vào vùng không có mạng | Không nhận location update, heartbeat miss | Detect và cleanup |
| User để điện thoại yên 1 chỗ | Vẫn nhận location update (vị trí không đổi) | Không cần xử lý — vẫn active |
| User tắt GPS | App không có GPS data → ngừng gửi location | Detect và thông báo user |
4.8.2 TTL Strategy
| Data | TTL | Lý do |
|---|---|---|
| Location cache (Redis) | 120 giây (2x update interval) | Nếu không nhận update trong 2 chu kỳ → user đã offline |
| WebSocket heartbeat | 60 giây | Server gửi ping, client trả lời pong. Miss 2 ping liên tiếp → disconnect |
| Pub/Sub subscription | Không có TTL — cleanup khi disconnect | Subscribe/unsubscribe là explicit |
Flow khi user inactive:
sequenceDiagram participant A as User A (Client) participant WS as WebSocket Server participant Cache as Redis Cache participant PubSub as Redis Pub/Sub participant Friends as Friends' WS Servers Note over A,WS: User A mất mạng / tắt app WS->>WS: Heartbeat timeout (60s, 2 missed pings) WS->>WS: Close WebSocket connection par Cleanup WS->>Cache: DEL user:A:location and WS->>PubSub: UNSUBSCRIBE all friend channels and WS->>PubSub: PUBLISH channel:user_A {status: "offline"} end PubSub->>Friends: User A offline notification Friends->>Friends: Remove A from nearby list,<br/>push friend_offline to clients
4.8.3 Redis TTL as Safety Net
Ngay cả khi server không kịp cleanup (ví dụ server crash), Redis TTL trên location key sẽ tự động xóa data:
Friends sẽ nhận ra user “đã cập nhật lần cuối 2 phút trước” → client có thể dim hoặc ẩn user này khỏi danh sách.
4.9 Geohash Optimization — Giảm Computation
4.9.1 Vấn đề
Với mỗi location update từ friend, WS server phải tính Haversine distance. Nếu user có 40 online friends và mỗi friend update mỗi 30s, mỗi user nhận:
40 calculations mỗi 30 giây là ít — chưa phải bottleneck. Nhưng với 50K users trên 1 WS server:
Haversine là nhẹ (~0.01ms), nên 67K calculations/s chỉ mất ~0.67ms CPU time. Không phải bottleneck.
Kết luận: Cho quy mô 10M concurrent users, geohash optimization KHÔNG cần thiết. Brute-force Haversine calculation đủ nhanh. Tuy nhiên, nếu quy mô tăng lên 100M+ concurrent users, hoặc friends count tăng lên 5000+, thì geohash optimization có thể cần.
4.9.2 Geohash Optimization (nếu cần)
Nếu cần optimize (quy mô cực lớn):
| Kỹ thuật | Mô tả | Tiết kiệm |
|---|---|---|
| Pre-filter by geohash | Mỗi user có geohash (tính từ lat/lng). Chỉ tính Haversine cho friends có geohash gần | Loại 80-90% friends ở xa |
| Lazy calculation | Chỉ tính khi friend’s geohash thay đổi (không tính lại nếu vẫn ở cùng cell) | Giảm 90%+ calculations cho friends ngồi yên |
| Batch calculation | Gom nhiều updates và tính 1 lần mỗi 5-10 giây thay vì mỗi update | Giảm CPU spikes |
4.10 Multi-Region Architecture
4.10.1 Tại sao cần Multi-Region?
| Lý do | Chi tiết |
|---|---|
| Latency | User ở Việt Nam connect đến server ở US → ~200ms latency. WebSocket updates sẽ chậm |
| Availability | 1 region down → toàn bộ hệ thống down. Multi-region → failover |
| Data sovereignty | GDPR yêu cầu data EU users lưu ở EU |
| User distribution | 100M DAU phân bổ toàn cầu — không thể serve từ 1 region |
4.10.2 Regional Architecture
flowchart TB subgraph "Region: US-East" US_LB["Load Balancer"] US_WS["WebSocket Servers<br/>60 servers"] US_Redis["Redis Cluster<br/>(Cache + Pub/Sub)"] end subgraph "Region: EU-West" EU_LB["Load Balancer"] EU_WS["WebSocket Servers<br/>50 servers"] EU_Redis["Redis Cluster<br/>(Cache + Pub/Sub)"] end subgraph "Region: AP-Southeast" AP_LB["Load Balancer"] AP_WS["WebSocket Servers<br/>90 servers"] AP_Redis["Redis Cluster<br/>(Cache + Pub/Sub)"] end subgraph "Cross-Region" Bridge["Cross-Region<br/>Message Bridge<br/>(for cross-region friend pairs)"] end US_Redis <-->|"Cross-region<br/>friend updates"| Bridge EU_Redis <-->|"Cross-region<br/>friend updates"| Bridge AP_Redis <-->|"Cross-region<br/>friend updates"| Bridge style Bridge fill:#ff9800,color:#000 style US_Redis fill:#ef5350,color:#fff style EU_Redis fill:#ef5350,color:#fff style AP_Redis fill:#ef5350,color:#fff
4.10.3 Cross-Region Friend Pairs
Vấn đề: User A ở Việt Nam (AP-Southeast), User B ở Mỹ (US-East). Cả hai là bạn bè và bật Nearby Friends. Làm sao A nhận location của B?
| Approach | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Cross-region Pub/Sub bridge (chosen) | Khi A update location, publish ở AP region. Message bridge forward đến US region cho B’s subscribers | Tách biệt regions, bridge chỉ cho cross-region pairs | Thêm latency (~100-200ms cross-region) |
| Global Pub/Sub | 1 Pub/Sub cluster serve toàn cầu | Đơn giản | Single point of failure, high latency cho remote users |
| Ignore cross-region | Chỉ hiển thị friends cùng region | Đơn giản nhất | Bad UX — không thấy bạn bè ở nước khác |
Trade-off: Cross-region friends sẽ nhận location update trễ hơn ~100-200ms (vì phải đi qua internet giữa regions). Nhưng vì nearby friends thường ở cùng thành phố (cùng region), đa số updates là intra-region và cực nhanh.
Thực tế: Hầu hết friend pairs cùng bật Nearby Friends sẽ ở cùng khu vực (ai bật Nearby Friends để xem bạn ở cách 10,000 km?). Cross-region pairs là edge case — có thể chấp nhận latency cao hơn.
4.10.4 Region Assignment
User được assign vào region gần nhất dựa trên IP hoặc GPS location:
| User location | Region | Ghi chú |
|---|---|---|
| Việt Nam, Thái Lan, Indonesia | AP-Southeast (Singapore) | RTT ~10-30ms |
| Mỹ, Canada | US-East hoặc US-West | RTT ~10-50ms |
| Châu Âu | EU-West (Ireland/Frankfurt) | RTT ~10-30ms |
| Nhật Bản, Hàn Quốc | AP-Northeast (Tokyo) | RTT ~10-20ms |
DNS-based routing (ví dụ: AWS Route 53 latency-based routing) tự động route user đến region có latency thấp nhất.
5. Estimation — Deep Dive Numbers
5.1 WebSocket Server Capacity Planning
Thêm 20% buffer cho failures và maintenance:
Memory per server:
Thêm OS, application, Redis connections: ~4 GB total. Server 8 GB RAM là đủ.
5.2 Redis Cluster Sizing
Location Cache (separate cluster):
1 Redis instance (với replication) là đủ.
Pub/Sub Cluster:
5.3 Network Bandwidth
Inbound (client → server):
Per server: — không đáng kể.
Outbound (server → client):
Per server: — chấp nhận được (server thường có 1-10 Gbps NIC).
Internal (server ↔ Redis):
Cộng với cache reads/writes:
5.4 Tóm tắt Capacity
| Resource | Quantity | Spec |
|---|---|---|
| WebSocket servers | 240 | 8 GB RAM, 4 vCPU |
| Redis (Location Cache) | 3 (1 primary + 2 replicas) | 4 GB RAM |
| Redis (Pub/Sub shards) | 30 | 2 GB RAM each |
| Total Redis memory | ~67 GB | Cache (1 GB) + Pub/Sub (60 GB) |
| Network bandwidth (internal) | ~1.4 GB/s | 10 Gbps network |
| Database (User/Friends) | 3 (1 primary + 2 replicas) | Standard PostgreSQL |
6. Security — Bảo vệ Location Privacy
6.1 Opt-in / Opt-out — Nguyên tắc số 1
| Nguyên tắc | Implementation | Chi tiết |
|---|---|---|
| Default OFF | Nearby Friends tắt mặc định | User phải chủ động bật. Không bao giờ tự động bật |
| Granular control | Cho phép chọn chia sẻ với ai | ”Chia sẻ với tất cả bạn bè” vs “Chỉ chia sẻ với Close Friends list” |
| Easy OFF | 1 tap để tắt | Không phải vào Settings → Privacy → Location → Nearby Friends → Off |
| Auto OFF | Tự động tắt sau thời gian | Option: tắt sau 1h, 4h, 8h. Giống WhatsApp Live Location |
| Visual indicator | Hiển thị rõ ràng khi đang chia sẻ | Icon trên status bar, periodic reminder “Bạn đang chia sẻ vị trí” |
6.2 Location Precision Control — Fuzzing
| Level | Precision | Use case | Implementation |
|---|---|---|---|
| Exact | ~10m (GPS accuracy) | Close friends, gia đình | Gửi raw GPS coordinates |
| Approximate | ~500m | Bạn bè bình thường | Round lat/lng đến 3 decimal places |
| City-level | ~10km | Acquaintances | Chỉ gửi city name, không gửi coordinates |
| Hidden | N/A | Không muốn chia sẻ | Không gửi location, unsubscribe |
Implementation: Server-side fuzzing. Client gửi raw GPS, server áp dụng precision level trước khi publish lên Pub/Sub. Như vậy client không cần biết logic — và không thể bypass.
6.3 Stalking Prevention
| Mối đe dọa | Giải pháp | Chi tiết |
|---|---|---|
| Theo dõi liên tục | Rate limit visibility | Không cho phép user X xem vị trí của Y nhiều hơn 1 lần/phút (client-side throttle) |
| Location history inference | Không lưu history | Server chỉ lưu latest location. Không có API để lấy history. Client chỉ hiển thị real-time |
| Fake accounts | Verification | Yêu cầu phone verification để bật Nearby Friends |
| Harassment | Block + Report | User block → tự động unsubscribe cả 2 chiều. Report → review bởi trust & safety team |
| Ghost Mode | Ẩn vị trí nhưng vẫn thấy bạn bè | User A bật Ghost Mode → A vẫn subscribe friends’ channels (thấy bạn bè). Nhưng A không publish location → bạn bè không thấy A |
| Invisible to specific people | Per-friend setting | ”Ẩn vị trí với Tuấn” → unsubscribe Tuấn khỏi channel của mình |
6.4 Rate Limiting Location Updates
| Tier | Limit | Lý do |
|---|---|---|
| Per user | 1 location update / 10 giây (min) | Ngăn client gửi quá nhiều (battery drain, bandwidth) |
| Per user | Max 1 update / 30 giây (expected) | Normal operation |
| Per server | 100K updates/s | Bảo vệ Redis khỏi overload |
| Burst | 3 updates trong 5 giây (khi mới bật) | Cho phép initial burst để lấy vị trí nhanh |
Tham chiếu Tuan-09-Rate-Limiter cho Token Bucket implementation.
6.5 Data Retention — Không lưu Location History
| Dữ liệu | Retention | Lý do |
|---|---|---|
| Current location (Redis) | TTL 120 giây | Chỉ cần latest, tự động xóa |
| Location updates (logs) | Không lưu | Privacy — không có lý do lưu |
| Aggregated analytics | 90 ngày | ”Bao nhiêu users bật Nearby Friends?” — không chứa location |
| Pub/Sub messages | 0 — fire and forget | Redis Pub/Sub không persist |
GDPR Article 5(1)(e): Dữ liệu cá nhân chỉ được lưu trong thời gian cần thiết cho mục đích xử lý. Location data cho Nearby Friends chỉ cần hiện tại — không cần lưu trữ.
6.6 GDPR và Data Protection Compliance
| Yêu cầu | Implementation |
|---|---|
| Lawful basis | Consent (opt-in). Không phải legitimate interest — location là “special category” |
| Right to be forgotten | User yêu cầu xóa → xóa location cache, unsubscribe tất cả channels, xóa account data |
| Data portability | Export: danh sách friends đã chia sẻ location (không có location data vì không lưu) |
| Purpose limitation | Location chỉ dùng cho Nearby Friends feature. Không dùng cho advertising, analytics, hoặc share với third party |
| Data minimization | Chỉ thu thập lat, lng, timestamp. Không thu thập altitude, speed, heading |
| Data Protection Impact Assessment (DPIA) | Bắt buộc vì xử lý location data ở quy mô lớn |
| Data Processing Agreement | Với cloud provider (AWS/GCP) |
7. DevOps & Monitoring
7.1 Key Metrics — WebSocket Health
| Metric | Alert Threshold | Dashboard | Ý nghĩa |
|---|---|---|---|
ws_connection_count per server | > 55K (110% capacity) | Gauge per server | Server gần đầy, cần scale out |
ws_connection_count total | < 8M (80% expected) | Single number | Có thể có vấn đề — users không connect được |
ws_connection_churn_rate | > 5K/min per server | Time series | Nhiều reconnections → network issue hoặc server issue |
ws_handshake_latency_p99 | > 500ms | Histogram | Connection chậm → check LB, check TLS |
ws_message_send_errors | > 0.1% | Percentage | Messages không gửi được đến client |
7.2 Key Metrics — Redis Pub/Sub
| Metric | Alert Threshold | Dashboard | Ý nghĩa |
|---|---|---|---|
pubsub_channels_count | > 12M (120% expected) | Gauge | Nhiều channels → nhiều users online (tốt) hoặc leak (xấu) |
pubsub_messages_per_second | > 15M/s (>113% expected) | Time series | Gần capacity → cần add shards |
pubsub_subscribers_per_channel max | > 1000 | Histogram | Popular user → có thể cần rate limit |
redis_memory_used per shard | > 80% max memory | Gauge | Cần increase memory hoặc add shards |
redis_connected_clients per shard | > 8K | Gauge | Nhiều connections từ WS servers |
7.3 Key Metrics — Location Propagation
| Metric | Alert Threshold | Dashboard | Ý nghĩa |
|---|---|---|---|
location_propagation_latency_p50 | > 50ms | Histogram | Trung bình — phải < 50ms |
location_propagation_latency_p99 | > 500ms | Histogram | Worst case — phải < 500ms |
location_propagation_latency_p999 | > 2s | Histogram | Extreme — investigate nếu > 2s |
location_update_rate | < 200K/s (< 60% expected) | Time series | Users không gửi updates → client bug? |
nearby_friends_count avg per user | N/A (informational) | Histogram | Trung bình mỗi user thấy bao nhiêu friends nearby |
7.4 Key Metrics — Business Health
| Metric | Alert Threshold | Dashboard | Ý nghĩa |
|---|---|---|---|
feature_opt_in_rate | N/A (informational) | Percentage | Bao nhiêu % DAU bật Nearby Friends |
avg_session_duration | N/A (informational) | Time series | Users dùng feature bao lâu |
error_rate_by_type | > 1% cho bất kỳ error type nào | Stacked bar | Chi tiết errors: auth, timeout, redis, etc. |
7.5 Alerting Rules
| Alert | Condition | Severity | Action |
|---|---|---|---|
| WebSocket server overloaded | connections > 55K for 5 min | P2 | Auto-scale, add servers |
| Redis Pub/Sub throughput high | > 90% capacity for 10 min | P1 | Add shards, page on-call |
| Location propagation slow | p99 > 1s for 5 min | P1 | Check Redis, check network |
| Connection churn spike | churn > 10K/min for 3 min | P2 | Check network, check DNS, check LB |
| Redis shard down | shard unreachable for 1 min | P1 | Auto-failover, page on-call |
| Cross-region bridge lag | > 5s for 3 min | P2 | Check inter-region network |
Tham chiếu Tuan-13-Monitoring-Observability cho alerting best practices và runbook structure.
7.6 Deployment Strategy
| Component | Strategy | Lý do |
|---|---|---|
| WebSocket servers | Rolling update với graceful drain | Stateful connections — cần drain trước khi shutdown |
| Redis Pub/Sub | Add shards without downtime (resharding) | Không được restart — mất subscriptions |
| Redis Cache | Blue-green với replication | Failover tới replica nếu primary down |
| Configuration changes (radius, TTL) | Feature flags (LaunchDarkly/Unleash) | Thay đổi không cần deploy |
| New features | Canary 5% → 25% → 100% | Phát hiện vấn đề sớm |
7.7 Load Testing Considerations
| Scenario | Test method | Target |
|---|---|---|
| 50K WebSocket connections per server | Locust/k6 WebSocket load test | Verify server handles 50K connections |
| 333K location updates/s | Distributed load generators | Verify Redis Pub/Sub throughput |
| Fan-out explosion (user với 5000 friends) | Synthetic test với high-fan-out users | Verify no cascading failures |
| Server crash during peak | Kill 1 WS server, observe reconnection | Verify reconnection < 30s |
| Redis shard failure | Kill 1 Redis shard, observe failover | Verify auto-failover < 10s |
8. Diagrams
8.1 Complete Location Update Flow
flowchart TB subgraph "1. Client sends location" Client_A["User A Mobile App"] GPS["GPS Module<br/>lat: 10.7769<br/>lng: 106.7009"] GPS --> Client_A end subgraph "2. WebSocket Server receives" WS_A["WS Server 1<br/>(serving User A)"] end subgraph "3. Parallel writes" Redis_Cache["Redis Cache<br/>SET user:A:location<br/>{lat, lng, ts}<br/>TTL=120s"] Redis_PubSub["Redis Pub/Sub<br/>PUBLISH channel:user_A<br/>{lat, lng, ts}"] end subgraph "4. Fan-out to subscribers" WS_B["WS Server 2<br/>(serving User B)<br/>subscribed to channel:user_A"] WS_C["WS Server 3<br/>(serving User C)<br/>subscribed to channel:user_A"] WS_D["WS Server 1<br/>(serving User D)<br/>subscribed to channel:user_A"] end subgraph "5. Distance check + push" Check_B["B at 10.78, 106.71<br/>d = 1.2km < 5mi ✓"] Check_C["C at 10.90, 106.85<br/>d = 20km > 5mi ✗"] Check_D["D at 10.77, 106.70<br/>d = 0.1km < 5mi ✓"] end subgraph "6. Client receives" Client_B["User B sees:<br/>A is 1.2km away"] Client_D["User D sees:<br/>A is 0.1km away"] end Client_A -->|"WebSocket"| WS_A WS_A --> Redis_Cache WS_A --> Redis_PubSub Redis_PubSub --> WS_B Redis_PubSub --> WS_C Redis_PubSub --> WS_D WS_B --> Check_B WS_C --> Check_C WS_D --> Check_D Check_B -->|"Push"| Client_B Check_C -->|"Skip — out of radius"| X["(no push)"] Check_D -->|"Push"| Client_D style Redis_Cache fill:#ef5350,color:#fff style Redis_PubSub fill:#ff7043,color:#fff style Check_C fill:#e0e0e0,color:#999 style X fill:#e0e0e0,color:#999
8.2 Redis Pub/Sub Fan-out Detail
flowchart LR subgraph "User A updates location" A_Update["User A<br/>location_update<br/>{lat, lng}"] end subgraph "Redis Pub/Sub Shard 3<br/>(channel:user_A hashed to shard 3)" Channel_A["channel:user_A<br/>Subscribers: B, C, D, E, F"] end subgraph "Fan-out (5 messages)" M1["→ WS Server 2 (for B)"] M2["→ WS Server 3 (for C)"] M3["→ WS Server 1 (for D)"] M4["→ WS Server 4 (for E)"] M5["→ WS Server 2 (for F)"] end A_Update -->|"PUBLISH"| Channel_A Channel_A --> M1 Channel_A --> M2 Channel_A --> M3 Channel_A --> M4 Channel_A --> M5 style Channel_A fill:#ef5350,color:#fff style A_Update fill:#42a5f5,color:#fff
8.3 WebSocket Server Scaling
flowchart TB subgraph "Load Balancer Layer" LB["L7 Load Balancer<br/>Least Connections strategy<br/>WebSocket upgrade support"] end subgraph "WebSocket Server Fleet (240 servers)" subgraph "AZ-1 (80 servers)" WS1_1["WS 1<br/>48K conn"] WS1_2["WS 2<br/>50K conn"] WS1_N["WS ...<br/>49K conn"] end subgraph "AZ-2 (80 servers)" WS2_1["WS 81<br/>50K conn"] WS2_2["WS 82<br/>47K conn"] WS2_N["WS ...<br/>50K conn"] end subgraph "AZ-3 (80 servers)" WS3_1["WS 161<br/>49K conn"] WS3_2["WS 162<br/>50K conn"] WS3_N["WS ...<br/>48K conn"] end end subgraph "Auto Scaling" ASG["Auto Scaling Group<br/>Min: 200 | Max: 400<br/>Target: 80% connection capacity"] end LB --> WS1_1 & WS1_2 & WS1_N LB --> WS2_1 & WS2_2 & WS2_N LB --> WS3_1 & WS3_2 & WS3_N ASG -.->|"Scale out/in"| WS1_N & WS2_N & WS3_N style LB fill:#42a5f5,color:#fff style ASG fill:#ff9800,color:#000
Tại sao 3 Availability Zones? Nếu 1 AZ down, 2 AZ còn lại vẫn handle 67% traffic. Với 20% buffer (240 vs 200 needed), hệ thống survive 1 AZ failure.
8.4 Full System Architecture — Production Grade
flowchart TB subgraph "Client Layer" iOS["iOS App"] Android["Android App"] end subgraph "Edge Layer" CDN["CDN<br/>(static assets)"] DNS["DNS<br/>Latency-based routing"] end subgraph "Gateway Layer" ALB["Application Load Balancer<br/>WebSocket support<br/>TLS termination"] AuthZ["Auth Service<br/>JWT validation"] end subgraph "Application Layer" WS_Fleet["WebSocket Server Fleet<br/>240 servers across 3 AZs<br/>50K connections/server"] end subgraph "Data Layer" Redis_Cache2["Redis Cluster<br/>Location Cache<br/>3 nodes (1P+2R)"] Redis_PS["Redis Pub/Sub Cluster<br/>30 shards"] end subgraph "Supporting Services" User_Svc["User Service<br/>(Friends list)"] Notif_Svc["Notification Service<br/>(push notifications)"] Config_Svc["Config Service<br/>(feature flags)"] end subgraph "Storage" PG[("PostgreSQL<br/>Users, Friends<br/>1P + 2R")] end subgraph "Observability" Metrics["Prometheus + Grafana"] Logs["ELK Stack"] Traces["Jaeger/Zipkin"] end iOS & Android --> DNS DNS --> ALB ALB -->|"WS Upgrade"| AuthZ AuthZ -->|"Valid token"| WS_Fleet WS_Fleet --> Redis_Cache2 WS_Fleet --> Redis_PS WS_Fleet --> User_Svc User_Svc --> PG WS_Fleet -.->|"User offline > 5min"| Notif_Svc WS_Fleet -.->|"Metrics"| Metrics WS_Fleet -.->|"Logs"| Logs WS_Fleet -.->|"Traces"| Traces Config_Svc -.->|"Feature flags"| WS_Fleet style ALB fill:#42a5f5,color:#fff style Redis_Cache2 fill:#ef5350,color:#fff style Redis_PS fill:#ff7043,color:#fff style PG fill:#66bb6a,color:#fff style Metrics fill:#ab47bc,color:#fff
9. Aha Moments & Pitfalls
9.1 Aha Moments — Những insight quan trọng
Aha 1: Pub/Sub đơn giản hơn Polling rất nhiều
Vấn đề: Làm sao để User B biết User A vừa update location?
Cách ngây thơ: Polling — mỗi 30 giây, server của B query Redis để lấy location của tất cả 40 online friends.
Cách Pub/Sub: B subscribe channels của friends. Khi A update, B tự động nhận.
Pub/Sub giảm Redis read load từ 13.3M xuống 333K — giảm 40x. Đây là sức mạnh của event-driven architecture.
Aha 2: WebSocket là stateful — và đó là OK
Nhiều backend devs sợ stateful services vì khó scale. Nhưng cho real-time features, stateful là bắt buộc — không có cách nào khác để maintain persistent bidirectional connection.
Bài học: Không phải mọi thứ đều phải stateless. Stateful services có chỗ riêng của chúng — key là biết cách scale chúng (connection draining, least-connections LB, graceful shutdown).
Aha 3: Redis Pub/Sub không persist — và đó là ưu điểm
Nếu miss 1 location update vì Pub/Sub là fire-and-forget, không sao — 30 giây sau có update mới. Tính chất “không persist” của Redis Pub/Sub biến từ nhược điểm thành ưu điểm: không tốn disk, không cần cleanup, không cần retention policy.
So sánh: Kafka persist mỗi message, cần manage offsets, cần disk space, cần compaction. Cho Nearby Friends, tất cả những thứ đó là overhead không cần thiết.
Aha 4: Fan-out là bottleneck, không phải latency
Mỗi location update mất ~10-15ms để propagate — cực nhanh. Bottleneck là số lượng messages cần fan-out: 13.3M/s. Đây là bài toán throughput, không phải latency.
Ý nghĩa cho interview: Khi interviewer hỏi “làm sao optimize?”, đừng trả lời “giảm latency”. Trả lời “giảm fan-out” hoặc “tăng throughput của Pub/Sub layer”.
Aha 5: Server-side filtering tiết kiệm bandwidth đáng kể
Chỉ push friends trong radius thay vì tất cả friends. Với avg 40 online friends nhưng chỉ 5-10 trong radius:
75% outbound bandwidth được tiết kiệm nhờ server-side distance calculation. Cho mobile users (4G/5G có data cap), đây là rất quan trọng.
Aha 6: Không cần Geospatial Index
Khác với Proximity Service (cần geohash/quadtree để tìm trong 100M businesses), Nearby Friends chỉ cần check khoảng cách giữa user và 40 friends. 40 Haversine calculations là trivial — không cần spatial index.
Điểm khác biệt chính: Proximity Service là “tìm người lạ trong bán kính” (hay: “search in a large dataset”). Nearby Friends là “check vị trí của người đã biết” (hay: “lookup known entities”). Search cần index. Lookup không cần.
9.2 Pitfalls — Những sai lầm phổ biến
Pitfall 1: Dùng HTTP Polling thay vì WebSocket
HTTP Polling cho Nearby Friends là sai lầm lớn nhất. Mỗi 30 giây, 10M clients gửi HTTP request → 333K QPS. Mỗi request có ~500 bytes HTTP headers. Và server không thể push — client phải pull.
Fix: WebSocket — persistent connection, bidirectional, minimal overhead (2-6 bytes/message).
Pitfall 2: Fan-out explosion không được xử lý
Một user có 5000 friends, 500 online → mỗi 30 giây, 500 messages fan-out. Nếu 1000 users như vậy update cùng lúc:
Có thể làm Redis Pub/Sub spike và ảnh hưởng toàn hệ thống.
Fix: Rate limit fan-out cho users có nhiều friends. Hoặc dùng batching: gom updates của popular users và publish 1 lần mỗi 5-10 giây thay vì mỗi 30 giây.
Pitfall 3: Không handle WebSocket reconnection đúng cách
Server restart, network blip, app bị background → connection mất. Nếu client không reconnect hoặc reconnect quá nhanh (thundering herd), hệ thống có thể overload.
Fix: Exponential backoff với jitter. Client retry: 1s + random(0-500ms), 2s + random, 4s + random, … Max 60s.
Pitfall 4: Quên cleanup khi user offline
User tắt app nhưng server không cleanup: location cache vẫn còn (TTL chưa hết), Pub/Sub channel vẫn active, friends vẫn thấy user “online”.
Fix: 3 lớp cleanup:
- WebSocket disconnect event → immediate cleanup
- Heartbeat miss → detect và cleanup
- Redis TTL → safety net, tự động xóa data cũ
Pitfall 5: Lưu location history “để sau này dùng”
“Để sau này làm feature này nọ” — và rồi GDPR audit phát hiện bạn lưu vị trí của 100M users mỗi 30 giây mà không có consent.
Fix: Không lưu những gì không cần thiết. Nearby Friends chỉ cần current location. Nếu sau này cần location history → thiết kế feature riêng với consent riêng.
Pitfall 6: Location precision vs privacy trade-off không được cân nhắc
Gửi raw GPS coordinates (10 decimal places, chính xác ~0.1mm) cho tất cả friends. Không ai cần biết bạn ở đâu chính xác đến 0.1mm.
Fix: Round coordinates đến 3-4 decimal places (~10-100m precision). Đủ chính xác để hiển thị trên bản đồ. Không đủ chính xác để xác định bạn đang ở phòng nào trong tòa nhà.
Pitfall 7: Không test fan-out ở production scale
Dev test với 100 users, mỗi user 10 friends → ok. Production 10M users, mỗi user 400 friends → Redis Pub/Sub overload.
Fix: Load test với realistic numbers trước khi launch. Simulate 10M connections, 333K updates/s, 13.3M fan-out messages/s.
10. Summary — Decision Framework
10.1 Khi nào dùng Pub/Sub pattern?
| Tình huống | Recommendation |
|---|---|
| Real-time updates cho known recipients | Pub/Sub — subscribers đã biết trước |
| Fan-out nhiều nhưng message không cần durable | Redis Pub/Sub — fire-and-forget |
| Cần guaranteed delivery | Kafka/RabbitMQ thay vì Redis Pub/Sub |
| 1-to-1 messaging | WebSocket trực tiếp, không cần Pub/Sub |
10.2 Khi nào dùng WebSocket?
| Tình huống | Recommendation |
|---|---|
| Server cần push data đến client | WebSocket |
| Chỉ client → server | HTTP là đủ |
| Chỉ server → client | SSE (Server-Sent Events) có thể đủ |
| Cả hai chiều + low latency | WebSocket — đây là Nearby Friends |
| Low frequency updates (mỗi vài phút) | Long polling có thể đủ |
10.3 Tổng kết Architecture Decisions
| Decision | Chosen | Alternative | Tại sao |
|---|---|---|---|
| Communication protocol | WebSocket | HTTP Polling, SSE | Bidirectional, low overhead |
| Location propagation | Redis Pub/Sub | Kafka, RabbitMQ | Low latency, no persistence needed |
| Location storage | Redis (in-memory cache) | Database | Chỉ cần latest, TTL tự động xóa |
| Distance calculation | Server-side Haversine | Client-side, Geohash pre-filter | Giảm bandwidth, 40 calculations là trivial |
| Channel design | 1 channel per user | Per geohash, per friend-pair | Granular, privacy-friendly |
| Scaling WebSocket | Least-connections LB | Consistent hashing | Đơn giản, reconnection cost thấp |
| Scaling Pub/Sub | Hash-based sharding | Consistent hashing | Đơn giản, deterministic |
| Multi-region | Regional clusters + cross-region bridge | Global cluster | Low latency cho local, bridge cho remote |
11. Internal Links — Liên kết với các bài khác
| Topic | Link | Liên quan |
|---|---|---|
| Proximity Service (businesses) | Case-Design-Proximity-Service | So sánh static vs dynamic location, geohash indexing |
| Chat System (WebSocket) | Tuan-17-Design-Chat-System | WebSocket management, connection lifecycle, message delivery |
| Consistent Hashing | Tuan-10-Consistent-Hashing | Shard assignment cho Redis Pub/Sub, server routing |
| Load Balancer | Tuan-05-Load-Balancer | WebSocket-aware LB, least-connections strategy |
| Message Queue | Tuan-08-Message-Queue | Pub/Sub vs Message Queue trade-offs |
| Cache Strategy | Tuan-06-Cache-Strategy | Redis caching patterns, TTL strategy |
| Rate Limiter | Tuan-09-Rate-Limiter | Rate limiting location updates, fan-out throttling |
| Monitoring | Tuan-13-Monitoring-Observability | WebSocket metrics, Pub/Sub monitoring, alerting |
| Security | Tuan-14-AuthN-AuthZ-Security | JWT cho WebSocket auth, privacy controls |
| Database Replication | Tuan-07-Database-Sharding-Replication | Redis replication, PostgreSQL for user data |
12. Interview Tips — Gợi ý cho phỏng vấn
12.1 Cấu trúc trả lời
| Bước | Nội dung | Thời gian |
|---|---|---|
| 1. Clarify requirements | Hỏi về scale, features, constraints | 3-5 phút |
| 2. High-level design | WebSocket + Redis Cache + Redis Pub/Sub | 5-7 phút |
| 3. Deep dive | Chọn 2-3 topics để đi sâu (Pub/Sub fan-out, scaling WebSocket, privacy) | 15-20 phút |
| 4. Wrap up | Trade-offs, alternatives, monitoring | 3-5 phút |
12.2 Câu hỏi interviewer thường hỏi
| Câu hỏi | Hướng trả lời |
|---|---|
| ”Tại sao không dùng HTTP polling?” | Bandwidth waste, server cần push, bidirectional need |
| ”Làm sao scale WebSocket?” | Least-connections LB, graceful drain, auto-scaling |
| ”Nếu Redis Pub/Sub down?” | Graceful degradation — nearby list freeze, client show “last updated X min ago" |
| "Làm sao xử lý popular users?” | Rate limit fan-out, batch updates, reduce frequency |
| ”Privacy concerns?” | Opt-in, fuzzing, no history, GDPR, Ghost Mode |
| ”Tại sao không dùng geohash?” | Chỉ 40 friends — brute force Haversine đủ nhanh, không cần spatial index |
| ”Alternative cho Redis Pub/Sub?” | Kafka (nếu cần durability), custom in-memory Pub/Sub (nếu cần ultra-low latency) |
12.3 Điểm thưởng (Bonus Points)
| Topic | Chi tiết | Ấn tượng |
|---|---|---|
| Battery optimization | Location updates mỗi 30s thay vì liên tục. Dùng significant location change API của iOS/Android | Show mobile awareness |
| Graceful degradation | Redis down → serve stale data, show “approximate” | Show resilience thinking |
| A/B testing | Test 30s vs 60s update interval, test radius defaults | Show product thinking |
| Cost estimation | 240 WS servers x 17K/month | Show business awareness |
“Nearby Friends không phải là bài toán khó về algorithm — không có geohash, không có quadtree, không có Dijkstra. Nó khó ở real-time communication ở quy mô lớn: 10 triệu WebSocket connections, 333 nghìn location updates mỗi giây, 13 triệu Pub/Sub messages mỗi giây. Đây là bài toán về infrastructure và engineering trade-offs, không phải về toán học.”