Elasticsearch Bị Spam Index: Nguyên Nhân và Cách Xử Lý

Hướng dẫn chi tiết cách phát hiện, debug và xử lý tình trạng Elasticsearch bị spam bởi logging middleware ghi toàn bộ request/response, gây ra cảnh báo DatasourceNoData trên Grafana.

Khau Van Nam 6 min read
Table of Contents

Tóm tắt vấn đề

Grafana bắn cảnh báo DatasourceNoData với alert rule tên Spam Alert, đồng nghĩa Grafana không thể query được dữ liệu từ Elasticsearch/Kibana. Nguyên nhân không phải Elasticsearch bị chết, mà là một hoặc nhiều index bị spam hàng trăm nghìn document mỗi ngày khiến cluster bị quá tải khi query.


Nguyên nhân gốc rễ

1. Logging middleware ghi toàn bộ request/response

Service TCHSYSTEM có một middleware logging ghi lại toàn bộ HTTP request và response vào Elasticsearch. Mỗi document chứa:

  • Full request headers (bao gồm JWT token, cookie, v.v.)
  • Full response body (toàn bộ JSON trả về, có thể 5–10KB mỗi doc)

Ví dụ một document thực tế nặng hàng KB chỉ vì field response chứa toàn bộ object đơn hàng:

{
  "application": "TCHSYSTEM",
  "method": "GET",
  "path": "api/get/699912801d3e2ef0dbc5f4a1",
  "response": "{ toàn bộ object đơn hàng bao gồm coupon, shipper, orderlines... }"
}

2. Python bot polling tần suất cao

Một Python bot (user-agent: python-requests/2.25.1) gọi endpoint api/store/current-process liên tục. Chỉ trong một ngày, endpoint này tạo ra 29,177 document. Endpoint này chỉ trả về {"status": true} nhưng vẫn bị log đầy đủ.

3. Kết quả

Các index tch_system-* phình lên đến 8–13GB mỗi ngày thay vì vài trăm KB như bình thường:

tch_system-2026-02-09   →  13.6 GB  (814k documents)
tch_system-2026-02-08   →   8.6 GB  (482k documents)
tch_system-2026-02-06   →   9.6 GB  (593k documents)

Elasticsearch phải scan lượng data khổng lồ này mỗi khi Grafana query, dẫn đến timeout → Grafana báo DatasourceNoData.


Cách debug từng bước

Lần sau nếu gặp tình huống tương tự, làm theo quy trình sau:

Bước 1: Kiểm tra sức khỏe cluster

curl http://localhost:9200/_cluster/health

Xem field status:

  • green → hoàn toàn bình thường
  • yellow → có shard chưa được assign (thường do single-node), không nguy hiểm
  • red → có shard bị mất, cần xử lý ngay

Nếu status là yellow hoặc green mà Grafana vẫn báo lỗi → vấn đề nằm ở dữ liệu, không phải cluster.

Bước 2: Tìm index bị phình

curl "http://localhost:9200/_cat/indices?v&s=store.size:desc"

Sort theo store.size để tìm index lớn bất thường. So sánh với các index cùng loại ở ngày khác — nếu một ngày nào đó đột nhiên lớn hơn bình thường nhiều lần thì đó là thủ phạm.

Dấu hiệu nhận biết:

  • Index cùng loại nhưng một ngày lớn gấp 10–100x ngày khác
  • Số docs.count quá cao so với bình thường

Bước 3: Xem nội dung bên trong index

curl -s "http://localhost:9200/{index_name}/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{"size": 2}'

Nếu bị lỗi No mapping found for [@timestamp], tức là index không dùng @timestamp chuẩn. Query không cần sort:

curl -s "http://localhost:9200/{index_name}/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{"size": 2}'

Nhìn vào _source để hiểu document chứa gì, từ service nào, field nào nặng.

Bước 4: Tìm endpoint nào spam nhiều nhất

curl -s "http://localhost:9200/{index_name}/_search?pretty" \
  -H "Content-Type: application/json" \
  -d '{
    "size": 0,
    "aggs": {
      "top_paths": {
        "terms": {
          "field": "path.keyword",
          "size": 10
        }
      }
    }
  }'

Output sẽ cho thấy endpoint nào tạo nhiều document nhất. Đây là điểm cần can thiệp trong code.

Bước 5: Kiểm tra mapping của index

curl -s "http://localhost:9200/{index_name}/_mapping?pretty"

Xác nhận field nào đang được index, field nào có thể bỏ qua hoặc truncate.


Cách xử lý

Xử lý tức thời: Xóa các index bị spam

Lưu ý: Thao tác này xóa dữ liệu vĩnh viễn. Chỉ làm nếu chấp nhận mất log lịch sử của các ngày đó.

curl -X DELETE "http://localhost:9200/tch_system-2026-02-04,\
tch_system-2026-02-05,\
tch_system-2026-02-06,\
tch_system-2026-02-07,\
tch_system-2026-02-08,\
tch_system-2026-02-09,\
tch_system-2026-02-10,\
tch_system-2026-02-11,\
tch_system-2026-02-12,\
tch_system-2026-02-13,\
tch_system-2026-02-14,\
tch_system-2026-02-15,\
tch_system-2026-02-20,\
tch_system-2026-02-21"

Sau khi xóa, Elasticsearch sẽ nhẹ hơn đáng kể và Grafana có thể query trở lại bình thường.

Nếu muốn chỉ xóa document spam mà giữ lại document hợp lệ, dùng delete by query:

curl -X POST "http://localhost:9200/tch_system-*/_delete_by_query" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "terms": {
        "path.keyword": [
          "api/store/current-process",
          "api/cancel_reason",
          "api/reason_cancel_shipper",
          "api/alert_reason",
          "api/sorry_reason"
        ]
      }
    }
  }'

Xử lý gốc rễ: Sửa logging middleware

Tìm đến middleware hoặc decorator logging của service TCHSYSTEM. Thêm logic lọc các endpoint có tần suất cao hoặc không cần log chi tiết.

Option 1: Bỏ qua hoàn toàn các endpoint không quan trọng

SKIP_LOG_PATHS = [
    "api/store/current-process",
    "api/cancel_reason",
    "api/reason_cancel_shipper",
    "api/alert_reason",
    "api/sorry_reason",
]

def should_log(path: str) -> bool:
    return path not in SKIP_LOG_PATHS

Option 2: Giữ log nhưng bỏ field nặng

HIGH_VOLUME_PATHS = [
    "api/store/current-process",
    "api/cancel_reason",
]

def build_log_document(request, response, path):
    doc = {
        "application": "TCHSYSTEM",
        "method": request.method,
        "path": path,
        "status_code": response.status_code,
        "response_time_ms": ...,
    }

    # Không lưu response body và headers cho endpoint tần suất cao
    if path not in HIGH_VOLUME_PATHS:
        doc["response"] = response.body
        doc["headers"] = dict(request.headers)

    return doc

Option 3: Truncate response body thay vì xóa hoàn toàn

MAX_RESPONSE_SIZE = 500  # bytes

def truncate_response(response_body: str) -> str:
    if len(response_body) > MAX_RESPONSE_SIZE:
        return response_body[:MAX_RESPONSE_SIZE] + "...[truncated]"
    return response_body

Xử lý dài hạn: Thiết lập Index Lifecycle Management (ILM)

Để tránh index phình to mãi mãi, cấu hình ILM tự động xóa index cũ:

curl -X PUT "http://localhost:9200/_ilm/policy/tch_system_policy" \
  -H "Content-Type: application/json" \
  -d '{
    "policy": {
      "phases": {
        "hot": {
          "actions": {}
        },
        "delete": {
          "min_age": "7d",
          "actions": {
            "delete": {}
          }
        }
      }
    }
  }'

Chỉnh min_age theo nhu cầu giữ log của bạn (7 ngày, 30 ngày, v.v.).


Checklist khi gặp lại vấn đề tương tự

[ ] 1. Kiểm tra cluster health → curl localhost:9200/_cluster/health
[ ] 2. Liệt kê index theo kích thước → _cat/indices?v&s=store.size:desc
[ ] 3. Xem nội dung index nghi ngờ → _search?size=2
[ ] 4. Aggregate theo path để tìm endpoint spam → _search với aggs terms
[ ] 5. Quyết định: xóa index hay delete by query
[ ] 6. Sửa middleware để lọc/truncate log
[ ] 7. Verify Grafana hoạt động lại bình thường
[ ] 8. Cài ILM để tự động dọn dẹp index cũ

Kết luận

Vấn đề không phải Elasticsearch bị hỏng mà là logging quá nhiều thứ không cần thiết. Một middleware ghi đầy đủ request/response kết hợp với một bot polling tần suất cao có thể tạo ra hàng chục GB dữ liệu rác mỗi ngày, làm tê liệt toàn bộ observability stack.

Nguyên tắc quan trọng khi thiết kế logging:

  • Chỉ log những gì thực sự cần để debug
  • Không log response body của các endpoint trả về object lớn trừ khi có lỗi
  • Loại trừ các health check và polling endpoint khỏi log
  • Luôn có ILM hoặc retention policy để tự động dọn dẹp
Message on Telegram