컨테이너는 Docker와 Kubernetes의 바탕이 되는 기술입니다. 이 기술은 리눅스 커널의 네임스페이스(namespace)와 리눅스 컨트롤 그룹(cgroup)을 이용해 자원을 격리하고 제어합니다. 컨테이너는 이 두 기능으로 단순한 이미지 실행 환경을 넘어서 리눅스의 강력한 자원 격리와 제한 기능을 활용합니다. 이 글에서는 리눅스 커널의 네임스페이스와 cgroup의 개념을 살펴보고, 두 기능의 사용법을 실습 예제와 함께 알아보겠습니다.

이 글은 AWS EC2의 Ubuntu 20.04 LTS 환경을 기준으로 작성했습니다.

컨테이너 격리의 핵심: 리눅스 커널의 네임스페이스

리눅스 커널의 네임스페이스는 커널 자원을 분할해 프로세스마다 격리된 시스템 환경을 제공하는 중요한 커널 기능입니다. 예를 들어, 하나의 건물 안에 여러 개의 독립된 방이 있는 것처럼, 네임스페이스로 컨테이너 내부의 프로세스가 호스트 시스템이나 다른 컨테이너 자원에 접근하지 못하도록 논리적 경계를 설정합니다. 이러한 격리 기능 덕분에 여러 컨테이너가 동일한 호스트에서 안전하고 효율적으로 실행될 수 있습니다.

주요 네임스페이스 종류

리눅스 커널은 다양한 네임스페이스를 제공하며, 각각 특정 자원을 격리합니다. 컨테이너 기술에서 주로 사용하는 8가지 네임스페이스는 다음과 같습니다.

  1. 마운트(mnt): 독립적인 파일 시스템 환경을 위한 마운트 지점 격리
  2. 프로세스 ID(pid): 컨테이너 내부의 프로세스를 독립적으로 관리하기 위한 PID 공간 분리
  3. 네트워크(net): IP 주소와 포트 등 독립적인 네트워크 스택 제공
  4. IPC: 프로세스 간 통신(IPC) 자원의 격리
  5. UTS: 호스트 이름과 도메인 이름의 격리
  6. 사용자(user): 사용자와 그룹 ID의 격리, 호스트 시스템에서 root 권한 제한
  7. cgroup: 자원 제어 그룹 계층 구조 격리
  8. 타임(time): 시스템 시간을 독립적으로 설정하도록 지원

unshare 명령어를 사용하면 원하는 네임스페이스를 생성해 지정한 프로그램을 격리된 환경에서 실행할 수 있습니다. 또 다양한 옵션으로 필요한 네임스페이스를 자유롭게 설정할 수 있습니다.

네임스페이스 격리 실습

간단한 예시로 PID 네임스페이스의 격리를 확인하겠습니다. 아래는 해당 명령어와 출력 예시입니다.

$ sudo unshare --pid --fork --mount-proc ps aux

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 10540 3128 pts/0 R+ 09:33 0:00 ps aux

위 명령어는 새로운 PID 네임스페이스를 생성하고 /proc 파일 시스템을 마운트한 뒤, ps aux를 실행합니다. 명령어 아래 출력 예시에서 보듯, 이 새로운 네임스페이스 안에서는 격리된 프로세스 목록만 확인할 수 있으며, 이때 프로세스는 PID 1부터 시작하는 독립된 프로세스 트리를 갖습니다.

네트워크 격리의 핵심: 네트워크 네임스페이스와 가상 이더넷(veth) 쌍

네트워크 네임스페이스는 각 컨테이너에 독립적인 네트워크 환경을 제공하는 핵심 기능입니다. 각 네임스페이스는 자체 네트워크 인터페이스, IP 주소, 라우팅 테이블, 방화벽 규칙이 있습니다. 이로써 여러 컨테이너가 한 호스트 위에서 동시에 동작해도 서로 네트워크 설정에 간섭받지 않습니다.

가상 이더넷(veth: virtual ethernet) 쌍은 네임스페이스 간 통신을 구성할 때 사용됩니다. veth는 두 개의 가상 네트워크 인터페이스로, 한쪽에서 전송한 패킷이 곧바로 다른 쪽에 수신됩니다. 이를 이용하면 한쪽 인터페이스를 컨테이너의 네트워크 네임스페이스에, 나머지 한쪽을 호스트나 다른 컨테이너 네임스페이스에 연결해 컨테이너와 호스트 간 또는 컨테이너 간에 네트워크 통신을 설정할 수 있습니다.

리눅스 네트워크 네임스페이스 환경에서 컨테이너 간 통신 구조

네트워크 네임스페이스 생성, veth 인터페이스 구성 실습

지금부터 네트워크 네임스페이스 두 개를 만들고, veth 쌍을 생성해 각각 연결하겠습니다. 네트워크 네임스페이스와 veth 인터페이스를 수동으로 구성해 리눅스 기반 컨테이너 네트워크가 격리된 환경을 만들고, 서로 통신하도록 연결되는 방식을 직접 확인할 수 있습니다.

  1. 네트워크 네임스페이스 생성: 독립적인 네트워크 환경을 테스트하기 위해 두 개의 네임스페이스(ns1, ns2)를 생성합니다.

    sudo ip netns add ns1
    sudo ip netns add ns2
  2. veth 쌍 생성, 네임스페이스 연결: 네임스페이스 간에 연결할 veth 쌍을 만들고, 각 네임스페이스에 인터페이스를 할당합니다.

    # ns1용
    sudo ip link add veth-host1 type veth peer name veth-ns1
    sudo ip link set veth-ns1 netns ns1

    # ns2용
    sudo ip link add veth-host2 type veth peer name veth-ns2
    sudo ip link set veth-ns2 netns ns2
  3. 네트워크 설정: 각 네임스페이스 내부에서 veth 인터페이스를 활성화하고, IP 주소와 기본 라우트를 설정합니다.

    sudo ip netns exec ns1 ip link set veth-ns1 up
    sudo ip netns exec ns1 ip addr add 10.0.0.101/24 dev veth-ns1
    sudo ip netns exec ns1 ip route add default via 10.0.0.1

    sudo ip netns exec ns2 ip link set veth-ns2 up
    sudo ip netns exec ns2 ip addr add 10.0.0.102/24 dev veth-ns2
    sudo ip netns exec ns2 ip route add default via 10.0.0.1

이후 단계에서는 bridge-utils를 사용해 브리지 네트워크를 구성하고, 실제 Docker 네트워크의 동작 방식을 함께 이해할 수 있습니다.

브리지 생성, veth 연결 실습

이어서 브리지를 생성하고, veth를 브리지에 연결하겠습니다. 브리지는 여러 네트워크 인터페이스를 하나의 네트워크 세그먼트로 묶는 가상 스위치 역할을 합니다. 컨테이너 간 통신을 위해 각 veth 인터페이스를 브리지에 연결해야 합니다.

  1. 브리지 생성, 활성화: 가상 브리지 br0를 생성하고 활성화한 뒤, 호스트에서 사용할 IP 주소를 설정합니다.

    sudo ip link add br0 type bridge
    sudo ip link set br0 up
    sudo ip addr add 10.0.0.1/24 dev br0
  2. veth를 브리지에 연결, 활성화: 호스트에 위치한 veth 인터페이스를 브리지에 연결하고, 동일한 네트워크 세그먼트에 포함합니다.

    sudo ip link set veth-host1 master br0
    sudo ip link set veth-host2 master br0

    sudo ip link set veth-host1 up
    sudo ip link set veth-host2 up

이제 두 네임스페이스가 br0 브리지로 동일한 네트워크에 연결됐습니다. 다음 단계에서는 ping 명령어로 통신이 가능한지 확인할 수 있습니다.

통신 테스트 실습

앞서 구성한 네트워크 구조의 정상 동작 여부를 확인하기 위해 ping 테스트를 진행하겠습니다.

  1. ns1 → 브리지 (10.0.0.1): ns1 네임스페이스에서 브리지 IP로 ping을 실행해 통신 가능 여부를 확인합니다.

    sudo ip netns exec ns1 ping 10.0.0.1
  2. ns1 → ns2 (10.0.0.102): ns1에서 ns2로 직접 패킷을 전송합니다.

    sudo ip netns exec ns1 ping 10.0.0.102
  3. 호스트(브리지) → ns1 (10.0.0.101): 호스트(브리지가 위치한 네임스페이스)에서 ns1 주소로 ping을 보내며 상호 통신 가능 여부를 확인합니다.

    ping 10.0.0.101
  4. ns2 → ns1 (10.0.0.101): ns2에서도 ns1로 ping을 시도해 양방향 통신 가능 여부를 확인합니다.

    sudo ip netns exec ns2 ping 10.0.0.101

모든 ping 응답이 정상적으로 돌아오면, 두 네임스페이스 간 통신과 호스트-네임스페이스 간 통신이 모두 올바르게 설정됐다는 의미입니다.

위 실습으로 네트워크 네임스페이스와 veth, 가상 브리지로 구성한 격리된 가상 네트워크 환경이 정상적으로 작동하는 걸 확인할 수 있습니다.

자원 관리의 핵심: 리눅스 컨트롤 그룹(cgroups)

리눅스 컨트롤 그룹(cgroups)은 프로세스 그룹의 자원 사용을 제어하고 제한하는 핵심 커널 기능입니다. cgroups로 CPU, 메모리, 디스크 I/O 등 시스템 자원을 효율적으로 제한·측정해 프로세스 그룹별로 격리할 수 있습니다. cgroups는 계층 구조를 기반으로, 부모 cgroup 아래에 여러 자식 cgroup을 생성해 각각 다른 자원 제한을 부여할 수 있습니다. 이 구조는 멀티 테넌트 환경에서 특정 프로세스의 자원 독점을 방지하고, 컨테이너별 자원 할당의 안정성 보장에 도움이 됩니다.

cgroup 구조 확인 실습

먼저 현재 시스템에서 cgroup이 어떻게 마운트됐는지 살펴보겠습니다. 이는 현재 시스템이 cgroup v1과 v2 중 어느 버전을 사용하는지, 어느 컨트롤러가 어떤 방식으로 마운트됐는지 확인하는 과정입니다.

$ mount | grep cgroup

tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755,inode64)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/misc type cgroup (rw,nosuid,nodev,noexec,relatime,misc)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)

출력 결과에서 보듯 cgroup v1과 v2가 혼용됐으며, 여러 cgroup 컨트롤러가 독립적으로 마운트됐습니다. 현재 사용 중인 cgroup 버전은 아래 명령어로 간단히 확인할 수 있습니다.

$ stat -fc %T /sys/fs/cgroup/

tmpfs

만약 cgroup2fs가 출력되면 cgroup v2, tmpfs가 출력되면 cgroup v1을 사용한다는 의미입니다. Ubuntu 21.10과 Debian 11 버전 이후로는 기본적으로 cgroup v2가 활성화됐습니다.

현재 시스템이 사용하는 cgroup 버전을 확인했으니, 다음 단계에서 cgroup을 생성하고 자원을 제어할 수 있습니다.

cgroup 생성, 자원 제한 실습

이제 cgroup을 생성하고, 메모리와 CPU 자원을 제한하겠습니다. 자원 제한의 적용 방식과 제한 조건을 초과할 때 발생하는 동작을 확인할 수 있습니다.

cgroup 생성

아래 명령어로 cpumemory 컨트롤러를 포함하는 /testgroup이라는 새로운 cgroup을 생성합니다.

sudo cgcreate -g "cpu,memory:/testgroup"

이 명령어를 실행하면 /sys/fs/cgroup/cpu/testgroup/과 같은 경로에 디렉터리가 생성됩니다.

메모리 사용 제한 설정

다음 설정으로 testgroup에 속한 프로세스가 150MB 이상 메모리를 사용하지 못하도록 제한합니다.

echo 150000000 | sudo tee /sys/fs/cgroup/memory/testgroup/memory.limit_in_bytes

이 제한을 초과하면 OOM(Out of Memory)이 발생하거나 해당 프로세스가 강제 종료됩니다.

CPU 사용 제한 설정 (50%)

아래 설정으로 CPU 스케줄링 주기(기본 100ms) 중 50ms만 CPU를 사용하도록 해 CPU 사용률을 약 50%로 제한합니다.

echo 100000 | sudo tee /sys/fs/cgroup/cpu/testgroup/cpu.cfs_period_us
echo 50000 | sudo tee /sys/fs/cgroup/cpu/testgroup/cpu.cfs_quota_us

다음 단계에서는 다양한 스크립트를 실행해 자원 제한의 실제 적용 여부를 테스트할 수 있습니다.

자원 소비 테스트 실습

이어서 CPU 부하와 메모리 점진 할당 스크립트를 작성하며 자원 제한 테스트를 준비하겠습니다. 앞서 설정한 자원 제한이 실제 실행 중인 프로세스에 적용되는 방식과 제한 조건을 초과했을 때 시스템 반응을 파악하는 방식을 설정할 수 있습니다.

CPU 부하 스크립트

아래 스크립트는 CPU를 100% 사용하도록 설계됐습니다. 그러나 앞서 설정한 CPU 사용 제한(cgroup) 때문에 CPU를 50% 이상 사용할 수 없습니다.

cat <<'EOF' > cpu_hog.sh
#!/bin/bash
echo "[PID $$] CPU 소비 시작..."
while :; do sha256sum /dev/urandom > /dev/null; done
EOF
chmod +x cpu_hog.sh


메모리 점진 할당 스크립트

다음 스크립트는 0.1초 간격으로 1MB씩 메모리를 할당하며, 최대 300MB까지 할당을 시도하도록 설계됐습니다. 150MB 제한에 도달하면 OOM이 발생하거나, 프로세스가 강제 종료됩니다.

cat <<'EOF' > mem_hog.py
import time, os

a = []
mb = 1024 * 1024

print(f"[PID: {os.getpid()}] 메모리 점진 할당 시작", flush=True)

for i in range(300):
a.append('x' * mb)
if i % 10 == 0:
print(f"{i}MB 할당됨", flush=True)
time.sleep(0.1)
EOF

제한된 환경에서 백그라운드 실행

두 스크립트를 각각 백그라운드로 실행합니다. 로그는 cpu.log, mem.log에 기록됩니다.

sudo cgexec -g "cpu,memory:/testgroup" ./cpu_hog.sh > cpu.log 2>&1 &
sudo cgexec -g "cpu,memory:/testgroup" python3 mem_hog.py > mem.log 2>&1 &

다음 단계에서는 기록된 로그를 확인하고, 메모리와 CPU 사용 제한의 실제 적용 결과를 분석할 수 있습니다.

실행, 확인 실습

위 스크립트를 실행한 뒤 기록된 로그를 살펴보고 메모리, CPU 사용량과 자원 제한의 적용 여부를 확인하겠습니다. 자원 제한이 추상적 설정에 그치지 않고, 실제 실행 흐름과 시스템 로그에 반영되는 방식을 직접 경험할 수 있습니다.

로그 확인

다음 명령어로 로그를 확인합니다.

$ cat cpu.log
[PID 11626] CPU 소비 시작...

$ cat mem.log
[PID: 11663] 메모리 점진 할당 시작
0MB 할당됨
10MB 할당됨
20MB 할당됨
30MB 할당됨
40MB 할당됨
50MB 할당됨
60MB 할당됨
70MB 할당됨
80MB 할당됨
90MB 할당됨
100MB 할당됨
110MB 할당됨
120MB 할당됨
130MB 할당됨

CPU 로그에서 PID가 11626인 프로세스가 CPU 사용을 시작한 걸 확인할 수 있습니다. 메모리 로그에서는 PID 11663 프로세스가 130MB까지 점진적으로 메모리를 할당했다고 나옵니다.

메모리 사용 확인

다음 명령어로 메모리 사용 현황을 확인합니다.

$ cat /sys/fs/cgroup/memory/testgroup/memory.usage_in_bytes
57753692

$ cat /sys/fs/cgroup/memory/testgroup/memory.max_usage_in_bytes
149999616

$ sudo dmesg | grep -i "killed process"
[19401.035237] Memory cgroup out of memory: Killed process 59860 (python3) total-vm:158728kB, anon-rss:145316kB, file-rss:5808kB, shmem-rss:0kB, UID:0 pgtables:344kB oom_score_adj:0

현재 메모리 사용량은 약 57MB이며, 최대 사용량은 약 149MB에 달했습니다. 이는 150MB 제한에 근접해 OOM이 발생했다는 걸 보여줍니다. 또 커널이 python3 프로세스(PID 59860)를 강제 종료한 흔적을 발견할 수 있습니다.

CPU 사용 확인

다음 명령어로 CPU 사용 현황을 확인합니다.

$ cat /sys/fs/cgroup/cpu/testgroup/cpu.stat
nr_periods 404
nr_throttled 391
throttled_time 10407935072

총 404번의 CPU 사용 주기 중 391번이 제한(throttling)됐고, 10.4초 동안 CPU가 사용 제한 상태에 있었다는 걸 알 수 있습니다. 이는 앞서 설정한 50% 사용 제한이 정상적으로 적용됐다는 걸 의미합니다.

이로써 앞서 설정한 메모리와 CPU 사용 제한이 정상적으로 적용됐으며, 자원 제한 초과 시 시스템이 해당 프로세스를 적절히 제어했다는 걸 확인할 수 있습니다.

Docker의 리눅스 커널 기능 자동화 방식

Docker를 활용하면 앞서 수동으로 설정한 리눅스 커널의 격리 기능을 자동화할 수 있습니다. 지금부터 Docker에서 해당 기능의 자동화 방식을 알아보고, Docker 자동화 의의를 살펴보겠습니다.

Docker를 사용하면 다음 명령어로 CPU 사용량을 0.5코어, 메모리를 200MB로 제한한 Ubuntu 컨테이너를 생성하고 접속할 수 있습니다.

# Docker로 유사한 컨테이너 실행
sudo docker run --rm -it --cpus=0.5 --memory=200m ubuntu bash

네임스페이스

Docker 컨테이너 안에서 다음 명령어를 실행하면, 컨테이너의 네임스페이스와 프로세스 환경을 확인할 수 있습니다.

$ hostname
5352704af40b

$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 4588 4044 pts/0 Ss 09:11 0:00 bash
root 362 0.0 0.2 7888 4088 pts/0 R+ 09:13 0:00 ps aux

$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether f6:01:39:e1:8b:1a brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever

Docker는 앞서 수동으로 구성했던 격리 환경을 자동화해 제공하며 이미지 관리·볼륨 마운트·포트 포워딩 등 다양한 추가 기능까지 지원합니다.

cgroup

Docker 컨테이너의 자원 제한을 살펴보면, cgroup의 적용 방식을 명확하게 확인할 수 있습니다.

$ cat /proc/self/cgroup
13:net_cls,net_prio:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
12:misc:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
11:blkio:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
10:perf_event:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
9:freezer:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
8:cpuset:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
7:memory:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
6:pids:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
5:hugetlb:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
4:cpu,cpuacct:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
3:devices:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
2:rdma:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
1:name=systemd:/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e
0::/docker/636658c189c89762bbc846802c7216db260831ab679654b7bc831cec34d25a9e

$ cat /sys/fs/cgroup/memory/memory.limit_in_bytes
209715200

$ cat /sys/fs/cgroup/memory/memory.usage_in_bytes
1695744

$ cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
50000

$ cat /sys/fs/cgroup/cpu/cpu.cfs_period_us
100000

위 출력 결과로 Docker가 cgroup으로 설정된 자원 제한을 정확히 적용하고 있다는 걸 확인할 수 있습니다. CPU에서는 quota/period 비율이 0.5(50000/100000)로 설정돼 정확히 0.5 CPU 코어로 제한되고, 메모리는 약 200MB(209715200 바이트)로 제한됐습니다. 현재 메모리 사용량은 약 1.7MB 정도로 매우 적습니다.

이처럼 Docker는 리눅스 커널 기능을 기반으로 자원 격리와 제한을 자동화해 제공합니다. 사용자는 복잡한 설정 없이 간단한 명령어로 컨테이너 환경을 손쉽게 구성할 수 있습니다.

맺음말

지금까지 Docker 컨테이너 기술의 핵심인 리눅스 커널의 네임스페이스와 cgroup 개념, 사용법을 실습 예제와 함께 살펴봤습니다. 컨테이너 격리는 리눅스 네임스페이스, 네트워킹은 가상 이더넷(veth) 쌍과 브리지, 자원 관리는 cgroups로 이뤄지며, Docker는 이 복잡한 커널 기능을 직관적인 인터페이스로 추상화하고, 자동화합니다.

이러한 내부 동작 원리를 이해하면, Docker를 더욱 안전하고 효과적으로 운영할 수 있습니다. 이는 실제 서비스 환경에서 리소스 격리, 보안 정책, 성능 제어를 정밀하고 안정적으로 설계하는 데 도움이 됩니다.

지금 이 기술이 더 궁금하세요? 인포그랩의 DevOps 전문가가 알려드립니다.