컨테이너와 Docker
컨테이너가 Linux 커널의 네임스페이스와 cgroups를 조합해 프로세스를 격리한다는 것을 PID 네임스페이스와 CPU 할당량 모델로 확인합니다. Docker 이미지의 레이어와 오버레이 파일 시스템의 쓰기 시점 복사, Dockerfile과 실행 옵션, 커널을 공유하는 격리의 한계와 보완 기술을 정리합니다.
앞 글의 가상 머신은 VM마다 운영체제를 통째로 실행하므로 격리가 튼튼하지만, 그만큼 무겁습니다. VM마다 게스트 커널과 사용자 공간 전체를 준비해야 하므로 부팅에 시간이 걸리고 메모리와 디스크도 많이 듭니다. 응용 프로그램 하나를 격리하려고 운영체제 하나를 통째로 띄우는 셈입니다.
컨테이너(container)는 커널을 따로 두지 않고, 호스트의 커널 하나를 함께 쓰면서 프로세스들을 서로 격리합니다. 2013년 Docker가 컨테이너를 만들고 배포하는 과정을 쉽게 만들면서, 컨테이너는 소프트웨어를 배포하는 표준 방식이 되었습니다. 이 글에서는 컨테이너가 커널의 어떤 기능으로 만들어지는지, Docker 이미지가 어떻게 구성되는지, 그리고 커널을 함께 쓰는 격리의 한계를 살펴봅니다.
컨테이너와 가상 머신
가상화 · 커널과 공유 자원
VM은 게스트 커널을 나누고, 일반 컨테이너는 호스트 커널을 공유합니다
커널 경계와 실행 환경을 먼저 비교합니다. 두 방식 모두 호스트 자원을 사용하므로 자원 제한과 장애 영향은 별도로 판단해야 합니다.
| 비교 기준 | 가상 머신 | 리눅스 컨테이너 |
|---|---|---|
| 커널 | 게스트마다 별도 커널을 실행 | 일반적인 런타임은 호스트 커널을 공유 |
| 실행 환경 | 게스트 OS와 가상 장치를 준비 | 이미지의 사용자 공간을 격리된 프로세스로 실행 |
| 공유되는 자원 | 호스트와 하이퍼바이저의 자원을 공유 | 호스트 커널과 자원을 공유하며 정책으로 제한 |
- 커널
- 가상 머신: 게스트마다 별도 커널을 실행리눅스 컨테이너: 일반적인 런타임은 호스트 커널을 공유
- 실행 환경
- 가상 머신: 게스트 OS와 가상 장치를 준비리눅스 컨테이너: 이미지의 사용자 공간을 격리된 프로세스로 실행
- 공유되는 자원
- 가상 머신: 호스트와 하이퍼바이저의 자원을 공유리눅스 컨테이너: 호스트 커널과 자원을 공유하며 정책으로 제한
시작 시간·이미지 크기·배치 밀도는 측정 조건에 따라 달라집니다. VM 위에 컨테이너를 실행하면 두 경계를 함께 사용할 수 있습니다.
그림처럼 VM은 게스트마다 커널을 따로 실행하고, 컨테이너는 호스트의 커널 위에서 격리된 프로세스로 실행됩니다. 컨테이너 안의 프로세스는 호스트에서 보면 보통의 프로세스이므로, 시작이 빠르고 메모리도 응용 프로그램이 쓰는 만큼만 듭니다. 대신 모든 컨테이너가 같은 커널을 쓰므로, 커널에 취약점이 있으면 한 컨테이너에서 시작된 공격이 호스트와 다른 컨테이너에까지 미칠 수 있습니다. 또 Linux 커널 위에서는 Linux 프로그램만 실행할 수 있습니다.
컨테이너는 커널 기능의 조합이다
컨테이너는 하나의 새로운 기술이라기보다, Linux 커널에 이미 있는 두 가지 기능을 조합한 것입니다. 네임스페이스는 프로세스가 무엇을 볼 수 있는지를, cgroups는 얼마나 쓸 수 있는지를 정합니다. Docker나 containerd 같은 컨테이너 런타임은 이 기능들로 프로세스를 감싸서 실행해 줄 뿐입니다.
네임스페이스: 보이는 범위를 나누기
네임스페이스(namespace)는 시스템 자원의 이름 공간을 나누어, 프로세스마다 자기만의 시스템을 가진 것처럼 보이게 합니다. namespaces(7)에 정리된 주요 네임스페이스는 다음과 같습니다.
| 네임스페이스 | 나누는 것 |
|---|---|
| PID | 프로세스 번호. 컨테이너의 첫 프로세스는 그 안에서 PID 1이 된다 |
| Network | 네트워크 장치, IP 주소, 라우팅 표, 포트 번호 |
| Mount | 마운트 목록. 컨테이너가 자기만의 루트 파일 시스템을 보게 한다 |
| UTS | 호스트 이름 |
| IPC | System V IPC와 POSIX 메시지 큐 |
| User | 사용자와 그룹 ID. 컨테이너 안의 root를 호스트의 일반 사용자에 대응시킬 수 있다 |
| Cgroup | 프로세스가 보는 cgroup 계층 |
PID 네임스페이스는 계층을 이룹니다. 새 PID 네임스페이스 안에서 만든 프로세스는 그 네임스페이스에서 하나, 그리고 위에 있는 모든 네임스페이스에서 각각 PID를 받습니다. 다음 코드는 이 구조를 흉내 냅니다.
class PidNamespace:
def __init__(self, parent=None):
self.parent, self.next_pid, self.procs = parent, 1, {}
def spawn(self, name):
pids, ns = [], self
while ns: # 자기 네임스페이스와 모든 조상에서 PID를 하나씩 받는다
pids.append(ns.next_pid)
ns.procs[ns.next_pid] = name
ns.next_pid += 1
ns = ns.parent
return pids
host = PidNamespace()
for name in ("systemd", "sshd", "containerd"):
host.spawn(name)
container = PidNamespace(parent=host)
for name in ("nginx", "worker"):
inside, outside = container.spawn(name)
print(f"{name}: 컨테이너 안 PID {inside}, 호스트에서 PID {outside}")
print("컨테이너 안에서 보이는 것:", list(container.procs.values()))
print("호스트에서 보이는 것:", list(host.procs.values()))nginx: 컨테이너 안 PID 1, 호스트에서 PID 4
worker: 컨테이너 안 PID 2, 호스트에서 PID 5
컨테이너 안에서 보이는 것: ['nginx', 'worker']
호스트에서 보이는 것: ['systemd', 'sshd', 'containerd', 'nginx', 'worker']컨테이너 안의 nginx는 자기가 PID 1이라고 보지만, 호스트에서는 PID 4인 보통의 프로세스입니다. 컨테이너 안에서는 호스트의 프로세스가 보이지 않고, 호스트에서는 컨테이너의 프로세스가 모두 보입니다. Linux에서는 unshare 명령으로 새 네임스페이스를 만들어 직접 확인할 수 있습니다.
sudo unshare --fork --pid --mount-proc readlink /proc/self # 새 PID 네임스페이스 안에서 자기 PID 출력
sudo unshare --uts sh -c 'hostname box; hostname' # 새 UTS 네임스페이스에서만 호스트 이름이 바뀐다
ls -l /proc/$$/ns # 지금 셸이 속한 네임스페이스들
lsns # 시스템의 네임스페이스 목록unshare(1)의 예제에 따르면 첫 명령은 새 PID 네임스페이스의 첫 프로세스로 readlink를 실행하므로 1을 출력합니다. --mount-proc은 마운트 네임스페이스도 새로 만들고 그 안에 /proc을 다시 마운트해서, ps 같은 도구가 새 네임스페이스의 프로세스만 보게 합니다. 두 번째 명령이 바꾼 호스트 이름은 새 UTS 네임스페이스 안에서만 보이고, 명령이 끝난 뒤 호스트의 이름은 그대로입니다.
cgroups: 쓸 수 있는 양을 제한하기
네임스페이스는 보이는 범위만 나눌 뿐, 한 컨테이너가 CPU와 메모리를 독차지하는 것은 막지 못합니다. 이를 막는 것이 cgroups(control groups)입니다. cgroups는 프로세스들을 계층적인 그룹으로 묶고, 그룹마다 CPU, 메모리, I/O, 프로세스 수의 사용량을 제한하거나 나누어 줍니다. 요즘 배포판이 쓰는 cgroup v2에서는 /sys/fs/cgroup 아래의 디렉터리가 그룹이고, 그 안의 파일에 값을 쓰면 제한이 걸립니다.
sudo mkdir /sys/fs/cgroup/demo
echo $((256 * 1024 * 1024)) | sudo tee /sys/fs/cgroup/demo/memory.max # 메모리 256MiB(바이트 단위)
echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max # 100ms마다 그룹 합계 50ms
echo 100 | sudo tee /sys/fs/cgroup/demo/pids.max # 스레드를 포함한 태스크 100개
echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs # 지금 셸을 그룹에 넣는다컨테이너 · 자원 한도
cgroup은 자원마다 서로 다른 한도를 적용합니다
cgroup v2 예제에서 메모리는 바이트, CPU는 기간별 시간 예산, PID는 스레드를 포함한 태스크 수를 제한합니다. CPU 예산은 코어 예약이나 CPU 고정과 다릅니다.
| 대상 | 코드의 설정 | 해석 |
|---|---|---|
| 메모리 | memory.max = 268435456 | 256MiB. 회수로 해소되지 않는 압박은 cgroup OOM으로 이어질 수 있음 |
| CPU 시간 | cpu.max = 50000 100000 | 공정 스케줄링 클래스: 100ms마다 그룹 합계 50ms. 예산 소진 시 제한되며 특정 CPU에 고정하지 않음 |
| 태스크 수 | pids.max = 100 | 스레드를 포함한 태스크 생성 한도. 한도 초과의 새 fork/clone이 실패할 수 있음 |
| 디스크 I/O | 이 예제는 설정하지 않음 | io.max는 장치별 처리량·IOPS를 제한할 때 별도로 구성 |
- 메모리
- 코드의 설정: memory.max = 268435456해석: 256MiB. 회수로 해소되지 않는 압박은 cgroup OOM으로 이어질 수 있음
- CPU 시간
- 코드의 설정: cpu.max = 50000 100000해석: 공정 스케줄링 클래스: 100ms마다 그룹 합계 50ms. 예산 소진 시 제한되며 특정 CPU에 고정하지 않음
- 태스크 수
- 코드의 설정: pids.max = 100해석: 스레드를 포함한 태스크 생성 한도. 한도 초과의 새 fork/clone이 실패할 수 있음
- 디스크 I/O
- 코드의 설정: 이 예제는 설정하지 않음해석: io.max는 장치별 처리량·IOPS를 제한할 때 별도로 구성
기존 cgroup 경로, 부모의 컨트롤러 활성화와 쓰기 권한을 전제합니다. 마지막 echo는 현재 셸을 이동하며 이미 존재하는 자식 프로세스까지 자동 이동시키지는 않습니다.
cpu.max의 두 값은 마이크로초 단위의 할당량과 주기로, 100ms마다 그룹 전체가 CPU를 50ms까지 쓸 수 있다는 뜻입니다. 이 할당량은 태스크 하나하나가 아니라 그룹 전체의 합계에 걸립니다.
quota, period = 50_000, 100_000 # cpu.max = "50000 100000"(마이크로초)
demand = {"web": 20_000, "worker": 20_000, "cron": 15_000} # 이번 주기에 원하는 CPU 시간
used = 0
for task, want in demand.items(): # 단순화: 차례로 실행된다고 본다
got = min(want, quota - used)
used += got
print(f"{task}: 원함 {want // 1000}ms, 받음 {got // 1000}ms")
print(f"그룹 합계 {used // 1000}ms = 예산 {quota // 1000}ms, 모자란 몫은 다음 주기까지 기다린다")web: 원함 20ms, 받음 20ms
worker: 원함 20ms, 받음 20ms
cron: 원함 15ms, 받음 10ms
그룹 합계 50ms = 예산 50ms, 모자란 몫은 다음 주기까지 기다린다세 태스크가 원한 55ms는 그룹의 예산 50ms를 넘으므로, 예산을 다 쓴 뒤에는 그룹 전체가 다음 주기까지 멈춥니다. 실제로는 스케줄러가 세 태스크를 번갈아 실행하므로 누가 얼마를 받을지는 이 모델과 다르지만, 그룹이 받는 합계가 예산을 넘지 않는다는 점은 같습니다. 메모리는 사정이 다릅니다. CPU는 기다리게 하면 되지만, 메모리가 memory.max에 이르면 커널은 먼저 그 그룹의 페이지를 회수하려 하고, 그래도 모자라면 그룹 안의 프로세스를 OOM 킬러로 종료합니다.
더 깊이: 끊는 한도 대신 늦추는 한도
cgroup v2에는 memory.max 말고 memory.high라는 메모리 한도가 하나 더 있습니다. cgroup v2 문서에 따르면 사용량이 memory.high를 넘은 그룹의 프로세스는 메모리를 할당할 때마다 직접 회수를 하도록 강제되어 느려지지만, 이 한도를 넘었다는 이유로 OOM 킬러가 실행되는 일은 없습니다. 할당이 아주 많이 몰려 회수가 따라가지 못하는 극단적인 경우에는 이 한도를 넘을 수도 있습니다.
문서는 오히려 memory.high를 메모리 사용을 조절하는 주된 방법으로 소개합니다. 작업 집합의 크기는 정확히 예측하기 어려운데, memory.max만 쓰면 너무 작게 잡았을 때 프로세스가 죽으므로 대개 넉넉히 잡게 되고 그만큼 메모리를 낭비합니다. memory.high는 너무 작게 잡아도 프로세스가 죽는 대신 성능이 서서히 떨어질 뿐이므로, 그 변화를 지켜보며 받아들일 만한 성능을 내는 가장 작은 값을 찾아갈 수 있습니다. memory.max는 그래도 넘치는 사용량을 막아, 결함이 있거나 악의적인 프로그램을 가두는 마지막 장치로 둡니다.
Docker 이미지와 레이어
Docker는 컨테이너를 만들고, 나누어 주고, 실행하는 도구입니다. Docker 이미지(image)는 컨테이너의 루트 파일 시스템이 될 파일들과, 실행할 명령이나 환경 변수 같은 설정을 담은 묶음입니다. 실행 중인 프로세스의 메모리를 떠 둔 것이 아니라 파일과 설정일 뿐이어서, 이미지로 컨테이너를 실행할 때마다 프로그램은 처음부터 시작됩니다.
이미지는 여러 개의 읽기 전용 레이어(layer)를 쌓아 만듭니다. 운영체제의 기본 파일이 든 베이스 레이어 위에 패키지를 설치한 레이어, 애플리케이션 코드를 담은 레이어가 차례로 올라가고, 컨테이너를 실행하면 맨 위에 그 컨테이너만의 쓰기 가능한 레이어가 하나 더 붙습니다. Linux에서는 보통 오버레이 파일 시스템(OverlayFS)이 이 레이어들을 하나의 디렉터리 트리로 겹쳐 보여 줍니다.
WHITEOUT = object() # 아래 레이어의 파일을 가리는 표시
base = {"/etc/os-release": "Debian 12", "/usr/bin/python3": "python"} # 베이스 이미지 레이어
app = {"/app/app.py": "print('hi')"} # 애플리케이션 레이어
def read(layers, path):
for layer in reversed(layers): # 맨 위 레이어부터 찾는다
if path in layer:
return None if layer[path] is WHITEOUT else layer[path]
return None
upper1, upper2 = {}, {} # 컨테이너마다 쓰기 가능한 맨 위 레이어
upper1["/etc/os-release"] = "patched" # 고치면 맨 위 레이어에 사본이 생긴다
upper2["/app/app.py"] = WHITEOUT # 지우면 맨 위 레이어에 가림 표시를 둔다
for name, upper in (("컨테이너1", upper1), ("컨테이너2", upper2)):
layers = [base, app, upper]
print(f"{name}: os-release={read(layers, '/etc/os-release')}, app.py={read(layers, '/app/app.py')}")
print("베이스 레이어의 os-release:", base["/etc/os-release"])컨테이너1: os-release=patched, app.py=print('hi')
컨테이너2: os-release=Debian 12, app.py=None
베이스 레이어의 os-release: Debian 12파일을 읽을 때는 맨 위 레이어부터 찾아 내려갑니다. 아래 레이어의 파일을 고치면 그 파일 전체가 맨 위 레이어로 복사된 뒤 고쳐지는데, 이것을 copy-up이라고 하며 10장 3편의 쓰기 시 복사와 같은 생각입니다. 파일을 지우면 아래 레이어는 그대로 둔 채 맨 위 레이어에 가림 표시(whiteout)를 남깁니다. 그래서 두 컨테이너는 같은 베이스 레이어를 함께 쓰면서도 서로의 변경을 보지 않고, 베이스 레이어는 끝까지 바뀌지 않습니다. 이 동작은 OverlayFS 문서에 설명되어 있습니다.
Dockerfile
이미지는 Dockerfile에 적은 단계를 차례로 실행해 만듭니다. 아래 파일은 Python 애플리케이션을 담는 예로, 한 줄이 한 단계입니다. 읽을 때는 requirements.txt만 먼저 복사해 패키지를 설치하고 나머지 코드는 나중에 복사하는 순서와, 실행 설정 직전에 USER로 사용자를 바꾸는 부분을 보면 됩니다.
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --system appuser
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]FROM은 베이스 이미지를 고르고, RUN과 COPY는 파일 시스템을 바꾸어 레이어를 만들며, USER, EXPOSE, CMD는 실행 설정만 바꿉니다. Docker는 각 단계의 결과를 캐시해 두었다가, 단계와 그 입력이 바뀌지 않았으면 다시 실행하지 않습니다. 그래서 자주 바뀌는 애플리케이션 코드보다 잘 바뀌지 않는 requirements.txt를 먼저 복사해 설치하면, 코드만 고쳤을 때 패키지 설치 단계를 건너뛸 수 있습니다. USER로 root가 아닌 사용자를 지정하는 것은 12장의 최소 권한 원칙을 따른 것입니다.
컨테이너 실행하기
docker build -t myapp:1.0 .
docker run -d --name web -p 8080:80 \
--memory=512m --cpus=1.0 \
--read-only --tmpfs /var/cache/nginx --tmpfs /var/run \
--security-opt no-new-privileges \
nginx:alpine
docker stats web # 컨테이너별 CPU, 메모리 사용량
docker inspect --format '{{.State.Pid}}' web # 호스트에서 본 컨테이너 첫 프로세스의 PID첫 줄은 앞의 Dockerfile로 myapp:1.0 이미지를 만드는 명령이고, 실행 예는 쓰기 경로가 알려진 공식 nginx:alpine 이미지로 자원과 보안 옵션을 보여 줍니다. --memory=512m과 --cpus=1.0은 앞에서 본 cgroup의 memory.max와 cpu.max로 바뀌어 적용됩니다. --read-only는 컨테이너의 루트 파일 시스템을 읽기 전용으로 만들고, nginx가 써야 하는 경로만 --tmpfs로 메모리 위의 쓰기 공간을 줍니다. docker inspect로 얻은 PID로 sudo ls -l /proc/PID/ns를 보면, 그 프로세스가 속한 네임스페이스가 호스트의 셸과 다르다는 것을 확인할 수 있습니다.
커널을 함께 쓰는 격리의 한계
컨테이너의 격리는 결국 하나의 커널이 지켜 주는 것이므로, 커널의 결함은 곧 격리의 결함입니다. 그래서 컨테이너를 실행할 때는 커널에 닿는 통로를 줄이는 장치를 함께 씁니다.
| 기술 | 하는 일 |
|---|---|
| 사용자 네임스페이스와 rootless 실행 | 컨테이너 안의 root를 호스트의 권한 없는 사용자에 대응시킨다 |
| 커패빌리티 제거 | root의 특권 가운데 필요 없는 것을 뺀다 |
| seccomp | 컨테이너가 부를 수 있는 시스템 콜을 제한한다 |
| AppArmor, SELinux | 파일과 자원에 강제 접근 제어 정책을 적용한다 |
| gVisor | 시스템 콜을 사용자 공간의 커널 구현이 받아 처리해, 호스트 커널에 닿는 부분을 줄인다 |
| Kata Containers, Firecracker | 컨테이너나 함수마다 가벼운 VM을 띄워 커널을 따로 둔다 |
Docker는 기본으로 seccomp 프로파일을 적용해 mount나 reboot처럼 시스템 전체에 영향을 주는 시스템 콜 수십 개를 막고, 커패빌리티도 일부만 남깁니다. 서로 믿을 수 없는 여러 고객의 코드를 한 호스트에서 실행하는 클라우드는 이것만으로 부족하다고 보고, AWS Lambda처럼 Firecracker 같은 마이크로 VM으로 고객마다 커널을 나누기도 합니다. 결국 무엇을 얼마나 믿을 수 있는지에 따라 컨테이너, 강화한 컨테이너, VM 가운데에서 고르게 됩니다.
정리
- 컨테이너는 호스트의 커널을 함께 쓰면서 프로세스를 격리하므로 VM보다 가볍지만, 커널의 결함이 모든 컨테이너에 영향을 줄 수 있습니다.
- 네임스페이스는 프로세스 번호, 네트워크, 마운트, 호스트 이름, 사용자 ID 같은 자원의 보이는 범위를 나누고, PID 네임스페이스 안의 프로세스는 호스트에서도 별도의 PID를 가집니다.
- cgroups는 그룹 단위로 CPU, 메모리, 프로세스 수를 제한하며,
cpu.max의 할당량은 그룹 전체의 합계에 걸립니다. - Docker 이미지는 읽기 전용 레이어를 쌓은 것이고, 컨테이너는 그 위에 자기만의 쓰기 레이어를 얹어 copy-up과 가림 표시로 변경을 기록합니다.
- seccomp, 커패빌리티 제거, rootless 실행, MAC으로 커널에 닿는 통로를 줄이고, 더 강한 격리가 필요하면 gVisor나 마이크로 VM을 씁니다.
연습 문제
1. 할당량과 공유 레이어 계산하기
cpu.max가 50000 100000인 그룹에서 100ms 주기 동안 세 태스크가 각각 20ms, 20ms, 15ms의 CPU 시간을 원합니다. 그룹이 원하는 합계와 예산의 차이를 구하세요. 또 70MiB짜리 이미지 하나로 컨테이너 A와 B를 실행했고, A의 쓰기 레이어가 5MiB, B의 쓰기 레이어가 8MiB라면 디스크를 얼마나 쓰나요? 압축과 메타데이터는 무시합니다.
해설 보기
원하는 합계는 55ms로 예산 50ms보다 5ms 많습니다. 그룹은 이번 주기에 50ms까지만 실행되고 나머지는 다음 주기까지 기다리며, 세 태스크 가운데 누가 기다리게 될지는 스케줄러가 정합니다. 디스크는 이미지 레이어를 두 컨테이너가 함께 쓰므로 70MiB를 한 번만 세고, 각자의 쓰기 레이어를 더해 70 + 5 + 8 = 83MiB입니다. VM이라면 디스크 이미지를 VM마다 따로 두므로 이렇게 나누어 쓰기 어렵습니다.
2. docker stop이 10초씩 걸리는 이유
어떤 애플리케이션을 컨테이너의 첫 프로세스로 실행했더니, docker stop을 할 때마다 정확히 10초를 기다린 뒤에야 컨테이너가 멈춥니다. 애플리케이션은 SIGTERM을 따로 처리하지 않습니다. 왜 이런 일이 생기는지 PID 네임스페이스의 규칙으로 설명하고, 해결 방법을 제시하세요.
해설 보기
docker stop은 먼저 컨테이너의 첫 프로세스에 SIGTERM을 보내고, 정해진 시간(기본 10초) 안에 끝나지 않으면 SIGKILL을 보냅니다. 첫 프로세스는 그 PID 네임스페이스의 PID 1, 곧 init인데, pid_namespaces(7)에 따르면 init에는 처리기를 등록한 신호만 전달되고 SIGKILL과 SIGSTOP만 예외입니다. 보통의 프로세스라면 SIGTERM의 기본 동작으로 종료되겠지만, 처리기가 없는 init에게 SIGTERM은 그냥 무시되므로 10초 뒤의 SIGKILL로만 멈춥니다. 애플리케이션에 SIGTERM 처리기를 넣어 정리하고 끝나게 하거나, docker run --init처럼 신호를 전달하고 자식을 거두는 작은 init 프로그램을 PID 1로 두어 해결합니다. 또 init이 끝나면 커널이 그 네임스페이스의 모든 프로세스를 SIGKILL로 종료하므로, 정리 작업은 PID 1이 끝나기 전에 마쳐야 합니다.
3. 큰 파일을 조금 고치면
이미지 레이어에 들어 있는 2GiB짜리 데이터 파일의 1바이트를 컨테이너 안에서 고쳤습니다. 오버레이 파일 시스템에서 어떤 일이 일어나나요? 데이터베이스의 데이터 파일을 어디에 두어야 하는지도 설명하세요.
해설 보기
아래 레이어의 파일을 쓰기용으로 열면 copy-up이 일어나, 1바이트만 고쳐도 2GiB 파일 전체가 컨테이너의 쓰기 레이어로 복사됩니다. 첫 쓰기가 크게 느려지고 디스크도 2GiB를 더 쓰며, 컨테이너를 지우면 쓰기 레이어와 함께 변경도 사라집니다. 그래서 데이터베이스처럼 자주 바뀌고 오래 남아야 하는 데이터는 이미지나 쓰기 레이어에 두지 않고, 볼륨(volume)이나 바인드 마운트로 호스트의 디렉터리를 연결해 그곳에 둡니다. 볼륨은 오버레이를 거치지 않아 copy-up이 없고, 컨테이너를 지우고 다시 만들어도 데이터가 남습니다.
다음 글 컨테이너 오케스트레이션과 실무에서는 많은 컨테이너를 여러 서버에 배치하고 관리하는 Kubernetes의 원리와, 운영 환경에서 VM, 컨테이너, 서버리스 가운데 무엇을 고를지 살펴봅니다.