Kubernetes Log Agent Slab 이슈에 대해서 알아보자
쿠버네티스 환경에서 로그 수집 에이전트를 운영하다 보면 예상치 못한 메모리 이슈를 겪을 수 있습니다. 이 글에서는 Alloy를 사용하여 /var/log/pods를 watch하는 과정에서 발생한 slab memory 이슈와 그 해결 과정을 살펴보겠습니다.
문제 상황
운영 환경 설정
운영 환경에서는 Alloy를 사용하여 Kubernetes 컨테이너 로그를 수집하고 있었습니다. Alloy는 /var/log/pods 디렉토리를 watch하여 컨테이너 런타임이 생성하는 모든 로그 파일을 모니터링하는 방식이었습니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Log Collection Setup │
3│ │
4│ ┌──────────────┐ │
5│ │ │ │
6│ │ Alloy │───▶ Watch /var/log/pods/**/*.log │
7│ │ │ (Deep directory structure) │
8│ └──────────────┘ │
9│ │
10│ /var/log/pods/ │
11│ └── default_mypod_1234567890abcdef/ │
12│ └── mycontainer/ │
13│ ├── 0.log │
14│ ├── 1.log │
15│ └── ... │
16└─────────────────────────────────────────────────────────────┘
문제 증상
운영 중 Node Exporter에서 수집하는 파드 메모리 메트릭이 지속적으로 우상향하는 그래프를 그리고 있었습니다.
1Pod Memory Usage (Node Exporter)
2│ ╱
3│ ╱
4│ ╱
5│ ╱
6│ ╱
7│ ╱
8│╱
9└──────────────────────────────▶ Time
하지만 파드 내부의 RSS (Resident Set Size) 메모리는 증가하지 않고 있었습니다. 이는 매우 이상한 현상이었습니다.
- Node Exporter 메트릭: 계속 증가
- 파드 내부 RSS: 변화 없음
- 실제 프로세스 메모리: 정상
Slab Memory란?
Slab Allocation의 개념
Slab Allocation은 리눅스 커널에서 사용하는 메모리 관리 기법입니다. 커널 객체(예: inode, dentry, 파일 디스크립터 등)를 빠르게 할당하고 해제하기 위해 미리 할당된 메모리 풀을 사용합니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Slab Allocator │
3│ │
4│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
5│ │ │ │ │ │ │ │
6│ │ Slab 1 │ │ Slab 2 │ │ Slab 3 │ │
7│ │ │ │ │ │ │ │
8│ │[obj][obj]│ │[obj][obj]│ │[obj][obj]│ │
9│ │[obj][obj]│ │[obj][obj]│ │[obj][obj]│ │
10│ └──────────┘ └──────────┘ └──────────┘ │
11│ │
12│ - Dentry Cache │
13│ - Inode Cache │
14│ - File System Cache │
15└─────────────────────────────────────────────────────────────┘
Dentry Cache
**Dentry (Directory Entry)**는 리눅스 VFS (Virtual File System)에서 디렉토리 항목을 나타내는 커널 객체입니다. 파일 시스템 성능을 향상시키기 위해 커널은 자주 접근하는 디렉토리 항목을 메모리에 캐시합니다. 이 캐시를 dentry cache라고 부릅니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Dentry Cache │
3│ │
4│ /var/log/pods/ │
5│ ├── default_pod1_abc123/ │
6│ │ ├── container1/ │
7│ │ │ ├── 0.log │
8│ │ │ └── 1.log │
9│ │ └── container2/ │
10│ │ └── 0.log │
11│ └── default_pod2_def456/ │
12│ └── container1/ │
13│ └── 0.log │
14│ │
15│ 각 경로 구성 요소마다 dentry 객체 생성 │
16│ → 깊은 디렉토리 구조 = 많은 dentry 객체 │
17└─────────────────────────────────────────────────────────────┘
Dentry cache는 파일 경로 해석(path resolution)을 빠르게 하기 위한 캐시입니다. 많은 파일을 watch하거나 깊은 디렉토리 구조를 탐색할 때 이 캐시가 급증할 수 있으며, 이는 slab memory 사용량 증가로 이어집니다.
cgroup과 Slab Memory의 관계
cgroup v2의 메모리 회계
cgroup v2에서는 커널 메모리도 cgroup에 할당됩니다. 이는 사용자 공간 메모리뿐만 아니라 **커널이 사용하는 메모리(slab memory 포함)**도 해당 cgroup에 속하게 됩니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Cgroup Memory Accounting │
3│ │
4│ Pod Cgroup │
5│ ├── User Space Memory (RSS) │
6│ │ └── Process memory │
7│ ├── Kernel Memory (Slab) │
8│ │ ├── Dentry cache │
9│ │ ├── Inode cache │
10│ │ └── File system cache │
11│ └── Page cache │
12│ │
13│ Node Exporter는 cgroup 전체 메모리를 측정 │
14│ → RSS + Slab Memory = Total Memory │
15└─────────────────────────────────────────────────────────────┘
왜 커널에서 관리하지 않을까?
일반적으로 slab memory는 커널에서 관리한다고 생각하지만, cgroup v2에서는 프로세스가 속한 cgroup에 slab memory가 할당됩니다. 이는 다음과 같은 이유 때문입니다:
- 리소스 격리: 각 파드가 사용하는 커널 리소스를 정확히 추적
- 메모리 제한: cgroup memory limit에 slab memory도 포함
- 공정한 할당: 어떤 파드가 커널 리소스를 많이 사용하는지 파악
왜 RSS는 증가하지 않는데 메트릭은 증가하는가?
메모리 측정 방식의 차이
1┌─────────────────────────────────────────────────────────────┐
2│ Memory Measurement Comparison │
3│ │
4│ RSS (Resident Set Size) │
5│ ├── 프로세스가 실제로 사용하는 물리 메모리 │
6│ ├── 사용자 공간 메모리만 포함 │
7│ └── /proc/<pid>/status의 RssAnon, RssFile │
8│ │
9│ Cgroup Memory (Node Exporter) │
10│ ├── cgroup 전체 메모리 사용량 │
11│ ├── RSS + Kernel Memory (Slab) │
12│ └── /sys/fs/cgroup/memory.stat │
13│ │
14│ 차이점: │
15│ - RSS: 사용자 공간만 │
16│ - Cgroup: 사용자 공간 + 커널 공간 (slab 포함) │
17└─────────────────────────────────────────────────────────────┘
실제 상황
1# 파드 내부에서 확인
2$ cat /proc/self/status | grep Rss
3RssAnon: 10240 kB # 사용자 공간 메모리
4RssFile: 2048 kB # 파일 매핑 메모리
5RssShmem: 0 kB
6
7# Node Exporter가 측정하는 메모리
8$ cat /sys/fs/cgroup/memory.stat
9cache 0
10rss 12288
11slab 524288 # ← 이 부분이 계속 증가!
RSS는 증가하지 않지만, slab memory가 계속 증가하여 Node Exporter 메트릭이 우상향하는 것입니다.
Alloy의 파일 Watch와 Dentry Cache 급증
/var/log/pods의 깊은 디렉토리 구조
Kubernetes는 컨테이너 로그를 다음과 같은 구조로 저장합니다:
1/var/log/pods/
2└── default_mypod_1234567890abcdef/ # 네임스페이스_파드명_UID
3 └── mycontainer/ # 컨테이너명
4 ├── 0.log # 재시작 횟수별 로그
5 ├── 1.log
6 └── 2.log
이 구조는 3단계 깊이를 가지며, 각 경로 구성 요소마다 dentry 객체가 생성됩니다.
파일 Watch와 Dentry 생성
Alloy가 /var/log/pods/**/*.log를 watch할 때:
- 디렉토리 탐색: 모든 하위 디렉토리를 재귀적으로 탐색
- Dentry 생성: 각 경로 구성 요소마다 dentry 객체 생성
- inotify/fanotify: 파일 변경 감지를 위한 커널 구조체 생성
- Dentry cache 증가: 빠른 경로 해석을 위한 캐시 항목 증가
1┌─────────────────────────────────────────────────────────────┐
2│ Dentry Cache Growth │
3│ │
4│ Pod 수: 100개 │
5│ Container 수: 200개 │
6│ Log 파일 수: 600개 (재시작 포함) │
7│ │
8│ Dentry 객체 수: │
9│ - /var/log/pods: 1 │
10│ - Pod 디렉토리: 100 │
11│ - Container 디렉토리: 200 │
12│ - Log 파일: 600 │
13│ - Total: ~900개 │
14│ │
15│ 각 dentry 객체는 slab memory에 저장 │
16│ → Slab memory 급증! │
17└─────────────────────────────────────────────────────────────┘
컨테이너 런타임의 로그 파일 생성
컨테이너 런타임(containerd, Docker 등)은 컨테이너가 재시작될 때마다 새로운 로그 파일을 생성합니다. 이로 인해:
- 새로운 디렉토리 항목 생성
- 새로운 dentry 객체 생성
- Dentry cache 증가
- Slab memory 증가
이로 인해 야기되는 문제들
1. 메모리 Limit 초과로 인한 메트릭 오류
1┌─────────────────────────────────────────────────────────────┐
2│ Memory Limit Exceeded in Metrics │
3│ │
4│ Pod Memory Limit: 512Mi │
5│ ├── RSS: 100Mi (정상) │
6│ ├── Slab: 450Mi (급증!) │
7│ └── Total (cgroup): 550Mi → Limit 초과! │
8│ │
9│ 실제 상황: │
10│ - RSS는 여유있지만 cgroup 메모리 사용량이 limit 초과 │
11│ - Node Exporter 메트릭에서 limit 초과로 표시 │
12│ - 실제 프로세스 메모리는 정상이지만 메트릭만 비정상 │
13│ - OOM kill은 발생하지 않음 (RSS가 낮기 때문) │
14└─────────────────────────────────────────────────────────────┘
문제점:
- RSS는 여유가 있지만 cgroup memory 사용량이 limit을 초과
- Node Exporter 등 메트릭 수집 도구에서 limit 초과로 표시
- 실제 프로세스 메모리는 정상이지만 모니터링 시스템에서 비정상으로 감지
- OOM kill은 발생하지 않지만, 메트릭 기반 알람이나 대시보드에서 문제로 표시됨
2. 커널 메모리 압박 및 시스템 전체 영향
Slab memory는 커널 메모리를 사용하므로, 단순히 파드의 메트릭 문제를 넘어서 시스템 전체에 악영향을 줄 수 있습니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Kernel Memory Pressure │
3│ │
4│ System Memory: 32Gi │
5│ ├── User Space: 24Gi │
6│ ├── Kernel Space: 8Gi │
7│ │ ├── Slab (Reclaimable): 2Gi │
8│ │ │ └── Dentry cache: 1.5Gi (급증!) │
9│ │ └── Slab (Unreclaimable): 1Gi │
10│ └── Available: 6Gi │
11│ │
12│ 문제점: │
13│ - 커널 메모리 풀 고갈 │
14│ - 다른 프로세스의 커널 객체 할당 실패 가능성 │
15│ - 메모리 회수(reclaim) 오버헤드 증가 │
16│ - 시스템 전체 성능 저하 │
17└─────────────────────────────────────────────────────────────┘
커널 메모리 회수(Reclaim) 문제
리눅스 커널은 메모리 압박 상황에서 slab cache를 회수하려고 시도하지만, dentry cache 같은 reclaimable slab도 즉시 회수되지 않을 수 있습니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Memory Reclamation Process │
3│ │
4│ 1. Page Cache 회수 │
5│ └── Clean pages 즉시 삭제 │
6│ │
7│ 2. Slab Cache 회수 │
8│ ├── Reclaimable: dentry, inode cache │
9│ │ └── vfs_cache_pressure에 따라 회수 속도 결정 │
10│ └── Unreclaimable: 활성 커널 객체 │
11│ └── 회수 불가능 │
12│ │
13│ 3. Swap (필요시) │
14│ └── Anonymous pages를 디스크로 스왑 │
15└─────────────────────────────────────────────────────────────┘
문제점:
- 커널 메모리 풀 고갈: Slab memory가 커널 메모리를 과도하게 사용하면 다른 커널 객체 할당이 실패할 수 있습니다.
- 메모리 회수 오버헤드:
vfs_cache_pressure가 기본값(100)일 때도 dentry cache 회수는 시간이 걸리며, 이 과정에서 CPU 오버헤드가 발생합니다. - 시스템 전체 성능 저하: 커널 메모리 압박은 파일 시스템 성능, 네트워크 성능 등 전반적인 시스템 성능에 영향을 줍니다.
- 다른 프로세스 영향: 같은 노드의 다른 파드나 시스템 프로세스도 커널 메모리 풀을 공유하므로, 한 파드의 slab memory 증가가 다른 프로세스에 영향을 줄 수 있습니다.
vfs_cache_pressure와의 관계
vfs_cache_pressure는 커널이 dentry와 inode cache를 회수하는 속도를 제어합니다.
1# 현재 설정 확인
2$ cat /proc/sys/vm/vfs_cache_pressure
3100 # 기본값
4
5# 값의 의미:
6# - 0: dentry/inode cache를 거의 회수하지 않음 (OOM 위험)
7# - 100: 다른 cache와 공정한 비율로 회수 (기본값)
8# - 200+: dentry/inode cache를 더 적극적으로 회수
주의사항:
vfs_cache_pressure를 0으로 설정하면 dentry cache가 회수되지 않아 OOM 상황이 발생할 수 있습니다.- 하지만 값을 높여도 dentry cache가 즉시 회수되는 것은 아니며, 회수 과정 자체가 오버헤드를 발생시킵니다.
- 근본적인 해결책은 dentry cache 자체를 줄이는 것입니다.
3. 스케줄링 문제
Kubernetes 스케줄러는 Node Exporter 메트릭을 기반으로 노드의 메모리 사용량을 판단합니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Scheduling Issue │
3│ │
4│ Node Memory: 8Gi │
5│ ├── Allocated: 6Gi (Node Exporter 기준) │
6│ │ ├── 실제 RSS: 4Gi │
7│ │ └── Slab: 2Gi (과다 측정) │
8│ └── Available: 2Gi (실제로는 4Gi 여유) │
9│ │
10│ 결과: │
11│ - 스케줄러가 노드를 메모리 부족으로 판단 │
12│ - 실제로는 여유가 있음에도 파드 스케줄링 실패 │
13│ - 클러스터 리소스 활용률 저하 │
14└─────────────────────────────────────────────────────────────┘
4. 모니터링 혼란
1┌─────────────────────────────────────────────────────────────┐
2│ Monitoring Confusion │
3│ │
4│ Grafana Dashboard │
5│ ├── Pod Memory (Node Exporter): 500Mi │
6│ ├── Pod RSS (cAdvisor): 100Mi │
7│ └── 차이: 400Mi (어디서 온 메모리?) │
8│ │
9│ 문제점: │
10│ - 알람 기준 설정 어려움 │
11│ - 실제 메모리 사용량 파악 불가 │
12│ - 리소스 계획 수립 어려움 │
13└─────────────────────────────────────────────────────────────┘
해결 방법: /var/log/containers로 전환
플랫한 디렉토리 구조
Kubernetes는 /var/log/containers 디렉토리에 심볼릭 링크를 제공합니다. 이 디렉토리는 플랫한 구조를 가지고 있습니다.
1# /var/log/pods (깊은 구조)
2/var/log/pods/
3└── default_mypod_1234567890abcdef/
4 └── mycontainer/
5 └── 0.log
6
7# /var/log/containers (플랫한 구조)
8/var/log/containers/
9└── mypod_default_mycontainer-abcdef1234567890.log
10 → /var/log/pods/default_mypod_1234567890abcdef/mycontainer/0.log
Dentry Cache 감소
플랫한 구조로 전환하면:
1┌─────────────────────────────────────────────────────────────┐
2│ Dentry Cache Reduction │
3│ │
4│ Before (/var/log/pods): │
5│ - Pod 디렉토리: 100개 │
6│ - Container 디렉토리: 200개 │
7│ - Log 파일: 600개 │
8│ - Total dentry: ~900개 │
9│ │
10│ After (/var/log/containers): │
11│ - 심볼릭 링크 파일: 600개 │
12│ - Total dentry: ~600개 │
13│ │
14│ 감소율: ~33% │
15│ → Slab memory 대폭 감소! │
16└─────────────────────────────────────────────────────────────┘
Alloy 설정 변경
1# Before
2discovery.file "logs" {
3 path_targets = [
4 {
5 __path__ = "/var/log/pods/**/*.log",
6 job = "kubernetes-pods",
7 },
8 ]
9}
10
11# After
12discovery.file "logs" {
13 path_targets = [
14 {
15 __path__ = "/var/log/containers/*.log",
16 job = "kubernetes-containers",
17 },
18 ]
19}
심볼릭 링크를 사용하는 것의 트레이드오프
장점
1. Dentry Cache 감소
- 플랫한 구조로 인한 dentry 객체 수 감소
- Slab memory 사용량 감소
- cgroup memory pressure 완화
2. 파일 탐색 성능 향상
- 단일 디렉토리에서 모든 로그 파일 접근
- 재귀적 디렉토리 탐색 불필요
- 파일 시스템 오버헤드 감소
3. 메타데이터 추출 용이
- 파일명에 파드명, 네임스페이스, 컨테이너명 포함
- 추가 파싱 없이 메타데이터 추출 가능
1# 파일명 패턴
2<pod_name>_<namespace>_<container_name>-<container_id>.log
단점 및 주의사항
1. 심볼릭 링크 해석 오버헤드
심볼릭 링크를 따라가려면 추가적인 시스템 콜이 필요합니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Symbolic Link Resolution │
3│ │
4│ 일반 파일 접근: │
5│ open("/var/log/pods/.../0.log") → 파일 열기 │
6│ │
7│ 심볼릭 링크 접근: │
8│ open("/var/log/containers/xxx.log") │
9│ → readlink() (심볼릭 링크 해석) │
10│ → open("/var/log/pods/.../0.log") (실제 파일 열기) │
11│ │
12│ 추가 오버헤드: readlink() 시스템 콜 │
13└─────────────────────────────────────────────────────────────┘
영향:
- 파일 열기 시 약간의 성능 오버헤드
- 대부분의 경우 무시할 수 있는 수준
- 로그 수집 빈도에 따라 영향 다름
2. 심볼릭 링크 업데이트 타이밍
컨테이너가 재시작되면 심볼릭 링크가 업데이트됩니다.
1┌─────────────────────────────────────────────────────────────┐
2│ Link Update Timing │
3│ │
4│ Container Restart: │
5│ 1. Old log file: /var/log/pods/.../container/0.log │
6│ 2. New log file: /var/log/pods/.../container/1.log │
7│ 3. Symlink update: /var/log/containers/xxx.log │
8│ → points to 1.log │
9│ │
10│ 잠재적 문제: │
11│ - 링크 업데이트 전에 파일을 열면 이전 로그 파일 참조 │
12│ - 로그 수집기가 파일 변경을 감지하지 못할 수 있음 │
13│ - inode 변경 감지 필요 │
14└─────────────────────────────────────────────────────────────┘
해결 방법:
- 로그 수집기가 inode 변경을 감지하도록 설정
- 파일 재오픈 메커니즘 구현
- Alloy, Fluent Bit 등은 이미 이를 처리함
3. 파일 시스템 의존성
심볼릭 링크는 원본 파일이 존재해야 합니다.
1┌─────────────────────────────────────────────────────────────┐
2│ File System Dependency │
3│ │
4│ 시나리오: │
5│ 1. 파드 삭제 │
6│ 2. /var/log/pods/.../ 파일 삭제 │
7│ 3. 심볼릭 링크는 남아있지만 broken link │
8│ │
9│ 문제점: │
10│ - 로그 수집기가 broken link를 만나면 에러 발생 │
11│ - 정기적인 정리 작업 필요 │
12│ │
13│ 완화 방법: │
14│ - kubelet이 자동으로 정리 (일반적) │
15│ - 로그 수집기가 broken link 무시 │
16└─────────────────────────────────────────────────────────────┘
4. 컨테이너 런타임별 차이
다른 컨테이너 런타임에서는 심볼릭 링크 구조가 다를 수 있습니다.
1# containerd
2/var/log/containers/pod_namespace_container-id.log
3 → /var/log/pods/namespace_pod_uid/container/0.log
4
5# Docker (구버전)
6/var/log/containers/pod_namespace_container-id.log
7 → /var/lib/docker/containers/container-id/container-id-json.log
고려사항:
- 컨테이너 런타임 변경 시 테스트 필요
- 링크 해석 경로가 다를 수 있음
요약
문제 원인:
/var/log/pods의 깊은 디렉토리 구조를 watch하면서 dentry cache가 급증하여 slab memory가 증가했습니다.메트릭 불일치: RSS는 증가하지 않았지만, cgroup에 할당된 slab memory가 증가하여 Node Exporter 메트릭이 우상향했습니다.
해결 방법:
/var/log/containers의 플랫한 구조로 전환하여 dentry 객체 수를 감소시켰습니다.트레이드오프: 심볼릭 링크 사용 시 약간의 성능 오버헤드와 파일 시스템 의존성이 있지만, 대부분의 로그 수집기는 이를 잘 처리합니다.
다른 에이전트: Fluentd, Filebeat 등도
/var/log/pods를 직접 watch하면 동일한 문제가 발생할 수 있으므로,/var/log/containers사용을 권장합니다.
이 이슈를 통해 Kubernetes 환경에서 로그 수집 시 파일 시스템 구조와 커널 메모리 관리의 관계를 이해하는 것이 중요함을 알 수 있습니다.