Elasticsearch Bài học 14: Mget & Multi-search (msearch) - Giảm tải Network Round-trip khi truy vấn
ở Bài 13 chúng ta đã thấy Bulk API giải quyết xuất sắc bài toán thắt cổ chai mạng (Network Round-trip) khi GHI dữ liệu. Vậy ở chiều ĐỌC thì sao?
Trong kiến trúc Microservices, hệ thống của bạn thường xuyên đối mặt với bài toán N+1 API Calls. Ví dụ: Service Giỏ hàng của Hasaki trả về một mảng gồm 50 cái Product IDs. Nếu bạn dùng một vòng lặp for để gọi 50 lệnh GET /products/_doc/{id} lên Elasticsearch, bạn sẽ phải chịu 50 lần độ trễ mạng.
Để giải quyết triệt để vấn đề này, Elasticsearch cung cấp 2 công cụ tối ưu cho luồng đọc: Multi-get (_mget) và Multi-search (_msearch).
1. Multi-Get (_mget): Lấy nhiều Document bằng ID
Nếu bạn đã biết trước chính xác danh sách các ID cần lấy, tuyệt đối không dùng vòng lặp, cũng không nên dùng câu lệnh tìm kiếm kiểu Terms query (vì search tốn tài nguyên tính toán hơn). Hãy dùng _mget.
Cú pháp:
JSON
POST products/_mget
{
"docs": [
{ "_id": "1" },
{ "_id": "2" },
{ "_id": "999" }
]
}
(Bạn cũng có thể chỉ định lấy các field cụ thể thay vì kéo toàn bộ _source bằng cách thêm tham số "_source": ["name", "price"] vào từng object).
Cơ chế hoạt động dưới nền: Thay vì bạn phải tự phân loại ID, khi request này đến Coordinating Node, nó sẽ đọc mảng ID, dùng hàm Hash định tuyến (Routing) để gom nhóm các ID lại.
-
ID
1và999nằm ở Shard 0 (Node A) -> Đóng gói thành 1 request mạng gửi cho Node A. -
ID
2nằm ở Shard 1 (Node B) -> Đóng gói gửi cho Node B. Như vậy, với 50 ID, Elasticsearch có thể chỉ tốn đúng 2-3 lượt kết nối nội bộ để gom toàn bộ dữ liệu, trả về cho backend của bạn trong một response duy nhất.
Lưu ý xử lý lỗi: Giống như Bulk API,
_mgettrả về200 OKngay cả khi một số ID không tồn tại. Trong mảngdocstrả về, bạn phải check cờ"found": true/falsecủa từng phần tử để biết kết quả thực sự.
2. Multi-Search (_msearch): Gộp nhiều câu Search phức tạp
Khác với mget (chỉ lấy bằng ID), _msearch dùng để gộp nhiều lệnh tìm kiếm toàn văn bản (Full-text search), Aggregations (Thống kê) hoàn toàn khác nhau vào một request.
Kịch bản thực tế: Khi người dùng mở trang chủ E-commerce, Frontend thường yêu cầu 3 khối dữ liệu hiển thị cùng lúc:
-
Top 5 sản phẩm bán chạy nhất.
-
Top 5 sản phẩm mới về.
-
Số liệu thống kê bộ lọc danh mục.
Thay vì gọi 3 request API tách biệt, API Backend có thể gom 3 câu Query DSL này bắn lên Elasticsearch qua _msearch.
Cú pháp NDJSON (Kế thừa từ Bulk API): _msearch cũng sử dụng định dạng Newline Delimited JSON, yêu cầu ghép cặp dòng:
-
Dòng 1: Header (Chỉ định Index nào? Nếu không ghi sẽ lấy index mặc định trên URL).
-
Dòng 2: Body (Câu query Query DSL).
JSON
POST _msearch
{"index": "products"}
{"query": {"match_all": {}}, "size": 5}
{"index": "products"}
{"query": {"match": {"category": "Sữa rửa mặt"}}, "size": 5}
{"index": "afc_logs"}
{"query": {"match": {"status": "error"}}, "size": 10}
(Đừng quên dấu xuống dòng \n ở cuối request).
Sức mạnh của _msearch: Khi Coordinating Node nhận được file NDJSON này, nó sẽ tách các câu search ra và quăng chúng vào Thread Pool (Tập hợp luồng) để thực thi song song (parallel) trên các Node. Tổng thời gian hoàn thành (Latency) của request này thường chỉ bằng thời gian của câu search chậm nhất trong lô đó, chứ không phải tổng thời gian của cả 3 cộng lại.
3. Cạm bẫy thiết kế: Đừng biến _msearch thành thảm họa DOS
Vì _msearch chạy song song, nó tiêu thụ tài nguyên của Cluster cực kỳ khủng khiếp (đặc biệt là các Thread thuộc nhóm search).
Nếu bạn gom 500 câu lệnh search phức tạp vào 1 request _msearch, Coordinating Node sẽ bắn ra hàng ngàn truy vấn con (do cơ chế Scatter-Gather tới các Shard). Điều này sẽ làm cạn kiệt toàn bộ Search Thread Pool, khiến các request của những người dùng khác bị đưa vào hàng đợi (Queue) hoặc bị từ chối thẳng thừng với lỗi es_rejected_execution_exception.
Best Practices:
-
Giới hạn số lượng: Chỉ nên gộp khoảng 3 đến 5 câu query liên quan mật thiết với nhau (như load trang chủ hoặc dashboard đồ thị). Không bao giờ dùng
_msearchđể thay thế cho một vòng lặp hàng nghìn phần tử. -
Cẩn thận với lỗi một phần: Một request
_msearchtrả về mảng"responses". Nếu câu lệnh số 2 bị sai cú pháp, câu 1 và câu 3 vẫn trả về dữ liệu bình thường, câu số 2 sẽ trả về object"error". Code backend của bạn phải dùng vòng lặp check từng phần tử thay vì mặc định mọi thứ đều màu hồng.
Kết thúc Bài 14, bạn đã trang bị đủ vũ khí cho cả hai chiều Đọc (mget, msearch) và Ghi (bulk) với hiệu năng tối đa.
Ở bài tiếp theo, chúng ta sẽ kết thúc Module 3 bằng việc giải quyết một bài toán hóc búa nhất của mọi hệ thống phân tán: Làm sao để xử lý những trường hợp xung đột dữ liệu khó nhằn nhất, nơi mà ngay cả Optimistic Concurrency Control (OCC) ở Bài 11 cũng cần được nâng cấp.
All rights reserved