Thiết kế tính năng xếp hạng doanh số theo danh mục của Amazon
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 nội dung. Xem nội dung được liên kết để biết các ý thảo luận chung, đánh đổi và phương án thay thế.
Bước 1: Xác định use case và ràng buộc
Thu thập yêu cầu và giới hạn phạm vi bài toán. Đặt câu hỏi để làm rõ use case và ràng buộc. 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õ, ta sẽ tự định nghĩa một số use case và ràng buộc.
Use case
Ta giới hạn bài toán ở use case sau
- Service tính các sản phẩm bán chạy nhất trong tuần qua theo danh mục
- User xem các sản phẩm bán chạy nhất trong tuần qua theo danh mục
- Service có tính sẵn sàng cao
Ngoài phạm vi
- Toàn bộ site thương mại điện tử
- Chỉ thiết kế các thành phần phục vụ việc tính xếp hạng doanh số
Ràng buộc và giả định
Nêu các giả định
- Traffic không phân bố đồng đều
- Một sản phẩm có thể thuộc nhiều danh mục
- Sản phẩm không thể đổi danh mục
- Không có danh mục con, tức là không có dạng
foo/bar/baz - Kết quả phải được cập nhật mỗi giờ
- Các sản phẩm bán chạy hơn có thể cần cập nhật thường xuyên hơn
- 10 triệu sản phẩm
- 1000 danh mục
- 1 tỷ giao dịch mỗi tháng
- 100 tỷ read request mỗi tháng
- Tỷ lệ đọc so với ghi là 100:1
Tính toán mức sử dụng
Làm rõ với người phỏng vấn xem có cần tính nhẩm mức sử dụng hay không.
- Kích thước mỗi giao dịch:
created_at- 5 byteproduct_id- 8 bytecategory_id- 4 byteseller_id- 8 bytebuyer_id- 8 bytequantity- 4 bytetotal_price- 5 byte- Tổng: ~40 byte
- 40 GB dữ liệu giao dịch mới mỗi tháng
- 40 byte mỗi giao dịch * 1 tỷ giao dịch mỗi tháng
- 1,44 TB dữ liệu giao dịch mới trong 3 năm
- Giả định phần lớn là giao dịch mới chứ không phải cập nhật giao dịch có sẵn
- Trung bình 400 giao dịch mỗi giây
- Trung bình 40.000 read request mỗi giây
Bảng quy đổi tiện dụng:
- 2,5 triệu giây mỗi tháng
- 1 request mỗi giây = 2,5 triệu request mỗi tháng
- 40 request mỗi giây = 100 triệu request mỗi tháng
- 400 request mỗi giây = 1 tỷ request mỗi tháng
Bước 2: Tạo thiết kế mức cao
Phác thảo thiết kế mức cao 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 của từng thành phần cốt lõi.
Use case: Service tính các sản phẩm bán chạy nhất trong tuần qua theo danh mục
Ta có thể lưu các file log thô của server Sales API trên một Object Store được quản lý sẵn như Amazon S3, thay vì tự quản lý một hệ thống file phân tán.
Làm rõ với người phỏng vấn xem bạn được kỳ vọng viết bao nhiêu code.
Giả sử đây là một dòng log mẫu, phân tách bằng tab:
timestamp product_id category_id qty total_price seller_id buyer_id
t1 product1 category1 2 20.00 1 1
t2 product1 category2 2 20.00 2 2
t2 product1 category2 1 10.00 2 3
t3 product2 category1 3 7.00 3 4
t4 product3 category2 7 2.00 4 5
t5 product4 category1 1 5.00 5 6
...Sales Rank Service có thể dùng MapReduce, lấy các file log của server Sales API làm đầu vào và ghi kết quả vào bảng tổng hợp sales_rank trong một SQL Database. Ta nên thảo luận về use case và đánh đổi khi chọn giữa SQL hoặc NoSQL.
Ta sẽ dùng MapReduce nhiều bước:
- Bước 1 - Chuyển dữ liệu thành
(category, product_id), sum(quantity) - Bước 2 - Thực hiện distributed sort
class SalesRanker(MRJob):
def within_past_week(self, timestamp):
"""Return True if timestamp is within past week, False otherwise."""
...
def mapper(self, _ line):
"""Parse each log line, extract and transform relevant lines.
Emit key value pairs of the form:
(category1, product1), 2
(category2, product1), 2
(category2, product1), 1
(category1, product2), 3
(category2, product3), 7
(category1, product4), 1
"""
timestamp, product_id, category_id, quantity, total_price, seller_id, \
buyer_id = line.split('\t')
if self.within_past_week(timestamp):
yield (category_id, product_id), quantity
def reducer(self, key, value):
"""Sum values for each key.
(category1, product1), 2
(category2, product1), 3
(category1, product2), 3
(category2, product3), 7
(category1, product4), 1
"""
yield key, sum(values)
def mapper_sort(self, key, value):
"""Construct key to ensure proper sorting.
Transform key and value to the form:
(category1, 2), product1
(category2, 3), product1
(category1, 3), product2
(category2, 7), product3
(category1, 1), product4
The shuffle/sort step of MapReduce will then do a
distributed sort on the keys, resulting in:
(category1, 1), product4
(category1, 2), product1
(category1, 3), product2
(category2, 3), product1
(category2, 7), product3
"""
category_id, product_id = key
quantity = value
yield (category_id, quantity), product_id
def reducer_identity(self, key, value):
yield key, value
def steps(self):
"""Run the map and reduce steps."""
return [
self.mr(mapper=self.mapper,
reducer=self.reducer),
self.mr(mapper=self.mapper_sort,
reducer=self.reducer_identity),
]Kết quả sẽ là danh sách đã sắp xếp dưới đây, ta có thể chèn vào bảng sales_rank:
(category1, 1), product4
(category1, 2), product1
(category1, 3), product2
(category2, 3), product1
(category2, 7), product3Bảng sales_rank có thể có cấu trúc sau:
id int NOT NULL AUTO_INCREMENT
category_id int NOT NULL
total_sold int NOT NULL
product_id int NOT NULL
PRIMARY KEY(id)
FOREIGN KEY(category_id) REFERENCES Categories(id)
FOREIGN KEY(product_id) REFERENCES Products(id)Ta sẽ tạo index trên id , category_id và product_id để tăng tốc truy vấn (thời gian logarit thay vì quét toàn bộ bảng) và để giữ dữ liệu trong bộ nhớ. Đọc tuần tự 1 MB từ bộ nhớ mất khoảng 250 microsecond, trong khi đọc từ SSD chậm hơn 4 lần và đọc từ đĩa chậm hơn 80 lần.1
Use case: User xem các sản phẩm bán chạy nhất trong tuần qua theo danh mục
- Client gửi request tới Web Server, chạy như một reverse proxy
- Web Server chuyển tiếp request tới server Read API
- Server Read API đọc từ bảng
sales_ranktrong SQL Database
Ta sẽ dùng một REST API công khai:
$ curl https://amazon.com/api/v1/popular?category_id=1234Response:
{
"id": "100",
"category_id": "1234",
"total_sold": "100000",
"product_id": "50",
},
{
"id": "53",
"category_id": "1234",
"total_sold": "90000",
"product_id": "200",
},
{
"id": "75",
"category_id": "1234",
"total_sold": "80000",
"product_id": "3",
},Với giao tiếp nội bộ, ta có thể dùng Remote Procedure Calls.
Bước 4: Mở rộng thiết kế
Xác định và xử lý các nút thắt cổ chai, dựa trên các ràng buộc đã cho.

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/Load Test, 2) Profile để tìm nút thắt cổ chai, 3) xử lý các nút thắt cổ chai trong khi cân nhắc các phương án thay thế và đánh đổi, rồi 4) lặp lại. Xem Thiết kế hệ thống mở rộng tới hàng triệu người dùng trên AWS như một ví dụ về cách mở rộng thiết kế ban đầu theo từng vòng lặp.
Điều quan trọng là thảo luận những nút thắt cổ chai bạn có thể gặp với thiết kế ban đầu và cách xử lý từng cái. Ví dụ, thêm một Load Balancer với nhiều Web Server giải quyết những vấn đề gì? CDN? Master-Slave Replicas? Phương án thay thế và đánh đổi của mỗi lựa chọn là gì?
Ta sẽ thêm 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 load balancer nội bộ không được vẽ ra để tránh rối sơ đồ.
Để tránh lặp lại các phần thảo luận, hãy xem các chủ đề system design sau để biết các ý thảo luận chính, đánh đổi và phương án thay thế:
- DNS
- CDN
- Load balancer
- Horizontal scaling
- Web server (reverse proxy)
- API server (application layer)
- Cache
- Relational database management system (RDBMS)
- SQL write master-slave failover
- Master-slave replication
- Các pattern về tính nhất quán
- Các pattern về tính sẵn sàng
Analytics Database có thể dùng giải pháp data warehouse như Amazon Redshift hoặc Google BigQuery.
Ta có thể chỉ muốn lưu dữ liệu của một khoảng thời gian giới hạn trong database, còn phần còn lại lưu trong data warehouse hoặc trong một Object Store. Một Object Store như Amazon S3 xử lý thoải mái ràng buộc 40 GB nội dung mới mỗi tháng.
Để xử lý trung bình 40.000 read request mỗi giây (cao hơn vào giờ cao điểm), traffic của nội dung phổ biến (và xếp hạng doanh số của chúng) nên do Memory Cache đảm nhận thay vì database. Memory Cache cũng hữu ích để xử lý traffic phân bố không đều và các đợt traffic tăng đột biến. Với lượng đọc lớn như vậy, các SQL Read Replicas có thể không chịu nổi các cache miss. Nhiều khả năng ta cần dùng thêm các pattern mở rộng SQL.
Trung bình 400 lượt ghi mỗi giây (cao hơn vào giờ cao điểm) có thể là quá sức với một 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 khác.
Các pattern mở rộng SQL gồm:
Ta cũng nên cân nhắc chuyển một phần dữ liệu sang NoSQL Database.
Các ý thảo luận thêm
Các chủ đề bổ sung để đào sâu, tùy theo phạm vi bài toán và thời gian còn lại.
NoSQL
Caching
- Cache ở đâu
- Cache cái gì
- Khi nào cập nhật cache
Xử lý bất đồng bộ và 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 REST
- Giao tiếp nội bộ - RPC
- Service discovery
Bảo mật
Xem phần bảo mật.
Các con số về latency
Xem Các con số về latency mà lập trình viên nào cũng nên biết.
Liên tục
- Tiếp tục benchmark và giám sát hệ thống để xử lý các nút thắt cổ chai khi chúng xuất hiện
- Mở rộng là một quá trình lặp