컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
os · 16

cgroups / namespace

namespacecgroups리눅스

컨테이너는 가상 머신처럼 운영체제를 따로 띄우지 않고도 프로세스를 독립된 환경처럼 보이게 만듭니다. 이 마법의 바탕에는 리눅스 커널의 두 가지 기능이 있습니다. namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 나누고, cgroups(control groups)는 프로세스 그룹이 쓸 수 있는 자원의 양을 제한하고 측정합니다. 한마디로 namespace는 "무엇이 보이는가", cgroups는 "얼마나 쓸 수 있는가"를 다룹니다.

namespace 덕분에 컨테이너 안의 프로세스는 자기만의 프로세스 목록, 네트워크, 파일 시스템을 가진 것처럼 보입니다. cgroups 덕분에 한 컨테이너가 호스트의 CPU나 메모리를 독점하지 못합니다. 이 둘은 컨테이너 전용 기능이 아니라 리눅스 커널의 일반 기능이고, Docker 같은 도구가 이를 조합해 컨테이너를 만듭니다. 이 토픽에서는 PID namespace가 보이는 범위를 나누는 방식, cgroups가 메모리와 CPU를 제한하는 방식, 둘이 합쳐져 컨테이너가 되는 과정을 봅니다.

핵심 개념

namespace

커널의 전역 자원을 추상화해, 한 namespace 안의 프로세스에게는 그 자원이 자기만의 독립된 인스턴스처럼 보이게 합니다. 같은 namespace 안의 프로세스끼리만 그 변화를 봅니다.

핵심 개념

namespace의 종류

PID(프로세스 번호), Mount(파일 시스템 마운트), Network(네트워크 인터페이스와 포트), UTS(호스트 이름), IPC(System V IPC, POSIX 메시지 큐), User(사용자와 그룹 ID), Cgroup, Time 등이 있습니다.

핵심 개념

PID namespace

프로세스 ID 공간을 분리합니다. 새 PID namespace의 첫 프로세스는 PID 1을 받고, 호스트에서는 같은 프로세스가 보통 다른 PID로 보입니다. 그 첫 프로세스가 끝나면 커널이 namespace 안의 나머지 프로세스를 종료합니다.

핵심 개념

cgroups

프로세스들을 계층적인 그룹으로 묶고, 그룹 단위로 CPU, 메모리, I/O 같은 자원의 사용량을 제한하고 계측합니다. 현재 리눅스는 통합 계층을 쓰는 cgroup v2를 권장합니다.

핵심 개념

메모리 상한과 OOM

그룹의 메모리 사용이 상한에 닿으면 커널은 먼저 메모리를 회수하려 하고, 줄일 수 없으면 그 그룹 안에서 OOM killer가 프로세스를 종료합니다. 종료 대상은 그 그룹 안에서 고르므로 다른 그룹의 프로세스는 보통 영향받지 않습니다.

핵심 개념

CPU 상한

주기마다 쓸 수 있는 CPU 시간(쿼터)을 정하면 그 이상 쓰려는 프로세스는 다음 주기까지 멈춥니다. 예를 들어 100,000µs 주기에 50,000µs면 코어 하나의 50%입니다.

namespace는 시야를, cgroups는 사용량을 나눕니다 — 서로 다른 문제를 푸는 기능이며, 컨테이너는 둘을 함께 씁니다.
컨테이너는 커널 기능의 조합입니다 — 별도의 "컨테이너"라는 커널 객체가 있는 것이 아니라, 프로세스에 여러 namespace와 cgroup을 적용한 결과입니다.
호스트에서는 일반 프로세스입니다 — 컨테이너 안의 프로세스도 호스트에서 보면 호스트 PID를 가진 평범한 프로세스입니다.
자원 상한 없이는 이웃이 피해를 봅니다 — 상한을 걸지 않은 컨테이너 하나가 메모리나 CPU를 다 써 같은 호스트의 다른 컨테이너를 느리게 하거나 종료시킬 수 있습니다.
이 시각화에서 확인할 것 — "namespace" 탭에서 PID가 컨테이너마다 1부터 매겨지는 모습을, "cgroups" 탭에서 메모리 상한 초과와 CPU 제한을, "컨테이너의 조합" 탭에서 일반 프로세스가 컨테이너가 되는 과정을 STEP으로 확인하세요.

namespace와 cgroups의 배경

namespace는 마운트 namespace를 시작으로 UTS, IPC, PID, 네트워크, 사용자 namespace 등이 리눅스 커널에 차례로 더해졌습니다. cgroups는 구글의 엔지니어(Paul Menage 등)가 시작해 리눅스 2.6.24에 처음 포함되었습니다. 이후 이 기능들을 조합해 쉽게 쓰게 해 주는 도구(LXC, Docker 등)가 나오면서 컨테이너가 널리 쓰이게 되었습니다.

숫자로 확인하기

PID 번호 — 호스트 PID가 3001, 3002인 컨테이너 A의 프로세스는 컨테이너 안에서 PID 1, 2로 보입니다. 호스트 PID 3101, 3102인 컨테이너 B의 프로세스도 B 안에서 PID 1, 2입니다.
메모리 상한 — 상한 512MB인 그룹이 128MB씩 요청하면 4번째 요청까지 512MB로 상한에 닿고, 5번째 요청(640MB가 되는 시점)에서 회수가 되지 않으면 OOM killer가 그 그룹의 프로세스를 종료합니다(값은 예시).
CPU 상한 — 주기 100,000µs마다 쿼터 50,000µs이면 50,000 ÷ 100,000 = 50%입니다. CPU를 100% 쓰려는 프로세스도 50%까지만 실행됩니다.

자주 하는 오해

namespace가 자원을 제한한다 — namespace는 보이는 범위를 나눌 뿐 사용량을 제한하지 않습니다. 사용량 제한은 cgroups의 일입니다.
cgroups가 프로세스를 숨긴다 — cgroups는 자원 사용량을 제한하고 계측할 뿐 다른 프로세스를 보이지 않게 하지 않습니다. 가리는 일은 namespace가 합니다.
컨테이너 안의 root는 호스트의 root와 똑같다 — 기본 설정에서는 UID가 같게 보일 수 있지만, 컨테이너의 권한은 제한되고 사용자 namespace를 쓰면 안의 root를 호스트의 일반 사용자에 대응시킬 수 있습니다.
컨테이너는 Docker만의 기술이다 — namespace와 cgroups는 리눅스 커널 기능이어서 Docker 없이도 쓸 수 있고, Docker 외의 컨테이너 런타임도 같은 기능을 씁니다.

면접에서 이렇게 물어요

면접 질문

namespace와 cgroups의 차이는?

namespace는 프로세스가 볼 수 있는 자원의 범위를 분리해 독립된 환경처럼 보이게 하고, cgroups는 프로세스 그룹이 쓸 수 있는 CPU, 메모리 등의 양을 제한하고 측정합니다.

면접 질문

컨테이너는 어떻게 격리되나요?

호스트 커널의 PID, 마운트, 네트워크 같은 namespace로 시야를 나누고, cgroups로 자원 상한을 걸어 프로세스를 격리합니다. 별도의 게스트 커널은 없습니다.

면접 질문

컨테이너가 메모리 상한을 넘으면?

커널이 먼저 메모리를 회수하려 하고, 줄일 수 없으면 그 cgroup 안에서 OOM killer가 프로세스를 종료합니다. 컨테이너가 예기치 않게 종료되는 대표 원인입니다.

면접 질문

컨테이너 안에서 PID 1은 무엇인가요?

PID namespace의 첫 프로세스입니다. 이 프로세스가 종료되면 커널이 namespace 안의 나머지 프로세스를 종료하므로 컨테이너도 끝납니다.

실제 시스템에서는

Docker 옵션 — docker run의 --memory와 --cpus 옵션은 컨테이너의 cgroup에 메모리와 CPU 상한을 설정합니다.
쿠버네티스 — 파드의 resources.requests와 limits는 컨테이너 런타임을 거쳐 cgroup 설정으로 반영되고, 메모리 limit을 넘으면 OOM으로 종료될 수 있습니다.
관찰 도구 — lsns는 시스템의 namespace 목록을, /proc/PID/ns는 프로세스가 속한 namespace를 보여 주고, cgroup v2는 /sys/fs/cgroup 아래 파일(memory.max, cpu.max 등)로 설정합니다.
systemd — systemd는 서비스마다 cgroup을 만들어 프로세스를 묶고, MemoryMax나 CPUQuota 같은 설정으로 자원 상한을 정합니다.

함께 보면 좋은 토픽

프로세스 — namespace가 나누는 대상인 프로세스가 무엇인지 봅니다.
메모리 관리 — cgroups의 메모리 상한이 걸리는 바탕을 봅니다.
CPU 스케줄링 — CPU 쿼터가 스케줄링과 어떻게 이어지는지 봅니다.
시스템 콜 — clone, unshare 같은 시스템 콜로 namespace가 만들어지는 과정을 봅니다.

참고 자료

셸 — PID namespace 만들어 보기 (unshare)

# 새 PID, mount namespace 안에서 셸 실행 (root 권한 필요)
sudo unshare --pid --fork --mount-proc bash

# 이 셸 안에서
echo $$             # 1  ← 새 namespace의 첫 프로세스
ps aux              # 이 namespace 안의 프로세스만 보임

# 다른 터미널(호스트)에서 namespace 목록
lsns -t pid

unshare는 새 namespace를 만들어 그 안에서 명령을 실행합니다. --pid와 --fork로 만든 셸은 새 PID namespace의 첫 프로세스라서 자기 PID가 1로 보입니다. --mount-proc은 이 namespace 전용으로 /proc을 다시 마운트해서, ps가 호스트 전체가 아니라 namespace 안의 프로세스만 보여 주게 합니다. 호스트의 다른 터미널에서 lsns -t pid를 실행하면 같은 셸이 호스트 PID로 보이는 것도 확인할 수 있습니다. 셸을 종료하면 namespace도 함께 사라집니다.

셸 — cgroup v2로 메모리와 CPU 상한 걸기

# cgroup v2 계층에 그룹 만들기 (root 권한, memory와 cpu 컨트롤러가 활성이어야 함)
sudo mkdir /sys/fs/cgroup/demo

echo 536870912   | sudo tee /sys/fs/cgroup/demo/memory.max   # 512MiB
echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max        # 주기 100ms당 50ms = 50%

# 현재 셸을 이 그룹에 넣기
echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs

# 사용량과 제한 횟수 확인
cat /sys/fs/cgroup/demo/memory.current
cat /sys/fs/cgroup/demo/cpu.stat

cgroup v2에서는 /sys/fs/cgroup 아래에 디렉터리를 만드는 것이 그룹을 만드는 일이고, 그 안의 파일에 값을 써서 설정합니다. memory.max는 바이트 단위의 메모리 상한이고, cpu.max는 쿼터와 주기를 마이크로초로 적습니다. cgroup.procs에 PID를 쓰면 그 프로세스가 그룹으로 옮겨져 상한을 적용받습니다. memory.current는 현재 메모리 사용량이고, cpu.stat에는 CPU 쿼터 때문에 멈춘 횟수(nr_throttled) 같은 통계가 있습니다. 하위 그룹에 컨트롤러를 켜려면 상위 그룹의 cgroup.subtree_control 설정이 필요할 수 있고, 배포판에서는 systemd가 이 계층을 관리하므로 환경에 따라 직접 쓰기 전에 확인해야 합니다.

Docker · 쿠버네티스 — 자원 상한 설정

# Docker: 메모리 512MB, CPU 0.5개
docker run -d --name web --memory 512m --cpus 0.5 nginx

# 컨테이너 안의 PID 1 확인
docker exec web cat /proc/1/comm

# 쿠버네티스 파드의 자원 요청과 상한
resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

docker run의 --memory와 --cpus는 컨테이너의 cgroup에 메모리 상한과 CPU 상한을 설정하며, 상한을 넘는 메모리를 쓰면 컨테이너 안의 프로세스가 OOM으로 종료될 수 있습니다. docker exec로 컨테이너 안에서 PID 1이 무엇인지 보면 컨테이너의 첫 프로세스가 PID namespace의 PID 1임을 확인할 수 있습니다. 쿠버네티스에서 requests는 스케줄러가 파드를 배치할 때 쓰는 값이며(CPU request는 경합 시 가중치로도 쓰입니다), limits는 런타임이 지키는 상한입니다. 메모리 limit을 넘으려 하면 OOM으로 종료될 수 있고, CPU limit은 throttling으로 제한됩니다.

Q1

리눅스 cgroups(control groups)의 역할은?

"얼마나 쓸 수 있는가"를 다룹니다.
cgroups는 프로세스 그룹 단위로 CPU, 메모리, I/O 등의 사용량을 제한하고 계측합니다. 컨테이너가 호스트 자원을 독점하지 못하게 하는 기반 기술입니다.

Q2

리눅스 namespace의 역할은?

"무엇이 보이는가"를 다룹니다.
namespace는 PID, 네트워크, 마운트, 호스트명 등 시스템 자원의 "보이는 범위"를 프로세스 그룹별로 분리해, 각 컨테이너가 자기만의 독립된 시스템을 쓰는 것처럼 보이게 합니다.

Q3

컨테이너 안에서 실행 중인 첫 프로세스가 자기 PID를 1로 인식하게 해 주는 기능은?

이름에 "프로세스 ID"가 들어 있습니다.
PID namespace는 프로세스 ID 공간을 분리해, 컨테이너 안의 첫 프로세스가 PID 1로 보이게 합니다. 호스트에서는 같은 프로세스가 다른 PID로 보입니다.