Docker Engine LXC, libcontainer
초기 도커 엔진 #

초기에는 Dockerd, LXC 라는 두 개의 주요 구성을 가지고 있었다.
dockerd #
이 때 dockerd는 현재의 dockerd와 다르게 모놀리 바이너리 형식이었다. (모듈화 x)
즉 daemon, client, api, runtime, image builds 등을 비롯한 많은 코드들을 담고있었다.
LXC #

LXC는 단일 컨트롤 호스트 상에서 여러개의 고립된 리눅스 컨테이너를 실행하기 위한 운영 시스템 레벨 가상화 방법이다.
namespace, cgroups을 설명해보자면
- namespace: 운영 시스템을 쪼개서 각각의 고립된 상태로 운영이 되는 개념
- cgroups: namespace로 고립된 환경에서 사용할 자원을 제한하는 역할등을 하는 개념
어쨋든 분리된 후 고립된 환경들이 만들어진다는 것을 확인할 수 있다.
이 기술이 바로 container기술의 근간이 되는 것 이다.
그러나 LXC에는 문제가 있었다. 바로 리눅스에만 특화 되어있다는 점이다.
Docker는 멀티 플랫폼을 목표로 하는데 이건 큰 리스크가 있었고, 시스템을 구성하는 핵심 요소가 외부 시스템에 의존한다는 문제도 있었다.
그렇기 때문에 docker사는 lxc를 대체하는 libcontainer라는 독자적인 툴을 개발했다 (go로 개발됨 그리고 불특정 플랫폼 툴로 개발되었다.)
그렇기에 Docker 9.0 부터는 기본 실행 드라이버는 libcontainer로 대체했다.
Libcontainer #
Libcontainer는 현재의 docker engine에서 사용되고 있는 주요 컴포넌트다.
container 생성시 namespaces, cgroups, capabilities를 제공하고 filesystem의 접근을 제한할 수 있다.
컨테이너가 생성된 후 작업을 수행할 수 있도록 컨테이너의 수명 주기를 관리할 수 잇다.

위에서 볼 수 있듯 libcontainer는 도커 내부에서 실행되며, libcontainer는 자체적으로 제작된 CLI wrapper인 runc를 사용한다.
Docker Engine #
Docker는 Client - Server 모델을 구현한 애플리케이션이다.
Docker Engine은 Docker Components와 서비스를 제공하는 컨테이너를 구축하고 실행하는 핵심 소프트웨어다.
Docker Engine은 Docker Daemon, Rest API, API를 통해서 도커 데몬과 통신하는 CLI로 모듈식으로 구성되어 있으며 개발자들은 도커라고 할 때 주로 도커 엔진을 의미한다.

컨테이너 빌드, 실행, 배포하는 등 무거운 작업은 도커 데몬이 하며, 도커 클라이언트는 이러한 로컬 혹은 원격의 도커 데몬과 통신한다
통신을 할 때에는 UNIX socket(/var/run/docker.sock) 또는 네트워크 인터페이스를 통한 REST API를 사용합니다.
Docker Client #
유저가 Docker CLI를 통해 아래와 같은 명령어를 입력한다.docker container run --name ctr1 -it alpine:latest sh
Docker CLI에 입력하면 Docker Client는 적절한 api payload로 변환해 도커 데몬에게 rest api 요청을 보낸다. POST /continers/create HTTP/1.1
Dockerd #

API Unix Socket을 통해 dockerd에게 전달된다.
새 컨테이너를 시작할 때, dockerd는 로컬 이미지가 있는지 확인하고 없다면 레지스트리 리퍼지토리에서 해당하는 이름의 이미지를 가져온다.
또한 로깅 드라이버나 볼륨이나 볼륨 드라이버를 설정하는 등 컨테이너에 대부분의 설정을 지정한다.
Dockerd가 새로운 컨테이너를 생성하라는 명령을 수신하면 containerd를 호출한다. dockerd는 이 호출을 CRUD 스타일의 api를 통해 gRPC로 통신한다. client.NewContainer(context, ....
Containerd #

containerd는 실제로 containers를 생성하지 못하고, runc를 통해 생성한다.
도커 이미지를 가져와서 컨테이너 구성을 적용하여 runc가 실행할 수 있는 OCI 번들로 변환한다.
runc #

runc는 (내부의 libcontainer를 사용) OS 커널에 접속해서 컨테이너를 만드는 모든 구성요소 네임스페이스, cgruops 등을 하나로 묶고 새로운 컨테이너를 생성한다.
shim #

그 다음 shim을 실행하고 이때 shim 프로세스는 runc의 하위 프로세스로 실행된다. 그리고 runc에서 생성한 컨테이너를 fork하고 runc를 exit한다. 그리고 docker containerd shim이 새로운 상위 프로세스가 되어 컨테이너 생명주기를 관리한다.
이때 shim이 runc에서 컨테이너를 fork할때 컨테이너의 file descriptor, 종료상태를 관리하는 데 최소의 코드를 메모리에 남긴다.