인프라 변천사¶
물리 서버에서 컨테이너까지의 진화 과정과 실무 트러블슈팅 방법을 정리한다.
모든 컨테이너와 오케스트레이션의 뿌리는 리눅스 커널에 있다. 인프라가 어떻게 변화해 왔는지, 왜 쿠버네티스가 필요한지, 그리고 실무에서 발생하는 OOM과 CPU 스로틀링 장애를 어떻게 분석하고 해결하는지 살펴본다.
인프라 변천사¶
인프라의 변천사: Physical 서버 → VM → 컨테이너
기술은 변했지만 인프라를 구축하는 목적은 단 하나도 바뀌지 않았다: 안정적인 서비스 제공.
핵심은 서비스가 죽지 않게 하는 것이다.
왜 쿠버네티스인가¶
인프라 현실의 변화:
- 예전: 서버 몇 대만 관리, 사람이 직접 배포하고 모니터링
- 현재: MSA (Microservice Architecture) 도입, 수백~수천 개의 작은 컨테이너가 복잡하게 얽혀서 동작
수천 개의 컨테이너를 사람이 일일이 배포하고 모니터링하고 장애 시마다 직접 복구하는 것은 물리적으로 불가능하다.
쿠버네티스 핵심 가치¶
1. 고가용성 (High Availability):
- 서버 일부가 죽어도 전체 서비스는 계속 살아 있게 만드는 힘
- 어느 한 곳이 고장 나도 전체 서비스는 안 죽는 구조
SPOF (Single Point of Failure) - 단일 장애점:
- SPOF가 있는 구조: 서버 한 대만 있고, 그 서버가 죽으면 서비스 전체 다운
- SPOF 제거 구조: 서버 A, B, C가 여러 대 있어서, 한 대가 죽어도 나머지가 요청 처리 → 사용자는 장애를 느끼지 못함
쿠버네티스는 단일 장애점 자체를 설계 단계에서부터 없앤다.
2. 자가 치유 (Self-Healing):
장애가 발생하면 시스템이 스스로 알아서 복구하는 능력이다.
상태 관리 3단계:
- 선언: 사용자가 "내 파드는 항상 3개를 유지해 줘"라고 선언
- 감지: 파드 한 개가 갑자기 죽으면 선언된 상태와 차이 발견
- 조치: 쿠버네티스 컨트롤러가 즉시 새 파드 하나를 더 생성해서 다시 3개로 맞춤
운영 패러다임 변화:
- 예전: 서버가 새벽에 죽으면 엔지니어가 알람 소리에 깨서 수동 대응
- 쿠버네티스: 알람이 울려도 시스템이 알아서 먼저 복구, 다음날 출근해서 원인 분석만 하면 됨
연쇄 장애 (Cascade Failure)¶
상황 가정: 새벽 3시에 PagerDuty 알림 발생 → 503 에러율 급증 → Service Unavailable
503 에러는 단순히 컨테이너 한두 개가 죽은 수준이 아니라 서비스 자체가 완전히 마비되었다는 뜻이다.
연쇄 장애 발생 과정:
- 트래픽 급증: 갑자기 트래픽 스파이크 발생
- 파드 1번 강제 종료: 메모리 초과 (OOM)로 강제 종료
- 파드 2번, 3번 연쇄 종료: 파드 1번이 감당하던 요청이 파드 2번, 3번으로 쏠림 → 메모리 임계점 도달 → 연쇄 종료
- 서비스 전체 다운: 모든 파드가 줄줄이 죽으면서 도미노 현상
방어 장치 (Defense Mechanism)¶
1. Rate Limiting (속도 제한):
- 들어오는 요청 속도를 강제로 제한
- 시스템이 감당할 수 있는 만큼만 요청 수용
2. Circuit Breaker (서킷 브레이커):
- 특정 지점에 장애 감지되면 연결 즉시 차단
- 다른 곳으로 장애가 번지지 않게 격리
OOM Killed 장애 분석¶
실습 환경¶
# throttling-pod.yaml
resources:
limits:
memory: 128Mi # 메모리 상한선 128MiB
args:
- --vm
- "1"
- --vm-bytes
- "200M" # 200MB 사용 시도 (상한선 초과)
설정 의미:
limits.memory: 128Mi: 쿠버네티스가 이 컨테이너에게 허용한 메모리 최대 상한선--vm-bytes 200M: 스트레스 도구가 200MB 사용 시도- 128Mi < 200MB → 메모리 초과로 OOM Killed 발생
CrashLoopBackOff 의미¶
graph LR
Start[프로세스 시작] -->|메모리 초과| Kill1[파드 죽음]
Kill1 -->|Kubelet 재시작 시도| Restart1[재시작]
Restart1 -->|메모리 초과| Kill2[다시 파드 죽음]
Kill2 -->|백오프 증가<br/>10초 → 20초 → 40초| Restart2[재시작 대기]
Restart2 -->|최대 5분까지 증가| Loop[CrashLoopBackOff]
과정:
- 프로세스가 메모리 초과로 파드 죽음
- Kubelet이 재시작 시도 (restart policy: Always)
- 다시 메모리 초과로 파드 죽음
- Kubelet이 다시 재시작 시도 (백오프 간격을 2배씩 증가: 10초 → 20초 → 40초 → 최대 5분)
- 무의미한 재시작 반복 → CrashLoopBackOff 상태
디버깅¶
출력:
Exit Code 137 의미:
137 = 128 + 9128 이상: 프로세스가 외부 시그널에 의해 강제 종료됨9: SIGKILL (프로세스를 즉시 강제 종료하라는 명령)
메모리 제어 경로¶
graph LR
Limit[메모리 Limit 발생] --> Kubelet[Kubelet]
Kubelet --> Containerd[Containerd]
Containerd --> RunC[runC]
RunC --> Cgroup[Linux Kernel<br/>Cgroup]
Cgroup -->|memory.max 기록| Monitor[커널 감시]
Monitor -->|초과 감지| OOM[OOM Killer 발동]
OOM -->|SIGKILL 9번| Process[프로세스 종료]
핵심: 쿠버네티스는 지시자일 뿐, 실제로 프로세스를 종료시키는 것은 리눅스 커널이다.
쿠버네티스는 커널이 보고한 결과를 사용자에게 "OOMKilled" 상태로 보여줄 뿐이다.
커널 로그 확인¶
출력:
OOM Score:
- 커널이 프로세스를 종료시킬 때 사용하는 종료 우선순위 점수
- 점수가 높을수록 먼저 종료됨 (-1000 ~ 1000)
OOM 발생 경로 두 가지¶
1. Cgroup OOM (파드 단위):
- 해당 파드가 자신의 한도를 넘긴 경우
- 해당 파드만 종료됨, 시스템 전체에는 영향 없음
2. System OOM (노드 단위):
- 노드 전체 메모리 고갈
- 커널이 OOM Score 높은 파드부터 순서대로 제거하며 시스템 보호
- 전체 영향 발생
해결 방법¶
임시 방편: 단순히 limits.memory 수치만 올리기
근본적 해결:
- 원인 파악: 왜 메모리 부족 현상이 일어났는지 분석
- 특정 요청에서만 메모리가 튀는가?
- 가비지 컬렉션이 원활하게 작동하지 않는가?
-
비효율적인 알고리즘을 사용하고 있는가?
-
근본 원인 제거: 메모리 누수 수정, 알고리즘 개선, 캐시 정리 등
수치를 조정하기 전에 왜 파드가 죽었는지 로그를 통해 파악하는 것이 중요하다.
CPU 스로틀링 장애 분석¶
실무에서는 노드 전체 CPU 자원이 충분해 보이는데도 특정 서비스 응답 속도만 비정상적으로 느려지는 상황을 겪는다. 더 까다로운 점은 애플리케이션 로그에 아무런 에러도 남지 않는다는 것이다.
실습 환경¶
# throttling-pod.yaml
args:
- --cpu
- "1" # CPU 1코어 전체 사용 시도 (1000m)
resources:
limits:
cpu: 50m # 상한선 50m (1코어의 5%)
설정 의미:
- 프로세스는 1000m (1코어) 전체를 사용하려고 시도
- 쿠버네티스가 허용한 상한선은 단지 50m (5%)
- 간극이 발생 → CPU 스로틀링
CFS (Completely Fair Scheduler)¶
쿠버네티스의 밀리코어 단위는 결국 리눅스 커널의 CFS (Completely Fair Scheduler)가 담당한다.
CFS Bandwidth Controller:
- Cgroup 단위로 CPU 사용량 상한선 설정
- 일정 주기 동안 사용할 수 있는 CPU 시간을 쿼터(할당량)로 제한
기본 주기:
- Period (주기): 100ms (0.1초)
- Quota (할당량): 설정한 limit에 따라 결정
50m 코어 설정 시:
- 100ms 중 단 5% (5ms)만 사용 가능
- 나머지 95% (95ms)는 강제 대기 상태 (스로틀링)
[ 5ms 사용 ][ 95ms 강제 대기 ][ 5ms 사용 ][ 95ms 강제 대기 ] ...
|<-------- 100ms 주기 -------->|<-------- 100ms 주기 -------->|
스트레스 프로세스가 1코어 전체를 쓰려고 하지만, 5ms 만에 할당량이 바닥나서 나머지 95ms 동안 무작정 기다려야 한다.
겉으로는 정상처럼 보이는 함정¶
출력:
kubectl top은 평균값을 보여준다. 리밋이 50m이면 내부에서 응답이 아무리 밀려 있어도 겉으로는 50m까지만 표시된다.
비유: 천장에 머리가 이미 닿은 상태인데, kubectl top은 천장 높이만 보여줄 뿐 얼마나 많은 횟수로 머리가 계속 천장에 부딪히는지는 보여주지 않는다.
커널 레벨 데이터 확인¶
# 워커 노드에서 확인
sudo -i
# 컨테이너 ID 추출
CONTAINER_ID=$(kubectl get pod throttling -o yaml | grep containerID | head -1 | awk '{print $NF}' | sed 's/containerd:\/\///')
# PID 추출
PID=$(crictl inspect $CONTAINER_ID | jq '.info.pid')
# Cgroup 경로 추출
CGROUP_PATH=$(cat /proc/$PID/cgroup | cut -d: -f3)
# CPU 스탯 확인
cat /sys/fs/cgroup$CGROUP_PATH/cpu.stat
출력:
지표 해석:
nr_periods: 총 주기 횟수 = 7,359번nr_throttled: 스로틀링 발생 횟수 = 6,449번- 비율: 6,449 / 7,359 = 87.6% (압도적으로 할당량을 다 써서 강제 멈춤)
시간 단위 환산:
usage_usec: 3,668,000 μs = 36.8초 (실제 CPU 사용 시간)throttled_usec: 603,000,000 μs = 603초 (강제 정지 시간)- 총 경과 시간: 36.8 + 603 = 640.7초
- CPU 사용 비율: 36.8 / 640.7 = 5.7%
- 강제 대기 비율: 603 / 640.7 = 94.3%
CPU를 사용할 시간보다 약 15배 이상 더 길게 강제 대기 상태였다.
현업 모니터링¶
현업에서는 워커 노드에 직접 접속해서 확인하는 것이 까다롭기 때문에, Prometheus나 Datadog 같은 모니터링 도구를 사용한다.
이 도구들은 Kubelet 내장 cAdvisor를 통해 커널의 cpu.stat 파일값을 주기적으로 수집한다.
Prometheus나 Datadog에서 보는 nr_throttled, throttled_usec 값의 실제 출처가 바로 리눅스 커널의 데이터다.
쿠버네티스 vs 커널¶
쿠버네티스:
- 지휘자 (Orchestrator)
- "이 파드는 이런 상태로 유지해야 된다"라고 의도를 선언
- 하드웨어 자원을 직접 제어하거나 프로세스를 강제 종료시키는 물리적 행위를 직접 하지 않음
리눅스 커널:
- 실행자 (Executor)
- 쿠버네티스 요청을 받아서 실제로 CPU 할당량 제한, 메모리 사용량 감시, 프로세스 정리 수행
- 자원 통제의 최종 집행 권한자
비유: 지휘자는 각 연주자에게 어떤 악보를 어떻게 연주할지 지시한다. 실제로 악기를 들고 현을 켜거나 물리적인 소리를 만들어내는 주체는 연주자 (리눅스 커널)이다.
커널의 핵심 4대 프리미티브¶
1. Namespace (네임스페이스):
- 프로세스의 시야를 물리적으로 분리
- 특정 컨테이너가 다른 컨테이너의 파일, 네트워크, 프로세스를 볼 수 없게 격리 (Isolation)
2. Cgroup (Control Group):
- CPU, 메모리, 네트워크 대역폭 등 하드웨어 자원의 상한선 설정
- OOM Killed, CPU 스로틀링을 실제로 집행하는 주체
3. OverlayFS (오버레이 파일 시스템):
- 여러 층의 파일 시스템 레이어를 하나로 합쳐서 보여줌
- 컨테이너 이미지를 레이어 형식으로 합성
4. veth (Virtual Ethernet Pair):
- 컨테이너와 호스트 네트워크 사이를 이어주는 가상 네트워크
- 파드끼리 통신을 가능하게 하는 가상 이더넷 페어
물리 서버 시대의 문제점¶
2000년대 이전~초반까지는 하나의 물리 서버 시대였다.
주요 문제점:
1. 의존성 충돌:
- 서비스 A: Python 2.4
- 서비스 B: Python 2.6
- 하나의 OS 위에서 서로 다른 라이브러리 버전 공존이 매우 어려움
- "내 로컬 PC에서는 잘 돌아가는데 왜 운영 서버에서는 안 돌아가냐?" (개발자 간 고전적 갈등)
2. 자원 격리 부재:
- 프로세스 A가 메모리 누수 발생
- 커널이 시스템 전체를 보호하기 위해 자원 과다 점유 프로세스 정리
- 같은 자원을 공유하던 프로세스 B까지 연쇄적으로 영향받음
3. 비효율적 확장성:
- DB에만 트래픽 부하 집중 → DB만 확장하고 싶음
- 하지만 물리 서버 장비를 통째로 추가 도입해야 함
- 부분적인 확장 불가능
4. 배포의 어려움:
- 새로운 기능 배포 시 바이너리 파일을 직접 덮어쓰거나 수동으로 설정 변경
- 장애 발생 시 즉각적인 롤백 거의 불가능
- "금요일 오후에는 배포하지 말라" (엔지니어 불문율)
가상 머신 (VM) 등장¶
2000년대 초반부터 가상 머신(VM) 기술이 본격 도입되었다.
VM 구조¶
하이퍼바이저: 하드웨어를 추상화해서 여러 대의 가상 서버로 나누는 계층
해결된 점¶
1. 완벽한 격리:
- VM 간 자원 간섭 실질적으로 차단
2. 의존성 독립:
- 각 VM마다 다른 버전의 OS, 라이브러리 설치 가능
3. 유연한 확장:
- 이미지 복제해서 빠르게 서버 대수 늘릴 수 있음
새로운 문제점¶
1. 무거움:
- 각 VM이 자신만의 커널을 포함한 전체 OS를 통째로 가져야 함
- 게스트 OS (가상 CPU, RAM, 디스크, 네트워크) 리소스 필요
2. 느린 속도:
- OS 부팅 과정을 매번 거쳐야 함
- 수분 이상 시간 소요
3. 리소스 오버헤드:
- 애플리케이션과 무관한 OS 유지 비용 발생
가장 큰 문제: 커널 중복
- VM 10대 운영 = 메모리상에 10개의 독립적인 게스트 커널 상주
- 애플리케이션 A가 요청 처리하지 않는 순간에도 VM의 커널과 OS 백그라운드 프로세스들은 지속적으로 자원 소모
- 자원 활용 효율성 급격히 떨어짐
하이퍼바이저 타입¶
타입 1 (Bare-Metal):
- 물리 서버 하드웨어 바로 위에 하이퍼바이저 직접 설치
- 중간 단계 없어서 성능 손실 최소화
- 기업용 데이터 센터, 클라우드 프로덕션 환경 사용
- 예: VMware ESXi, KVM, Xen
타입 2 (Hosted):
- 기존 OS (Windows, macOS) 위에 소프트웨어적으로 설치
- 호스트 OS를 한 번 더 거치기 때문에 성능 손실 발생
- 개발자 로컬 테스트 환경, 개인용 PC 사용
- 예: VirtualBox, VMware Workstation, Parallels
2026년 기준 타입 2 하이퍼바이저도 성능이 많이 개선되었다.
컨테이너 등장¶
가상 머신의 무거운 구조를 경험하며 엔지니어들은 근본적인 의문이 생겼다:
"단순히 애플리케이션 하나를 격리해서 실행하고 싶은데, 매번 기가바이트 단위 운영 체제를 통째로 띄워야 하는가?"
컨테이너 기술적 정의¶
컨테이너 = 격리된 리눅스 프로세스 그룹
세 가지 핵심 기술 조합:
1. Namespace (격리):
- 프로세스가 참조할 수 있는 시스템 리소스 (네트워크, PID, 마운트)의 가시성을 논리적으로 격리
- 프로세스가 시스템 내 다른 프로세스나 리소스에 접근하는 것을 원천 차단
2. Cgroup (리소스 제한):
- 프로세스 그룹이 점유할 수 있는 물리적 하드웨어 리소스 (CPU, 메모리, 블록 I/O)의 상한선 통제
- 자원 사용량 모니터링 및 제한
3. 파일 시스템 레이어 (OverlayFS):
- 베이스 이미지와 변경 사항을 층층이 쌓아서 독립적인 하나의 파일 시스템처럼 구성
결론: 컨테이너는 완전히 새로운 발명품이 아니다. 리눅스 커널이 수십 년 전부터 가지고 있었던 기능들을 조합해서, 특정 프로세스가 마치 독립된 서버에서 실행되는 것처럼 논리적으로 분리해 낸 것뿐이다.
컨테이너의 본체는 리눅스 프로세스다.
VM vs 컨테이너 비교¶
| 구분 | VM | 컨테이너 |
|---|---|---|
| 가상화 대상 | 하드웨어 가상화 | OS 레벨 격리 |
| 커널 | 독립적인 게스트 커널 | 호스트 커널 공유 |
| 격리 수준 | 완전 격리 | 상대적으로 낮음 |
| 이미지 크기 | 기가바이트 단위 | 메가바이트 단위 |
| 기동 시간 | 초~분 단위 | 밀리초~초 단위 |
| 리소스 사용 | 많음 (OS 전체) | 적음 (필요한 것만) |
| 밀도 | 낮음 (서버당 수십 대) | 높음 (서버당 수백 대) |
VM 구조:
컨테이너 구조:
가장 큰 차이: 커널을 각자 가지느냐 (VM), 하나를 공유하느냐 (컨테이너).
도커의 역할¶
도커 등장 이전 (2013년 이전):
- Namespace, Cgroup, Union 파일 시스템 등을 직접 명령어로 조합
- 리눅스 커널에 깊은 이해가 있는 고급 엔지니어만 가능한 전문가 영역
도커 등장 이후 (2013년, Solomon Hykes 발표):
- 복잡한 커널 기술들을
docker run한 줄 명령어로 포장 (추상화) - 누구나 손쉽게 컨테이너 기술 사용 가능 → 대중화
도커의 본질:
- 새로운 기술을 발명한 것이 아님
- 이미 존재하던 강력한 커널 기술들을 누구나 사용할 수 있도록 패키징한 혁신적인 도구
이 편의성이 전 세계 개발자들을 열광시키고, 오늘날 클라우드 네이티브 생태계의 토대가 되었다.
쿠버네티스 런타임 변화¶
초기 단계 (K8s 1.0 ~ 1.4):
CRI 표준화 시기:
- CRI (Container Runtime Interface) 표준 도입
- Docker는 CRI 미지원 → Kubelet 내부에 Dockershim (중계기) 내장
- Dockershim이 변환기 역할 수행
현재 (K8s 1.24+):
- Dockershim 완전 제거
- Containerd가 CRI 표준을 따라 직접 통신
- 중간 변환기 없이 커널 기능을 더 효율적으로 제어
쿠버네티스는 리눅스 커널이 제공하는 정교한 격리 기술들을 표준화된 인터페이스 (CRI)를 통해 수천 대의 서버에 걸쳐 자동화하는 거대한 시스템이다.
VM과 컨테이너 공존 이유¶
컨테이너 기술이 발전하면서 "VM 시대는 끝났다"라고 생각했지만, 실제 클라우드 인프라 내부를 들여다보면 VM 위에 컨테이너를 올리는 계층 구조를 따른다.
실제 인프라 스택 (AWS EC2 기준)¶
VM을 제거한 게 아니라 VM 위에 컨테이너를 올리는 구조다.
이유: 보안과 멀티테넌시¶
컨테이너의 보안 허점:
- 컨테이너는 호스트 OS 커널을 공유
- 고객 A가 컨테이너를 해킹당하면 → 커널 탈취 가능성
- 같은 커널을 공유하는 고객 B도 데이터 위험
- 컨테이너 탈출 (Container Breakout) 발생 가능
VM의 하드웨어 경계:
- 고객 A와 고객 B 사이에 VM으로 하드웨어 레벨 벽을 세움
- 고객 A가 해킹당해도 하드웨어적 경계 때문에 고객 B 안전
멀티테넌시 (Multi-Tenancy):
- 퍼블릭 클라우드: 수천, 수만 명의 고객이 하나의 물리 인프라 공유
- VM이 고객 간 안전한 격리 제공
결론: VM과 컨테이너는 서로 대체하는 경쟁 관계가 아니라, 안정성 (VM)과 효율성 (컨테이너)을 담보해 주는 상호 보완적 관계다.
AWS Nitro 시스템¶
일반 하이퍼바이저는 네트워크, 스토리지, 보안 등을 메인 CPU가 처리한다. 과거에는 약 15~20%까지 메인 CPU 소모 → 가상화 관리 오버헤드 발생.
AWS Nitro 해결책:
- 전용 하드웨어 칩 (Nitro 카드) 개발
- 하드웨어 오프로딩: 가상화 관리를 별도 하드웨어가 담당
- 메인 CPU는 거의 100% 애플리케이션에 할당 가능
Near Bare-Metal:
- 베어메탈과 거의 근접한 성능
- 업계에서는 "Real Bare-Metal"이라고도 부름
베어메탈 (Bare-Metal)¶
가상화 레이어를 완전히 제거한 순수 하드웨어다.
특징:
- 가상화 레이어 없음 → 지연 시간 거의 0에 가까움
- 하드웨어 자원 직접 제어
사용 환경:
1. 고성능 연산:
- 대규모 데이터 분석
- AI/머신러닝 모델 학습
- GPU 자원 직접 접근 필요
2. 초저지연 (Low Latency):
- 금융권 트레이딩 시스템
- 마이크로초 단위 체결 필요
3. 대규모 배치:
- 수천만~수억 건의 데이터를 최단 시간 내 처리
- VM의 조그마한 오버헤드조차 허용 불가
정리¶
인프라 진화 과정:
- 물리 서버: 의존성 충돌, 격리 부재, 비효율적 확장, 배포 어려움
- VM: 격리 해결, 의존성 독립, 유연한 확장 → 하지만 무겁고 느리고 커널 중복
- 컨테이너: 커널 공유로 가볍고 빠름, 높은 밀도 → 하지만 격리 수준은 VM보다 낮음
- 클라우드 네이티브: VM (보안 경계) + 컨테이너 (효율) 공존 구조
핵심 교훈:
- 쿠버네티스 리소스만큼 중요한 것이 리눅스 커널
- 쿠버네티스 = 지시자, 리눅스 커널 = 집행자
- OOM, CPU 스로틀링 같은 장애는 커널 레벨에서 정확히 어떤 경로로 발생하는지 이해해야 근본 해결 가능
- 단순히 수치만 조정하는 것은 임시 방편, 원인 파악과 근본 제거가 진정한 해결책