Fluentd OOM — 메모리 버퍼 vs 파일 버퍼
로그 파이프라인 중간에 있는 Fluentd 가 며칠에 한 번씩 죽었습니다. 죽어도 재시작되고 나면 Kafka 에서 다시 구독을 이어 가니 Kibana 로는 그 구간만 비어 있는 정도라, 죽었다는 사실을 파드 재시작 횟수로 뒤늦게 알게 되는 상태였습니다.
파이프라인은 이렇게 생겼습니다. 노드마다 뜬 Fluent Bit 이 컨테이너 로그 파일을 읽어 Kafka 로 넣고, Fluentd 가 Kafka 를 구독해서 파싱한 다음 Elasticsearch 로 넣으면, 사람은 그걸 Kibana 로 봅니다.
중간에 Kafka 를 둔 건 Elasticsearch 가 잠깐 느려지거나 재시작해도 로그를 잃지 않으려는 것이었고, Kafka 에 남아 있으니 뒤가 밀리면 그만큼 늦게 가져가면 됩니다.
메모리 버퍼 무제한 증가
설정을 열어 보니 버퍼가 메모리에만 있고 상한이 없었습니다.
@type memory buffer가 메모리에만 있음
total_limit_size 미설정 → 무제한 증가
resources: {} 메모리 제한 없음 → OOM 대상
volumeClaimTemplates 없음 → 재시작하면 버퍼 유실
Elasticsearch 로 나가는 쪽이 잠깐 느려지면 버퍼가 쌓이고, 상한이 없으니 메모리를 계속 먹다가 결국 OOM 으로 죽습니다. 메모리에만 있는 버퍼라 죽는 순간 쌓여 있던 게 통째로 사라지고 파드 재시작과 로그 유실이 같이 옵니다. 로그를 잃지 않으려고 Kafka 를 중간에 뒀지만 바로 그다음 단계에서 잃고 있었습니다.
파일 버퍼 + 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_size 와 overflow_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"
버퍼가 파일에 쌓이니 뒤가 밀려도 메모리는 안 늘어나고, limits 는 그래도 다른 경로로 새는 걸
막으려고 같이 걸었습니다.
배포하면서 막힌 곳들
먼저 StorageClass 이름이 안 맞아서 PVC 가 안 만들어졌습니다. 클러스터마다 이름이 다르니 매니페스트에 적기 전에 뭐가 있는지부터 확인해야 합니다.
kubectl get sc
# gp2 (default), ebs-sc, local-storage
그다음에 걸린 건 Elasticsearch 플러그인이고, Fluentd 는 Ruby 로 만들어져 있어서 플러그인이 gem
— npm 이나 pip 의 패키지에 해당하는 Ruby 쪽 패키지 — 으로 배포되고, 설치 명령도 fluent-gem
입니다. 기본 Fluentd 이미지에는 fluent-plugin-elasticsearch 가 안 들어 있어서, 컨테이너가 뜰
때 이 명령으로 설치한 다음 원래 엔트리포인트를 이어받게 했습니다.
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
플러그인을 넣어 이미지를 새로 빌드하는 게 정석이지만, 우선 돌아가게 해야 해서 파드가 뜰 때마다 설치하는 쪽으로 뒀습니다.
resources 를 넣고 나서는 CPU 가 모자라 스케줄이 안 됐습니다. requests 를 적으면 노드에 그만큼 여유가 있어야 하니 값이 없을 때는 잘 뜨던 파드가 Pending 이 됩니다.
PVC 도 Pending 에서 안 넘어갔고, StatefulSet 의 volumeClaimTemplates 가 immutable 이라
kubectl apply 로는 안 바뀌고, 지우고 다시 만들어야 반영됩니다.
마지막에 걸린 건 Kustomize patch 였습니다. Kustomize 는 원본 YAML 을 복사해 고치는 대신 바꿀 부분만 따로 적어 두고 배포할 때 합쳐 주는데, patch 대상의 이름이나 종류가 한 글자라도 다르면 에러도 없이 조용히 무시됩니다. 적용됐겠거니 하지 말고 결과를 직접 열어 봐야 합니다.
버린 로그를 세지 않고 있음
drop_oldest_chunk 는 버퍼가 5GB 를 넘으면 로그를 버립니다. 파이프라인을 살리려고 고른
동작이지만 지금은 버린 양을 세지 않으니 유실이 나도 알 방법이 없습니다. Fluentd 가 노출하는 버퍼
지표를 Prometheus 로 가져오면 상한에 얼마나 근접했는지와 실제로 버려진 양이 같이 보입니다.
플러그인을 파드가 뜰 때마다 fluent-gem 으로 받아 오는 것도 임시로 둔 겁니다. 네트워크가 안
되면 파드가 아예 못 뜨고 뜰 때마다 설치 시간이 붙으니, 플러그인을 미리 넣어 둔 이미지를 만들어
쓰는 쪽으로 가야 합니다.
댓글 0