0. Contents

1. Overview

호스트 커널방 — 네임스페이스 여섯1nginx안: PID 1 · 밖: 2778436cgroupmemory.max 256M2containerd-shim방 밖, 부모3runC켜고 사라짐4밖으로 나가는 길브리지 · 모드 — 03-2① 방 — 무엇을 보는가네임스페이스 여섯이 한 묶음 · 4.1② 계량기 — 얼마나 쓰는가cgroup 은 방이 아니라 폴더 하나 · 4.2③ 부모는 방 밖에shim 이 파이프를 쥐고 있다 · 3.4④ 켠 것은 사라진다runC 는 띄우고 종료 · 3.4컨테이너는 커널의 새 객체가 아니다 — 방에 들어가 계량기가 달린 프로세스 하나
ps 로 보면 평범한 프로세스 하나다. 다른 점은 어느 방에 있고, 어느 계량기에 묶였고, 부모가 누구냐뿐이다

2장 정리: 손잡이, 문, 방

  • 2장은 커널이 가진 손잡이, 문, 방을 하나씩 만져 봤다
    • 소켓은 손잡이였다
    • 인터페이스는 문이었다
    • 네트워크 네임스페이스는 문·주소·라우팅이 통째로 따로인 방이었다
  • 02-1 의 마지막에 도커 컨테이너의 방에 nsenter 로 들어가 보기도 했다

3장의 주제: 방이 만들어지는 과정

  • 3장은 그 방이 어떻게 만들어지는지를 본다
  • 컨테이너라는 말은 커널에 없다
    • 커널이 아는 것은 프로세스뿐이다
  • 어떤 프로세스가 다른 방(네임스페이스)에 들어가 있고 어떤 계량기(cgroup)에 묶여 있을 때 우리가 그걸 컨테이너라고 부른다
  • 그래서 이 편의 질문은 둘이다
    • 누가 그 프로세스를 방에 넣어 주는가
    • 방에 들어간 프로세스는 어떤 길로 밖에 나가는가

다루는 범위

  • 이 편은 책의 3장 앞부분, 컨테이너의 역사와 런타임과 cgroup·네임스페이스까지를 다룬다
  • 방이 밖으로 나가는 길 — 도커의 네트워크 모드와 CNI — 는 03-2 로 넘긴다

2. Three Problems — 집 하나, 집 셋, 방 셋

2.1 한 호스트의 긴장 — 라이브러리, 절차, 포트

공용 서버 시절의 문제 세 가지

  • 책은 컨테이너를 역사로 설명한다
  • 서버 한 대를 여러 팀이 같이 쓰던 시절의 문제는 셋이었다
  • 라이브러리 — A 팀은 파이썬 3.8 이 필요하고 B 팀은 3.11 이 필요한데 호스트에는 하나만 깔린다
  • 배포 절차 — 회사마다, 심지어 팀마다 배포 스크립트가 달라서 새로 온 사람은 그 스크립트를 해독하는 것부터 시작한다
  • 한 호스트를 나눠 쓰는 데서 오는 긴장
    • 관리자는 서버를 놀리기 아까워 앱을 더 받는다
    • 그러면서도 한 팀이 다른 팀을 망가뜨릴까 봐 배포 권한을 안 준다

네트워크 관점에서 본 셋째 문제

  • OS 가 하나면 TCP/IP 스택도 하나고, 스택이 하나면 포트 공간도 하나다
    • 02-1 의 4.2 에서 본 그대로다
  • A 팀 Tomcat 이 8080 을 잡으면 B 팀 Tomcat 은 8081 로 비켜야 한다
  • 누가 몇 번을 쓰는지 표로 관리해야 하고, 방화벽 담당자에게도 알려야 한다
  • 포트 하나 바꾸는 데 사람 셋이 모인다

2.2 VM — 집을 한 채 더 짓는다

VM 이 푸는 문제: 포트 공간

  • 하이퍼바이저는 하드웨어를 흉내 내서 머신 한 대 위에 게스트 OS 를 여럿 띄운다
  • 게스트마다 OS 가 있으니 스택도 하나씩이고, 포트 공간도 따로다
  • A 팀과 B 팀이 둘 다 8080 을 쓸 수 있다 → 셋째 문제가 풀린다

VM 의 한계: 무게와 남는 문제

  • 대신 집을 한 채씩 더 지은 것이라 무겁다
  • 게스트마다 커널을 통째로 깔아야 한다
  • 그 안에서 라이브러리를 어떻게 깔고 어떤 순서로 배포할지는 여전히 개발자 몫이다
  • 첫째와 둘째 문제는 그대로 남는다

2.3 Container — 방을 나눈다

컨테이너가 세 문제를 푸는 방식

  • 컨테이너는 집을 더 짓지 않는다 — 커널 하나 안에 방을 나눈다
  • 방마다 자기 파일시스템이 있어서 파이썬 3.8 과 3.11 이 옆방에 공존한다
  • 방에 무엇을 깔아 둘지는 이미지 한 장에 적혀 있어서 배포는 “이미지 받아서 켜기” 로 줄어든다
  • 방마다 네트워크 네임스페이스가 따로 있으니 8080 도 셋이 동시에 쓴다
OS 하나스택 하나 — 포트를 나눠 쓴다1eth0:8080:8081:8082두 번째 8080 은 못 연다 — 비켜야 한다VM 셋 — 하이퍼바이저집마다 문과 스택 — 8080 이 셋2:8080:8080:8080집마다 OS 한 벌 — 무겁다컨테이너 셋 — 커널 하나방마다 스택 — 8080 이 셋, OS 는 하나3eth0:8080:8080:8080커널은 하나 — 가볍다① 문 하나에 스택 하나 ② 집(게스트 OS)마다 문과 스택 ③ 방(네임스페이스)마다 문과 스택 — 작은 파란 상자가 문문제는 셋이었다 — 라이브러리 충돌 · 회사마다 다른 배포 절차 · 한 호스트를 나눠 쓰는 포트와 권한VM 은 셋째를 풀었고, 컨테이너는 그 효율을 지키며 앞의 둘까지 푼다
8080 을 셋이 동시에 쓰려면 스택이 셋이어야 한다 — 집을 더 짓거나(VM), 방을 나누거나(컨테이너)

책의 덧붙임: “컨테이너는 실체가 없다” 는 지적

  • 컨테이너는 실체가 없다고 말하는 사람들이 있고, 그 말은 기술적으로 맞다
    • 커널이 원래 갖고 있던 기능에 이름을 붙인 것뿐이다
  • 하지만 그 지적은 핵심을 놓친다
    • 어려운 문제는 “앱을 어떻게 배포하고 관리하느냐” 였다
    • 컨테이너는 그 문제를 푼 도구다
  • 이 편의 순서
    • 그 “커널이 원래 갖고 있던 기능” 이 무엇인지를 4장에서 본다
    • 그 전에 누가 그 기능을 호출하는지를 3장에서 본다

3. Runtimes — 누가 켜고 누가 관리하나

3.1 저수준과 고수준 — 켜는 순간과 그 전후

책의 구분: 저수준과 고수준

  • 책은 컨테이너 런타임을 저수준과 고수준으로 나눈다
    • 저수준: 컨테이너를 만들고 실행하는 것
    • 고수준: 이미지를 빌드하고 보관하고 공유하고 인스턴스를 관리하는 것
  • 목록으로 보면 밋밋한데, 시간 순서로 늘어놓으면 경계가 보인다

시간 순서로 본 경계

  • 저수준은 “켜는 순간” 만 담당한다
    • 입력은 이미 풀려 있는 폴더 하나(루트 파일시스템)와 설정 파일 하나(config.json)다
    • 그걸 받아 커널에 시스템콜을 날려 앱 프로세스를 띄우면 종료한다
  • 고수준은 켜기 전과 켠 후 전부다
    • 켜기 전 — 이미지 받기, 레이어 풀기, 설정 쓰기
    • 켠 후 — 감시, 로그, 목록, 중지, 삭제
    • 켜는 순간 자체는 저수준을 불러 시킨다
docker run -d nginx 한 줄이 시키는 일 — 누가 하는지 색으로1pull nginx // 레지스트리2unpack layers → rootfs // overlayfs3write config.json // 명령·한도4start containerd-shim5shim: runc create+start6runc: fork→unshare→execve; exit7shim stays; containerd watches8on exit: code → containerd; umount주황 = containerd · 파랑 = shim · 초록 = runC없음 — containerd 가 준비한다pull1rootfs2config.json3shim 띄움4켜는 순간 — shim 이 runC 를 부른다shim5runc: fork→unshare→execve6runc 종료있음 — 앱이 사는 동안shim (부모)7nginx · 방 안ps · logs · stop · rm죽음 — 뒷정리shim: 종료 코드8containerd: 상태 갱신 · 마운트 해제저수준(초록)은 ⑥ 한 줄, 고수준(주황·파랑)은 나머지 전부 — 켜는 순간만 떼어 규격(OCI)으로 못 박은 이유
왼쪽은 순서, 오른쪽은 상태 — 같은 번호가 같은 일이다. 초록 자리에 crun · gVisor · Kata 가 들어올 수 있다

나눈 이유: OS 와 가장 밀접한 부분

  • 이렇게 나눈 이유는 켜는 부분이 OS 와 가장 밀접하기 때문이다
  • 시스템콜을 직접 치는 부분만 작게 떼어 규격으로 못 박아 두면
    • 그 위는 커널 세부를 몰라도 된다
    • 그 자리에 다른 저수준을 끼울 수도 있다

3.2 스택 — dockerd, containerd, runC, 그리고 kubelet

VM 의 ps -eo pid,ppid,comm --forest — 컨테이너 하나를 띄운 뒤1644805 1 dockerd21659053 1 containerd32777998 1 containerd-shim42778024 2777998 \_ nginx5(pgrep runc: 없음)shim 의 부모가 1 — containerd 가 아니다층 — 위는 오래 살고, 아래로 갈수록 짧게 산다1dockerdkubelet요청을 받는 쪽도커 / 쿠버네티스CRI (kubelet 쪽만)2containerdCRI-O이미지 · 수명 관리shim 을 띄운다3containerd-shim-runc-v2컨테이너마다 하나앱의 부모로 남는다OCI — 폴더 + config.json5runccrunrunsckata켜고 종료4nginx커널 — namespace · cgroup 안의 프로세스 하나① ② 는 데몬 — 부팅 때부터 떠 있다③ 은 컨테이너와 같이 살고⑤ 는 켜는 순간만 살아서 ps 에 없다④ 가 컨테이너 — 그냥 프로세스'런타임' 은 ② 와 ⑤ 둘 다를 부르는 말쿠버네티스 문서의 런타임은 ②OCI 문서의 런타임은 ⑤ps 의 다섯 줄이 층 다섯 칸이다 — 번호가 같은 칸끼리 같은 것
왼쪽은 실제 출력, 오른쪽은 그 출력을 층으로 세운 것 — kubelet 은 dockerd 자리에 들어오고 아래는 같다

도커 엔진의 분리: 바이너리 하나 → 셋

  • 도커 엔진은 처음에 바이너리 하나였다
  • 2016년의 도커 1.11 에서 셋으로 갈라졌다
    • dockerd — 사용자 요청을 받는다
    • containerd — 이미지와 수명을 관리한다
    • runC — 켜기만 한다

runC 의 출처와 범위

  • 원래 도커 안에 있던 libcontainer 라이브러리를 꺼내 독립 실행 파일로 만든 것이다
  • libcontainer 는 지금도 runC 안의 라이브러리로 남아 있다
    • containerd 는 거기서 발전한 것이 아니라 별도로 만든 관리 서비스다
  • runC 는 리눅스 전용이다
    • 책은 runC 기능 목록에 Windows 컨테이너 지원을 넣었지만, Windows 컨테이너는 별도 런타임이 맡는다
  • 지금 “도커 엔진” 이라고 부르는 것은 이 세 프로그램의 묶음이다

쿠버네티스 노드: 맨 위만 다르다

  • dockerd 자리에 kubelet 이 오고, kubelet 은 containerd 에 직접 말을 건다
  • 그 아래 shim, runC, 앱은 도커 단독일 때와 같다

예전 경로: dockershim

  • 예전에는 한 층을 더 거쳤다
  • kubelet 안에 dockershim 이라는 번역기가 있었다
    • kubelet → dockershim → dockerd → containerd → runC 였다
  • 도커 안에 이미 containerd 가 있으니 중복이었다
    • 1.20 에서 폐기를 예고하고 1.24 에서 뺐다
  • “쿠버네티스에서 도커를 못 쓴다” 는 오해가 여기서 나왔다
    • 빠진 건 노드에서 켜는 경로뿐이고 이미지를 만드는 쪽(docker build)은 그대로다
    • 이미지가 규격품이라 누가 켜든 같기 때문이다

“런타임” 이라는 말의 두 뜻

  • “런타임” 이라는 말은 ②와 ③ 둘 다를 가리킨다
  • 쿠버네티스 문서가 “컨테이너 런타임을 설치하라” 고 하면 containerd 나 CRI-O 다
  • OCI 문서의 runtime 은 runC 다
  • 누가 런타임이라고 하면 켜는 쪽인지 관리하는 쪽인지 한 번 되묻는 게 안전하다

3.3 OCI 와 CRI — 약속은 두 장

그림의 점선 두 개

  • 그림의 점선 둘이 이 스택을 갈아 끼울 수 있게 만드는 약속이다
  • 자리가 다르고 주인도 다르다

OCI: 규격 문서 세 장

  • OCI 는 리눅스 재단 아래 단체이고, 규격 문서 세 장을 낸다
    • Runtime Spec — config.json 에 무엇을 적을 수 있고 런타임이 그걸 어떤 순서로 처리해야 하는지
    • Image Spec — 이미지가 어떤 파일들(레이어 tar, 매니페스트, 설정)로 이루어지는지
    • Distribution Spec — 레지스트리와 주고받는 HTTP API
  • 그래서 도커로 빌드한 이미지를 Harbor 에 올리고, containerd 가 받아서, runC 나 gVisor 가 켜는 식으로 단계마다 다른 제품이어도 된다
  • 고수준 런타임이 runC 를 부를 때 지키는 약속이 이 중 Runtime Spec 이다
    • 그림에서 containerd 아래의 선이다

CRI: 쿠버네티스 프로젝트의 gRPC 인터페이스

  • CRI 는 OCI 소속이 아니다
  • 쿠버네티스 프로젝트가 자기 저장소에 .proto 파일로 정의한 gRPC 인터페이스다
    • kubelet 이 고수준 런타임에 말을 걸 때 쓴다
    • 그림에서 kubelet 아래의 선이다
  • containerd 와 CRI-O 는 둘 다 이 두 선 사이에 앉아서 위로는 CRI, 아래로는 OCI 를 지킨다
  • 둘의 차이는 이 정도다
    • containerd 는 도커에서 나와 범용이다
    • CRI-O 는 처음부터 쿠버네티스만 보고 만들어서 자체 CLI 문화가 없다

OCI 선 아래의 저수준 런타임 세 가지

  • OCI 선 아래에 runC 말고 다른 것이 올 수 있다는 점이 뒤에서 중요해진다
  • runC — 앱을 호스트 커널 위에 직접 띄운다
    • 네임스페이스로 가려 놓았을 뿐 시스템콜은 호스트 커널이 처리한다
  • gVisor(runsc) — 앱과 커널 사이에 Go 로 짠 가짜 커널을 끼워 시스템콜 대부분을 자기가 흉내 낸다
  • Kata — 컨테이너마다 작은 VM 을 띄운다

고르는 방법: RuntimeClass

  • 셋 다 containerd 입장에서는 똑같이 “폴더와 config.json 줄 테니 켜라” 로 부른다
  • 쿠버네티스에서는 RuntimeClass 로 Pod 마다 고른다
  • 컨테이너가 “VM 은 무겁다” 에서 출발했는데 Kata 는 보안 때문에 그 자리에 VM 을 다시 넣은 셈이다

3.4 runC 는 사라지고 shim 이 남는다 [실습]

실습: 컨테이너 하나의 프로세스 트리

  • 3.1 의 “켜고 나면 종료한다” 를 확인하려고 VM 에서 컨테이너를 하나 띄우고 프로세스 트리를 봤다
docker run -d --name c3 --memory 256m nginx:alpine
ps -eo pid,ppid,comm --forest | grep -E "dockerd|containerd|nginx|runc"
pgrep -a runc
 644805       1 dockerd
1659053       1 containerd
2777998       1 containerd-shim
2778024 2777998  \_ nginx
2778074 2778024      \_ nginx
(no runc process)

결과: runC 는 없고 shim 이 부모

  • runC 가 목록에 없다
  • nginx 의 부모는 containerd-shim 이다
  • shim 의 부모는 PID 1 이다 — containerd 의 자식이 아니다
    • 띄우자마자 부모와 연을 끊어서(double fork) PID 1 의 자식이 되기 때문이다
runC 가 하는 일을 여섯 줄로 적으면1pid = fork(); // 자식 하나if (pid == 0) { // 자식만2unshare(NEWPID|NEWNET|NEWNS|...);3write(cgroup.procs, getpid());4pivot_root(rootfs);5execve("nginx", ...); // 몸 교체6} else exit(0); // 부모는 끝unshare 뒤는 전부 방 안에서 — 자식은 아직 runC 코드밖 — 호스트의 방runC1자식runC 종료6unshare — 경계를 넘는다2안 — 새 방 (PID 1 로 보인다)cgroup.procs3pivot_root4execve → nginx (같은 PID)5shim 이 부모방 밖에서방에 들어간 그 프로세스가 앱이 된다 — 컨테이너를 켜는 것과 앱을 켜는 것은 한 동작, PID 도 하나
02-1 의 ip netns exec room python3 과 같은 모양 — 들어간 뒤 execve 로 몸을 바꾼다

runC 가 한 일: 프로세스 단위로 풀기

  • runC 가 fork 로 자식을 하나 만든다
  • 그 자식이 스스로 unshare 로 새 네임스페이스에 들어간다
  • 자기 PID 를 cgroup 파일에 쓴다
  • pivot_root 로 루트를 바꾼다
  • execve 로 자기 몸을 nginx 로 갈아 끼운다

execve: 프로세스의 내용물 교체

  • execve 는 새 프로세스를 만드는 게 아니라 지금 프로세스의 내용물을 다른 프로그램으로 바꾸는 시스템콜이다
  • 방에 들어가서 준비한 “그 프로세스” 가 그대로 앱이 된다
    • 컨테이너를 띄우는 일과 앱을 띄우는 일이 두 단계가 아니라 한 동작인 이유다
  • 02-1 의 5.1 에서 ip netns exec 로 방 안에 python3 를 띄운 것과 정확히 같은 구조다
    • ip 가 방에 들어간 뒤 execve 로 python3 가 됐다
    • 방 안에는 python3 만 남았다

shim 이 필요한 이유: 부모 자리

  • 리눅스에서 프로세스는 부모가 있어야 한다
    • 부모가 죽으면 자식의 표준 출력 파이프가 끊기고 종료 코드를 받아 줄 사람도 없어진다
  • runC 가 nginx 의 부모로 남아 있으면 컨테이너 수만큼 runC 가 떠 있어야 한다
  • dockerd 가 부모면 dockerd 업그레이드 한 번에 컨테이너가 전부 죽는다
  • 그래서 작고 할 일 없는 shim 을 하나 끼워 부모 자리를 맡긴다

shim 이 하는 일 두 가지

  • 파이프를 쥐고 있는다
  • 앱이 죽으면 종료 코드를 containerd 에 전해 준다
  • 덕분에 dockerd 를 재시작해도 컨테이너는 그대로 돈다

“데몬 없는 컨테이너” 의 범위

  • 책은 이를 “데몬 없는 컨테이너” 라고 부르는데 범위를 좁혀 읽는다
  • 없어진 것은 컨테이너마다 오래 사는 런타임 프로세스이지 데몬 전체가 아니다
    • runC 라는 명령이 종료할 뿐 containerd 와 shim 은 여전히 떠 있다
  • 쿠버네티스가 아니라 도커 단독이어도 같은 구조다
  • Podman 은 containerd 대신 conmon 이라는 자기 감시 프로세스를 두는데 자리는 같다

전체 명령과 출력 — 01-runtime

4. Primitives — 방과 계량기

runC 가 쓰는 커널 기능 두 가지

  • 도커를 쓰든 containerd 를 쓰든 실제로 커널을 건드리는 건 runC 다
  • runC 가 쓰는 커널 기능은 둘이다 — 책이 한 줄로 정리한 구분이 이 절의 전부다
    • 네임스페이스: 프로세스가 무엇을 볼 수 있는지
    • cgroup: 얼마나 쓸 수 있는지

4.1 Namespaces — 같은 이름, 다른 실체 [실습]

네임스페이스를 가리키는 번호

  • 네임스페이스는 이름이 없고 번호만 있다
  • 어떤 프로세스가 어느 방에 있는지는 /proc/PID/ns/ 아래의 링크가 가리키는 번호로 안다
  • 호스트의 PID 1 과 방금 띄운 nginx 를 나란히 놓고 비교했다
PID=$(docker inspect -f '{{.State.Pid}}' c3)
for n in net pid mnt uts ipc user; do
  printf "%-5s %s  %s\n" $n "$(sudo readlink /proc/1/ns/$n)" "$(sudo readlink /proc/$PID/ns/$n)"
done
net   net:[4026531840]   net:[4026532265]
pid   pid:[4026531836]   pid:[4026532263]
mnt   mnt:[4026531841]   mnt:[4026532255]
uts   uts:[4026531838]   uts:[4026532261]
ipc   ipc:[4026531839]   ipc:[4026532262]
user  user:[4026531837]  user:[4026531837]

결과: user 하나만 같은 번호

  • 다섯 개는 번호가 다르고 user 하나만 같다
  • 여섯 개가 전부 “같은 이름을 다른 프로세스가 다르게 본다” 라는 점은 같다
  • 무엇이 달라지는지는 종류마다 다르다
pid — 프로세스 번호
호스트에서 → nginx 는 2778024
방 안에서 → 같은 nginx 가 PID 1. ps 를 쳐도 자기 식구만 보인다
net — 네트워크 스택
호스트에서 → eth0 은 진짜 NIC ens3 10.0.0.198
방 안에서 → eth0 은 veth 한쪽 끝 172.17.0.2. 02-1 의 그 방
mnt — 마운트
호스트에서 → / 는 디스크
방 안에서 → / 는 이미지 레이어를 겹친 폴더. pivot_root 가 바꿨다
uts — 호스트 이름
호스트에서 → VM 의 hostname
방 안에서 → 컨테이너 ID 앞 12자리. 같은 커널인데 이름이 둘
ipc — 프로세스 간 통신
공유 메모리·세마포어 식별자가 방마다 따로. 옆방의 공유 메모리에 못 붙는다
user — 사용자 ID (같은 번호)
도커 기본값은 분리하지 않는다. 방 안의 root 가 곧 호스트의 root. 켜면 안의 UID 0 이 밖의 100000 번대로 매핑된다

user 네임스페이스와 보안

  • user 네임스페이스가 호스트와 같은 번호인 것은 보안 이야기다
  • 켜면 컨테이너 안에서 root 로 돌아도 호스트에서는 일반 사용자로 매핑된다 → 탈출해도 호스트 root 가 아니다
  • 도커는 기본으로 안 켜고(userns-remap 옵션), Podman 은 기본으로 켠다
  • 쿠버네티스는 1.30 무렵에 Pod 단위 지원이 들어갔다
  • 목록에 cgroup 이라는 일곱 번째 항목도 있는데, cgroup 경로 자체를 격리하는 것이라 몰라도 된다

PID 비교: 컨테이너 안과 밖

echo "outside: $PID"; docker exec c3 sh -c 'ps -o pid,comm'
outside: 2778024
PID   COMMAND
    1 nginx
   30 nginx

결과: 같은 프로세스, 다른 번호

  • 같은 프로세스를 두 방이 다른 번호로 부른다
  • 02-1 의 2.4 에서 소켓 하나를 FD 번호와 inode 로 따로 가리킨 것과 같은 모양이다

4.2 cgroup — 폴더 하나와 파일 몇 개 [실습]

cgroup: 방이 아니라 계량기

  • cgroup 은 방이 아니라 계량기다
  • 방에 누가 있든 상관없이, 등록된 프로세스들이 쓰는 양만 센다
  • 실체는 /sys/fs/cgroup 아래의 디렉터리 하나다
  • 3.4 에서 --memory 256m 으로 띄운 컨테이너의 폴더를 찾아갔다
CG=$(cut -d: -f3 /proc/$PID/cgroup)
cat /sys/fs/cgroup$CG/memory.max
cat /sys/fs/cgroup$CG/cgroup.procs
268435456
2778024
2778074

결과: 파일에 적힌 한도와 PID

  • memory.max 에 256 MiB 가 바이트로 적혀 있다
  • cgroup.procs 에 nginx 마스터와 워커들의 PID 가 적혀 있다
  • runC 가 한 일이 정확히 이 파일 쓰기다
    • 폴더를 만든다
    • memory.max 에 숫자를 쓴다
    • cgroup.procs 에 자기 PID 를 쓴다
  • 그 순간부터 그 프로세스와 자식들은 합쳐서 256 MiB 를 넘게 못 쓴다
    • docker run --memory 의 숫자가 이 파일로 간다

네트워크와 닿는 지점: net_cls

  • cgroup 에는 그 cgroup 에서 나가는 패킷에 꼬리표를 붙이는 기능(net_cls)이 있다
  • 02-2 에서 본 Netfilter 나 tc 가 “어느 컨테이너에서 나온 패킷인가” 로 규칙을 걸 수 있다
  • Cilium 이 eBPF 로 Pod 를 식별하는 것이 이 발상의 발전형이다
    • CNI 장에서 다시 만난다

전체 명령과 출력 — 01-runtime

4.3 Pod 에서는 — 방 여섯 중 둘을 나눠 쓴다

Pod 의 변형: 방 일부를 같이 쓴다

  • 여기까지는 도커 컨테이너 하나의 이야기다
    • 컨테이너 하나가 방 여섯을 전부 새로 받는다
  • Pod 는 여기서 변형이 생긴다
  • Pod 안의 컨테이너들은 net 과 ipc(그리고 uts)는 하나를 같이 쓰고, pid 와 mnt 는 각자 갖는다
  • 그래서 같은 Pod 의 컨테이너끼리 localhost 로 통하면서도 파일은 서로 안 보인다
    • 02-1 의 5.3 에서 그린 분해도가 그 그림이다

방을 붙잡는 pause 컨테이너

  • 그 방을 들고 있는 것이 pause 컨테이너다
    • pause 가 먼저 떠서 네트워크 방을 만들어 붙잡는다
    • 앱 컨테이너들은 runC 의 unshare 대신 setns 로 그 방에 합류한다
    • 앱이 재시작돼도 pause 가 살아 있으니 Pod IP 가 안 바뀐다

5. Discussion

5.1 도구 지도 — 질문에서 명령으로

도구를 나누는 두 갈래

  • 이 편에서 쓴 도구는 두 질문으로 나뉜다
    • 누가 이 프로세스를 켰고 지금 부모가 누구인가
    • 이 프로세스는 어느 방에 있고 얼마나 쓸 수 있나
켠 것 — 런타임
부모가 누구인가 → ps -eo pid,ppid,comm --forest
runC 가 남아 있나 → pgrep -a runc
호스트 쪽 PID 는 → docker inspect -f '{{.State.Pid}}'
어느 저수준인가 → docker info | grep -i runtime
방과 계량기 — primitives
어느 방인가 → readlink /proc/PID/ns/*
안에서 보면 → docker exec … ps
계량기 폴더는 → cat /proc/PID/cgroup
한도는 → cat /sys/fs/cgroup/…/memory.max

주의: 권한 오류

  • /proc/PID/ns 는 root 가 아니면 다른 사용자의 프로세스에 대해 Permission denied 가 난다
  • 호스트의 PID 1 을 보려면 sudo 가 필요하다
  • 비어 있다고 네임스페이스가 없는 게 아니다

핵심 메시지

세 문제
라이브러리·절차·포트. VM 은 집을 더 지어 포트를 풀고, 컨테이너는 방을 나눠 셋 다 푼다
런타임
고수준은 전후를, 저수준은 켜는 순간만. OCI 와 CRI 두 약속이 층을 갈아 끼우게 한다
runC
방에 들어간 자식이 execve 로 앱이 되고, runC 는 종료. 부모 자리는 shim 이 밖에서 지킨다
방과 계량기
네임스페이스는 무엇을 보느냐, cgroup 은 얼마나 쓰느냐. 둘 다 /proc 와 /sys 의 파일로 보인다

컨테이너를 켠다는 것은 프로세스 하나를 방에 넣고 계량기를 달아 띄우는 것이고, 그 일을 하는 runC 는 띄우자마자 사라진다. ps 로 보면 남는 것은 평범한 프로세스 하나와 그 부모 shim 뿐이다. 그래서 02-1 에서 ip netns list 가 비어 있던 것도, 호스트의 ps 에 컨테이너의 nginx 가 그대로 보이는 것도 이상하지 않다. 다음 편은 그 방이 밖으로 나가는 길을 도커의 네트워크 모드로 본다 — bridge, host, none, macvlan, overlay 가 각각 방과 문을 어떻게 조합한 것인지, 그리고 CNI 는 도커 모델과 무엇이 다른지.


6. References

저수준 런타임이 받는 입력이 "폴더 + config.json" 이라는 것은 OCI Runtime Specification 의 번들 정의에 있다.
"A Standard Container bundle contains all the information needed to load and run a container."
[해석] "표준 컨테이너 번들은 컨테이너를 적재하고 실행하는 데 필요한 모든 정보를 담는다." — 3.1 의 "입력은 폴더 하나와 설정 파일 하나" 의 근거다. 같은 문서에 번들이 config.json 과 루트 파일시스템 디렉터리로 이루어진다는 것, 그리고 런타임의 수명 주기(create → start → kill → delete)가 적혀 있다. runC 저장소의 README 는 runC 를 리눅스용 OCI 런타임으로 소개한다 — 책의 runC 기능 목록에 있는 "Windows 10 컨테이너 지원" 은 맞지 않고, libcontainer 가 containerd 로 발전했다는 서술도 맞지 않는다(libcontainer 는 runC 의 하위 디렉터리에 남아 있다).
→ opencontainers/runtime-spec — Filesystem Bundle
→ opencontainers/runtime-spec — Runtime and Lifecycle
→ opencontainers/runc
shim 이 왜 있는지는 containerd 의 runtime v2 문서가 말한다.
"the shim ... is used to keep the container's stdio open and report the exit status"
[해석] "shim 은 컨테이너의 표준 입출력을 열어 두고 종료 상태를 보고하는 데 쓰인다." — 3.4 의 "파이프를 쥐고 종료 코드를 전한다, 이 둘만 한다" 의 근거다. 책의 containerd 절도 같은 세 가지(런타임이 종료할 수 있게, I/O 를 열어 두게, 종료 상태를 보고하게)를 든다. dockershim 제거의 범위(노드의 실행 경로만, 빌드는 그대로)는 쿠버네티스 블로그의 FAQ 에 있다.
→ containerd — Runtime v2 (shim)
→ kubernetes.io — Dockershim removal FAQ
→ kubernetes.io — Runtime Class
네임스페이스가 "같은 자원을 다르게 보이게" 한다는 정의는 namespaces(7) 첫 문단에 있다.
"A namespace wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance"
[해석] "네임스페이스는 전역 시스템 자원을 감싸서, 그 안의 프로세스들에게는 자기만의 격리된 인스턴스가 있는 것처럼 보이게 한다." — 4.1 의 "같은 이름, 다른 실체" 의 근거다. /proc/PID/ns 의 링크와 번호의 의미, 그리고 두 프로세스의 링크가 같은 inode 를 가리키면 같은 네임스페이스라는 것도 같은 페이지에 있다. cgroup 이 "자원 사용을 제한·집계·격리" 한다는 책의 정의는 cgroups(7) 과 같다.
→ man7.org — namespaces(7)
→ man7.org — cgroups(7)
→ man7.org — user_namespaces(7)