도커 컨테이너는 가상머신인가요? 프로세스인가요?
들어가며
도커 컨테이너와 가상머신은 어떻게 다른 건가요?
이 글에서는 도커Docker 컨테이너와 프로세스가 어떻게 다른지 알아보려고 합니다. 도커 컨테이너와 가상머신의 차이에 대해 가상머신은 운영체제 위에 하드웨어를 에뮬레이션하고 그 위에 운영체제를 올리고 프로세스를 실행하는 반면에, 도커 컨테이너는 하드웨어 에뮬레이션 없이 리눅스 커널을 공유해서 바로 프로세스를 실행한다고 설명하면, 군더더기 없는 아주 훌륭한 설명입니다 👏
그럼 다음 질문들은 어떤가요. 도커 컨테이너가 진짜 프로세스에요? 그럼 호스트 시스템에서 ps
치면 보이나요? 호스트에서 kill
해서 죽일 수 있어요? 프로세스 ID도 있어요? 자, 이론적인 설명보다는 직접 리눅스에 들어가서 리눅스 프로세스와 도커 컨테이너 사이의 표면적인 차이에 집중해서 한 번 살펴보겠습니다.
도커 컨테이너가 사용하는 커널과 파일 시스템 확인하기
도커 컨테이너를 이야기하기에 앞서서 도커의 시작을 잠깐 돌아보겠습니다. 도커는 PYCON 2013 US에서 솔로몬 하이크Solomon Hykes의 라이트닝 토크로 처음 공개되었습니다.
리눅스 컨테이너의 미래The Future of Linux Container라는 제목을 가진 이 발표에서 솔로몬 하이크는 docker
라는 어려운 명령어로 Hello world
를 출력하는 아주 기묘한 데모를 시연합니다.
지금도 따라해볼 수 있습니다. -a
옵션만 빼고 실행하면 (적어도 겉보기에는) 똑같이 동작합니다.
$ docker run busybox echo hello world
hello world
아직 컨테이너를 모르신다면 “echo hello world
라고 하면 되잖아”하고 호통을 칠지도 모릅니다. 맞습니다.
$ echo hello world
hello world
(겉보기에는) 정말 아무런 차이도 없습니다. 오히려 도커로 실행하면 처음에 busybox
이미지를 풀 받는 시간이 걸리기 때문에 느리게 실행됩니다. busybox
가 무엇인지 궁금해할 수도 있습니다. 이것은 도커 컨테이너가 실행되는 환경(=파일시스템)입니다. 도커로 실행되는 컨테이너는 위에서 명시한 busybox
라는 환경에서 실행됩니다. 반면에 아래와 같이 리눅스 시스템에서 바로 echo hello world
명령어를 실행하는 경우 호스트 환경(루트 파일 시스템)에서 프로세스가 실행됩니다. 간단한 예를 통해서 도커 컨테이너들이 서로 다른 환경에서 실행되고 있다는 것을 확인해보겠습니다.
먼저 ubuntu:latest
이미지에서 커널과 배포판 정보를 확인해보겠습니다.
$ docker version
Client: Docker Engine - Community
Version: 19.03.5
...
$ docker run -it ubuntu:latest bash
root@bfccfb4136ae:/# uname -a
Linux f44e33a332f2 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
root@bfccfb4136ae:/# cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
...
이번에는 centos:latest
이미지에서 확인해봅니다.
$ docker run -it centos:latest bash
[root@bb0a9b851dbd /]# uname -a
Linux bb0a9b851dbd 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
[root@bb0a9b851dbd /]# cat /etc/*-release
CentOS Linux release 8.0.1905 (Core)
NAME="CentOS Linux"
VERSION="8 (Core)"
컨테이너 안에서 2개의 명령어를 실행해보았는데, 두 결과 모두 흥미롭습니다. 먼저 cat /etc/*-release
파일을 출력한 결과를 통해 배포판(파일시스템)이 다르다는 것을 확인할 수 있습니다. 위에서 실행한 2개의 도커 컨테이너는 분명히 다른 환경에서 실행되고 있습니다.
그리고 uname -a
의 출력 결과가 동일하다는 것도 재미있습니다. 서로 다른 환경에서 컨테이너가 실행되었지만 커널이 같다는 것에 주목해주세요. 위의 예제는 우분투Ubuntu 18.04 베이그런트Vagrant 가상머신에서 실행되었습니다. 호스트의 커널을 확인해보겠습니다.
$ uname -a
Linux ubuntu1804.localdomain 4.15.0-70-generic #79-Ubuntu SMP Tue Nov 12 10:36:11 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
오호. 호스트 이름만 다를 뿐 거의 똑같네요? 그렇다면 이번에는 맥OS에 설치된 도커 데스크탑에서도 똑같이 실행해보겠습니다.
$ uname -a
Darwin nacyotui-MacBookAir.local 18.6.0 Darwin Kernel Version 18.6.0: Sun Apr 28 18:06:45 PDT 2019; root:xnu-4903.261.4~6/RELEASE_X86_64 x86_64
$ docker run -it ubuntu:latest bash
root@11f071759deb:/# uname -a
Linux 11f071759deb 4.9.184-linuxkit #1 SMP Tue Jul 2 22:58:16 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
root@11f071759deb:/# cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
먼저 호스트 맥OS에서 uname -a
로 커널을 확인해보면 Darwin kernel
이 출력되는 것을 알 수 있습니다. 그런데 docker
를 실행하면 이번에는 4.9.184-linuxkit
커널이 나타납니다. 알쏭달쏭하죠? 이미 알고 있는 분들도 계시겠지만 맥OS는 리눅스가 아니므로 네이티브하게 도커를 사용할 수 없습니다. 따라서 도커 데스크탑은 리눅스킷 기반 경량 가상머신 위에서 도커를 실행합니다. 어쨌건 재미있는 사실을 확인했습니다. 우분투 18.04에서 실행한 컨테이너가 사용하는 커널과 맥OS의 도커 데스크탑에서 실행한 컨테이너에서 사용하는 커널은 다릅니다.
여기까지 확인한 내용을 정리해보겠습니다.
- 컨테이너는 호스트 시스템의 커널을 사용한다.
- 컨테이너는 이미지에 따라서 실행되는 환경(파일 시스템)이 달라진다.
이미지에 대한 더 자세한 내용은 만들면서 이해하는 도커(Docker) 이미지의 구조 편을 참고해주세요. 컨테이너가 서로 다른 파일 시스템을 가질 수 있는 이유는 이미지(파일의 집합)를 루트 파일 시스템으로 강제로 인식시켜 프로세스를 실행하기 때문이빈다. 프로세스는 자신의 루트 디렉터리가 /
가 아닌 그 아래의 특정 디렉터리인 것처럼 동작합니다. 이에 대한 더 자세한 원리는 컨테이너 기초 - chroot를 사용한 프로세스의 루트 디렉터리 격리와 컨테이너 기초 - 정적 링크 프로그램을 chroot와 도커(Docker) scratch 이미지로 실행하기를 참고해주세요.*
* 실제로 도커에서 chroot를 사용해 루트 디렉터리를 격리하지는 않습니다. 하지만 원리를 이해하는 데는 chroot면 충분합니다.
도커 컨테이너의 프로세스 아이디는 1번
이미 도커 컨테이너는 가상머신이 아니고, 프로세스다라는 얘기를 들어보셨을 수도 있습니다. 그렇다면 이건 어떤가요, 도커 컨테이너의 PID를 생각해보신 적이 있으신가요? 이번에는 우분투 이미지에서 실행한 bash
프로세스의 PID를 확인해보겠습니다. 셸의 PID를 확인하려면 $$
를 실행해보면 됩니다.
$ echo $$
5673
$ docker run -it ubuntu:latest bash
root@9a09675d42ed:/# echo $$
1
흥미로운 결과입니다. 먼저 호스트 상에서 실행중인 셸의 PID는 5673입니다. 그런데 컨테이너로 실행한 bash
셸의 PID는 1번입니다. 일반적으로 리눅스에서 1번 프로세스는 init 프로세스로 특별한 의미를 가지고 있습니다. 실제로 호스트 상에서 pstree
를 실행해보면 모든 프로세스가 1번 프로세스(systemd
)에 물려있는 것을 확인할 수 있습니다.
$ pstree
systemd(1)─┬─VBoxService(996)─┬─{VBoxService}(999)
│ ├─{VBoxService}(1002)
│ ├─{VBoxService}(1004)
│ ├─{VBoxService}(1005)
│ ├─{VBoxService}(1006)
│ ├─{VBoxService}(1007)
│ └─{VBoxService}(1008)
├─accounts-daemon(808)─┬─{accounts-daemon}(861)
│ └─{accounts-daemon}(882)
├─agetty(985)
├─atd(803)
├─containerd(3866)─┬─{containerd}(3872)
...
그런데 도커 컨테이너 안에서 pstree
를 실행해보면 정말로 1번 프로세스 bash
인 것을 확인할 수 있습니다.
root@2d1239925257:/# apt-get update
root@2d1239925257:/# apt-get install psmisc
root@2d1239925257:/# pstree -p
bash(1)---pstree(256)
컨테이너는 프로세스지만, 프로세스라고 부르기보다는 컨테이너라고 부르는 데는 이유가 있는 법입니다. 컨테이너는 (주로) 리눅스 커널에 포함된 프로세스 격리 기술들을 사용해서 생성된 특별한 프로세스입니다. 처음 도커를 보면 가상머신이라고 느끼는 건 바로 이런 이유 때문입니다. 예를 들어 앞선 예제에서 bash
명령어로 마치 컨테이너라는 가상머신에 접속해서 명령어를 실행하는 것처럼 느껴집니다만, 사실 bash
는 가상머신에 접속하는 명령어가 아니라 그냥 호스트 상에서는 프로세스일 뿐입니다. 그런데 PID가 1인 특별한 bash
프로세스입니다 (?!). 어떻게 하나의 하늘 아래 두 개의 태양(1번 프로세스)이 존재할 수 있을까요.
root@47672c55680d:/# kill -9 1
root@47672c55680d:/#
일반적인 프로세스와 달리 1번 프로세스는 내부적으로 SIGKILL 시그널로 죽일 수도 없습니다.
PID 1번의 비밀, 도커 없이 일반 프로세스의 PID를 1번으로 실행하기
어떻게 이게 가능한 걸까요? 이를 구현하는 데 사용된 기능이 바로 리눅스 네임스페이스입니다. 정확히는 PID 네임스페이스가 분리되어있기 때문에 도커 컨테이너의 프로세스는 1번이 됩니다. 도커 없이도 unshare
명령어로 프로세스의 PID 네임스페이스를 분리해볼 수 있습니다.*
* 여기서부터는 루트 계정으로 로그인하거나, 루트 권한을 얻어서 따라해보시는 것을 추천합니다.
$ echo $$
5673
$ mkdir busybox-image
$ docker run --name busybox-image busybox:latest
$ docker export busybox-image > ./busybox-image/image.tar
$ cd busybox-image; tar xf image.tar; cd ..
$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ # echo $$
1
생각만큼 간단하지는 않네요. 최대한 간단히 설명해보겠습니다. unshare
는 특정 프로세스를 실행할 때 특정 네임스페이스를 분리해주는 기능입니다. 여기서 특정 네임스페이스라 함은, 네임스페이스에도 UTS, PID, 네크워크, 마운트 등 다양한 종류가 있기 때문입니다. 그 중에서 여기서 분리할 네임스페이스는 PID 네임스페이스로, -p
옵션을 지정해주면 됩니다. 그런데 이것만으로는 잘 되지 않습니다.
$ sudo unshare -p /bin/sh
# echo $$
2170
분명 PID 네임스페이스는 분리가 되었을 텐데, PID가 1번이 아닙니다. 왜냐면 unshare
명령어를 사용하더라도 호스트의 파일 시스템을 그대로 사용하고 있고 PID 관련 정보는 /proc
아래에 있는 정보를 사용하기 때문입니다. 여기도 컨테이너가 프로세스라는 아주 중요한 힌트가 숨어있습니다만, 그 얘기는 뒤에서 다루겠습니다. 어쨌건 PID 네임스페이스가 분리되지 않은 것처럼 보입니다. unshare
명령어는 --mount-proc
으로 /proc
을 분리하는 기능을 지원합니다. 하지만, 호스트의 /proc
에 마운트를 시도하므로 당연히 동작하지 않습니다. 😢
$ sudo unshare -p --mount-proc /bin/sh
unshare: mount /proc failed: Device or resource busy
PID 네임스페이스를 제대로 분리해보기 위해서는 독자적인 환경이 필요합니다. chroot 글에서 자세히 다루고 있습니다만, 루트 디렉터리를 바꿔서 프로세스를 실행한다는 게 생각보다 간단한 일은 아닙니다. 독자적인 환경에는 프로세스를 실행하기 위한 모든 파일들이 준비되어있어야합니다. 직접 준비하는 것도 가능하지만, 도커 이미지를 복사해오는 게 가장 간단합니다. 그래서 여기서는 docker
를 사용해 busybox
이미지 내용을 그대로 복사하고, chroot
를 사용해 실행 환경(파일시스템)을 격리해 셸을 실행했습니다.
$ mkdir busybox-image
$ docker run --name busybox-image busybox:latest
$ docker export busybox-image > ./busybox-image/image.tar
$ cd busybox-image; tar xf image.tar; cd ..
$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ # echo $$
1
busybox
이미지의 내용을 복사한 디렉터리를 기반으로 chroot
를 실행해서 프로세스 실행 경로를 분리하고 unshare
의 --mount-proc
옵션으로 /proc
을 마운트 해줍니다. 이렇게 실행하면, 짠 프로세스 아이디 1번이 되었습니다.
2개의 PID: 프로세스가 바라보는 프로세스 ID, 호스트가 바라보는 프로세스 ID
여기서는 PID 네임스페이스에 대해서 좀 더 알아보겠습니다. 앞서 살짝 힌트가 나왔습니다만, 단순히 PID를 분리해서 셸을 실행하니 PID가 1이 아니라 2170이라는 일반 프로세스의 값이 되었습니다.
$ sudo unshare -p /bin/sh
# echo $$
2170
결론부터 이야기하자면, 1과 2170은 둘 다 맞습니다. 여기서는 잘 확인이 안 됩니다만, 이 프로세스의 아이디는 1이기도 하고, 2170이기도 합니다. 그럼 다시 한 번 unshare
로 PID 네임스페이스를 분리해보겠습니다.
$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ # echo $$
1
PID가 1번입니다. 이 프로세스를 그대로 놔두고, 이제 호스트의 셸을 하나 더 띄웁니다. 거기서 ps
로 최근에 실행된 프로세스들을 확인해봅니다.
$ ps aux --sort +start_time | tail -n 5
root 2376 0.0 0.1 21472 3700 pts/0 S 03:49 0:00 bash
root 2386 0.0 0.0 7456 732 pts/0 S 03:49 0:00 unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
root 2387 0.0 0.0 1312 4 pts/0 S+ 03:49 0:00 /bin/sh
vagrant 2396 0.0 0.1 37516 3480 pts/1 R+ 03:50 0:00 ps aux --sort +start_time
vagrant 2397 0.0 0.0 7508 856 pts/1 S+ 03:50 0:00 tail -n 5
방금 실행한 unshare
(2386) 명령어가 보입니다. 그리고 그 바로 아래에 /bin/sh
(2387)이 보입니다. 뭔가 감이 오지 않나요? 이 프로세스를 강제로 kill
해보겠습니다.
$ sudo kill -9 2387
2387 프로세스를 죽였는데, unshare
로 실행된 sh
도 종료되었을 것입니다. 이는 둘이 같은 프로세스이기 때문입니다. 즉, 프로세스에게는 자신의 PID가 1번으로 보이지만, 호스트 입장에서는 PID가 2387인 일반적인 프로세스라는 의미입니다.
프로세스 ID를 격리하는 PID 네임스페이스
PID 네임스페이스라는 게, 이렇게 보니까 아주 생소하게 느껴집니다만 사실 모든 프로세스는 PID 네임스페이스를 가지고 있습니다. 리눅스 시스템에 대해서 공부해보신 분이라면, /proc
디렉터리에 시스템과 프로세스에 대한 정보가 담겨져있다는 것을 알고 계실 겁니다. 호스트 상에서 /proc
디렉터리로 이동해보면 프로세스 아이디 이름을 가진 디렉터리들이 옹기종기 모여 있습니다.
숫자로 된 이 디렉터리들에는 특정 프로세스에 대한 정보들이 담겨있습니다. 위에서 몇 번 언급한 1번 프로세스도 보이네요. 여기서 1번 프로세스는 systemd
입니다. 모든 프로세스 디렉터리 아래에는 ns
라는 디렉터리가 있는데 여기서 ns
가 바로 네임스페이스를 줄인 단어입니다. 1번 프로세스 아래의 ns
디렉터리의 내용을 살펴보겠습니다.
/proc/1/ns# ll -l
total 0
dr-x--x--x 2 root root 0 Jan 11 04:11 ./
dr-xr-xr-x 9 root root 0 Jan 11 03:10 ../
lrwxrwxrwx 1 root root 0 Jan 11 04:12 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 net -> 'net:[4026531993]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jan 11 04:12 uts -> 'uts:[4026531838]'
앞서 네임스페이스는 PID 뿐만 아니라 UTS, 네트워크, 마운트 등 다양한 네임스페이스가 있다고 언급했습니다. 여기서도 바로 확인할 수 있네요. 다양한 네임스페이스가 있습니다만 지금은 PID 네임스페이스를 살펴보고 있습니다. 자세히 보면 pid:[4026531836]
에 10자리 숫자가 적혀있습니다. 바로 이 숫자가 현재 프로세스의 PID 네임스페이스입니다. systemd
는 4026531836
PID 네임스페이스의 첫번째 프로세스이며 PID는 1번입니다. 기본적으로 모든 프로세스는 init 프로세스의 네임스페이스를 그대로 공유합니다.
/proc# ll **/ns/pid
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1002/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1046/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1047/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1059/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 106/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1079/ns/pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 11 04:16 1084/ns/pid -> 'pid:[4026531836]'
...
다른 프로세스의 PID 네임스페이스를 출력해보면 대부분 1번 프로세스의 값과 같은 것을 알 수 있습니다. unshare
를 사용하면 PID 네임스페이스가 정말로 분리되는 걸까요? 다시 한번 unshare
로 sh
를 실행해두고, 다른 창을 띄워서 ps
로 최근에 실행된 프로세스 목록을 확인해봅니다.
$ sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
/ #
## 다른 창에서
$ ps aux --sort +start_time | tail -n 5
root 2680 0.0 0.2 63516 4216 pts/0 S 04:21 0:00 sudo unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
root 2681 0.0 0.0 7456 764 pts/0 S 04:21 0:00 unshare -p -f --mount-proc chroot ./busybox-image /bin/sh
root 2682 0.0 0.0 1308 4 pts/0 S+ 04:21 0:00 /bin/sh
root 2684 0.0 0.1 37516 3516 pts/1 R+ 04:22 0:00 ps aux --sort +start_time
root 2685 0.0 0.0 7508 868 pts/1 S+ 04:22 0:00 tail -n 5
우리가 확인하고자 하는 프로세스는 unshare
바로 아래에 있는 /bin/sh
프로세스입니다. PID는 2682입나디. /proc/2682/ns/pid
의 내용을 확인해보겠습니다.
/proc# ll /proc/1/ns/pid
lrwxrwxrwx 1 root root 0 Jan 11 04:12 /proc/1/ns/pid -> 'pid:[4026531836]'
/proc# ll /proc/2682/ns/pid
lrwxrwxrwx 1 root root 0 Jan 11 04:24 /proc/2682/ns/pid -> 'pid:[4026532296]'
1번 프로세스의 PID 네임스페이스는 4026531836
인데, 2682번 프로세스의 PID 네임스페이스는 4026532296
입니다. 즉, 값이 다르므로 다른 네임스페이스라는 것을 알 수 있습니다.
도커 컨테이너는 정말로 프로세스인가요?
도커 컨테이너 이야기를 하다가 갑자기 unshare
와 PID 네임스페이스로 빠져버렸습니다만, 다시 돌아와보겠습니다. 감이 오시나요? 도커 컨테이너의 PID가 1번인 원리도 위에서 살펴본 것과 똑같습니다. 이번에는 도커 컨테이너를 찾아가보겠습니다. 먼저 실험용으로 Nginx 컨테이너를 하나 실행하겠습니다.
$ docker run -d -p 80:80 nginx:latest
622eb0c2acac4d6c9304e57dcc8ea83fdaec31020ce124e281942dc44ee30725
$ docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
622eb0c2acac nginx:latest "nginx -g 'daemon of…" 6 seconds ago Up 4 seconds 0.0.0.0:80->80/tcp gracious_rubin
$ curl 0.0.0.0:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
curl
명령어로 정상 동작하는지도 살펴보았습니다. 아, 프로세스를 찾기에 앞서 프로세스 아이디가 1인지부터 확인해보겠습니다. nginx:latest
이미지에는 ps
도 없어서 확인이 쉽지는 않습니다만, /proc/1
의 명령어를 확인해보겠습니다.
$ docker exec -it 622eb0c2acac cat /proc/1/comm
nginx
1번 프로세스의 명령어가 nginx
인 것을 확인할 수 있습니다. 자, 이제 이 컨테이너 = 프로세스에 대해서 알아볼 차례인데요, 도커 이미지에 대해 알아본 글에서는 /var/lib/docker
디렉터리의 내용을 뒤적여보았습니다. 이번에는 /var/run/docker
아래의 내용을 살펴봅니다.
$ cd /var/run/docker/runtime-runc/moby/
$ ls
622eb0c2acac4d6c9304e57dcc8ea83fdaec31020ce124e281942dc44ee30725
앞서 실행한 nginx
컨테이너로 추정되는 디렉터리가 보이네요. 컨테이너 아이디와 앞부분이 같습니다. 이 디렉터리 안에는 state.json
파일이 하나 있습니다. 내용이 꽤 많아서 여기서는 jq로 필요한 부분만 추출해보겠습니다.*
* jq는 커맨드라인에서 JSON 파일 내용을 바로 프로세싱할 수 있는 도구입니다. 우분투에서는 apt install jq
로 설치할 수 있습니다. 더 자세한 내용은 커맨드라인 JSON 프로세서 jq : 기초 문법과 작동원리 글을 참고해주세요.
$ cd 622eb0c2acac4d6c9304e57dcc8ea83fdaec31020ce124e281942dc44ee30725
$ cat state.json | jq '.init_process_pid'
2899
init_process_pid
의 내용을 보니 프로세스 아이디가 2899
인 것을 확인할 수 있습니다. 여기에는 더 많은 정보들이 있습니다. 그 중에서도 네임스페이스에 관련된 정보들을 한 번 살펴보겠습니다.
$ cat state.json | jq '.namespace_paths'
{
"NEWCGROUP": "/proc/2899/ns/cgroup",
"NEWIPC": "/proc/2899/ns/ipc",
"NEWNET": "/proc/2899/ns/net",
"NEWNS": "/proc/2899/ns/mnt",
"NEWPID": "/proc/2899/ns/pid",
"NEWUSER": "/proc/2899/ns/user",
"NEWUTS": "/proc/2899/ns/uts"
}
네임스페이스들이 /proc/2899
아래에 새로 정의되어있다는 것을 확인할 수 있네요. 이는 호스트에서 확인할 수 있습니다. 먼저 1번 프로세스와 2899 프로세스의 네임스페이스들을 비교해보겠습니다.
$ diff \
<(ls -Al /proc/1/ns | awk '{ print $9 $10 $11 }') \
<(ls -Al /proc/2899/ns | awk '{ print $9 $10 $11 }')
3,7c3,7
< ipc->ipc:[4026531839]
< mnt->mnt:[4026531840]
< net->net:[4026531993]
< pid->pid:[4026531836]
< pid_for_children->pid:[4026531836]
---
> ipc->ipc:[4026532238]
> mnt->mnt:[4026532236]
> net->net:[4026532298]
> pid->pid:[4026532239]
> pid_for_children->pid:[4026532239]
9c9
< uts->uts:[4026531838]
---
> uts->uts:[4026532237]
ipc
, mnt
, net
, pid
, pid_for_children
, uts
네임스페이스가 다르다는 것을 확인할 수 있습니다. 네임스페이스는 다르지만 ps
로 일반적인 프로세스라는 것을 한 번 확인해보겠습니다.
$ ps aux | grep 2899
root 2899 0.0 0.2 10632 5348 ? Ss 06:20 0:00 nginx: master process nginx -g daemon off;
이렇게 보면 그냥 nginx
프로세스네요. 이 2899 프로세스가 nginx:latest
이미지로 실행된 컨테이너라는 것을 어떻게 확인할 수 있을까요? kill
로 SIGKILL
시그널을 보내보면 되겠죠? 😅
$ docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
622eb0c2acac nginx:latest "nginx -g 'daemon of…" 29 minutes ago Up 29 minutes 0.0.0.0:80->80/tcp gracious_rubin
$ sudo kill -9 2899
$ docker ps -al
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
622eb0c2acac nginx:latest "nginx -g 'daemon of…" 29 minutes ago Exited (137) 2 seconds ago gracious_rubin
먼저 docker ps
로 컨테이너가 잘 실행중인 것을 확인하고, kill
을 실행하고 다시 docker ps -al
로 컨테이너의 상태를 확인해보았습니다. 위에서 실행중이던 컨테이너가 SIGKILL
시그널을 받고 사망한 것을 확인할 수 있습니다. ps
로 찾아보아도 더 이상 2899 프로세스는 보이지 않습니다.
어떤가요, 이제 확실해졌죠? 컨테이너는 그냥 프로세스였습니다. 좀 더 정확히는 호스트 입장에서는 컨테이너도 그냥 하나의 프로세스에 불과합니다.
마치며
여기까지 도커 컨테이너가 프로세스에 불과하다는 점에 대해서 알아보았습니다. 알고 보면 별 게 아니긴 합니다만, 단순히 도커 컨테이너가 프로세스라는 말을 들어오다가 직접 확인해보면 느낌이 많이 다릅니다. 예를 들어 도커 이미지는 여전히 가상머신의 이미지처럼 느껴지곤 합니다만, 도커 컨테이너가 프로세스라는 걸 정확히 이해하면 도커 이미지를 ’단 하나의 타깃 프로세스를 실행하기 위한 파일들의 집합’으로 이해할 수 있게됩니다. docker exec
명령어도 SSH 접속이나 가상머신에서 셸을 실행하는 것이 아니라, 컨테이너가 사용중인 네임스페이스나 파일 시스템에 접근하기 위한 특별한 명령어에 불과하다는 것을 알 수 있습니다.
여담입니다만, 최근에는 컨테이너가 가상머신이 아니라는 것도 아주 정확한 표현은 아닙니다. 리눅스 기반의 도커 컨테이너는 여전히 리눅스 프로세스입니다만, 카타 컨테이너Kata Containers나 파이어크랙커Firecracker와 같은 도구들은 마이크로VM을 기반으로 컨테이너를 실행하거나 서버리스 환경을 구축하려는 프로젝트들입니다. 기존에는 가상머신과 컨테이너를 기동 시간의 차이로도 많이 설명했습니다만 마이크로VM 기술이 많이 발전하면서 VM으로 컨테이너를 실행하는 시간도 현저히 줄어들고 있으며, 리눅스 프로세스보다 훨씬 격리 수준이 높기 때문에 시간이 지나면 컨테이너가 VM인 게 당연한 날이 올지도 모르겠네요.