cgroup, image layer, request/limit, Docker·containerd·Podman, HPA를 한 번씩 만져보는 체험형 랩
1. 실습 목표
이 실습의 목표는 단순히 개념을 읽는 것이 아니라, 아래 내용을 직접 손으로 확인해보는 것이다.
- cgroup이 실제로 어디에 적용되는지
- cgroup v1 / v2 구조가 어떻게 다른지
- 컨테이너 이미지 layer가 어떻게 쌓이는지
- Docker / containerd / Podman의 차이를 어떻게 체감할 수 있는지
- Kubernetes의 request / limit이 스케줄링과 런타임에서 어떻게 다르게 작동하는지
- CPU limit은 왜 throttling이 되고, Memory limit은 왜 OOMKilled가 되는지
- HPA가 왜 바로 반응하지 않는지
- “이걸 알면 실무에서 무엇을 할 수 있는지”를 스스로 연결할 수 있는지
2. 실습 컨셉
이번 랩은 정석 구축보다 “개념 체험”에 초점을 둔다.
즉, 운영용 클러스터를 완벽하게 만드는 것이 목적이 아니라:
- 빠르게
- 반복 가능하게
- 블로그에 정리할 수 있게
- 각 개념을 한 번씩 명확히 체험할 수 있게
구성하는 것이 목적이다.
3. 권장 환경
3-1. 가장 추천하는 방식
VirtualBox(또는 VMware Workstation) + Rocky Linux 9.x VM 4대
| VM | 역할 | 권장 사양 | IP 예시 |
|---|---|---|---|
| lab-tools | Docker / Podman / 빌드 / 부하발생 | 4 vCPU / 8GB / 60GB | 192.168.56.10 |
| k8s-cp | Kubernetes Control Plane | 2 vCPU / 4GB / 40GB | 192.168.56.20 |
| k8s-w1 | Kubernetes Worker 1 | 2 vCPU / 4GB / 40GB | 192.168.56.21 |
| k8s-w2 | Kubernetes Worker 2(선택) | 2 vCPU / 4GB / 40GB | 192.168.56.22 |
메모리가 부족하면
k8s-w2는 생략 가능하다.
다만 HPA, Pending, 스케줄링 체감은 3노드보다 4노드가 훨씬 좋다.
3-2. 왜 이렇게 나누는가?
- lab-tools
- Docker / Podman 비교
- image layer 실습
- 부하 발생 도구 실행
- k8s 노드
- containerd 확인
- request/limit
- OOM / throttling
- HPA
- Pod Pending
즉,
- 개발/빌드/로컬 컨테이너 체험은 lab-tools
- 오케스트레이션과 리소스 제어는 Kubernetes 노드
로 분리하면 개념이 훨씬 잘 잡힌다.
4. 실습 아키텍처
[ lab-tools ]
├─ Docker
├─ Podman
├─ Dockerfile / image layer 실습
├─ 부하 발생(hey / curl / busybox 등)
└─ kubectl 접속
[ k8s-cp ]
├─ k3s server (containerd 내장)
└─ kubectl / control plane
[ k8s-w1 ]
└─ k3s agent
[ k8s-w2 ] (선택)
└─ k3s agent
5. 이번 랩에서 왜 k3s를 권장하는가?
정석적으로 kubeadm이나 Kubespray를 써도 된다.
하지만 이번 글의 목적은 클러스터 구축 자체가 아니라 개념 체험이다.
그래서 이번 실습은:
- 구축이 빠르고
- containerd가 기본 포함되고
- HPA 실습까지 빠르게 가기 쉬운
k3s를 권장한다.
이미 Kubespray 실습 환경이 있다면 그대로 사용해도 된다.
다만 처음부터 새로 만들 거라면 이번 체험 랩은 k3s가 더 효율적이다.
6. 사전 준비
6-1. Rocky Linux 공통 초기 설정
모든 VM에서 공통으로 수행
sudo dnf update -y
sudo dnf install epel-release -y
sudo dnf install -y vim git curl wget jq tar net-tools bind-utils tree htop
sudo systemctl disable --now firewalld
sudo setenforce 0
sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab
6-2. /etc/hosts 등록
모든 VM에 등록
cat <<EOF | sudo tee -a /etc/hosts
192.168.56.10 lab-tools
192.168.56.20 k8s-cp
192.168.56.21 k8s-w1
192.168.56.22 k8s-w2
EOF
7. 1단계 – lab-tools VM 준비
7-1. Podman 설치
Rocky 9 계열은 Podman이 비교적 자연스럽다.
sudo dnf install -y podman buildah skopeo
podman --version
확인:
podman info | grep -i rootless -A 3
7-2. Docker 설치
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
docker version
7-3. 실습용 도구 설치
sudo dnf install -y stress-ng
부하 발생 도구 예시:
sudo dnf install -y golang
go install github.com/rakyll/hey@latest
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc
hey -h
hey설치가 번거로우면curl,wget,ab,busybox만 써도 된다.
8. 2단계 – Kubernetes(k3s) 구축
8-1. k8s-cp 에서 k3s server 설치
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --write-kubeconfig-mode 644 --node-ip 192.168.56.20" sh -
상태 확인:
sudo systemctl status k3s
kubectl get nodes
토큰 확인:
sudo cat /var/lib/rancher/k3s/server/node-token
8-2. k8s-w1, k8s-w2 에서 agent 설치
아래의 K3S_TOKEN 값은 위에서 확인한 토큰으로 교체
curl -sfL https://get.k3s.io | \
K3S_URL=https://192.168.56.20:6443 \
K3S_TOKEN="여기에_토큰값" \
INSTALL_K3S_EXEC="agent --node-ip 192.168.56.21" sh -
# 만약 아래와 같은 에러가 발생한다면:
# Job for k3s-agent.service failed because the control process exited with error code.
# 1. 서비스 상태 확인
sudo systemctl status k3s-agent.service
# 2. 상세 로그 확인
sudo journalctl -xeu k3s-agent.service
# 3. 자주 발생하는 원인 및 해결 방법
# (1) K3S_URL 접속 불가 (네트워크 문제)
# - control plane 노드(k8s-cp)와 통신 가능한지 확인
ping 192.168.56.20
curl -k https://192.168.56.20:6443
# (2) 토큰 불일치
# - k8s-cp에서 토큰 재확인
sudo cat /var/lib/rancher/k3s/server/node-token
# 예시 출력: K10d52b055e72d4e3eee85317a00483358f2e854328f398ed2976918972fe67c521::server:b5e12e7bbb1ab8965ac312fdf0e8a706
# 실제 환경에서는 위 명령어로 출력된 값을 그대로 복사해서 K3S_TOKEN에 넣으면 된다.
# (3) 이전 설치 잔여물 충돌
sudo /usr/local/bin/k3s-agent-uninstall.sh
sudo rm -rf /var/lib/rancher/k3s /etc/rancher/k3s
# (4) 방화벽 / SELinux 문제
sudo systemctl disable --now firewalld
sudo setenforce 0
# (5) node-ip 설정 오류
# - 실제 인터페이스 IP와 일치하는지 확인
ip a
# 문제 해결 후 다시 실행
curl -sfL https://get.k3s.io | \
K3S_URL=https://192.168.56.20:6443 \
K3S_TOKEN="여기에_토큰값" \
INSTALL_K3S_EXEC="agent --node-ip 192.168.56.21" sh -
w2는 IP만 192.168.56.22로 바꿔서 동일하게 수행
8-3. 클러스터 확인
k8s-cp 또는 lab-tools에서 kubeconfig를 가져와 확인 (예: scp root@192.168.56.20:/etc/rancher/k3s/k3s.yaml ~/.kube/config 로 복사한 뒤, 파일 내 server 주소를 127.0.0.1에서 192.168.56.20으로 수정하여 사용)
k8s-cp에서 kubeconfig 복사
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
kubectl get nodes -o wide
lab-tools에서도 kubectl 사용하고 싶다면
k8s-cp:/etc/rancher/k3s/k3s.yaml 파일을 복사한 뒤
server 주소를 127.0.0.1 → 192.168.56.20 으로 수정
8-4. metrics 확인
k3s에서는 보통 metrics-server가 포함되어 있어 kubectl top이 가능하다.
확인:
kubectl top nodes
kubectl top pods -A
만약 동작하지 않으면 별도 metrics-server를 설치해야 한다.
이번 랩은kubectl top이 되는 상태를 전제로 진행한다.
9. 실습 디렉토리 구조
이 디렉토리는 실습 파일을 직접 만들고 Docker/Podman 빌드 및 부하 테스트를 수행하는 lab-tools 서버에 생성하는 것을 권장한다.
container-lab/
├── 01-cgroup
├── 02-image-layer
├── 03-runtime
├── 04-k8s-request-limit
├── 05-hpa
├── manifests
└── screenshots
# 디렉토리 생성 명령어
mkdir -p container-lab/{01-cgroup,02-image-layer,03-runtime,04-k8s-request-limit,05-hpa,manifests,screenshots}
10. 실습 1 – cgroup 구조 확인 + 장애 시나리오 체험
10-1. 목표
- 현재 호스트가 cgroup v1인지 v2인지 확인
- 컨테이너에 limit을 걸면 어디에 반영되는지 확인
- namespace와 달리 cgroup은 “얼마나 쓸 수 있는가”를 제어한다는 것을 체감
- cgroup 제한으로 인해 실제 장애가 어떻게 발생하는지 체험
- 장애 발생 시 어떤 지표와 로그를 통해 원인을 추적할 수 있는지 이해
10-2. 호스트에서 cgroup 버전 확인
lab-tools 또는 k8s 노드에서 실행
stat -fc %T /sys/fs/cgroup
mount | grep cgroup
예상:
cgroup2fs가 나오면 v2
추가 확인:
cat /sys/fs/cgroup/cgroup.controllers
10-3. systemd 계층 확인
systemd-cgls
또는
systemd-cgtop
이걸 보면 프로세스가 cgroup tree 안에 어떻게 들어가는지 감이 잡힌다.
10-4. Docker 컨테이너에 CPU / Memory 제한 걸기
docker run -d --name cg-demo --cpus 0.5 --memory 256m alpine sleep 300
docker inspect -f '{{.State.Pid}}' cg-demo
PID를 확인한 뒤:
cat /proc/<PID>/cgroup
이 경로를 보면 해당 컨테이너 프로세스가 어떤 cgroup에 속했는지 볼 수 있다.
10-5. Podman으로도 확인
podman run -d --name podman-cg-demo --cpus 0.5 --memory 256m alpine sleep 300
podman inspect --format '{{.State.Pid}}' podman-cg-demo
cat /proc/<PID>/cgroup
여기서 Docker와 Podman의 cgroup path가 조금 다르게 보일 수 있다.
10-6. cgroup 기반 장애 시나리오 체험
시나리오 1 – CPU 제한으로 인한 성능 저하 (느려지는 장애)
docker run --rm --name cpu-issue --cpus 0.2 polinux/stress stress --cpu 2 --timeout 60s
다른 터미널에서:
docker stats
또는:
top
관찰 포인트:
- CPU를 계속 사용하려 하지만 20% 수준에서 제한됨
- 프로세스는 죽지 않지만 응답이 매우 느려짐
- 실제 서비스라면 “장애처럼 느껴지는 성능 저하” 상황
원인 분석:
docker inspect cpu-issue | grep -i cpu
또는 cgroup 확인:
docker inspect -f '{{.State.Pid}}' <컨테이너이름>
cat /proc/<PID>/cgroup # 이걸 보면 해당 컨테이너 프로세스가 어떤 cgroup 경로에 속해 있는지 확인할 수 있음
여기서 나온 결과 예시:
0::/system.slice/docker-430b6fc6b889ac5664faf8f6b5678d9d9b3257c4f4d9e3374865a14e674e3586.scope
이걸로 알 수 있는 핵심은 다음이야:
- cgroup v2 환경이라는 것
0::형태는 cgroup v2 unified hierarchy를 의미함- v1이면 cpu, memory 등 컨트롤러별로 여러 줄이 나옴
- 이 프로세스가 속한 cgroup 경로
/system.slice/docker-<컨테이너ID>.scope- 즉, systemd가 관리하는 docker 컨테이너 단위 cgroup 안에 들어가 있음
- 컨테이너 ID와 매핑 가능
docker-430b6f...scope부분이 실제 컨테이너 ID와 연결됨- 어떤 컨테이너인지 추적 가능
- 리소스 제한이 실제로 적용되는 위치
- 이 경로 아래에서 CPU, memory 제한이 적용됨
- 예:
cd /sys/fs/cgroup/system.slice/docker-430b6f...scope cat cpu.stat cat memory.current
즉, 이 명령은 단순히 “어디에 속해있다”를 넘어서
👉 컨테이너 리소스 제한이 실제로 적용되는 커널 위치를 찾는 출발점이라고 보면 된다.
시나리오 2 – Memory 제한으로 인한 프로세스 종료 (OOM 장애)
docker run --rm --name mem-issue -m 128m polinux/stress stress --vm 1 --vm-bytes 256M --vm-hang 1
관찰 포인트:
- 일정 시간 후 컨테이너 종료
- 단순 성능 저하가 아니라 “프로세스가 죽는 장애”
로그 확인:
docker logs mem-issue
커널 로그 확인:
dmesg | grep -i oom
또는:
journalctl -k | grep -i oom
원인 분석 포인트:
- 메모리 limit 초과
- 커널 OOM Killer 동작
- cgroup memory 제한에 의해 강제 종료
시나리오 3 – cgroup 내부 상태 직접 확인
컨테이너 PID 확인:
docker rm -f cg-demo 2>/dev/null || true
docker run -d --name cg-demo alpine sleep 300
docker inspect -f '{{.State.Pid}}' cg-demo
cgroup 경로 확인:
cat /proc/<PID>/cgroup | grep docker
해당 경로에서:
cat /sys/fs/cgroup/<경로>/cpu.stat # throttled_usec 값이 0에서 증가하면 CPU throttling이 발생한 것이며, 값이 커질수록 제한으로 인해 지연된 시간이 누적된 것
cat /sys/fs/cgroup/<경로>/memory.current # 현재 컨테이너가 실제로 사용 중인 메모리 양 (bytes 단위)
cat /sys/fs/cgroup/<경로>/memory.max # 해당 cgroup에 설정된 메모리 최대 제한값 (limit), "max"로 나오면 제한 없음
확인 포인트:
- CPU throttling 발생 여부 (
nr_throttled) - 현재 메모리 사용량
- 설정된 메모리 limit
10-7. 실습 포인트
확인할 것:
docker/podman각각 cgroup path는 어디인지- 루트 권한 기반과 rootless 경로 차이는 없는지
systemd와 cgroup 계층이 어떤 관계인지- CPU 제한 시 “죽지 않고 느려지는” 이유
- Memory 제한 시 “바로 종료되는” 이유
- 장애 발생 시 어떤 로그(dmesg, journalctl)를 보면 되는지
11. 실습 2 – CPU throttling과 Memory OOM 체험
11-1. 목표
- CPU limit 초과 시 왜 “느려지는지”
- Memory limit 초과 시 왜 “죽는지”
- 두 limit이 같은 limit이 아니라는 점을 실감
11-2. Docker에서 CPU throttling 체험 (lab-tools VM에서 수행)
이 실습은 Docker가 설치된 lab-tools VM에서 수행한다.
docker run --rm --cpus 0.5 polinux/stress stress --cpu 2
다른 터미널에서:
docker stats
관찰 포인트:
- CPU를 계속 쓰려고 하지만 제한값 이상 올라가지 않음
- 프로세스가 바로 죽지는 않음
예상 결과:
docker stats출력에서 해당 컨테이너의 CPU 사용률이 약 **50% 내외(0.5 CPU 기준)*\*로 제한되는 것을 확인할 수 있다.- 예를 들어 아래와 비슷한 형태로 보일 수 있다:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT
abcd1234 hopeful_x 49.80% 5MiB / 256MiB
- stress 프로세스는 내부적으로 CPU를 2개 쓰려고 시도하지만,
--cpus 0.5제한 때문에 실제로는 절반 수준으로만 실행된다. - 컨테이너는 종료되지 않고 계속 실행되며, CPU 사용이 제한된 상태로 유지된다.
11-3. Docker에서 Memory OOM 체험 (lab-tools VM에서 수행)
docker run --rm -m 128m polinux/stress stress --vm 1 --vm-bytes 256M --vm-hang 1
실행 결과 예시:
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [1] (415) <-- worker 7 got signal 9
stress: WARN: [1] (417) now reaping child worker processes
stress: FAIL: [1] (421) kill error: No such process
stress: FAIL: [1] (451) failed run completed in 1s
관찰 포인트:
- CPU처럼 그냥 “조금 느려짐”이 아니라
- 메모리 한도를 넘기면 프로세스가 종료됨
signal 9는 SIGKILL이며, 이는 커널의 OOM Killer에 의해 강제 종료된 것- 실행 직후 바로 종료되는 이유는 컨테이너 메모리 제한(128MB)보다 훨씬 큰 메모리(256MB)를 즉시 할당하려 했기 때문
정리:
이 경우는 실행과 동시에 OOM이 발생한 것이 맞다.
컨테이너가 시작되자마자 메모리 할당 요청이 limit을 초과했고, 커널이 즉시 해당 프로세스를 종료 시킨 상황이다.
11-4. Podman으로도 동일 실습
podman run --rm --cpus 0.5 docker.io/polinux/stress stress --cpu 2
podman run --rm -m 128m docker.io/polinux/stress stress --vm 1 --vm-bytes 256M --vm-hang 1
12. 실습 3 – cgroup v1 / v2 비교(선택)
12-1. 왜 선택 실습인가?
Rocky 9는 기본적으로 cgroup v2가 자연스럽다.
그래서 v1은 비교용 VM을 하나 더 복제해서 보는 것이 가장 안전하다.
12-2. 권장 방법
lab-tools를 복제해서 lab-tools-v1 같은 VM을 하나 더 만든 뒤
부팅 파라미터를 조정해 cgroup v1 형태를 비교한다.
주의: 배포판/커널 조합에 따라 동작 방식이 조금 다를 수 있다.
이 실습은 “정확히 운영 전환”이 아니라 “구조 비교”가 목적이다.
예시 개념:
sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=0"
sudo reboot
재부팅 후:
stat -fc %T /sys/fs/cgroup
mount | grep cgroup
12-3. 비교 포인트
| 확인 항목 | v1에서 볼 것 | v2에서 볼 것 |
|---|---|---|
| 파일시스템 | controller별 분리 | unified hierarchy |
| 경로 | cpu, memory, blkio 분리 | /sys/fs/cgroup 아래 통합 |
| 체감 | 유연하지만 복잡 | 단순하고 일관적 |
13. 실습 4 – 컨테이너 이미지 layer 구조 체험 (lab-tools VM에서 진행)
13-1. 목표
- 이미지가 단일 파일이 아니라 layer의 집합이라는 점 체험
- Dockerfile 작성 방식이 이미지 크기와 캐시에 어떤 영향을 주는지 체험
13-2. 실습 파일 준비
~/container-lab/02-image-layer 에서 진행
cd ~/container-lab/02-image-layer
index.html 생성
방법 1 – echo 사용
echo "<h1>Hello Layer Lab</h1>" > index.html
방법 2 – 편집기로 생성
vi index.html
내용 입력:
<h1>Hello Layer Lab</h1>
Dockerfile.bad 생성
방법 1 – echo 사용
cat <<EOF > Dockerfile.bad
FROM rockylinux:9
RUN dnf install -y nginx
RUN dnf clean all
COPY index.html /usr/share/nginx/html/index.html
CMD ["nginx", "-g", "daemon off;"]
EOF
방법 2 – 편집기로 생성
vi Dockerfile.bad
내용 입력:
FROM rockylinux:9
RUN dnf install -y nginx
RUN dnf clean all
COPY index.html /usr/share/nginx/html/index.html
CMD ["nginx", "-g", "daemon off;"]
Dockerfile.good 생성
방법 1 – echo 사용
cat <<EOF > Dockerfile.good
FROM rockylinux:9
RUN dnf install -y nginx && \
dnf clean all && \
rm -rf /var/cache/dnf
COPY index.html /usr/share/nginx/html/index.html
CMD ["nginx", "-g", "daemon off;"]
EOF
방법 2 – 편집기로 생성
vi Dockerfile.good
내용 입력:
FROM rockylinux:9
RUN dnf install -y nginx && \
dnf clean all && \
rm -rf /var/cache/dnf
COPY index.html /usr/share/nginx/html/index.html
CMD ["nginx", "-g", "daemon off;"]
13-3. 이미지 빌드
docker build -t layer-demo:bad -f Dockerfile.bad .
docker build -t layer-demo:good -f Dockerfile.good .
13-4. history 확인
docker history layer-demo:bad
docker history layer-demo:good
docker images | grep layer-demo
Podman으로도 가능:
podman build -t layer-demo:good -f Dockerfile.good .
podman history layer-demo:good
13-5. 캐시 체험
index.html만 수정하고 다시 빌드
docker build -t layer-demo:good -f Dockerfile.good .
확인 포인트:
COPY index.html이후 layer만 다시 빌드되는지- 앞단 layer는 캐시 재사용되는지
14. 실습 5 – Docker / Podman / containerd 차이 체험
14-1. 목표
- 셋이 “비슷하게 보여도” 계층과 목적이 다르다는 것 체험
- 개발용, 운영용, K8s 노드용의 구분 감 잡기
14-2. Docker 확인
systemctl status docker
ps -ef | grep dockerd
docker ps
docker images
포인트:
dockerddaemon이 존재함
14-3. Podman 확인
podman ps
podman images
podman info --format '{{.Host.Security.Rootless}}'
포인트:
- daemonless(별도의 백그라운드 데몬 프로세스 없이, 명령을 실행할 때마다 필요한 컨테이너 작업을 직접 수행하는 구조) 구조
- rootless(일반 사용자 권한으로 컨테이너를 실행하여 root 권한 없이도 격리된 환경을 구성할 수 있는 방식) 친화적
추가로 systemd unit 생성도 가능:
podman run -d --name podman-nginx -p 8080:80 docker.io/library/nginx
podman generate systemd --name podman-nginx --new
14-4. Kubernetes 노드에서 containerd 확인
k8s-cp 또는 k8s-w1 에서
crictl ps
crictl images
ctr -n k8s.io containers ls
ctr -n k8s.io images ls
포인트:
- Kubernetes는 보통 containerd/CRI-O 같은 런타임과 직접 연결된다.
- Docker CLI가 없더라도 Kubernetes 컨테이너는 containerd에서 실행될 수 있다.
15. 실습 6 – Kubernetes request / limit / Pending 체험 (k8s-cp 또는 kubectl 접근 가능한 VM에서 진행)
15-1. 목표
- request는 스케줄링 기준
- limit은 런타임 상한
- request가 너무 크면 Pending
- CPU limit은 throttling
- Memory limit은 OOM
을 Kubernetes에서 직접 체험
15-2. 네임스페이스 생성
kubectl create namespace lab
15-3. CPU limit 체험용 Deployment
mkdir -p ~/container-lab/manifests
cd ~/container-lab
vi cpu-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cpu-demo
namespace: lab
spec:
replicas: 1
selector:
matchLabels:
app: cpu-demo
template:
metadata:
labels:
app: cpu-demo
spec:
containers:
- name: stress
image: docker.io/polinux/stress
command: ["stress"]
args: ["--cpu", "2"]
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
적용:
kubectl apply -f manifests/cpu-demo.yaml
kubectl get pods -n lab -o wide -w
kubectl top pod -n lab
관찰 포인트:
- CPU를 더 쓰고 싶어도 limit 근처에서 제어됨
- Pending이 아니라 정상 스케줄링됨
15-4. Memory OOM 체험용 Pod
vi manifests/mem-oom.yaml
apiVersion: v1
kind: Pod
metadata:
name: mem-oom
namespace: lab
spec:
restartPolicy: Never
containers:
- name: stress
image: docker.io/alpine:3.18
command: ["/bin/sh", "-c"]
args:
- apk add --no-cache stress-ng && stress-ng --vm 1 --vm-bytes 256M --vm-hang 1
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
적용:
kubectl apply -f manifests/mem-oom.yaml
kubectl get pod mem-oom -n lab -w
kubectl describe pod mem-oom -n lab
관찰 포인트:
OOMKilledLast StateReason
15-5. Pod Pending 체험
vi manifests/pending-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: pending-demo
namespace: lab
spec:
containers:
- name: nginx
image: docker.io/library/nginx
resources:
requests:
cpu: "8"
memory: "16Gi"
limits:
cpu: "8"
memory: "16Gi"
적용:
kubectl apply -f manifests/pending-demo.yaml
kubectl get pod pending-demo -n lab -w
kubectl describe pod pending-demo -n lab
관찰 포인트:
- 상태는 Pending
- 이벤트에
Insufficient cpu,Insufficient memory같은 메시지 확인 가능
15-6. 고급 확인 – cgroup까지 내려가 보기
worker 노드에서:
crictl ps | grep cpu-demo
crictl inspect <컨테이너ID> | jq '.info.pid'
cat /proc/<PID>/cgroup
해당 cgroup 경로에서 cpu.stat 확인:
cat /sys/fs/cgroup/<해당경로>/cpu.stat
여기서 nr_throttled 같은 값으로 throttling 흔적을 볼 수 있다.
16. 실습 7 – HPA 체험
16-1. 목표
- HPA가 어떻게 scale-out 하는지 확인
- 왜 즉시 반응하지 않는지 확인
- stateless 구조에서 scale-out이 잘 맞는 이유 체감
16-2. 샘플 애플리케이션 배포
vi manifests/php-apache.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-apache
namespace: lab
spec:
replicas: 1
selector:
matchLabels:
app: php-apache
template:
metadata:
labels:
app: php-apache
spec:
containers:
- name: php-apache
image: registry.k8s.io/hpa-example
ports:
- containerPort: 80
resources:
requests:
cpu: 200m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: php-apache
namespace: lab
spec:
selector:
app: php-apache
ports:
- port: 80
targetPort: 80
적용:
kubectl apply -f manifests/php-apache.yaml
kubectl get all -n lab
16-3. HPA 생성
vi manifests/php-apache-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
namespace: lab
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
적용:
kubectl apply -f manifests/php-apache-hpa.yaml
kubectl get hpa -n lab
16-4. 부하 발생
방법 1 – kubectl run으로 간단 부하 생성
kubectl run -it --rm load-generator -n lab --image=busybox:1.28 -- /bin/sh
쉘 안에서:
while true; do wget -q -O- http://php-apache; done
방법 2 – lab-tools에서 hey 사용
Service를 NodePort나 port-forward로 노출한 뒤:
kubectl port-forward svc/php-apache 8080:80 -n lab
hey -z 2m -c 20 http://127.0.0.1:8080/
16-5. HPA 관찰
다른 터미널에서:
kubectl get hpa -n lab -w
kubectl get deploy,pod -n lab -w
kubectl top pod -n lab
kubectl top pod -n lab --containers
레플리카 수를 확인하려면:
kubectl get deploy php-apache -n lab
kubectl get deploy php-apache -n lab -o wide
kubectl get deploy php-apache -n lab -o jsonpath='{.status.replicas}'
kubectl get deploy php-apache -n lab -o jsonpath='{.status.availableReplicas}'
또는 HPA 기준으로 확인하려면:
kubectl get hpa php-apache -n lab
출력에서 TARGETS, MINPODS, MAXPODS, REPLICAS 값을 보면 현재 스케일 상태를 확인할 수 있다.
예를 들어 아래와 같은 결과가 나온다면:
kubectl get deploy php-apache -n lab -o wide
kubectl get hpa php-apache -n lab
- Deployment:
5/5(READY / AVAILABLE) - HPA:
cpu: 66%/50%,REPLICAS: 5
이 의미는 다음과 같다:
- 현재 평균 CPU 사용률이 목표치(50%)보다 높은 상태 (66%)
- HPA가 이를 감지하고 scale-out을 수행
- 최대 replica 수(
maxReplicas=5)까지 이미 확장된 상태
즉, 이런 출력이 보이면 HPA가 정상적으로 동작 중이며, 이미 스케일 아웃이 완료된 상태라고 볼 수 있다.
추가로 HPA와 기존 파드를 함께 모니터링하려면:
kubectl describe hpa php-apache -n lab
kubectl get hpa php-apache -n lab -o yaml
관찰 포인트:
- CPU 사용률 증가 (각 Pod별로 확인)
- HPA가 target 초과를 인식하는 시점
- replica 수 증가 과정
- 기존 Pod와 새로 생성된 Pod의 리소스 사용량 비교
- 새 Pod가 뜨고 Ready 되기까지 시간 필요
부하가 줄어들면 어떻게 되는가?
결론부터 말하면 부하가 줄어들면 HPA는 scale-in(레플리카 감소)도 수행한다.
하지만 중요한 포인트가 있다:
- 즉시 줄어들지 않는다
- 일정 시간 동안 안정적으로 낮은 상태가 유지되어야 한다
예를 들어:
- 부하 테스트를 멈추고 CPU 사용률이 20% 이하로 떨어졌다고 해도
- 바로 5 → 1로 줄어들지 않는다
이유는 다음과 같다:
- stabilization window (안정화 시간)
- 급격한 scale-in으로 인한 서비스 불안정을 방지하기 위해 일정 시간 대기
- metrics 수집 주기
- metrics-server → HPA 반영까지 시간 지연 존재
- scale-down 정책
- 한 번에 줄이는 개수에도 제한이 있을 수 있음
실제로 확인하려면:
kubectl get hpa php-apache -n lab -w
kubectl get deploy php-apache -n lab -w
부하를 멈춘 뒤 관찰하면:
- CPU 사용률이 점점 낮아짐
- 일정 시간 후 replica 수가 서서히 감소
예시 흐름:
5 replicas → (부하 감소) → 5 유지 → 4 → 3 → 2 → 1
즉, HPA는 단순히 “늘리는 기능”이 아니라
부하에 따라 자동으로 늘리고 줄이는 양방향(auto scale) 기능이다.
실무에서 중요한 포인트
- scale-out보다 scale-in이 더 위험할 수 있다
- 너무 빨리 줄이면 트래픽이 다시 올라올 때 대응이 늦어질 수 있다
- 그래서 기본적으로 Kubernetes는 scale-in을 더 보수적으로 수행한다
정리하면:
- 부하 증가 → 비교적 빠르게 scale-out
- 부하 감소 → 천천히 scale-in
이 차이를 이해하는 것이 HPA를 제대로 쓰는 핵심이다.
16-6. 왜 HPA가 즉시 반응하지 않는지 체감하는 포인트
확인할 것:
- 메트릭 수집 시간
- HPA sync 주기
- 새 Pod 생성 시간
- 이미지 pull 시간
- readiness 통과 시간
즉, “부하가 오르면 바로 scale-out”이 아니라
부하 감지 → 판단 → 생성 → Ready 의 시간이 필요하다.
17. 선택 실습 – Podman rootless 체험
17-1. 목표
- rootless가 무엇인지 체험
- Docker와 Podman의 운영 철학 차이를 느껴보기
17-2. 일반 사용자로 실행
podman run -d --name rootless-nginx -p 8081:80 docker.io/library/nginx
podman ps
curl http://127.0.0.1:8081
17-3. 확인 포인트
- daemon 없이 동작
- 사용자 단위로 컨테이너 실행
podman generate systemd와 연결 가능