Harbor

Ứng dụng theo dõi chạy bộ

Trung bìnhOffline-firstDữ liệu GPSBảng xếp hạngReal-timeGiống Strava, Garmin Connect, Nike Run Club

Hiểu đề

Ứng dụng theo dõi chạy bộ là gì? App ghi lại buổi chạy bộ, đạp xe bằng GPS trên điện thoại, cho xem bản đồ, pace, so thành tích trên từng đoạn đường (segment) và chia sẻ với bạn bè. Ví dụ một nhóm chạy hồ Tây sáng chủ nhật cùng bấm Start lúc 5 giờ.

Yêu cầu chức năng

  1. Người dùng có thể ghi một hoạt động bằng GPS kể cả khi mất sóng, app tự đồng bộ khi có mạng
  2. Người dùng có thể xem chi tiết hoạt động: bản đồ, quãng đường, pace, biểu đồ độ cao
  3. Người dùng có thể xem bảng xếp hạng của từng segment (toàn bộ và trong nhóm bạn bè)
  4. Người dùng có thể chia sẻ vị trí trực tiếp cho bạn bè khi đang chạy và đăng hoạt động lên feed

Yêu cầu phi chức năng

  • Không mất điểm GPS nào đã ghi, kể cả khi app bị tắt, hết pin hay mất mạng cả buổi
  • Upload xong hoạt động hiện trên feed và có kết quả segment trong 30 s (p95)
  • Mở chi tiết hoạt động p95 < 500 ms, bảng xếp hạng segment p95 < 200 ms
  • Vị trí live đến người xem trễ tối đa 15 s, tốn pin thêm dưới 10%/giờ
  • Độ sẵn sàng 99,9% cho đọc; upload chấp nhận chậm vì app tự thử lại

Ngoài phạm vi: Kế hoạch tập luyện, phân tích sức khỏe nâng cao; Đồng bộ với đồng hồ Garmin/Apple Watch (chỉ nhắc ở mức nhận file FIT); Tìm đường, gợi ý cung đường; Thanh toán gói Premium.

Thực thể và API

Athlete
Người dùng: hồ sơ, cài đặt riêng tư (ẩn vùng quanh nhà), danh sách đang theo dõi.
Activity
Một buổi chạy/đạp: id (UUID do app sinh), loại, thời gian, quãng đường, pace trung bình, polyline rút gọn, đường dẫn file track.
Track (file)
Toàn bộ điểm GPS của hoạt động (thời gian, lat, lng, độ cao, nhịp tim), nén thành một file trên object storage.
Segment
Một đoạn đường do người dùng tạo (ví dụ một vòng hồ Tây): polyline, điểm đầu, điểm cuối, độ dài.
SegmentEffort
Một lần chạy qua segment: athlete_id, segment_id, activity_id, thời gian hoàn thành, ngày.
LiveSession
Phiên chia sẻ trực tiếp: activity_id, danh sách người được xem, vị trí gần nhất, hạn hết.
MethodEndpointGhi chú
POST/v1/activities/{uuid}/upload-urlXin presigned URL để app upload file track nén thẳng lên object storage. Gọi lại nhiều lần vẫn cùng uuid.
PUT/v1/activities/{uuid}Hoàn tất hoạt động: loại, tiêu đề, tóm tắt. Idempotent theo uuid nên app thử lại thoải mái.
GET/v1/activities/{id}Chi tiết: tóm tắt + polyline từ DB, link file track chi tiết qua CDN.
GET/v1/segments/{id}/leaderboard?scope=all|friends&limit=50Bảng xếp hạng segment, kèm thứ hạng của chính người gọi.
POST/v1/live/{sessionId}/pointsApp gửi lô điểm GPS mỗi 10 s khi đang chia sẻ live.
GET/v1/live/{sessionId}/streamNgười xem mở WebSocket/SSE để nhận vị trí mới.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Người dùng có thể ghi hoạt động offline và đồng bộ khi có mạng

Điện thoại là nơi ghi chính, server chỉ nhận kết quả. App lưu từng điểm vào SQLite trên máy, khi kết thúc thì nén thành một file và upload; mọi bước đều thử lại được nhờ UUID do app sinh.

  1. 1Bấm Start: app sinh activity UUID, ghi điểm GPS mỗi 1 s vào SQLite theo lô 60 điểm
  2. 2Bấm Stop: app nén track (delta encoding + gzip) thành file khoảng 27 KB
  3. 3Có mạng: gọi POST /upload-url, PUT file lên object storage, rồi PUT /activities/{uuid}
  4. 4API lưu Activity vào DB, đẩy job activity_uploaded vào queue
  5. 5Thất bại ở bước nào thì app giữ dữ liệu cục bộ và thử lại với backoff

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Detailed design" và "Stream a file" ; Chưa có nguồn trong thư viện: SQLite và delta encoding phía app không có trong sách

2) Người dùng có thể xem chi tiết hoạt động

Trang chi tiết chia làm hai phần. Tóm tắt và polyline rút gọn (vài trăm điểm) nằm trong DB để mở nhanh; file track đầy đủ chỉ tải khi người dùng mở biểu đồ chi tiết.

  1. 1Client gọi GET /v1/activities/{id}, API đọc tóm tắt + polyline từ DB
  2. 2Bản đồ vẽ polyline trên tile của Mapbox/Google Maps
  3. 3Mở tab phân tích thì client tải file track qua CDN và tự vẽ biểu đồ pace, độ cao
  4. 4Ảnh bản đồ thu nhỏ cho feed được worker render sẵn và lưu trên object storage

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Detailed design" và "Stream a file"

3) Người dùng có thể xem bảng xếp hạng segment

Sau khi upload, worker tìm các segment mà track đi qua rồi tính thời gian cho từng segment. Kết quả ghi vào bảng SegmentEffort và sorted set của segment, nên đọc bảng xếp hạng chỉ là một lệnh ZRANGE.

  1. 1Worker nhận activity_uploaded, đọc file track
  2. 2Lấy bounding box của track, tra index không gian ra các segment ứng viên
  3. 3Với mỗi segment: kiểm tra track đi qua điểm đầu, điểm cuối (sai số 25 m) và bám theo đường
  4. 4Ghi SegmentEffort; ZADD LT vào sorted set lb:{segmentId} với score là số giây
  5. 5Thành tích mới tốt nhất thì gửi push 'Bạn vừa lên hạng 3'

Nguồn: Redis in Action ↗ · ch.3 · §3.5 Sorted sets ; Chưa có nguồn trong thư viện: khớp segment theo không gian là thiết kế riêng của khoá học

4) Người dùng có thể chia sẻ vị trí trực tiếp và đăng lên feed

Khi bật live, app gửi lô điểm mỗi 10 s thay vì từng điểm để tiết kiệm pin. Server phát vị trí qua pub/sub đến gateway đang giữ kết nối của người xem.

  1. 1App tạo LiveSession, gửi link cho bạn bè hoặc người thân
  2. 2Mỗi 10 s app POST lô điểm; API lưu vị trí gần nhất vào Redis và publish lên kênh live:{sessionId}
  3. 3Live gateway subscribe kênh và đẩy xuống WebSocket của người xem
  4. 4Người xem vào muộn thì lấy 100 điểm gần nhất từ Redis trước
  5. 5Kết thúc buổi chạy: hoạt động đi qua luồng upload và xuất hiện trên feed người theo dõi

Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" ; Redis in Action ↗ · ch.3 · §3.6 Publish/subscribe ; Grokking Modern System Design · ch.30 Uber ↗ · mục "Update driver location" (cập nhật theo chu kỳ)

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 DAU
Tải đỉnh
≈ 17 req/s đọc, 0,35 upload/s đỉnh sáng sớm
Chi phí ước lượng
≈ 50–120 USD/tháng (ước lượng)

App của một câu lạc bộ chạy hoặc startup mới ra mắt. Một API Node, một Postgres và object storage cho file track là đủ; khớp segment chạy trong một worker nền nhỏ.

Người dùngApp di độngBên ngoàiMapboxDịch vụAPI Node.jsWorkerWorker nềnLưu trữObject storageDatabasePostgres+PostGISBên ngoàiFCM / APNstile bản đồHTTPSfile trackactivityeffortthông báo

Ước lượng

  • Hoạt động: 10.000 DAU × 30% ghi hoạt động = 3.000 hoạt động/ngày
  • Track: 45 phút × 60 = 2.700 điểm × 30 B = 81 KB thô, nén ×3 ≈ 27 KB
  • Lưu trữ: 3.000 × 27 KB = 81 MB/ngày × 365 ≈ 29,6 GB/năm
  • Đọc: 10.000 × 30 req / 86.400 ≈ 3,5 req/s, đỉnh ×5 ≈ 17 req/s → ceil(17 / 600) = 1, lấy tối thiểu 2 instance
  • Upload: 3.000 / 86.400 ≈ 0,035/s, đỉnh sáng sớm ×10 ≈ 0,35/s
  • Live: 40% hoạt động trong 2 giờ sáng = 1.200 × 2.700 s / 7.200 s = 450 người chạy cùng lúc, 10% bật live = 45

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Standard numbers to remember" và "Requests estimation" ; Quy tắc ước lượng của khoá học · /hoc/chon-server

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
APINode.js (NestJS), Docker trên Cloud Run22 vCPU · 4 GB RAM17 req/s chỉ cần 1 instance, chạy 2 để dự phòng.
DatabasePostgreSQL managed + PostGIS12 vCPU · 8 GB · 50 GB SSDTóm tắt 3.000 × 1 KB ≈ 1,1 GB/năm; PostGIS tìm segment theo bounding box; bảng xếp hạng bằng index (segment_id, elapsed).
Object storageS3 / GCS1 bucket≈ 30 GB/nămFile track ghi một lần đọc nguyên khối, rẻ hơn lưu trong DB nhiều lần.
WorkerNode.js + pg-boss (queue trên Postgres)11 vCPU · 2 GB RAM0,35 hoạt động/s × 0,1 vCPU-s = 0,035 vCPU, một worker nhỏ còn dư nhiều.
Bản đồMapbox / Google Maps SDK1Dịch vụ bên ngoàiTile bản đồ tải thẳng từ nhà cung cấp, không đi qua server mình.
PushFirebase Cloud Messaging / APNs1Dịch vụ bên ngoàiBáo thành tích segment, lượt kudos, bạn đang chạy live.
Thay đổi so với mức trước
  • Điểm xuất phát: một API, một Postgres, file track trên object storage
  • App ghi offline vào SQLite và upload idempotent theo UUID ngay từ đầu
  • Live dùng polling 10 s vì chỉ vài chục người bật cùng lúc
Nút thắt tiếp theo

Bảng xếp hạng bằng SQL chậm dần khi segment phổ biến có hàng trăm nghìn effort, và worker chung với queue trên Postgres bắt đầu tranh tài nguyên với API vào sáng chủ nhật.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 DAU1 triệu DAU20 triệu DAU, 6 triệu hoạt động/ngày
Chi phí≈ 50–120 USD/tháng (ước lượng)≈ 1.500–4.000 USD/tháng (ước lượng)≈ 30.000–70.000 USD/tháng (ước lượng)
API
Node.js (NestJS), Docker trên Cloud Run
2 × 2 vCPU · 4 GB RAM
Node.js (NestJS), container autoscale
3–6 (autoscale) × 2 vCPU · 4 GB RAM
Node.js trên Kubernetes, 3 AZ
58 pod (HPA) × 2 vCPU · 4 GB mỗi pod
Database
PostgreSQL managed + PostGIS
1 × 2 vCPU · 8 GB · 50 GB SSD
PostgreSQL managed (HA) + PostGIS
1 primary + 1 read replica × 4 vCPU · 16 GB · 500 GB SSD
Chưa cần
Object storage
S3 / GCS
1 bucket × ≈ 30 GB/năm
Chưa cầnChưa cần
Worker
Node.js + pg-boss (queue trên Postgres)
1 × 1 vCPU · 2 GB RAM
Node.js consumer (khớp segment, ảnh, feed)
3 × 2 vCPU · 4 GB RAM
Node.js consumer trên Kubernetes
18 pod (HPA theo lag) × 2 vCPU · 4 GB mỗi pod
Bản đồ
Mapbox / Google Maps SDK
1 × Dịch vụ bên ngoài
Chưa cầnChưa cần
Push
Firebase Cloud Messaging / APNs
1 × Dịch vụ bên ngoài
FCM / APNs
1 × Dịch vụ bên ngoài
FCM / APNs qua dịch vụ notification riêng
1 × Consumer Kafka
Live gatewayChưa cần
Node.js WebSocket (ws) + Redis pub/sub
2 × 2 vCPU · 4 GB RAM
Node.js WebSocket trên Kubernetes
15 pod × 2 vCPU · 4 GB mỗi pod
QueueChưa cần
Amazon SQS / Google Pub/Sub
1 × Managed, có dead-letter queue
Chưa cần
RedisChưa cần
ElastiCache / Memorystore
1 primary + 1 replica × 2 vCPU · 13 GB
Chưa cần
Object storage + CDNChưa cần
S3 + CloudFront / GCS + Cloud CDN
1 × ≈ 3 TB/năm, lifecycle sang lớp lưu trữ rẻ sau 90 ngày
S3 + CloudFront
1 × ≈ 59 TB/năm, lifecycle sang S3 Glacier IR sau 1 năm
KafkaChưa cầnChưa cần
MSK / Confluent
3 broker × Topic activities, efforts, live-points, mỗi topic 12 partition, RF 3
Redis ClusterChưa cầnChưa cần
ElastiCache Redis Cluster
6 shard × (1 primary + 1 replica) × 4 vCPU · 32 GB mỗi node
CassandraChưa cầnChưa cần
Cassandra / ScyllaDB
6 node × 8 vCPU · 32 GB · 2 TB NVMe
PostgreSQLChưa cầnChưa cần
Aurora / Cloud SQL HA + PostGIS
1 primary + 2 replica × 8 vCPU · 32 GB · 1 TB SSD

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Ghi GPS trên điện thoại thế nào để không mất dữ liệu khi mất sóng, app bị tắt hay hết pin?

Cách chưa tốt
Gửi từng điểm GPS lên server ngay khi có

Mỗi giây gọi API một lần. Chạy vào vùng không có sóng (đường núi, hầm) thì điểm bị mất, và giữ radio di động bật liên tục làm hao pin rất nhanh.

Đánh đổi: Server thấy dữ liệu sớm nhất nhưng dữ liệu không đầy đủ và người dùng bỏ app vì tốn pin.

⚠ Chưa có nguồn trong thư viện: ghi GPS trên app là thực hành chung

Cách tốt
Giữ điểm trong RAM của app, kết thúc thì upload một lần

Không phụ thuộc mạng khi đang chạy và chỉ tốn một request. Phù hợp buổi chạy ngắn và máy ổn định.

Đánh đổi: App bị hệ điều hành tắt hoặc điện thoại sập nguồn giữa chừng là mất cả buổi; upload hỏng không có cách tiếp tục.

⚠ Chưa có nguồn trong thư viện: giữ điểm trong RAM của app là nhận định chung

Cách tốt nhất
Ghi append vào SQLite theo lô, UUID do client sinh, upload idempotent qua presigned URL

Mỗi 60 điểm được ghi xuống SQLite trong một transaction, nên tối đa mất 1 phút khi máy sập. App lọc điểm nhiễu (độ chính xác kém hơn 30 m) và dùng thời gian GPS thay vì đồng hồ máy. Khi kết thúc, file được nén rồi upload; UUID sinh ở app giúp server bỏ qua bản trùng khi app gửi lại. Hàng đợi upload cục bộ chạy nền, thử lại với backoff đến khi server xác nhận mới xóa dữ liệu trên máy.

Đánh đổi: Logic phía app phức tạp hơn và phải hỗ trợ nhiều phiên bản app cũ; server phải chấp nhận hoạt động đến trễ vài ngày.

Nguồn: Chưa có nguồn trong thư viện: SQLite theo lô là thực hành chung ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Task idempotency"

2) Lưu track GPS ở đâu: từng điểm trong DB, time series DB hay file blob?

Cách chưa tốt
Mỗi điểm GPS là một hàng trong Postgres

Ở 6 triệu hoạt động/ngày × 2.700 điểm là 16,2 tỷ hàng/ngày, khoảng 187.500 insert/s. Index phình to, DB chậm dần và tốn tiền SSD.

Đánh đổi: Truy vấn linh hoạt nhưng trả giá rất đắt cho dữ liệu hầu như chỉ đọc nguyên khối.

Nguồn: Quy tắc ước lượng của khoá học · /hoc/chon-server ; Chưa có nguồn trong thư viện: ước lượng số hàng là phép tính của khoá học

Cách tốt
Time series DB (TimescaleDB, InfluxDB) theo activity_id

Nén theo cột và chia chunk theo thời gian nên nhẹ hơn Postgres thường nhiều lần, query khoảng thời gian nhanh. Hợp với bài toán cần phân tích điểm qua nhiều hoạt động.

Đánh đổi: Vẫn là một cụm DB phải vận hành cho dữ liệu mà 99% lượt đọc là lấy nguyên track của một hoạt động.

⚠ Chưa có nguồn trong thư viện: TimescaleDB/InfluxDB không có trong sách; xem /hoc/time-series-database

Cách tốt nhất
File track nén trên object storage, tóm tắt và polyline trong DB

Track chỉ được ghi một lần và đọc nguyên khối, nên lưu thành một file khoảng 27 KB trên S3/GCS (Grokking ch.20 Blob Store), phục vụ qua CDN. DB chỉ giữ tóm tắt, polyline rút gọn và kết quả đã tính (split theo km, segment effort). Khi cần phân tích hàng loạt thì chạy job đọc file theo lô; dữ liệu đã tổng hợp mới đưa vào time series DB.

Đánh đổi: Không query được từng điểm bằng SQL; muốn tính lại chỉ số mới thì phải chạy job đọc lại file.

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Detailed design" và "Stream a file"

3) Bảng xếp hạng cho hàng triệu segment, có segment hàng trăm nghìn người chạy qua, làm thế nào?

Cách chưa tốt
SELECT MIN(elapsed) GROUP BY athlete ORDER BY mỗi lần mở trang

Query trên bảng SegmentEffort mỗi lần có người xem. Segment phổ biến có hàng triệu effort nên mỗi lần xem là một lần gom nhóm và sắp xếp lớn.

Đánh đổi: Đúng và đơn giản ở quy mô nhỏ, vỡ khi segment nổi tiếng được xem nhiều.

⚠ Chưa có nguồn trong thư viện: GROUP BY mỗi lần xem là nhận định chung

Cách tốt
Bảng best_effort (segment, athlete) cập nhật khi có effort mới, index (segment_id, elapsed)

Worker chỉ cập nhật khi effort mới tốt hơn. Đọc top 50 là quét index theo thứ tự, rất nhanh. Thứ hạng của chính mình cần COUNT(*) WHERE elapsed < x.

Đánh đổi: COUNT để lấy thứ hạng chậm với segment lớn và mọi lượt xem đều chạm DB.

⚠ Chưa có nguồn trong thư viện: bảng best_effort là thiết kế của khoá học

Cách tốt nhất
Redis sorted set mỗi segment (ZADD LT), bạn bè tính riêng, DB là nguồn sự thật

lb:{segmentId} lưu athlete_id với score là số giây; ZADD LT chỉ ghi khi thời gian mới tốt hơn, ZRANGE lấy top, ZRANK lấy hạng của mình trong O(log N) (Redis in Action §3.5). Bảng xếp hạng bạn bè lấy best_effort của vài trăm người mình theo dõi rồi sắp xếp tại chỗ. Bảng theo tháng dùng key riêng có TTL. Effort nghi gian lận (tốc độ vô lý) bị giữ lại kiểm tra trước khi vào bảng. Redis mất dữ liệu thì dựng lại từ bảng best_effort.

Đánh đổi: Tốn RAM (1 tỷ cặp khoảng 151 GB) nên phải chạy Redis Cluster; hai nơi lưu cần job đối soát định kỳ.

Nguồn: Redis in Action ↗ · ch.3 · §3.5 Sorted sets (ZADD, ZRANGE, ZRANK)

4) Chia sẻ vị trí trực tiếp cho bạn bè khi đang chạy thế nào để vừa kịp thời vừa đỡ tốn pin?

Cách chưa tốt
App ghi từng điểm vào DB, người xem poll DB mỗi 2 s

Mỗi người chạy tạo 1 ghi/s và mỗi người xem tạo 0,5 đọc/s vào DB chính. Pin điện thoại hao vì gửi liên tục.

Đánh đổi: Không cần thêm thành phần nhưng DB gánh tải không cần thiết và pin không chịu nổi buổi chạy dài.

⚠ Chưa có nguồn trong thư viện: polling DB là nhận định chung

Cách tốt
App gửi lô 10 s, vị trí gần nhất trong Redis, người xem poll 10 s

Gom 10 điểm một lần gửi nên radio được nghỉ. Redis giữ vị trí gần nhất và 100 điểm cuối; endpoint poll chỉ đọc Redis.

Đánh đổi: Trễ đến 20 s và nhiều request rỗng khi người chạy dừng nghỉ.

⚠ Chưa có nguồn trong thư viện: poll theo lô là nhận định chung

Cách tốt nhất
Lô 10 s + pub/sub + gateway WebSocket/SSE cho người xem

API nhận lô điểm, lưu vào Redis và publish lên kênh live:{sessionId} (Grokking ch.18 Pub-sub, Redis in Action §3.6). Gateway giữ kết nối người xem, chỉ subscribe kênh có người đang xem nên không tốn tài nguyên cho phiên không ai mở. Người vào muộn lấy 100 điểm cuối từ Redis. Ở quy mô lớn kênh đi qua Kafka để gateway có thể đọc lại sau khi khởi động lại. Giống cách app gọi xe cập nhật vị trí tài xế theo chu kỳ (Grokking ch.30 Uber).

Đánh đổi: Thêm gateway giữ hàng trăm nghìn kết nối; mất sóng thì người xem thấy vị trí đứng yên, app phải gửi bù khi có mạng lại.

Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" ; Redis in Action ↗ · ch.3 · §3.6 Publish/subscribe ; Grokking Modern System Design · ch.30 Uber ↗ · mục "Update driver location"

Trong phỏng vấn

Nên nói
  • “Điện thoại là nơi ghi chính; tôi ghi append vào SQLite và upload idempotent theo UUID do app sinh, nên mất mạng không mất dữ liệu.”
  • “Track ghi một lần, đọc nguyên khối nên lưu thành file nén trên object storage; DB chỉ giữ tóm tắt và polyline rút gọn.”
  • “Bảng xếp hạng segment là sorted set với ZADD LT; DB giữ best_effort làm nguồn sự thật để dựng lại Redis.”
  • “Live gửi lô 10 s để tiết kiệm pin, đẩy qua pub/sub đến gateway WebSocket của người xem.”
Bẫy hay gặp
  • Lưu mỗi điểm GPS thành một hàng trong DB quan hệ.
  • Thiết kế upload như thể điện thoại luôn có mạng, không có retry và idempotency.
  • Tính bảng xếp hạng bằng GROUP BY mỗi lần có người xem.
  • Gửi vị trí live mỗi giây và quên chuyện pin.

Đọc thêm

Xử lý file lớn
Cách upload, lưu và phân phối file lớn mà không để chúng đi qua app server: presigned URL, multipart và resumable upload, chia chunk và dedup theo hash, CDN, signed URL khi tải về, pipeline hậu xử lý bằng sự kiện, tách metadata khỏi blob store.
Time-series database
Time-series database lưu dữ liệu đo theo thời gian như metric server, giá cổ phiếu, vị trí GPS. Bài này giải thích đặc điểm của loại dữ liệu này, cách nén, downsampling và retention, so sánh TimescaleDB, InfluxDB, Prometheus, ClickHouse, và bẫy cardinality.
Redis
Redis là kho key-value trong RAM với nhiều cấu trúc dữ liệu: dùng làm cache, bộ đếm, leaderboard, hàng đợi, GEO và pub/sub. Bài này đi qua mô hình dữ liệu, persistence, replication, Cluster, Lua và cách tính RAM.
Cập nhật realtime
Đẩy thay đổi tới client trong vài trăm mili giây: chọn giữa polling, long polling, SSE, WebSocket, rồi fan-out qua nhiều gateway, xử lý reconnect và backpressure.
Kafka
Kafka là log phân tán bền vững: topic chia partition, consumer đọc theo offset và có thể đọc lại. Bài này đi qua thứ tự theo partition, consumer group, replication với ISR và acks, giới hạn của exactly-once, cách chọn số partition và so sánh với SQS, Pub/Sub, RabbitMQ.
Cassandra
Cassandra là database wide-column phân tán, ghi rất nhanh nhờ LSM tree, mở rộng gần tuyến tính bằng ring và consistent hashing. Bài này giải thích partition key, clustering column, cách mô hình hoá theo câu truy vấn, tunable consistency và những cái bẫy như tombstone.

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.