Mục lục
Thiết kế timeline và tìm kiếm của Twitter
Nội dung gốc
Lưu ý: Tài liệu này liên kết trực tiếp tới các phần liên quan trong các chủ đề system design để tránh lặp lại. Hãy tham khảo nội dung được liên kết để nắm các điểm thảo luận chung, các đánh đổi (tradeoff) và phương án thay thế.
Thiết kế news feed của Facebook và Thiết kế chức năng tìm kiếm của Facebook là những câu hỏi tương tự.
Bước 1: Phác thảo các trường hợp sử dụng và ràng buộc
Thu thập yêu cầu và khoanh vùng bài toán. Đặt câu hỏi để làm rõ các trường hợp sử dụng (use case) và ràng buộc (constraint). Thảo luận các giả định.
Vì không có người phỏng vấn để trả lời các câu hỏi làm rõ, chúng ta sẽ tự định nghĩa một số trường hợp sử dụng và ràng buộc.
Các trường hợp sử dụng
Chúng ta khoanh vùng bài toán, chỉ xử lý các trường hợp sử dụng sau
- Người dùng đăng một tweet
- Dịch vụ đẩy tweet tới những người theo dõi (follower), gửi thông báo đẩy (push notification) và email
- Người dùng xem user timeline (hoạt động của chính người dùng đó)
- Người dùng xem home timeline (hoạt động của những người mà người dùng đang theo dõi)
- Người dùng tìm kiếm theo từ khóa
- Dịch vụ có tính sẵn sàng cao (high availability)
Ngoài phạm vi
- Dịch vụ đẩy tweet vào Twitter Firehose và các luồng (stream) khác
- Dịch vụ lọc bỏ tweet dựa trên thiết lập hiển thị của người dùng
- Ẩn @reply nếu người dùng không đồng thời theo dõi người được trả lời
- Tôn trọng thiết lập "ẩn retweet"
- Phân tích số liệu (analytics)
Ràng buộc và giả định
Nêu các giả định
Chung
- Lưu lượng truy cập không phân bố đều
- Đăng tweet phải nhanh
- Phát tán (fan out) một tweet tới tất cả follower phải nhanh, trừ khi bạn có hàng triệu follower
- 100 triệu người dùng hoạt động
- 500 triệu tweet mỗi ngày, tức 15 tỷ tweet mỗi tháng
- Trung bình mỗi tweet được phát tán tới 10 nơi nhận
- Tổng cộng 5 tỷ lượt giao tweet qua fan-out mỗi ngày
- 150 tỷ lượt giao tweet qua fan-out mỗi tháng
- 250 tỷ yêu cầu đọc mỗi tháng
- 10 tỷ lượt tìm kiếm mỗi tháng
Timeline
- Xem timeline phải nhanh
- Twitter đọc nhiều hơn ghi (read heavy)
- Tối ưu cho việc đọc tweet nhanh
- Việc tiếp nhận (ingest) tweet thì nặng về ghi (write heavy)
Tìm kiếm
- Tìm kiếm phải nhanh
- Tìm kiếm nặng về đọc
Tính toán mức sử dụng
Hãy hỏi rõ người phỏng vấn xem bạn có nên thực hiện các phép ước lượng nhanh (back-of-the-envelope) hay không.
- Kích thước mỗi tweet:
tweet_id- 8 byteuser_id- 32 bytetext- 140 bytemedia- trung bình 10 KB- Tổng: ~10 KB
- 150 TB nội dung tweet mới mỗi tháng
- 10 KB mỗi tweet * 500 triệu tweet mỗi ngày * 30 ngày mỗi tháng
- 5,4 PB nội dung tweet mới trong 3 năm
- 100 nghìn yêu cầu đọc mỗi giây
- 250 tỷ yêu cầu đọc mỗi tháng * (400 yêu cầu mỗi giây / 1 tỷ yêu cầu mỗi tháng)
- 6.000 tweet mỗi giây
- 15 tỷ tweet mỗi tháng * (400 yêu cầu mỗi giây / 1 tỷ yêu cầu mỗi tháng)
- 60 nghìn lượt giao tweet qua fan-out mỗi giây
- 150 tỷ lượt giao tweet qua fan-out mỗi tháng * (400 yêu cầu mỗi giây / 1 tỷ yêu cầu mỗi tháng)
- 4.000 yêu cầu tìm kiếm mỗi giây
- 10 tỷ lượt tìm kiếm mỗi tháng * (400 yêu cầu mỗi giây / 1 tỷ yêu cầu mỗi tháng)
Bảng quy đổi tiện dụng:
- 2,5 triệu giây mỗi tháng
- 1 yêu cầu mỗi giây = 2,5 triệu yêu cầu mỗi tháng
- 40 yêu cầu mỗi giây = 100 triệu yêu cầu mỗi tháng
- 400 yêu cầu mỗi giây = 1 tỷ yêu cầu mỗi tháng
Bước 2: Tạo thiết kế tổng quan
Phác thảo thiết kế tổng quan (high level design) với tất cả các thành phần quan trọng.

Bước 3: Thiết kế các thành phần cốt lõi
Đi sâu vào chi tiết từng thành phần cốt lõi.
Trường hợp sử dụng: Người dùng đăng một tweet
Chúng ta có thể lưu các tweet của chính người dùng để dựng user timeline (hoạt động của người dùng) trong một cơ sở dữ liệu quan hệ (relational database). Chúng ta nên thảo luận về các trường hợp sử dụng và đánh đổi giữa việc chọn SQL hay NoSQL.
Việc giao tweet và dựng home timeline (hoạt động của những người mà người dùng đang theo dõi) khó hơn. Phát tán tweet tới tất cả follower (60 nghìn lượt giao tweet qua fan-out mỗi giây) sẽ làm quá tải một cơ sở dữ liệu quan hệ truyền thống. Có lẽ chúng ta sẽ muốn chọn một kho dữ liệu ghi nhanh như cơ sở dữ liệu NoSQL hoặc bộ nhớ đệm trong RAM (Memory Cache). Đọc tuần tự 1 MB từ bộ nhớ mất khoảng 250 micro giây, trong khi đọc từ SSD lâu hơn 4 lần và từ ổ đĩa cứng lâu hơn 80 lần.1
Chúng ta có thể lưu media như ảnh hoặc video trên một kho lưu trữ đối tượng (Object Store).
- Client gửi một tweet tới Web Server, vốn đang chạy như một reverse proxy
- Web Server chuyển tiếp yêu cầu tới máy chủ Write API
- Write API lưu tweet vào user timeline của người dùng trên một cơ sở dữ liệu SQL
- Write API liên hệ với Fan Out Service, dịch vụ này thực hiện các việc sau:
- Truy vấn User Graph Service để tìm các follower của người dùng, được lưu trong Memory Cache
- Lưu tweet vào home timeline của các follower trong một Memory Cache
- Thao tác O(n): 1.000 follower = 1.000 lần tra cứu và chèn
- Lưu tweet vào Search Index Service để hỗ trợ tìm kiếm nhanh
- Lưu media vào Object Store
- Dùng Notification Service để gửi thông báo đẩy tới các follower:
- Dùng một hàng đợi (Queue) (không có trong hình) để gửi thông báo một cách bất đồng bộ
Hãy hỏi rõ người phỏng vấn bạn cần viết bao nhiêu code.
Nếu Memory Cache của chúng ta là Redis, ta có thể dùng kiểu list gốc của Redis với cấu trúc sau:
tweet n+2 tweet n+1 tweet n
| 8 bytes 8 bytes 1 byte | 8 bytes 8 bytes 1 byte | 8 bytes 8 bytes 1 byte |
| tweet_id user_id meta | tweet_id user_id meta | tweet_id user_id meta |
Tweet mới sẽ được đặt vào Memory Cache, nơi dựng nên home timeline của người dùng (hoạt động của những người mà người dùng đang theo dõi).
Chúng ta sẽ dùng một REST API công khai:
$ curl -X POST --data '{ "user_id": "123", "auth_token": "ABC123", \
"status": "hello world!", "media_ids": "ABC987" }' \
https://twitter.com/api/v1/tweet
Phản hồi:
{
"created_at": "Wed Sep 05 00:37:15 +0000 2012",
"status": "hello world!",
"tweet_id": "987",
"user_id": "123",
...
}
Với giao tiếp nội bộ, chúng ta có thể dùng lời gọi thủ tục từ xa (Remote Procedure Call - RPC).
Trường hợp sử dụng: Người dùng xem home timeline
- Client gửi yêu cầu xem home timeline tới Web Server
- Web Server chuyển tiếp yêu cầu tới máy chủ Read API
- Máy chủ Read API liên hệ với Timeline Service, dịch vụ này thực hiện các việc sau:
- Lấy dữ liệu timeline lưu trong Memory Cache, gồm các tweet id và user id - O(1)
- Truy vấn Tweet Info Service bằng một lệnh multiget để lấy thêm thông tin về các tweet id - O(n)
- Truy vấn User Info Service bằng một lệnh multiget để lấy thêm thông tin về các user id - O(n)
REST API:
$ curl https://twitter.com/api/v1/home_timeline?user_id=123
Phản hồi:
{
"user_id": "456",
"tweet_id": "123",
"status": "foo"
},
{
"user_id": "789",
"tweet_id": "456",
"status": "bar"
},
{
"user_id": "789",
"tweet_id": "579",
"status": "baz"
},
Trường hợp sử dụng: Người dùng xem user timeline
- Client gửi yêu cầu xem user timeline tới Web Server
- Web Server chuyển tiếp yêu cầu tới máy chủ Read API
- Read API lấy user timeline từ cơ sở dữ liệu SQL
REST API sẽ tương tự home timeline, chỉ khác là mọi tweet đều đến từ chính người dùng thay vì từ những người mà người dùng đang theo dõi.
Trường hợp sử dụng: Người dùng tìm kiếm theo từ khóa
- Client gửi yêu cầu tìm kiếm tới Web Server
- Web Server chuyển tiếp yêu cầu tới máy chủ Search API
- Search API liên hệ với Search Service, dịch vụ này thực hiện các việc sau:
- Phân tích cú pháp/tách token (parse/tokenize) truy vấn đầu vào, xác định những gì cần tìm
- Loại bỏ markup
- Tách văn bản thành các từ (term)
- Sửa lỗi chính tả
- Chuẩn hóa chữ hoa chữ thường
- Chuyển truy vấn sang dạng dùng các phép toán boolean
- Truy vấn Search Cluster (ví dụ Lucene) để lấy kết quả:
- Scatter gather tới từng máy chủ trong cụm để xác định có kết quả nào cho truy vấn không
- Gộp (merge), xếp hạng (rank), sắp xếp (sort) và trả về kết quả
- Phân tích cú pháp/tách token (parse/tokenize) truy vấn đầu vào, xác định những gì cần tìm
REST API:
$ curl https://twitter.com/api/v1/search?query=hello+world
Phản hồi sẽ tương tự home timeline, chỉ khác là gồm các tweet khớp với truy vấn đã cho.
Bước 4: Mở rộng thiết kế
Xác định và xử lý các điểm nghẽn (bottleneck), dựa trên các ràng buộc.

Quan trọng: Đừng nhảy thẳng từ thiết kế ban đầu sang thiết kế cuối cùng!
Hãy nói rằng bạn sẽ 1) Benchmark/kiểm thử tải (Load Test), 2) Profile để tìm điểm nghẽn, 3) xử lý các điểm nghẽn trong khi đánh giá các phương án thay thế và đánh đổi, và 4) lặp lại. Xem Thiết kế một hệ thống mở rộng tới hàng triệu người dùng trên AWS để có ví dụ về cách mở rộng thiết kế ban đầu theo từng bước lặp.
Điều quan trọng là thảo luận những điểm nghẽn bạn có thể gặp với thiết kế ban đầu và cách xử lý từng điểm. Ví dụ, việc thêm một bộ cân bằng tải (Load Balancer) với nhiều Web Server giải quyết được vấn đề gì? CDN? Bản sao Master-Slave (Master-Slave Replicas)? Mỗi thứ có những phương án thay thế và đánh đổi nào?
Chúng ta sẽ đưa vào một số thành phần để hoàn thiện thiết kế và xử lý các vấn đề về khả năng mở rộng. Các bộ cân bằng tải nội bộ không được vẽ ra để hình đỡ rối.
Để tránh lặp lại các thảo luận, hãy tham khảo các chủ đề system design sau để nắm các điểm thảo luận chính, đánh đổi và phương án thay thế:
- DNS
- CDN
- Bộ cân bằng tải (Load balancer)
- Mở rộng theo chiều ngang (Horizontal scaling)
- Web server (reverse proxy)
- API server (tầng ứng dụng - application layer)
- Bộ nhớ đệm (Cache)
- Hệ quản trị cơ sở dữ liệu quan hệ (RDBMS)
- Fail-over master-slave cho SQL ghi
- Nhân bản master-slave (Master-slave replication)
- Các mẫu nhất quán (Consistency patterns)
- Các mẫu sẵn sàng (Availability patterns)
Fanout Service là một điểm nghẽn tiềm năng. Những người dùng Twitter có hàng triệu follower có thể mất vài phút để tweet của họ đi hết quá trình fan-out. Điều này có thể dẫn tới tình trạng tranh chấp (race condition) với các @reply cho tweet đó, mà ta có thể giảm thiểu bằng cách sắp xếp lại thứ tự tweet tại thời điểm phục vụ (serve time).
Chúng ta cũng có thể tránh fan-out tweet của những người dùng có rất nhiều follower. Thay vào đó, ta có thể tìm kiếm để lấy tweet của những người dùng này, gộp kết quả tìm kiếm với kết quả home timeline của người dùng, rồi sắp xếp lại thứ tự tweet tại thời điểm phục vụ.
Các tối ưu bổ sung gồm:
- Chỉ giữ vài trăm tweet cho mỗi home timeline trong Memory Cache
- Chỉ giữ thông tin home timeline của người dùng đang hoạt động trong Memory Cache
- Nếu một người dùng không hoạt động trong 30 ngày qua, ta có thể dựng lại timeline từ cơ sở dữ liệu SQL
- Truy vấn User Graph Service để xác định người dùng đang theo dõi những ai
- Lấy tweet từ cơ sở dữ liệu SQL và thêm vào Memory Cache
- Nếu một người dùng không hoạt động trong 30 ngày qua, ta có thể dựng lại timeline từ cơ sở dữ liệu SQL
- Chỉ lưu tweet của một tháng trong Tweet Info Service
- Chỉ lưu người dùng đang hoạt động trong User Info Service
- Search Cluster nhiều khả năng cần giữ tweet trong bộ nhớ để giữ độ trễ thấp
Chúng ta cũng sẽ muốn xử lý điểm nghẽn ở cơ sở dữ liệu SQL.
Mặc dù Memory Cache sẽ giảm tải cho cơ sở dữ liệu, nhưng khó mà chỉ riêng các bản sao đọc SQL (SQL Read Replicas) là đủ để xử lý các lần trượt cache (cache miss). Có lẽ chúng ta cần áp dụng thêm các mẫu mở rộng SQL khác.
Lượng ghi lớn sẽ làm quá tải một cụm SQL Write Master-Slave duy nhất, điều này cũng cho thấy cần thêm các kỹ thuật mở rộng:
- Liên hợp (Federation)
- Phân mảnh (Sharding)
- Phi chuẩn hóa (Denormalization)
- Tinh chỉnh SQL (SQL Tuning)
Chúng ta cũng nên cân nhắc chuyển một phần dữ liệu sang cơ sở dữ liệu NoSQL.
Các điểm thảo luận bổ sung
Các chủ đề bổ sung để đi sâu, tùy theo phạm vi bài toán và thời gian còn lại.
NoSQL
- Kho khóa-giá trị (Key-value store)
- Kho tài liệu (Document store)
- Kho cột rộng (Wide column store)
- Cơ sở dữ liệu đồ thị (Graph database)
- SQL hay NoSQL
Bộ nhớ đệm (Caching)
- Cache ở đâu
- Cache cái gì
- Khi nào cập nhật cache
Bất đồng bộ và microservices
- Hàng đợi thông điệp (Message queues)
- Hàng đợi tác vụ (Task queues)
- Áp lực ngược (Back pressure)
- Microservices
Giao tiếp
- Thảo luận các đánh đổi:
- Giao tiếp bên ngoài với client - HTTP API theo kiểu REST
- Giao tiếp nội bộ - RPC
- Khám phá dịch vụ (Service discovery)
Bảo mật
Tham khảo mục bảo mật.
Các con số độ trễ
Xem Các con số độ trễ mà mọi lập trình viên nên biết (Latency numbers every programmer should know).
Liên tục
- Tiếp tục benchmark và giám sát hệ thống để xử lý các điểm nghẽn khi chúng xuất hiện
- Mở rộng là một quá trình lặp đi lặp lại