0

Elasticsearch Bài học 10: Read Flow - Cơ chế Scatter-Gather khi tìm kiếm phân tán trên nhiều Shards

Khi bạn gõ từ khóa "sữa rửa mặt" trên thanh tìm kiếm của một sàn E-commerce, hệ thống mất chưa tới 20 mili-giây để lùng sục qua hàng chục triệu bản ghi và trả về Top 10 sản phẩm liên quan nhất. Ở các bài trước, chúng ta đã biết dữ liệu bị băm nhỏ ra nhiều Node (Bài 7) và mỗi Shard sở hữu một Inverted Index siêu tốc (Bài 8).

Nhưng nếu dữ liệu nằm rải rác ở 5 server khác nhau, làm thế nào Elasticsearch biết sản phẩm nào thực sự xứng đáng nằm ở Top 1 toàn cục? Lời giải nằm ở một cơ chế kinh điển của hệ thống phân tán: Scatter-Gather (Phân tán - Thu thập), hay trong thuật ngữ của Elasticsearch gọi là Query and Fetch.

Quá trình đọc dữ liệu toàn văn bản (Full-text search) diễn ra qua 2 giai đoạn (Phase) tách biệt hoàn toàn.

Giai đoạn 1: Query Phase (Scatter - Phân tán yêu cầu)

Khi request GET /products/_search từ API backend của bạn bay vào Cluster, một Node bất kỳ sẽ đứng ra nhận request. Lúc này nó đóng vai trò là Coordinating Node (Trạm điều phối).

  1. Lập bản đồ định tuyến: Coordinating Node nhìn vào cấu hình Index products và biết rằng Index này có 5 Primary Shards. Nó cũng biết chính xác các Shard này (và Replica của chúng) đang nằm ở các Data Node nào nhờ vào Cluster State.

  2. Scatter (Phóng request): Trạm điều phối sẽ nhân bản câu query của bạn ra làm 5 bản, và phóng (forward) đến 5 Shard. Để tối ưu tải, nó thực hiện cơ chế Round-robin: lúc thì gửi vào Primary Shard, lúc thì gửi vào Replica Shard.

  3. Thực thi cục bộ (Local Execution): Tại mỗi Shard, engine Lucene độc lập sẽ dùng Inverted Index để tìm ra các Document khớp từ khóa. Nó tự tính toán điểm Relevance Score (TF/IDF) cho từng Document trong phạm vi của riêng nó.

  4. Trả kết quả siêu nhẹ: Đây là chìa khóa hiệu năng! Các Shard không trả về toàn bộ file JSON khổng lồ. Chúng chỉ trả về cho Coordinating Node một danh sách cực kỳ gọn nhẹ gồm: [Document_ID, Relevance_Score]. Nếu request của bạn yêu cầu lấy Top 10 (size: 10), mỗi Shard sẽ gửi về đúng 10 cái ID có điểm cao nhất của nó.

Giai đoạn 2: Fetch Phase (Gather - Thu thập và Lấy dữ liệu)

Lúc này, Coordinating Node đang cầm trong tay 5 danh sách từ 5 Shard, tổng cộng là 50 cái ID (5 Shards x 10 kết quả).

  1. Global Sorting (Sắp xếp toàn cục): Coordinating Node sẽ gộp (merge) 50 kết quả này lại, so sánh điểm Score và sắp xếp chúng để lọc ra Top 10 Document xuất sắc nhất trên toàn bộ hệ thống.

  2. Fetch (Đi lấy hàng): Bây giờ trạm điều phối đã biết chính xác 10 cái ID chiến thắng thuộc về những Shard nào. Nó thực hiện vòng gọi API thứ hai (gọi là Multi-GET) trực tiếp tới các Shard đang giữ 10 ID đó.

  3. Trả JSON gốc (_source): Lần này, các Shard sẽ lôi toàn bộ dữ liệu gốc (phần JSON trong block _source) của các ID được yêu cầu và gửi ngược về cho Coordinating Node.

  4. Phản hồi Client: Coordinating Node đóng gói 10 file JSON này thành một HTTP Response hoàn chỉnh và trả về cho API Backend của bạn.

Phân tích thiết kế: Tại sao phải tách làm 2 giai đoạn?

Bạn có thể thắc mắc: Tại sao ở Giai đoạn 1, các Shard không trả luôn dữ liệu JSON (Fetch) cho nhanh, đỡ mất công Coordinating Node phải gọi vòng 2?

Hãy làm một phép toán mạng lưới (Network Bandwidth):

  • Nếu mỗi Shard trả luôn 10 file JSON (_source), Coordinating Node sẽ nhận về tổng cộng 50 file JSON. Nó tải hết 50 file này qua mạng bộ nhớ, lọc ra 10 file để trả cho Client, và vứt bỏ 40 file JSON còn lại.

  • Lượng băng thông mạng (Network I/O) và RAM bị lãng phí là 80%.

  • Bằng cách chỉ gửi [ID, Score] ở vòng 1, hệ thống tiết kiệm được một lượng băng thông khổng lồ, giúp Coordinating Node không bị tràn RAM (Out of Memory) khi xử lý hàng nghìn request cùng lúc.

Cạm bẫy chí mạng: Thảm họa Deep Paging

Hiểu được cơ chế Scatter-Gather, bạn sẽ nhận ra một lỗ hổng chết người mà 90% các Backend Developer mới làm quen Elasticsearch đều mắc phải.

Giả sử khách hàng bấm chuyển sang Trang thứ 1.000 (tức là yêu cầu bỏ qua 9.990 kết quả đầu tiên, lấy 10 kết quả tiếp theo: from: 9990, size: 10).

  1. Scatter: Coordinating Node yêu cầu 5 Shard trả kết quả. Nhưng để tìm ra top từ 9.990 đến 10.000, mỗi Shard phải tự truy xuất 10.000 Document có điểm cao nhất của riêng nó gửi về.

  2. Gather: 5 Shard x 10.000 = 50.000 Document IDs và Score bay ập vào Coordinating Node.

  3. Node điều phối phải nạp 50.000 phần tử này vào RAM, sắp xếp chúng lại để chọn ra... đúng 10 cái ID (từ 9.990 đến 10.000) và ném đi 49.990 cái còn lại.

Hành động này (Deep Paging) ngốn CPU và RAM đến mức Elasticsearch mặc định đưa ra một chốt chặn cứng: Cấm phân trang vượt quá 10.000 kết quả (index.max_result_window). Nếu bạn cố tình vượt qua, query sẽ nổ lỗi SearchPhaseExecutionException.

Để giải quyết bài toán lướt dữ liệu khổng lồ (như export file CSV vài triệu dòng), chúng ta sẽ phải dùng đến Search After hoặc Scroll API (Chi tiết sẽ được mổ xẻ ở Bài 30).

Khép lại Bài 10, chúng ta đã hoàn thành trọn vẹn Module 2: Kiến trúc lõi của Elasticsearch. Bạn đã nắm trong tay luồng ghi (Write Flow) và luồng đọc (Read Flow) ở tầng hệ thống phân tán.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.