본문
[OpenSearch 트러블슈팅] 대용량 단일 인덱스 병목 현상과 시계열 데이터 최적화
운영 중인 OpenSearch 클러스터에서 간헐적인 검색 지연과 쓰기 차단 현상이 발생했습니다.
모니터링 지표와 인덱스 구조를 분석하여 근본적인 원인을 도출하고 아키텍처 레벨의 해결책을 적용하는 과정을 정리합니다.
1. 클러스터 주요 증상 및 지표 분석
CloudWatch 대시보드와 클러스터 상태를 확인한 결과, 전형적인 리소스 병목 현상이 관찰되었습니다.
- JVM 메모리 압력 폭주: Data Node의 JVM 압력이 20%~90% 사이를 오가며 급격한 변동폭을 보였습니다.
- 빈번한 쓰기 차단(Writes Blocked): ClusterIndexWritesBlocked 이벤트가 다수 발생하여 붉은색 스파이크가 대시보드를 덮었습니다.
- 저조한 Query Cache 적중률: 총 206만 건의 쿼리 중 적중률(Hit)은 21.7%에 불과하고, Miss 비율이 78.3%에 달했습니다.
- 세그먼트 병합 지연: 전체 문서 대비 삭제 문서 비율이 11.2%(약 15만 건)로 누적되어 있었습니다.
- 지연 시간 스파이크: 인덱싱 및 검색 지연 시간(Latency) 그래프에서 주기적인 튀어오름 현상이 확인되었습니다.




2. 근본 원인 파악
GET /_cat/shards?v&s=store:desc
index shard prirep state docs store ip node
fnc_news_info 1 p STARTED 100744 2.9gb x.x.x.x 846647e61f7f8bac57381fd236de6d51
fnc_news_info 3 p STARTED 101023 2.8gb x.x.x.x 5bd396f63039ec7240df412239b739df
fnc_news_info 3 r STARTED 101023 2.8gb x.x.x.x 72bdd59d0872e90b499b19d7cac14ca1
fnc_news_info 2 r STARTED 100879 2.8gb x.x.x.x 23688983b8691a17bc89e799e4b6bde2
fnc_news_info 2 p STARTED 100879 2.8gb x.x.x.x 72bdd59d0872e90b499b19d7cac14ca1
fnc_news_info 1 r STARTED 100744 2.8gb x.x.x.x aa402d9a2cdbcd20611ecc2b1b0f4dbe
fnc_news_info 4 p STARTED 100828 2.8gb x.x.x.x 597ccda92161a60e134197899649432d
fnc_news_info 4 r STARTED 100828 2.8gb x.x.x.x bf22f730f038f2482a4b11e1261915dc
fnc_news_info 0 r STARTED 100772 2.8gb x.x.x.x 13a09183f6345c0ef1faddcc4c66cda9
fnc_news_info 0 p STARTED 100772 2.8gb x.x.x.x 834c737c3f5e24dfba589a6868447b53
rsch_repo_info 0 r STARTED 10033 270.8mb x.x.x.x 846647e61f7f8bac57381fd236de6d51
rsch_repo_info 0 p STARTED 10033 270.4mb x.x.x.x aa402d9a2cdbcd20611ecc2b1b0f4dbe
...
가장 큰 원인은 28.8GB에 달하는 거대한 단일 인덱스(fnc_news_info)에 있었습니다.
🤔 의문점: 샤드(Shard)가 여러 개로 나뉘어 있는데도 문제가 될까?
명령어 결과를 확인해 보면 fnc_news_info 데이터는 5개의 프라이머리 샤드(0,1,2,3,4)로 쪼개져 분산 저장되어 있습니다. 하지만 물리적으로 파티셔닝(샤딩)이 되어 있다 하더라도, 논리적으로는 하나의 거대한 단일 인덱스(Index)로 묶여 있기 때문에 본질적인 병목이 발생합니다.
* 인덱스(Index - 논리적 단위): RDBMS의 '테이블(Table)'과 같으며, 애플리케이션이 접근하는 단일 호출 지점입니다.
* 샤드(Shard - 물리적 단위): 하나의 인덱스가 너무 커지는 것을 막기 위해 데이터를 여러 노드에 분산 저장하는 물리적 공간입니다.
뉴스나 리포트 같은 시계열(Time-Series) 데이터는 지속적으로 누적(Append-Only)되는 특성이 있습니다. 이를 거대한 단일 인덱스에서 운영할 경우, 물리적인 샤딩 여부와 관계없이 다음과 같은 악순환이 발생합니다.
- 동시다발적인 캐시 무효화(Invalidation): 5개의 샤드 중 어느 한 곳에라도 새로운 쓰기나 변경이 일어나면, 데이터 정합성을 위해 전체 인덱스에 대한 쿼리 캐시가 무효화됩니다. 이로 인해 캐시 효율이 극도로 떨어집니다.
- 과도한 GC 및 JVM 부하 (세그먼트 병합 오버헤드): 과거 데이터와 최신 데이터가 모든 샤드에 무작위로 섞여 있습니다. 데이터 업데이트나 삭제가 발생하면 5개 샤드 모두에서 산발적으로 백그라운드 세그먼트 병합(Segment Merge)이 끊임없이 발생하여 CPU와 JVM 압력을 높입니다.
- 비효율적인 검색 (Scatter and Gather): 최신 데이터만 검색하려 해도 데이터가 5개 샤드 전체에 흩어져 있어, 항상 모든 샤드를 깨워 검색을 수행해야 하는 비효율이 발생합니다.
3. 핵심 해결 방안
하드웨어 스케일업(r8g.4xlarge → r7g.8xlarge)은 근본적 해결책이 아닌 임시방편이므로, 아키텍처 개선을 우선적으로 진행했습니다.
- 시계열 인덱스 구조 전환 (ISM Rollover 적용): 가장 강력하고 근본적인 해결책입니다. 단일 뉴스 인덱스를 월별, 혹은 용량별(예: 5GB)로 자동 롤오버되도록 변경합니다.
- 쓰기와 읽기 부하 격리: 롤오버를 적용하면 쓰기 작업은 크기가 작은 최신(Hot) 인덱스에만 집중됩니다. 과거 데이터 인덱스는 읽기 전용(Read-Only) 상태가 되어 캐시가 영구적으로 유지되므로 검색 성능이 비약적으로 상승합니다.
- 안전한 Force Merge 스케줄링: 현재 활성화된 인덱스에 Force Merge를 날리는 것은 심각한 I/O 병목을 유발해 클러스터가 죽을 수 있습니다. 롤오버된 과거 인덱스에만 비피크 시간대를 활용해 Force Merge를 실행하여, 삭제 문서를 정리하고 세그먼트를 압축합니다.
4. 마무리 및 인사이트
거대한 단일 인덱스에 읽기와 쓰기를 동시에 던지는 것은 검색엔진의 아키텍처 안티 패턴입니다. 시계열 데이터는 반드시 Data Stream이나 ISM(Index State Management)을 활용해 인덱스 라이프사이클을 관리해야 노드 증설 없이도(FinOps) 안정적인 운영이 가능합니다.
p.s. 뉴스 데이터를 위한 OpenSearch ISM 정책 구성 예시
시계열 데이터의 롤오버와 과거 데이터 최적화(Force Merge)를 자동화하려면 OpenSearch의 ISM 기능을 활용해야 합니다.
아래는 클러스터에 적용할 수 있는 뉴스 데이터 전용 생명주기(Lifecycle) 관리 정책 예시입니다.
1. ISM Policy JSON 정의
PUT _plugins/_ism/policies/news_time_series_policy
{
"policy": {
"description": "뉴스 데이터(fnc_news_info)를 위한 시계열 롤오버 및 비용 최적화 정책",
"default_state": "hot",
"states": [
{
"name": "hot",
"actions": [
{
"rollover": {
"min_size": "5gb",
"min_index_age": "30d"
}
}
],
"transitions": [
{
"state_name": "warm"
}
]
},
{
"name": "warm",
"actions": [
{
"read_only": {}
},
{
"force_merge": {
"max_num_segments": 1
}
}
],
"transitions": [
{
"state_name": "delete",
"conditions": {
"min_index_age": "365d"
}
}
]
},
{
"name": "delete",
"actions": [
{
"delete": {}
}
],
"transitions": []
}
],
"ism_template": [
{
"index_patterns": ["fnc_news_info-*"],
"priority": 100
}
]
}
}
2. 핵심 설정 포인트 해설
위의 정책이 적용되면 거대한 단일 인덱스에서 발생하던 병목이 다음과 같이 해결됩니다.
- 안전한 파티셔닝 (rollover): 인덱스 용량이 5GB에 도달하거나 생성된 지 30일이 지나면, 자동으로 새로운 인덱스(예: fnc_news_info-000002)를 생성하여 쓰기 작업을 넘깁니다. 28GB짜리 거대한 인덱스가 만들어지는 것을 원천 차단합니다.
- 캐시 영구 유지 (read_only): 롤오버가 끝난 과거 인덱스는 더 이상 쓰기가 발생하지 않도록 읽기 전용으로 전환합니다. 이 순간부터 과거 데이터의 쿼리 캐시는 무효화(Invalidation)되지 않고 100%의 효율을 발휘합니다.
- I/O 병목 없는 압축 (force_merge): 쓰기가 완전히 멈춘 warm 상태의 인덱스에만 max_num_segments: 1 조건으로 Force Merge를 수행합니다. 단일 인덱스에서 실행할 때 발생하던 클러스터 뻗음 현상 없이, 삭제된 문서를 깔끔하게 정리하고 검색 성능을 극대화합니다.
- 스토리지 비용 최적화 (delete): 뉴스 데이터의 유효성을 고려해 1년(365일)이 지난 오래된 인덱스는 자동으로 삭제하여 디스크 용량을 확보합니다.
3. 논리적 단일 진입점 유지 (Alias)
애플리케이션(Backend) 단에서는 매번 바뀌는 인덱스 이름(fnc_news_info-000001, 000002...)을 알 필요가 없습니다. 아래와 같이 Alias(별칭)를 생성해 두면, 백엔드 코드는 수정 없이 그대로 fnc_news_info라는 이름으로 호출하되, OpenSearch가 알아서 최신 인덱스에 데이터를 쓰고 전체 인덱스에서 검색을 수행합니다.
POST /_aliases
{
"actions": [
{
"add": {
"index": "fnc_news_info-000001",
"alias": "fnc_news_info",
"is_write_index": true
}
}
]
}
p.s. 시계열 인덱스 구현 방식 비교: Data Stream vs Alias + ISM Rollover
시계열 데이터 구조로 전환할 때 OpenSearch에서 제공하는 두 가지 주요 기법이 있습니다. 데이터의 '수정(Update) 발생 여부'에 따라 적합한 방식을 선택해야 합니다.
| 비교 항목 | Alias + ISM Rollover | Data Stream |
| 동작 방식 | 수동으로 Alias(별칭)를 생성하고 ISM으로 롤오버 제어 | OpenSearch가 백그라운드 인덱스(Backing Indices)를 완전 자동 관리 |
| 수정/삭제 (_id 기준) | 자유롭게 가능 (O) | 원칙적으로 불가 (X, Append-Only 지향) |
| 초기 세팅 | 첫 인덱스 및 별칭(Alias) 부트스트랩 수동 진행 필요 | 템플릿 설정만으로 완전 자동화 (매우 간편) |
| 필수 필드 | 제한 없음 | @timestamp 필드 강제 |
| 적합한 유즈케이스 | 뉴스, 리포트 (발행 후 오타 수정, 내용 업데이트 발생) | 서버 로그, 시스템 메트릭, 주식 호가 데이터 |
* 결론 및 아키텍처 선택 가이드:
만약 다루는 데이터가 서버 로그나 호가 데이터처럼 한 번 쌓이면 절대 변하지 않는다면 설정이 간편한 Data Stream이 유리합니다.
하지만 뉴스 데이터(fnc_news_info)처럼 기사 발행 이후에도 오타 수정, 본문 업데이트, 삭제 등이 발생할 수 있다면 제약이 많은 Data Stream보다는, 개별 문서의 수정/삭제가 자유로운 'Alias + ISM Rollover' 방식을 도입하는 것이 운영상 훨씬 유연하고 안전합니다.
댓글