0

Elasticsearch Bài học 40: Tối ưu hóa truy vấn Aggregations cho các Dashboard Report nặng dữ liệu

Chúc mừng bạn đã đi đến bài cuối cùng của Module Phân tích & Thống kê.

Khi bạn làm Dashboard (sử dụng Kibana, Grafana, hoặc tự build bằng React/Vue) cho team Data Analyst, số lượng dữ liệu phải xử lý không dừng lại ở mức vài nghìn sản phẩm. Họ sẽ muốn xem báo cáo trên tập 50 triệu bản ghi log AFC, hay 10 triệu đơn hàng của năm qua.

Chạy Aggregations trên tập dữ liệu "khủng" như vậy sẽ tạo ra độ trễ (latency) tính bằng giây, thậm chí hàng chục giây. CPU của Cluster sẽ liên tục chạy ở mức 100%. Để đưa Dashboard đạt tốc độ tải "chớp nhoáng" (dưới 100ms), một Backend Engineer kiến trúc sư cần áp dụng 4 chiến thuật tối ưu sau đây.

Chiến thuật 1: Kích hoạt Eager Global Ordinals (Tối ưu RAM và Tốc độ chia giỏ)

Khi bạn chạy terms aggregation trên một trường keyword (như brand), Elasticsearch phải gom các Document có chung tên thương hiệu vào một giỏ.

  • Vấn đề: So sánh chuỗi (String comparison) cực kỳ tốn CPU. Để tối ưu, Elasticsearch ngầm gán cho mỗi chữ một số nguyên (gọi là Ordinal). Ví dụ: Cetaphil = 1, L'Oreal = 2. Việc chia giỏ và gom nhóm dựa trên số nguyên 1 và 2 sẽ nhanh hơn gấp ngàn lần so với so sánh chuỗi.

  • Điểm nghẽn: Bảng ánh xạ (Map) từ Chữ sang Số này (gọi là Global Ordinals) mặc định chỉ được xây dựng VÀO LÚC CÓ REQUEST SEARCH BẮN VÀO (Lazy loading). Lần đầu tiên sếp mở Dashboard, request đó sẽ phải cõng thêm thời gian build cái bảng Map khổng lồ này, dẫn đến truy vấn mất 5 giây.

Giải pháp: Hãy ép Elasticsearch build sẵn cái bảng Map này ngay lúc nó ghi dữ liệu mới (Index-time) thay vì chờ đến lúc bị truy vấn. Cấu hình này gọi là eager_global_ordinals.

JSON

PUT products/_mapping
{
  "properties": {
    "brand": {
      "type": "keyword",
      "eager_global_ordinals": true
    }
  }
}

Lưu ý: Chỉ bật tính năng này trên những trường keyword có số lượng giá trị duy nhất (Cardinality) vừa phải và thường xuyên được dùng để vẽ biểu đồ/chia giỏ. Không bật bừa bãi vì nó sẽ làm chậm tốc độ ghi (Write/Indexing) của hệ thống.

Chiến thuật 2: Pre-aggregation với Data Rollup / Transforms (Đánh đổi không gian lấy thời gian)

Đừng cố gắng bắt Elasticsearch tính toán lại từ đầu 50 triệu bản ghi AFC log mỗi lần sếp F5 (Refresh) trang Dashboard. Nếu báo cáo chỉ yêu cầu xem Tổng doanh thu theo từng ngày, tại sao chúng ta phải giữ lại chi tiết của từng giao dịch nhỏ lẻ để tính toán on-the-fly (tính trực tiếp)?

Giải pháp Transform: Elasticsearch cung cấp một tính năng cực mạnh gọi là Transforms (hoặc Rollup). Nó là một Job chạy ngầm ở background, liên tục "cuộn" dữ liệu chi tiết thành dữ liệu tổng hợp và ghi ra một Index mới.

Ví dụ: Bạn cấu hình một Transform Job chạy mỗi giờ 1 lần. Nó lướt qua Index orders_raw (chứa 10 triệu đơn hàng), nhóm theo ngày và tính tổng tiền, sau đó ghi kết quả vào Index orders_daily_summary (chỉ chứa 365 bản ghi cho 365 ngày).

Khi Backend của bạn vẽ Dashboard, thay vì gọi aggs vào Index 10 triệu dòng, bạn gọi thẳng vào orders_daily_summary. Tốc độ sẽ tăng từ 10 giây xuống còn... 5 mili-giây.

Chiến thuật 3: Tận dụng Shard Request Cache

Giống như cách Filter Context cache các dải bit, Elasticsearch cũng có cơ chế cache kết quả của Aggregations ở cấp độ Shard (Shard Request Cache).

Quy luật để hit Cache:

  1. BẮT BUỘC dùng "size": 0: Cache này chỉ lưu kết quả của aggs. Nếu bạn set size: 10, request sẽ không bao giờ được cache.

  2. Tránh cạm bẫy thời gian thực (now): Như đã nhắc ở Bài 27, nếu bạn vẽ biểu đồ doanh thu tháng này và dùng câu query:

    JSON

    "range": { "created_at": { "gte": "now-30d" } }
    
    

    Mỗi mili-giây trôi qua, chữ now lại mang một giá trị khác. Cache sẽ bị miss (trượt) 100%. Cách sửa: Luôn làm tròn thời gian trong Dashboard. Hãy ép nó thành now-30d/d (Làm tròn xuống đầu ngày) hoặc now/h (Làm tròn theo giờ). Nhờ vậy, trong suốt 1 ngày (hoặc 1 giờ) đó, toàn bộ các request vẽ biểu đồ của mọi nhân viên sẽ đánh trúng Cache và không tốn 1 giọt CPU nào.

Chiến thuật 4: Thu hẹp không gian bằng Filter Context (Chia để trị)

Đừng bao giờ để khối aggs chạy trần truồng trên toàn bộ Index nếu không thực sự cần thiết. Nếu Dashboard của bạn có một nút chọn "Quý 3 năm 2026", hãy sử dụng khối query (với filter) để gạt bỏ Quý 1, Quý 2, Quý 4 ra khỏi cuộc chơi trước khi dữ liệu kịp đi vào cỗ máy Aggregations.

JSON

GET orders/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "created_at": {
              "gte": "2026-07-01",
              "lte": "2026-09-30"
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "doanh_thu_theo_tuan": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "1w"
      }
    }
  }
}

Nhờ có phễu lọc range siêu tốc, cỗ máy date_histogram chỉ phải tính toán trên 25% tổng số dữ liệu, giúp tốc độ tăng gấp 4 lần.

Tóm tắt lại, để làm chủ Aggregations cho Dashboard:

  1. Đẩy trước các phép tính nặng ra background (Rollup/Transform).

  2. Tối ưu cấu trúc RAM để so sánh số thay vì so sánh chữ (eager_global_ordinals).

  3. Dùng size: 0 và làm tròn thời gian (now/d) để đánh lừa Cache.

  4. Lọc tối đa dữ liệu rác trước khi đưa vào rổ thống kê.

Chúng ta đã đi qua 40 bài học, bao phủ gần như toàn bộ tính năng của một cụm Elasticsearch. Từ Bài 41 trở đi, chúng ta sẽ bước sang Module quan trọng nhất dành cho các System Admin và DevOps: Phần 7: Quản trị, Vận hành & Tối ưu (Production-Ready). Làm sao để hệ thống không bị "vỡ" khi có ổ cứng bị hỏng, và làm sao để dữ liệu cũ tự động được xóa đi?


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí