Fluentd OOM — 메모리 버퍼 vs 파일 버퍼

데이터·관측성3분조회

각 노드에서 Fluent Bit 이 로그를 걷어 Kafka 로 보내고, Fluentd 가 그걸 받아 Elasticsearch 에 넣는 파이프라인을 쓰고 있었습니다. 중간에 Kafka 를 둔 건 Elasticsearch 가 잠깐 느려져도 로그를 잃지 않으려는 것이었는데, 정작 그 뒤에 선 Fluentd 가 며칠에 한 번씩 죽었습니다.

메모리 버퍼 무제한 증가

설정을 열어보니 네 가지가 겹쳐 있었습니다.

@type memory        buffer가 메모리에만 있음
total_limit_size    미설정 → 무제한 증가
resources: {}       메모리 제한 없음 → OOM 대상
volumeClaimTemplates 없음 → 재시작하면 버퍼 유실

Elasticsearch 로 나가는 쪽이 잠깐 느려지면 버퍼가 쌓입니다. 상한이 없으니 메모리를 계속 먹고, 결국 OOM 으로 죽습니다. 죽으면 메모리에 있던 버퍼가 통째로 사라집니다. 로그 유실과 파드 재시작이 같이 옵니다.

파일 버퍼 + PVC + 상한

버퍼를 디스크로 내리고 상한을 걸었습니다.

<buffer tag, app_name>
    @type file
    path /mnt/buffer/es/stdout
    flush_mode interval
    flush_interval 10s
    flush_at_shutdown true
    flush_thread_count 4
    chunk_limit_size 8m
    total_limit_size 5GB
    overflow_action drop_oldest_chunk
    retry_type exponential_backoff
    retry_wait 5s
    retry_max_interval 60s
    retry_timeout 24h
</buffer>

total_limit_sizeoverflow_action 이 짝입니다. 상한만 걸고 넘쳤을 때 동작을 안 정하면 기본값으로 예외가 나면서 파이프라인이 멈춥니다. drop_oldest_chunk 는 오래된 것부터 버립니다. 로그가 유실되지만 파이프라인은 살아 있습니다.

flush_at_shutdown: true 는 재시작할 때 버퍼를 비우고 나가게 합니다.

버퍼가 디스크에 있어야 하므로 StatefulSet 에 PVC 를 붙였습니다.

volumeClaimTemplates:
  - metadata:
      name: buffer-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 30Gi
      storageClassName: gp2

메모리 상한이 없으면 파일 버퍼로 바꿔도 다른 경로로 다시 커질 수 있어서, requests·limits 도 같이 박았습니다.

resources:
  requests:
    memory: "512Mi"
    cpu: "100m"
  limits:
    memory: "1Gi"
    cpu: "1000m"

배포하면서 막힌 다섯 곳

StorageClass 이름이 안 맞았습니다. 클러스터에 어떤 게 있는지부터 확인해야 합니다.

kubectl get sc
# gp2 (default), ebs-sc, local-storage

Elasticsearch 플러그인이 이미지에 없었습니다. 기본 Fluentd 이미지에는 fluent-plugin-elasticsearch 가 안 들어 있습니다. 컨테이너가 뜰 때 fluent-gem 으로 설치하고 원래 엔트리포인트를 이어받게 했습니다.

command:
  - /bin/sh
  - -c
  - |
    fluent-gem install fluent-plugin-s3
    fluent-gem install fluent-plugin-rewrite-tag-filter
    fluent-gem install fluent-plugin-elasticsearch
    exec /fluentd/entrypoint.sh

이미지를 새로 빌드하는 게 정석이지만, 시작 때마다 gem 을 받는 방식으로 먼저 넘어갔습니다.

CPU 가 부족해 스케줄이 안 됐습니다. requests 를 넣으면 노드에 그만큼 여유가 있어야 합니다. 넣기 전에는 뜨던 파드가 넣고 나서 Pending 이 됩니다.

PVC 가 Pending 에서 안 넘어갔습니다. StatefulSet 의 volumeClaimTemplates 는 immutable 이라 kubectl apply 로 안 바뀝니다. StatefulSet 을 지우고 다시 만들어야 합니다.

Kustomize patch 가 적용되지 않았습니다. patch 대상의 이름이나 종류가 한 글자라도 다르면 조용히 무시됩니다. 에러가 안 나므로 적용 결과를 직접 확인해야 합니다.

남은 것

drop_oldest_chunk 는 버퍼가 5GB 를 넘으면 로그를 버립니다. 지금은 버린 양을 세지 않습니다. Fluentd 가 노출하는 버퍼 지표를 Prometheus 로 가져오면 상한에 얼마나 근접했는지, 실제로 얼마나 버려졌는지가 보입니다.

gem 을 시작할 때마다 설치하는 것도 임시입니다. 네트워크가 안 되면 파드가 못 뜨고, 시작 시간도 길어집니다. 플러그인이 포함된 이미지를 만들어 두는 게 맞습니다.

  1. 불러오는 중