3장. 구글의 컨테이너 — cgroups·namespace·LXC
같은 이미지를 옵션 하나만 바꿔 띄웠더니 컨테이너가 보는 프로세스가 4개에서 167개로 늘었습니다. 리눅스는 격리를 한 덩어리로 만들지 않고 종류별로 쪼갰기 때문입니다. jail이 못 한 그 일이 오늘 컨테이너의 모양을 정했어요.
{/* 삽화 자리: 벽 하나를 통째로 세우는 대신, 바닥에 여러 겹의 선을 따로따로 긋는 장면. 선마다 다른 것을 나눈다 — 하나는 사람들의 시야를, 하나는 소리를, 하나는 드나드는 문을. 작업자가 선을 골라서 그리고 있고, 어떤 선은 일부러 긋지 않아 그 방향으로는 양쪽이 이어져 있다. 옆에는 계량기와 밸브가 달린 배관(cgroups)이 따로 서 있다. 프롬프트: docs/book/illustration-prompts-container.md */}
배경
구글의 걱정은 훔쳐보기가 아니라 자원이었습니다.
2장의 호스팅 업체는 고객끼리 서로를 들여다보는 걸 막으려 했죠. 2006년 구글의 문제는 달랐습니다. 데이터센터의 기계 한 대에 여러 작업을 몰아넣어야 하는데, 한 작업이 CPU나 메모리를 다 써버리면 옆 작업이 같이 죽습니다. 기계를 놀리자니 아깝고, 몰아넣자니 서로 잡아먹고요.
Paul Menage와 Rohit Seth가 이 문제를 풀 물건을 만들었습니다. 프로세스 묶음마다 CPU·메모리·디스크 I/O·네트워크 사용량을 제한하고 계량하고 격리하는 장치였어요. 처음 이름은 process containers였습니다.
그런데 2007년 이름을 바꿉니다. 리눅스 커널에서 “container”라는 말이 이미 여러 뜻으로 쓰이고 있어 헷갈린다는 이유였어요. 새 이름은 control groups, 줄여서 cgroups입니다. 2008년 1월에 나온 커널 2.6.24에 병합됐습니다(📄 문서 기반 — cgroups ).
이 장의 제목이 「구글의 컨테이너」인데, 정작 구글이 만든 물건에서 제일 먼저 사라진 게 그 이름입니다. 자원을 재고 제한하는 장치 하나로는 컨테이너가 되지 않는다는 걸, 만든 쪽이 이름을 내리는 것으로 인정한 셈이에요.
그럼 나머지 조각은 누가 만들고 있었을까요.
스토리
이미 만들어지고 있었습니다. 그것도 훨씬 오래전부터, 그리고 따로따로요.
| 나누는 것 | 이름 | 커널 | 연도 |
|---|---|---|---|
| 마운트 지점 | mount (mnt) | 2.4.19 | 2002 |
| 호스트명·도메인 | UTS | 2.6.19 | 2006 |
| 프로세스 간 통신 | IPC | 2.6.19 | 2006 |
| 프로세스 목록 | PID | 2.6.24 | 2008 |
| 네트워크 | net | 2.6.29 | 2009 |
| 사용자·권한 | user | 3.8 | 2013 |
| cgroup 계층 자체 | cgroup | 4.6 | 2016 |
| 시계 | time | 5.6 | 2020 |
📄 문서 기반 — Linux namespaces . 연도는 그 기능이 쓸 만해진 시점입니다. net과 user는 더 이른 커널에서 뼈대가 들어간 뒤 표의 버전에서 완성됐어요.
첫 줄을 보세요. 2002년입니다. Al Viro가 만든 mount namespace가 프로세스마다 다른 마운트 지점을 보게 했어요. FreeBSD가 jail을 낸 지 3년 뒤고, 아직 아무도 이걸 “컨테이너”라고 부르지 않던 때입니다.
mount를 시작으로 18년에 걸쳐 나머지가 하나씩 붙습니다. 호스트명이 2006년, 프로세스 목록이 2008년, 네트워크가 2009년. 2장에서 jail이 한 번에 갈아치우던 것들이 리눅스에서는 6~7년 동안 따로따로 도착한 거예요.
2008년 1월이 분기점입니다. 커널 2.6.24에 PID namespace와 cgroups가 같이 들어왔습니다. 무엇을 보는지 나누는 장치와 얼마나 쓰는지 나누는 장치가 같은 릴리스에 실린 거예요.
2장에서 jail은 PID 번호를 그대로 두고 손만 막았죠. 리눅스는 번호 자체를 다시 매깁니다. PHK가 “누가 여기서부터 가져가달라”고 쓴 지 9년 뒤입니다.
리눅스에서 컨테이너를 돌리는 물건은 그 전에도 있었습니다. Linux-VServer가 2001년, OpenVZ가 2005년이요. 다만 둘 다 커널에 패치를 얹어야 했습니다.
그해 8월 6일 IBM이 내놓은 LXC는 달랐어요. 이미 메인라인에 들어간 조각만 써서 묶은 첫 도구였습니다(📄 문서 기반 — LXC ).
여기서 5년이 더 지나야 Docker가 나옵니다.
핵심
두 축, 다른 이름
2장 끝에서 격리에 두 축이 있다고 했습니다. 무엇을 보는가, 그리고 얼마나 쓰는가. 리눅스는 이 둘을 아예 다른 물건으로 만들었습니다.
| namespace | cgroups | |
|---|---|---|
| 답하는 질문 | 무엇이 보이는가 | 얼마나 쓸 수 있는가 |
| 나누는 것 | 프로세스 목록, 마운트, 네트워크, 호스트명… | CPU, 메모리, 디스크 I/O, 장치 접근 |
| 안 하는 것 | 사용량 제한 | 가리기 |
| 만든 곳 | 여러 커널 개발자, 2002년부터 | 구글, 2006년 |
| 이 책에서 | 6장 | 7장 |
Solaris가 Zone에 자원 관리를 붙여 Containers라고 팔던 그 조합이, 리눅스에서는 두 개의 독립된 기능으로 존재합니다. 붙이는 건 위쪽 도구의 몫이 됐어요.
쪼갠 게 왜 이겼나
2장에서 jail이 표준이 되지 못한 이유로 “통짜라서”를 들었습니다. 쪼개면 뭐가 좋은지 눈으로 봅시다.
같은 이미지를 옵션 하나씩만 바꿔 띄워봤습니다.
=== 같은 컨테이너, 네트워크 namespace 만 다르게 ===
기본 (분리) lo=127.0.0.1/8 eth0=172.17.0.2/16
--network host (공유) lo=127.0.0.1/8 eth0=192.168.65.3/24 services1=192.168.65.6/32
br-116b344f5f54=172.19.0.1/16 br-3d9c3a97f774=172.18.0.1/16
docker0=172.17.0.1/16
=== 같은 컨테이너, PID namespace 만 다르게 ===
기본 (분리) PID 1 = sh, 프로세스 4개
--pid=host (공유) PID 1 = initd, 프로세스 167개✅ 실측 (Docker Engine 29.7.2 / alpine 3.20 / Apple M1 / macOS 26.6, 2026-09. 전체 소스: docs/book/code/container/ch03/pick-and-choose.sh)
같은 이미지, 같은 명령입니다. 바뀐 건 플래그 하나뿐이에요.
--network host를 주면 네트워크 namespace만 호스트와 공유하고 나머지는 그대로 나눕니다. 위쪽에서 컨테이너는 자기 주소 172.17.0.2 하나만 봤는데, 아래쪽에서는 호스트의 인터페이스가 전부 보이죠. 그중 docker0=172.17.0.1이 위쪽 컨테이너의 eth0이 붙어 있던 다리입니다. 그 다리 이야기는 9장이고요.
--pid=host를 주면 프로세스 목록만 공유합니다. 2장의 jail로는 이 조합이 불가능했어요. jail()을 부르면 전부 갈렸으니까요.
기본 쪽 “프로세스 4개”는 컨테이너에 원래 있는 게 아닙니다. 방금 제가 친 명령이 만든 셸과 그 자식들이에요. 컨테이너 안에는 내가 띄운 것 말고 아무것도 없습니다.
그리고 167이라는 숫자. 1장 실측에서 chroot 바깥 프로세스가 다섯 개뿐이라 이상했죠. 리눅스 서버면 수백 개라고만 하고 넘어갔는데, 그 수백 개가 이겁니다. 1장 실험은 이미 PID namespace가 걸린 컨테이너 안이었고, 그 선을 걷어내면 이렇게 나옵니다.
여기서 “호스트”는 macOS가 아니라 Docker Desktop의 리눅스 VM입니다. 리눅스 서버에서 직접 하면 PID 1은 보통 systemd고 숫자는 더 큽니다.
직접 하나만 켜보기
Docker 플래그 말고 커널 기능을 직접 쓸 수도 있습니다. unshare는 “지금 이 프로세스를 부모와 분리해달라”고 커널에 요청하는 명령입니다.
unshare 전 : PID 1 = sh
unshare 후 : PID 1 = cat
--mount-proc 없이 : PID 1 = sh✅ 실측 (같은 환경, 같은 소스. unshare는 루트 권한이 필요해 특권 컨테이너 안에서 돌렸습니다)
unshare --pid --fork --mount-proc 한 줄로 PID namespace만 새로 만들었습니다. 그 안에서 처음 뜬 프로세스가 PID 1이 됐어요. 바깥에서 그 프로세스는 여전히 다른 번호를 달고 있고요.
플래그 두 개가 왜 붙었는지도 볼 만합니다. --fork는 PID namespace가 자기 자신이 아니라 자식부터 적용되기 때문에 필요합니다. --mount-proc은 더 재미있어요. 세 번째 줄이 그걸 뺀 결과인데, PID namespace를 새로 만들었는데도 PID 1이 여전히 바깥의 sh로 보입니다. /proc이 아직 바깥 것이라서예요.
선을 하나 그었는데 다른 선이 따라오지 않으면 거짓말이 앞뒤가 안 맞는 겁니다. 2장에서 “선이 한 겹이 아니다”라고 한 그 이야기고, 종류별로 쪼갠 대가이기도 합니다.
여는 글이 남긴 질문 하나가 여기서 닫힙니다. 컨테이너 안의 자바가 자기를 PID 1이라고 믿는 건, 누가 자바를 상자에 넣어서가 아니라 새 PID namespace에서 처음 뜬 프로세스라서입니다. 하필 그 PID 1이라는 자리가 왜 시그널을 삼키는지는 12장, namespace를 종류별로 손수 만들어보는 건 6장입니다.
첫 Docker가 실제로 한 일
2008년에 LXC가 이미 있었는데 왜 2013년 Docker가 판을 바꿨을까요.
초기 Docker는 격리를 직접 구현하지 않았습니다. LXC를 실행 드라이버로 썼어요. 2014년 3월 0.9.0에서야 자체 드라이버를 기본으로 돌렸고, LXC 드라이버는 2015년 8월 1.8.0에서 폐기 예고, 2016년 2월 1.10.0에서 완전히 제거됩니다(📄 문서 기반 — LXC ).
그러니까 Docker의 첫 버전이 한 일은 격리 기술을 만든 게 아니라, 이미 있던 걸 감싼 것이었습니다. 무엇으로 감쌌길래 5년 먼저 나온 LXC를 제쳤는지가 4장입니다.
정리
- 2006년 구글의 Paul Menage와 Rohit Seth가 자원 제한 장치를 만들었습니다. 처음 이름은 process containers, 2007년에 control groups(cgroups)로 바뀌었고 2008년 1월 커널 2.6.24에 병합됐습니다.
- namespace는 따로, 더 먼저 시작됐습니다. 2002년 mount를 시작으로 18년에 걸쳐 종류별로 하나씩 추가됐습니다. jail이 한 번에 하던 일을 리눅스는 조각으로 나눈 겁니다.
- 그래서 골라 쓸 수 있습니다. 실측에서
--network host는 네트워크만,--pid=host는 프로세스 목록만 호스트와 공유했습니다(프로세스 4개 → 167개). jail로는 불가능한 조합입니다. - 2008년 8월 IBM의 LXC가 메인라인 조각만으로 묶은 첫 도구였습니다. 그 전의 Linux-VServer·OpenVZ는 커널 패치가 필요했어요. 초기 Docker는 LXC를 실행 드라이버로 썼고, 2014년 3월에야 자체 드라이버로 옮겼습니다.
unshare --pid --fork한 줄이면 PID namespace가 생기고, 그 안의 첫 프로세스가 PID 1이 됩니다. 여는 글의 자바가 자기를 PID 1이라고 믿은 이유예요.
생각해볼 질문
여기까지 오면 이상한 점이 하나 있습니다.
Docker가 쓴 격리 기능은 2009년이면 커널에 다 있었습니다. 그걸 묶은 도구도 2008년에 나왔고요. 그런데 “컨테이너”라는 말이 개발자들의 일상어가 된 건 2013년입니다.
그사이 들어온 namespace는 2013년 2월의 user namespace 하나인데, 이게 재미있습니다. 커널 3.8이 2월 18일에 나왔고 Docker의 첫 공개 데모가 3월 15일이에요. 한 달 빨랐죠. 그런데 Docker는 그걸 한참 뒤에야 씁니다.
그러니까 2013년에 바뀐 건 격리가 아니었습니다.
Docker가 격리 말고 요긴하게 쓴 커널 기능이 하나 더 있긴 합니다. 여러 층을 겹쳐 하나처럼 보여주는 union 파일시스템인데, 초기에 쓴 AUFS는 메인라인에 들어간 적이 없고 지금 쓰는 OverlayFS는 Docker보다 늦게(2014년) 들어왔어요. 그 이야기는 8장입니다.
Docker가 판을 바꿨다면, 격리가 아닌 무엇을 바꾼 걸까요. 그리고 5분짜리 데모 하나가 어떻게 5년 먼저 나온 도구를 제쳤을까요.