Elasticsearch Bài học 29: Pagination (Phân trang) - From/Size cơ bản và thảm họa Deep Paging
Giả sử danh mục "Chăm sóc da mặt" trên Hasaki có tới 50.000 sản phẩm. Bạn không thể trả về toàn bộ 50.000 cục JSON này trong một API request, vì nó sẽ đánh sập băng thông mạng và làm treo trình duyệt của khách hàng. Phân trang (Pagination) là giải pháp bắt buộc.
Trong MySQL, bạn giải quyết bài toán này cực kỳ dễ dàng bằng mệnh đề LIMIT và OFFSET. Elasticsearch cũng có một cơ chế tương đương mang tên from và size. Tuy nhiên, áp dụng tư duy của cơ sở dữ liệu tập trung (MySQL) vào hệ thống phân tán (Elasticsearch) sẽ dẫn đến một trong những thảm họa hiệu năng kinh điển nhất: Deep Paging.
1. Phân trang cơ bản với From và Size
Cú pháp phân trang mặc định của Elasticsearch rất trực quan. Nó là sự thay thế 1-1 cho OFFSET và LIMIT của SQL.
-
size: Số lượng kết quả trả về trên mỗi trang (tương đươngLIMIT). Mặc định là 10. -
from: Số lượng kết quả bị bỏ qua từ vị trí bắt đầu (tương đươngOFFSET). Mặc định là 0.
Ví dụ: Lấy dữ liệu cho Trang 3 (Mỗi trang 20 sản phẩm)
JSON
GET products/_search
{
"from": 40,
"size": 20,
"query": {
"match": {
"category": "chăm sóc da"
}
}
}
(Công thức tính from quen thuộc của Backend: from = (page_number - 1) * size).
Với những trang đầu tiên (Trang 1, Trang 2, Trang 10), cơ chế này hoạt động hoàn hảo và tốc độ phản hồi chỉ tính bằng vài mili-giây.
2. Thảm họa Deep Paging: Khi phân trang đi quá xa
Bài toán bắt đầu nảy sinh khi khách hàng (hoặc một con bot cào dữ liệu) kiên nhẫn bấm Next liên tục đến Trang thứ 500 (Tức là from = 9980, size = 20).
Hãy nhớ lại Cơ chế Scatter-Gather ở Bài 10. Dữ liệu của Index products không nằm ở một chỗ, mà bị băm ra làm 5 Shard nằm ở 5 Node khác nhau.
Để trả về 20 sản phẩm xếp hạng cao nhất từ vị trí 9.980, hệ thống phải trải qua các bước cực hình sau:
-
Scatter (Phân tán): Coordinating Node nhận request và forward câu lệnh tìm kiếm đến cả 5 Shard.
-
Local Sorting (Sắp xếp cục bộ): Tại mỗi Shard, Elasticsearch không thể chỉ lấy 20 kết quả. Vì nó không biết 20 kết quả ở vị trí 9980 trên Shard của nó xếp thứ mấy trong bảng tổng sắp toàn cục. Bắt buộc, mỗi Shard phải tự tìm và sắp xếp 10.000 Document có điểm cao nhất của riêng nó.
-
Gather (Thu thập): 5 Shard gửi trả 5 danh sách (mỗi danh sách chứa 10.000 IDs và Score) về cho Coordinating Node.
-
Global Sorting (Sắp xếp toàn cục): Coordinating Node bị "dội bom" bởi 50.000 kết quả (5 x 10.000). Nó phải nạp toàn bộ 50.000 phần tử này vào RAM (Heap Memory), tiến hành sắp xếp (Sort) lại từ đầu để chọn ra đúng 20 Document nằm ở vị trí 9980 -> 10000.
-
Vứt bỏ phần thừa: Nó ném đi 49.980 phần tử còn lại, đi fetch dữ liệu JSON gốc của 20 ID kia rồi trả về cho Frontend.
Hậu quả: Càng lùi sâu về các trang cuối (from càng lớn), lượng RAM và CPU mà Coordinating Node phải cấp phát để sắp xếp mảng kết quả ảo càng khổng lồ. Nếu có 10 user cùng bấm vào Trang 500 một lúc, Node điều phối sẽ cạn kiệt Heap Memory và kích hoạt lỗi OOM (Out Of Memory), kéo sập toàn bộ Cluster.
3. Chốt chặn bảo vệ sinh tử: index.max_result_window
Để bảo vệ hệ thống khỏi những đoạn code Backend viết vòng lặp vô hạn hoặc các user rảnh rỗi click quá sâu, Elasticsearch áp đặt một giới hạn cứng:
Tổng của from + size không được vượt quá 10.000.
Nếu API Backend của bạn cố tình gọi from: 9990, size: 20, request sẽ bị Elasticsearch từ chối thẳng thừng với mã lỗi 500 (SearchPhaseExecutionException - Result window is too large).
Cách ứng xử trên giao diện (UI) E-commerce: Các sàn thương mại điện tử lớn (như Tiki, Shopee, và cả Hasaki) đều biết giới hạn này. Thay vì cố gắng thay đổi cấu hình lõi của hệ thống, họ áp dụng thủ thuật UI/UX:
-
Mặc dù danh mục có 50.000 sản phẩm, giao diện phân trang ở dưới cùng thường chỉ cho phép người dùng click tối đa đến Trang 100 (Ví dụ: Trang 100 x 50 item = 5000 kết quả).
-
Sau trang 100, nút "Next" sẽ bị ẩn đi.
-
Lý do: Không có người dùng thực nào lướt quá 5000 sản phẩm mà không mua. Nếu họ lướt xa đến vậy, tức là từ khóa tìm kiếm hoặc bộ lọc của họ quá chung chung. Ta nên ép họ dùng bộ lọc (Filter) thay vì cố lướt phân trang tiếp.
Tuy nhiên, với tư cách là kỹ sư Backend, sẽ có những nghiệp vụ bạn bắt buộc phải lướt qua hàng triệu dòng dữ liệu. Ví dụ: Viết một worker export toàn bộ 2 triệu log soát vé AFC ra file CSV để báo cáo cuối tháng. Bạn không thể dùng UI để né tránh, cũng không thể dùng from/size vì sẽ nổ lỗi max_result_window.
Đó là lúc chúng ta phải triệu hồi các API phân trang hạng nặng chuyên biệt cho xử lý dữ liệu lớn.
All Rights Reserved