Elasticsearch 대용량 데이터 집계 쿼리 최적화

문제 상황

사용자 행동 로그를 Elasticsearch에 저장하고 있는데, 최근 3개월치 데이터(약 5억 건)에서 일별/카테고리별 집계를 뽑는 쿼리가 30초 이상 걸리며 종종 타임아웃되었다.

기존에는 단순 terms aggregation을 중첩해서 사용했다.

{
  "aggs": {
    "by_date": {
      "terms": {
        "field": "date",
        "size": 90
      },
      "aggs": {
        "by_category": {
          "terms": {
            "field": "category",
            "size": 100
          }
        }
      }
    }
  }
}

원인 분석

  1. terms aggregation은 모든 샤드에서 결과를 수집 후 정렬하므로 메모리 부담이 크다
  2. date 필드가 keyword 타입이라 불필요한 오버헤드 발생
  3. 시간 범위 필터 없이 전체 데이터 스캔

해결 방법

1. Date Histogram 사용

날짜별 집계는 date_histogram을 사용하도록 변경했다.

{
  "query": {
    "range": {
      "timestamp": {
        "gte": "now-90d/d"
      }
    }
  },
  "aggs": {
    "by_date": {
      "date_histogram": {
        "field": "timestamp",
        "calendar_interval": "day"
      },
      "aggs": {
        "by_category": {
          "terms": {
            "field": "category"
          }
        }
      }
    }
  }
}

2. 인덱스 매핑 수정

날짜 필드를 date 타입으로 변경하고, 자주 집계하는 필드에 eager_global_ordinals 설정을 추가했다.

{
  "mappings": {
    "properties": {
      "timestamp": {
        "type": "date"
      },
      "category": {
        "type": "keyword",
        "eager_global_ordinals": true
      }
    }
  }
}

3. Composite Aggregation 활용

페이지네이션이 필요한 경우 composite aggregation을 사용했다.

{
  "aggs": {
    "my_buckets": {
      "composite": {
        "size": 100,
        "sources": [
          {"date": {"date_histogram": {"field": "timestamp", "calendar_interval": "day"}}},
          {"category": {"terms": {"field": "category"}}}
        ]
      }
    }
  }
}

결과

  • 쿼리 응답 시간: 30초 → 2.8초
  • 타임아웃 에러 제로
  • 힙 메모리 사용량 40% 감소

인덱스 리인덱싱이 필요해서 배포는 주말 새벽에 진행했다. 앞으로는 집계가 빈번한 필드는 처음부터 타입과 설정을 신경 써야겠다.