Mục lục
Thiết kế một web crawler
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ế.
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
- Dịch vụ thu thập (crawl) một danh sách url:
- Tạo chỉ mục ngược (reverse index) ánh xạ từ các từ tới những trang chứa từ khóa tìm kiếm
- Tạo tiêu đề (title) và đoạn trích (snippet) cho các trang
- Tiêu đề và đoạn trích là tĩnh, chúng không thay đổi theo truy vấn tìm kiếm
- Người dùng nhập một từ khóa tìm kiếm và thấy danh sách các trang liên quan cùng tiêu đề và đoạn trích mà crawler đã tạo
- Chỉ phác thảo các thành phần tổng quan và tương tác cho trường hợp sử dụng này, không cần đi sâu
- Dịch vụ có tính sẵn sàng cao (high availability)
Ngoài phạm vi
- Phân tích số liệu tìm kiếm (search analytics)
- Kết quả tìm kiếm cá nhân hóa
- Page rank
Ràng buộc và giả định
Nêu các giả định
- Lưu lượng truy cập không phân bố đều
- Một số truy vấn tìm kiếm rất phổ biến, trong khi số khác chỉ được thực hiện một lần
- Chỉ hỗ trợ người dùng ẩn danh
- Tạo kết quả tìm kiếm phải nhanh
- Web crawler không được mắc kẹt trong vòng lặp vô hạn
- Chúng ta sẽ mắc kẹt trong vòng lặp vô hạn nếu đồ thị có chu trình (cycle)
- 1 tỷ liên kết cần crawl
- Các trang cần được crawl định kỳ để đảm bảo độ tươi mới (freshness)
- Tần suất làm mới trung bình khoảng một lần mỗi tuần, thường xuyên hơn với các trang phổ biến
- 4 tỷ liên kết được crawl mỗi tháng
- Kích thước lưu trữ trung bình mỗi trang web: 500 KB
- Để đơn giản, tính các thay đổi giống như trang mới
- 100 tỷ lượt tìm kiếm mỗi tháng
Hãy luyện tập dùng các hệ thống truyền thống hơn - không dùng các hệ thống có sẵn như solr hoặc nutch.
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.
- 2 PB nội dung trang được lưu mỗi tháng
- 500 KB mỗi trang * 4 tỷ liên kết được crawl mỗi tháng
- 72 PB nội dung trang được lưu trong 3 năm
- 1.600 yêu cầu ghi mỗi giây
- 40.000 yêu cầu tìm kiếm mỗi giây
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: Dịch vụ crawl một danh sách url
Giả sử ta có một danh sách ban đầu links_to_crawl được xếp hạng dựa trên mức độ phổ biến chung của trang web. Nếu đây không phải giả định hợp lý, ta có thể khởi tạo (seed) crawler bằng các trang phổ biến có liên kết tới nội dung bên ngoài như Yahoo, DMOZ, v.v.
Chúng ta sẽ dùng bảng crawled_links để lưu các liên kết đã xử lý và chữ ký trang (page signature) của chúng.
Ta có thể lưu links_to_crawl và crawled_links trong một cơ sở dữ liệu NoSQL dạng khóa-giá trị (key-value). Với các liên kết đã xếp hạng trong links_to_crawl, ta có thể dùng Redis với sorted set để duy trì thứ hạng của các liên kết trang. 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.
- Crawler Service xử lý từng liên kết trang bằng cách thực hiện các việc sau trong một vòng lặp:
- Lấy liên kết trang có thứ hạng cao nhất cần crawl
- Kiểm tra
crawled_linkstrong cơ sở dữ liệu NoSQL xem có bản ghi nào có chữ ký trang tương tự không- Nếu đã có trang tương tự, giảm độ ưu tiên của liên kết trang này
- Điều này giúp ta tránh rơi vào chu trình
- Tiếp tục (continue)
- Ngược lại, crawl liên kết đó
- Thêm một tác vụ (job) vào hàng đợi của Reverse Index Service để tạo chỉ mục ngược
- Thêm một tác vụ vào hàng đợi của Document Service để tạo tiêu đề và đoạn trích tĩnh
- Tạo chữ ký trang
- Xóa liên kết khỏi
links_to_crawltrong cơ sở dữ liệu NoSQL - Chèn liên kết trang và chữ ký vào
crawled_linkstrong cơ sở dữ liệu NoSQL
- Nếu đã có trang tương tự, giảm độ ưu tiên của liên kết trang này
- Kiểm tra
- Lấy liên kết trang có thứ hạng cao nhất cần crawl
Hãy hỏi rõ người phỏng vấn bạn cần viết bao nhiêu code.
PagesDataStore là một lớp trừu tượng bên trong Crawler Service, sử dụng cơ sở dữ liệu NoSQL:
class PagesDataStore(object):
def __init__(self, db);
self.db = db
...
def add_link_to_crawl(self, url):
"""Thêm liên kết đã cho vào `links_to_crawl`."""
...
def remove_link_to_crawl(self, url):
"""Xóa liên kết đã cho khỏi `links_to_crawl`."""
...
def reduce_priority_link_to_crawl(self, url)
"""Giảm độ ưu tiên của một liên kết trong `links_to_crawl` để tránh chu trình."""
...
def extract_max_priority_page(self):
"""Trả về liên kết có độ ưu tiên cao nhất trong `links_to_crawl`."""
...
def insert_crawled_link(self, url, signature):
"""Thêm liên kết đã cho vào `crawled_links`."""
...
def crawled_similar(self, signature):
"""Xác định xem ta đã crawl một trang khớp với chữ ký đã cho hay chưa"""
...
Page là một lớp trừu tượng bên trong Crawler Service, đóng gói một trang, nội dung của trang, các url con và chữ ký:
class Page(object):
def __init__(self, url, contents, child_urls, signature):
self.url = url
self.contents = contents
self.child_urls = child_urls
self.signature = signature
Crawler là lớp chính bên trong Crawler Service, được cấu thành từ Page và PagesDataStore.
class Crawler(object):
def __init__(self, data_store, reverse_index_queue, doc_index_queue):
self.data_store = data_store
self.reverse_index_queue = reverse_index_queue
self.doc_index_queue = doc_index_queue
def create_signature(self, page):
"""Tạo chữ ký dựa trên url và nội dung."""
...
def crawl_page(self, page):
for url in page.child_urls:
self.data_store.add_link_to_crawl(url)
page.signature = self.create_signature(page)
self.data_store.remove_link_to_crawl(page.url)
self.data_store.insert_crawled_link(page.url, page.signature)
def crawl(self):
while True:
page = self.data_store.extract_max_priority_page()
if page is None:
break
if self.data_store.crawled_similar(page.signature):
self.data_store.reduce_priority_link_to_crawl(page.url)
else:
self.crawl_page(page)
Mã nguồn đầy đủ của các đoạn trên: web_crawler_snippets.py.
Xử lý trùng lặp
Chúng ta cần cẩn thận để web crawler không mắc kẹt trong vòng lặp vô hạn, điều xảy ra khi đồ thị có chu trình.
Hãy hỏi rõ người phỏng vấn bạn cần viết bao nhiêu code.
Chúng ta sẽ muốn loại bỏ các url trùng lặp:
- Với danh sách nhỏ, ta có thể dùng thứ như
sort | unique - Với 1 tỷ liên kết cần crawl, ta có thể dùng MapReduce để chỉ xuất ra các mục có tần suất bằng 1
class RemoveDuplicateUrls(MRJob):
def mapper(self, _, line):
yield line, 1
def reducer(self, key, values):
total = sum(values)
if total == 1:
yield key, total
Mã nguồn đầy đủ: web_crawler_mapreduce.py.
Phát hiện nội dung trùng lặp phức tạp hơn. Ta có thể tạo chữ ký dựa trên nội dung trang rồi so sánh hai chữ ký để đo độ tương tự. Một số thuật toán có thể dùng là chỉ số Jaccard (Jaccard index) và độ tương tự cosin (cosine similarity).
Xác định khi nào cập nhật kết quả crawl
Các trang cần được crawl định kỳ để đảm bảo độ tươi mới. Kết quả crawl có thể có một trường timestamp cho biết lần cuối trang được crawl. Sau một khoảng thời gian mặc định, chẳng hạn một tuần, mọi trang nên được làm mới. Các trang cập nhật thường xuyên hoặc phổ biến hơn có thể được làm mới với chu kỳ ngắn hơn.
Dù không đi sâu vào phân tích số liệu, ta có thể khai phá dữ liệu (data mining) để xác định thời gian trung bình trước khi một trang cụ thể được cập nhật, và dùng thống kê đó để quyết định tần suất crawl lại trang.
Ta cũng có thể chọn hỗ trợ file Robots.txt, cho phép quản trị viên trang web (webmaster) kiểm soát tần suất crawl.
Trường hợp sử dụng: Người dùng nhập một từ khóa tìm kiếm và thấy danh sách các trang liên quan cùng tiêu đề và đoạn trích
- Client gửi yêu cầu 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ủ Query API
- Máy chủ Query API thực hiện các việc sau:
- Phân tích cú pháp (parse) truy vấn
- 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
- Dùng Reverse Index Service để tìm các tài liệu khớp với truy vấn
- Reverse Index Service xếp hạng các kết quả khớp và trả về những kết quả đứng đầu
- Dùng Document Service để trả về tiêu đề và đoạn trích
- Phân tích cú pháp (parse) truy vấn
Chúng ta sẽ dùng một REST API công khai:
$ curl https://search.com/api/v1/search?query=hello+world
Phản hồi:
{
"title": "foo's title",
"snippet": "foo's snippet",
"link": "https://foo.com",
},
{
"title": "bar's title",
"snippet": "bar's snippet",
"link": "https://bar.com",
},
{
"title": "baz's title",
"snippet": "baz's snippet",
"link": "https://baz.com",
},
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).
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
- 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)
- NoSQL
- Các mẫu nhất quán (Consistency patterns)
- Các mẫu sẵn sàng (Availability patterns)
Một số truy vấn tìm kiếm rất phổ biến, trong khi số khác chỉ được thực hiện một lần. Các truy vấn phổ biến có thể được phục vụ từ một Memory Cache như Redis hoặc Memcached để giảm thời gian phản hồi và tránh làm quá tải Reverse Index Service và Document Service. Memory Cache cũng hữu ích để xử lý lưu lượng phân bố không đều và các đợt tăng đột biến (traffic spike). Đọ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
Dưới đây là một vài tối ưu khác cho Crawling Service:
- Để xử lý kích thước dữ liệu và tải yêu cầu, Reverse Index Service và Document Service nhiều khả năng cần dùng nhiều phân mảnh (sharding) và liên hợp (federation).
- Tra cứu DNS có thể là điểm nghẽn, Crawler Service có thể tự duy trì bộ tra cứu DNS riêng được làm mới định kỳ
- Crawler Service có thể cải thiện hiệu năng và giảm sử dụng bộ nhớ bằng cách giữ nhiều kết nối mở cùng lúc, gọi là connection pooling
- Chuyển sang UDP cũng có thể tăng hiệu năng
- Crawl web tiêu tốn nhiều băng thông, hãy đảm bảo có đủ băng thông để duy trì thông lượng cao
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.
Các mẫu mở rộng SQL
- Bản sao đọc (Read replicas)
- Liên hợp (Federation)
- Phân mảnh (Sharding)
- Phi chuẩn hóa (Denormalization)
- Tinh chỉnh SQL (SQL Tuning)
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