반응형
그니_
삽질탐방기
그니_
  • 분류 전체보기 (24)
    • 개발 (15)
    • ETC (1)
    • 트러블슈팅 & 삽질기록 (7)
    • 성능개선 (1)

인기 글

최근 글

최근 댓글

태그

  • springboot
  • easyrandom
  • chatgpt 정리
  • chatgpt 히스토리 삭제
  • Spring
  • spring docker compose
  • chatgpt 채팅 삭제
  • 마이크로소프트 ai tour
  • timeunit
  • spring docker
  • GPT 플러그인
  • chatgpt 기록 삭제
  • spring log
  • java
  • chatgpt 확장 추천
  • db
  • chatgpt 확장 프로그램
  • Database
  • 네트워크
  • index
hELLO · Designed By 정상우.
그니_

삽질탐방기

트러블슈팅 & 삽질기록

엘라스틱서치 index_closed_exception 트러블슈팅

2026. 3. 29. 20:22
반응형

발단

기존 스프링 앱에서 돌고 있던 스케줄 잡을 Quartz로 이관하는 작업을 맡게 됐습니다. 기존 앱을 내리기로 했고, 해당 잡을 Quartz로 옮기는 게 제 담당이었습니다.

기존 코드를 뜯어보니 특정 예외를 로그도 없이 그냥 삼키고 있는 부분이 있었고, 관련 내용이 팀 내에 공유된 적도 없었습니다.

이관하면서 로깅을 제대로 붙이고 나서야 매일 새벽 특정 시간대에 아래 에러가 간헐적으로 발생하고 있었다는 걸 처음 알게 됐습니다.

org.elasticsearch.ElasticsearchStatusException:
  Elasticsearch exception [type=index_closed_exception, reason=closed]

서비스 영향은 없었고 다음 실행에서 자연스럽게 복구되고 있었지만, 매일 같은 시간대에 반복된다는 게 신경 쓰여서 원인을 파악하기로 했습니다.

 


 

원인 파악

에러 메시지를 보면 항상 특정 인덱스가 closed 상태라고 나왔습니다. 처음엔 단순 타이밍 문제인가 싶었는데, 날짜를 보니 항상 동일한 시점의 인덱스였습니다.

저희는 ILM(Index Lifecycle Management)을 사용하고 있었고, warm 전환 시 인덱스가 일시적으로 close 상태로 전환됩니다. 바로 그 타이밍에 스케줄 잡이 해당 인덱스를 건드리면서 에러가 발생한 것이었습니다.

index_closed_exception 자체는 ILM 외에도 수동 close, 디스크 부족, 노드 복구 과정 등 다양한 원인으로 발생할 수 있습니다. 다만 "매일 같은 시간대에, 항상 특정 일수가 지난 인덱스" 라는 패턴이 명확하다면 ILM을 먼저 의심해보는 게 좋습니다.

여기서 한 가지 더 확인한 게 있는데, 저희 검색은 alias를 통해 이루어지고 있었고 이 alias가 전체 인덱스를 바라보고 있었습니다. 쿼리에 시간 범위 필터가 있더라도 ES는 alias 하위 인덱스 전체에 요청을 브로드캐스트한 뒤 각 샤드에서 필터링하는 방식으로 동작합니다. 즉, 필터 조건과 무관하게 오래된 인덱스도 검색 대상에 포함됩니다.

 

정리하면 이렇습니다.

[ILM] warm 전환 → 인덱스 close 상태 진입
      ↓
[Quartz] alias 검색 → 전체 인덱스 브로드캐스트
      ↓
[충돌] close 상태 인덱스 접근 → index_closed_exception
      ↓ ILM poll interval(기본 10분)마다 반복
[완료] 모든 롤오버 완료 → 이후 정상

매일 같은 시간대에 발생한 이유는 인덱스가 생성된 시간 기준으로 ILM이 트리거되기 때문입니다. 간헐적으로 여러 번 실패하는 경우는 인덱스 사이즈나 세그먼트 수에 따라 close 작업 자체가 오래 걸리는 케이스로, 그 시간 동안 잡이 연속으로 실패하는 것이었습니다. warm 전환 시 force merge가 함께 설정된 경우라면 수 분 이상 걸릴 수도 있습니다. (저희는 이 부분까지는 확인하지 못했습니다.)


대응

ILM 정책 수정도 검토했지만 영향 범위가 넓고, 해당 잡이 서비스에 직접적인 영향을 주는 게 아니었기 때문에 애플리케이션 레벨에서 처리하는 방향을 택했습니다.

index_closed_exception은 일시적인 상태이고 다음 실행에서 자연스럽게 복구되기 때문에, catch 후 INFO 로깅하고 skip하는 방식으로 처리했습니다.

 

처리방식에 대한 코드 처리는 하기와 같습니다.

try {
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
} catch (ElasticsearchStatusException e) {
    if (isIndexClosedException(e)) {
        log.info("[ScheduleJob] ES index temporarily closed (ILM rollover). Skipping. index={}", targetIndex);
        return;
    }
    throw e;
} catch (IOException e) {
    log.error("[ScheduleJob] ES search failed", e);
    throw new RuntimeException(e);
}

private boolean isIndexClosedException(ElasticsearchStatusException e) {
    return e.getMessage() != null
        && e.getMessage().contains("index_closed_exception");
}

 

index_closed_exception은 항상 400 BAD_REQUEST로 오기 때문에 status 조건을 함께 체크하면 오탐 가능성을 줄일 수 있습니다.

주의할 점은 index_closed_exception만 명시적으로 구분해야 한다는 것입니다. 아래처럼 넓게 catch하면 다른 에러도 같이 묻힙니다.

// 모든 exception을 핸들링해버리기에 취지와 맞지 않음.
} catch (Exception e) {
    log.info("에러 무시");
}

// ElasticsearchStatusException에서 index close 케이스만 처리.
} catch (ElasticsearchStatusException e) {
    if (isIndexClosedException(e)) {
        log.info("...");
        return;
    }
    log.error("[ScheduleJob] Unexpected ES error", e);
    throw e;
}

다른 선택지

1. ignore_unavailable 옵션

closed 인덱스를 검색 대상에서 자동으로 제외해줍니다. 다만 데이터 누락이 발생해도 에러가 나지 않기 때문에 집계 정합성이 중요한 경우라면 적합하지 않습니다.

searchRequest.indicesOptions(
    IndicesOptions.fromOptions(true, true, true, false)
);

 

2. 재시도 로직

close 상태가 짧게 끝나는 케이스라면 재시도로도 해결할 수 있습니다.

for (int i = 0; i < 3; i++) {
    try {
        return executeSearch();
    } catch (ElasticsearchStatusException e) {
        if (isIndexClosedException(e) && i < 2) {
            log.info("[ScheduleJob] Retrying... ({}/3)", i + 1);
            Thread.sleep(3000);
            continue;
        }
        throw e;
    }
}

 

3.alias 검색 대상 범위 제한

alias가 전체 인덱스를 물고 있는 구조라면, 실제로 필요한 범위의 인덱스만 검색 대상으로 지정하는 방식으로 개선할 수 있습니다. 충돌 자체를 원천 차단할 수 있지만 인덱스 설계 변경이 수반되므로 영향 범위를 충분히 검토해야 합니다.


마무리

이번 케이스의 핵심은 두 가지였습니다. ILM warm 전환 시 발생하는 일시적인 close 상태, 그리고 alias 검색이 필터 조건과 무관하게 전체 인덱스를 브로드캐스트한다는 점입니다.

그리고 한 가지 더 — 로그 없이 예외를 삼키는 코드는 생각보다 오래 살아남습니다. 이관이나 리팩토링 작업이 이런 히스토리를 수면 위로 올리는 계기가 되기도 합니다.

반응형
저작자표시 (새창열림)

'트러블슈팅 & 삽질기록' 카테고리의 다른 글

Apache HTTPD 버전 업그레이드 중 MPM 이슈와 해결 과정  (5) 2025.07.20
Spring Boot 3.4.3에서 Auto-configuration이 동작하지 않는 문제 해결  (0) 2025.03.09
IntelliJ 특정 버전 JDK 21 사용 시 컴파일 오류  (2) 2025.02.22
[spring] gradle build fail  (0) 2023.10.18
[mac] 문제가 발생했기 때문에 컴퓨터를 종료했습니다. 경고창 뜨는 경우 대처법  (2) 2023.09.07
    '트러블슈팅 & 삽질기록' 카테고리의 다른 글
    • Apache HTTPD 버전 업그레이드 중 MPM 이슈와 해결 과정
    • Spring Boot 3.4.3에서 Auto-configuration이 동작하지 않는 문제 해결
    • IntelliJ 특정 버전 JDK 21 사용 시 컴파일 오류
    • [spring] gradle build fail
    그니_
    그니_
    머리속에서만 존재하는 내용을 글로 정리

    티스토리툴바