Container Internals - 리눅스 커널부터 살펴보는 컨테이너 기술과 도커의 구조
이 포스트에 있는 스케치들은 draw.io 라는 툴을 사용하여 직접 제작하였습니다.
Container 기술 전에는 Virtual Machine(가상 머신) 기술이 있었다. 가상 머신을 통해 우리는 가상화 소프트웨어 시장을 열었고 대표적인 가상화 소프트웨어, 즉 Hypervisor에는 VMWare와 오라클의 VirtualBox, 마소의 Hyper-V 정도가 있다.
가상 머신은 소프트웨어로 하드웨어 자원을 구현한 것으로 완벽하게 하나의 가상 컴퓨터를 구현한다고 보면 된다. 이것은 매우 비효율적인 구성이다. 무려 OS위에 OS를 하나 더 두었기 때문이다.
하지만 Container 기술은 한 OS를 통해 가볍고 빠르게 가상화와 비슷한 작업을 진행할 수 있다. 어떻게 이것이 가능할까?
VM과 Container의 차이
새로운 머신을 완벽히 소프트웨어로 구현한 가상 머신 기술과 달리 컨테이너 기술은 어플리케이션을 그대로 호스트의 OS로 돌린다.
이때 실행된 어플리케이션의 자원을 제한시키고 어플리케이션의 외부 리소스 접근 권한을 제한시키므로서 가상화와 비슷한, 즉 격리화 과정을 통해 어플리케이션을 특정 환경에서 동작하도록 할 수 있다.
이 격리화 과정에 필요한 기능들은 커널단에서 직접 지원해야 하며 리눅스 커널에는 프로세스 격리 기능이 구현되어 있다.
이 이유로 가상머신은 Windows, MacOS, Linux 등 다양한 OS위에서 돌릴 수 있지만 Docker와 Container 기술은 사실상 Linux OS위에서만 작동할 수 있다. Linux 커널에 있는 기능을 활용하는 것이기 때문이다.
Daemon은 Supervisor와 다르게 Linux 커널에게 "이 프로그램을 cpu는 이정도 사용하고, ram은 이정도만 사용할 수 있도록 실행해줘." 라고 명령하는 역할만 할 뿐 어플리케이션과 Linux 커널 사이에 끼어 관여하지는 않는다.
당연히 Container 기술은 한 OS만을 필요로 하기 때문에 가상 머신과 비교하여 빠르고 가볍다.
하지만 Container 기술은 완벽한 가상화가 아니기 때문에 호스트의 Linux 커널이 변조되었다면 보안적인 문제가 발생하게 된다.
Docker internals
자 그럼 Container 기술 시장에서 가장 많이 사용되고 있는 Docker를 뜯어보자.
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
자 그럼 저 구조가 맞는지 프로세스 목록을 보며 확인해보자.
먼저 Idle 상태에서는 containerd와 dockerd가 켜져있는 것을 볼 수 있다. 여기서 dockerd의 커멘드라인을 잘 보면 containerd의 Unix Socket이 명시되어 있는것을 알 수 있다. 이 Unix Socket을 통해 gRPC 통신을 진행하게 된다.
자 그럼 docker run -it ubuntu
를 실행해보자
먼저 이미지가 없으므로 dockerd는 이미지를 다운로드 받고 컨테이너 생성전 필요한 잡다한 연산들을 진행하게 된다. 이때 dockerd의 CPU가 급증하는 것을 볼 수 있다.
이후 dockerd는 containerd에게 컨테이너를 생성하도록 명령하고 containerd는 containerd-shim을 생성하는 것을 볼 수 있다.
스크린샷에는 안나왔지만 containerd-shim에도 역시 containerd의 Unix Socket이 명시되어 있고 gRPC 통신을 한다.
이후 containerd-shim은 자식으로 runc를 실행해 컨테이너가 Linux 커널 기능을 이용해 격리화된 프로세스를 만들도록 명령한다.
격리화를 완료한 runc는 종료되고 격리화된 프로세스인 /bin/bash
를 containerd-shim의 자식으로 만든다.