LabHub
배우기 러닝패스 코스

Designing a Log Pipeline

Why You Split the Index by Date

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

로그 인덱스를 logs-2026.08.21 처럼 날짜로 나누는 이유는 지우기 위해서다. 문서 하나씩 지우는 것은 느리지만 인덱스 통째로 지우는 것은 파일 삭제라 즉시 끝난다.

Cycle map: 지우기 위해서 · 인덱스 템플릿 · ISM(수명주기 관리) · 클러스터 상태(cluster state)

왜 이게 필요했나

검색 엔진에서 문서를 지우는 것은 실제로 지우는 것이 아니다. "지워졌음" 표시를 하고, 나중에 세그먼트를 병합할 때 실제로 빠진다. 그래서 오래된 로그 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)이라 한다.

막는 방법은 셋이다.

  1. "dynamic": false — 정의하지 않은 필드는 저장은 하되 색인하지 않는다(검색 안 됨, 조회는 됨)
  2. "dynamic": "strict" — 정의하지 않은 필드가 오면 거부한다
  3. index.mapping.total_fields.limit — 상한을 둔다(기본 1000)

strict 는 안전해 보이지만 위험하다. 수집기가 예상 못 한 필드 하나를 붙이면 문서가 통째로 거부되고 로그가 유실된다. 실제로 Fluent Bit 이 내부 필드 _p 를 붙여 400 오류가 무한 재시도로 돌면서 로그가 통째로 막힌 적이 있다. 그래서 로그 인덱스에는 false 가 낫다.

흔한 착각

샤드를 많이 두면 빠르다는 것. 샤드 하나는 루씬 인덱스 하나이고 각각 파일 핸들과 메모리를 쓴다. 하루 몇 GB 수준의 로그에 샤드 5개는 낭비다. 경험칙은 샤드 하나당 10~50GB, 그보다 작으면 샤드를 줄인다.

textkeyword 를 헷갈리는 것. 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 같은 이름을 정해 두었다. 나중에 바꾸려면 옛 인덱스를 전부 재색인해야 한다.