Why You Split the Index by Date
한국어 원문으로 표시합니다.
한 줄 요약
로그 인덱스를 logs-2026.08.21 처럼 날짜로 나누는 이유는 지우기 위해서다. 문서 하나씩 지우는 것은 느리지만 인덱스 통째로 지우는 것은 파일 삭제라 즉시 끝난다.
왜 이게 필요했나
검색 엔진에서 문서를 지우는 것은 실제로 지우는 것이 아니다. "지워졌음" 표시를 하고, 나중에 세그먼트를 병합할 때 실제로 빠진다. 그래서 오래된 로그 1억 건을 delete_by_query 로 지우면 몇 시간이 걸리고 그동안 클러스터가 느려진다.
인덱스를 날짜로 나눠 두면 "8월 1일치 지우기" 가 DELETE logs-2026.08.01 한 번이다. 디렉터리를 지우는 것과 같은 비용이다.
어떻게 동작하나
인덱스 템플릿이 새로 생기는 인덱스에 설정을 자동으로 입힌다.
PUT _index_template/labhub-logs
{
"index_patterns": ["labhub-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"refresh_interval": "10s"
},
"mappings": {
"dynamic": false,
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"trace_id": { "type": "keyword" },
"pod": { "type": "keyword" }
}
}
}
}
"dynamic": false 가 이 설정의 핵심이다. 이유는 아래에.
ISM(수명주기 관리) 이 지우는 일을 자동화한다.
hot (7일) → warm (23일, 복제본 0, 강제 병합) → delete
매핑 폭발
dynamic 을 켜 두면 새 필드가 들어올 때마다 매핑이 자동으로 생긴다. 그런데 애플리케이션이 로그에 사용자 ID 를 필드 이름으로 넣거나(user_12345: "..."), 오류 객체를 통째로 JSON 으로 넣으면 필드가 수천 개가 된다.
필드가 늘면 클러스터 상태(cluster state) 가 커지고, 그건 모든 노드에 복제되므로 클러스터 전체가 느려진다. 심하면 마스터 노드가 죽는다. 이걸 매핑 폭발(mapping explosion)이라 한다.
막는 방법은 셋이다.
"dynamic": false— 정의하지 않은 필드는 저장은 하되 색인하지 않는다(검색 안 됨, 조회는 됨)"dynamic": "strict"— 정의하지 않은 필드가 오면 거부한다index.mapping.total_fields.limit— 상한을 둔다(기본 1000)
strict 는 안전해 보이지만 위험하다. 수집기가 예상 못 한 필드 하나를 붙이면 문서가 통째로 거부되고 로그가 유실된다. 실제로 Fluent Bit 이 내부 필드 _p 를 붙여 400 오류가 무한 재시도로 돌면서 로그가 통째로 막힌 적이 있다. 그래서 로그 인덱스에는 false 가 낫다.
흔한 착각
샤드를 많이 두면 빠르다는 것. 샤드 하나는 루씬 인덱스 하나이고 각각 파일 핸들과 메모리를 쓴다. 하루 몇 GB 수준의 로그에 샤드 5개는 낭비다. 경험칙은 샤드 하나당 10~50GB, 그보다 작으면 샤드를 줄인다.
text 와 keyword 를 헷갈리는 것. text 는 분석해서 토큰으로 쪼개므로 전문 검색은 되지만 집계와 정렬이 안 된다. keyword 는 통째로 저장해 집계·정렬은 되지만 부분 검색이 안 된다. 레벨·파드명·트레이스ID 는 keyword, 메시지 본문은 text 다.
보관 기간과 비용을 설계에 넣기
로그 인덱스 설계의 절반은 검색 성능이고, 나머지 절반은 언제 무엇을 버릴 것인가다. 뒤의 것을 안 정하면 어느 날 디스크가 차고, 그때는 급하게 지우느라 필요한 것까지 지우게 된다.
단계를 나눈다. 최근 것은 빠른 디스크에, 오래된 것은 느린 저장소에, 더 오래된 것은 객체 스토리지에 두고 마지막에 지운다. 인덱스 수명 주기 관리(ILM)가 이 전이를 자동으로 한다.
| 단계 | 기간(예) | 무엇을 하는가 |
|---|---|---|
| hot | 0~3일 | 쓰기와 검색이 모두 일어난다 |
| warm | 3~14일 | 읽기 전용. 세그먼트를 합치고 복제본을 줄인다 |
| cold | 14~90일 | 느린 노드나 객체 스토리지로 |
| delete | 90일~ | 지운다 |
샤드를 잘게 쪼개는 것이 가장 흔한 실수다. 샤드 하나마다 메모리와 파일 핸들이
붙으므로, 작은 샤드가 수천 개면 클러스터가 느려진다. 샤드 하나에 10~50GB
를 목표로 잡고, 하루치가 그보다 작으면 인덱스를 날짜별이 아니라 주별로 만들거나
데이터 스트림의 롤오버 조건(max_primary_shard_size)으로 맡긴다.
모든 필드를 검색 가능하게 만들지 않는다. 검색은 안 하고 보기만 하는 필드는
index: false 로 두면 색인 비용과 저장 공간이 준다. 반대로 집계만 하는 숫자는
doc_values 만 있으면 된다.
서로 다른 모양의 로그를 한 인덱스에 넣지 않는다. 필드 이름이 겹치면서 타입이 다르면 색인이 거부되고, 그 문서는 조용히 유실된다. 서비스마다 인덱스를 나누고, 공통 필드는 이름 규약을 정해 둔다.
비용은 대개 검색이 아니라 색인에서 나온다. 초당 문서 수가 곧 CPU 다. 디버그 수준 로그를 운영에서 그대로 보내면 그 비용을 매일 낸다. 샘플링을 걸거나, 정상 요청은 추적으로 대신하고 로그는 이상한 경우에만 남기는 쪽으로 옮긴다.
실무에서 진짜 중요한 것
로그에 트레이스 ID 를 넣는 것이 관측성의 절반이다. 그게 있으면 "이 느린 요청이 남긴 로그" 를 한 번에 찾을 수 있다. 없으면 시각과 파드 이름으로 더듬어야 한다.
필드 이름은 처음부터 표준을 따르는 것이 좋다. ECS(Elastic Common Schema)는 @timestamp, log.level, service.name, trace.id 같은 이름을 정해 두었다. 나중에 바꾸려면 옛 인덱스를 전부 재색인해야 한다.