Container Internals - 리눅스 커널부터 살펴보는 컨테이너 기술과 도커의 구조
이 포스트에 있는 스케치들은 draw.io 라는 툴을 사용하여 직접 제작하였습니다.
Container 기술 전에는 Virtual Machine(가상 머신) 기술이 있었다. 가상 머신을 통해 우리는 가상화 소프트웨어 시장을 열었고 대표적인 가상화 소프트웨어, 즉 Hypervisor에는 VMWare와 오라클의 VirtualBox, 마소의 Hyper-V 정도가 있다.
가상 머신은 소프트웨어로 하드웨어 자원을 구현한 것으로 완벽하게 하나의 가상 컴퓨터를 구현한다고 보면 된다. 이것은 매우 비효율적인 구성이다. 무려 OS위에 OS를 하나 더 두었기 때문이다.
하지만 Container 기술은 한 OS를 통해 가볍고 빠르게 가상화와 비슷한 작업을 진행할 수 있다. 어떻게 이것이 가능할까?
VM과 Container의 차이
![](https://pmh.codes/content/images/2023/11/image-15.png)
새로운 머신을 완벽히 소프트웨어로 구현한 가상 머신 기술과 달리 컨테이너 기술은 어플리케이션을 그대로 호스트의 OS로 돌린다.
이때 실행된 어플리케이션의 자원을 제한시키고 어플리케이션의 외부 리소스 접근 권한을 제한시키므로서 가상화와 비슷한, 즉 격리화 과정을 통해 어플리케이션을 특정 환경에서 동작하도록 할 수 있다.
이 격리화 과정에 필요한 기능들은 커널단에서 직접 지원해야 하며 리눅스 커널에는 프로세스 격리 기능이 구현되어 있다.
이 이유로 가상머신은 Windows, MacOS, Linux 등 다양한 OS위에서 돌릴 수 있지만 Docker와 Container 기술은 사실상 Linux OS위에서만 작동할 수 있다. Linux 커널에 있는 기능을 활용하는 것이기 때문이다.
Daemon은 Supervisor와 다르게 Linux 커널에게 "이 프로그램을 cpu는 이정도 사용하고, ram은 이정도만 사용할 수 있도록 실행해줘." 라고 명령하는 역할만 할 뿐 어플리케이션과 Linux 커널 사이에 끼어 관여하지는 않는다.
당연히 Container 기술은 한 OS만을 필요로 하기 때문에 가상 머신과 비교하여 빠르고 가볍다.
하지만 Container 기술은 완벽한 가상화가 아니기 때문에 호스트의 Linux 커널이 변조되었다면 보안적인 문제가 발생하게 된다.
Docker internals
자 그럼 Container 기술 시장에서 가장 많이 사용되고 있는 Docker를 뜯어보자.
![](https://pmh.codes/content/images/2023/11/image-18.png)
Docker는 대략적으로 dockerd
, containerd
, container-shim
, runc
등으로 이루워져 있다.
dockerd는 컨테이너 기술을 사용자가 이용하기 편하도록 도커 이미지 관리, 네트워크 관리, 볼륨 계산 등을 하고 containerd에게 gRPC 통신으로 명령을 내린다.
(여기서 gRPC는 HTTP/2 기반의 cross-language 통신(protobuf IDL)이다. 나중에 한번 정리해서 포스팅 해봐야 겠다.)
containerd는 dockerd의 명령을 받아 컨테이너들의 메타데이터를 관리하고 스토리지 & 이미지 레이어 계산 등을 한다. 최종적으로 containerd는 여러개 containerd-shim를 실행시키며 컨테이너들의 라이프사이클을 관리한다.
containerd-shim은 격리된 컨테이너의 부모 프로세스로 컨테이너가 종료될때까지 상주하며 컨테이너의 입력을 처리하고 상태 및 로그를 수집해 gRPC로 containerd에게 전송한다.
runc는 리눅스 커널의 기능을 사용해 containerd-shim의 자식 프로세스로 격리된 컨테이너를 생성하며 생성후 종료된다.
Step by Step
자 그럼 저 구조가 맞는지 프로세스 목록을 보며 확인해보자.
![](https://pmh.codes/content/images/2023/11/image-20.png)
먼저 Idle 상태에서는 containerd와 dockerd가 켜져있는 것을 볼 수 있다. 여기서 dockerd의 커멘드라인을 잘 보면 containerd의 Unix Socket이 명시되어 있는것을 알 수 있다. 이 Unix Socket을 통해 gRPC 통신을 진행하게 된다.
자 그럼 docker run -it ubuntu
를 실행해보자
![](https://pmh.codes/content/images/2023/11/image-22.png)
먼저 이미지가 없으므로 dockerd는 이미지를 다운로드 받고 컨테이너 생성전 필요한 잡다한 연산들을 진행하게 된다. 이때 dockerd의 CPU가 급증하는 것을 볼 수 있다.
![](https://pmh.codes/content/images/2023/11/image-24.png)
이후 dockerd는 containerd에게 컨테이너를 생성하도록 명령하고 containerd는 containerd-shim을 생성하는 것을 볼 수 있다.
스크린샷에는 안나왔지만 containerd-shim에도 역시 containerd의 Unix Socket이 명시되어 있고 gRPC 통신을 한다.
이후 containerd-shim은 자식으로 runc를 실행해 컨테이너가 Linux 커널 기능을 이용해 격리화된 프로세스를 만들도록 명령한다.
![](https://pmh.codes/content/images/2023/11/image-23.png)
격리화를 완료한 runc는 종료되고 격리화된 프로세스인 /bin/bash
를 containerd-shim의 자식으로 만든다.