[ Docker ] Container & Docker 작동 원리
이전 게시물에서 다뤘던 가상화 기술(VM, Hypervisor)의 단점을 보완하는 Container 기술이 등장했다.
VM의 단점
- 프로세스 단위로 격리하고 싶은 경우, OS 레벨조차도 가상화해야 할 파트가 너무 많다.
- VM 마다 고유한 OS(게스트 OS)가 필요하기 때문에, 각각 CPU, RAM 및 기타 리소스 등 별도의 자원를 소비하며 보다 많은 시간과 자원을 낭비하게 된다.
- 완전한 운영체제가 올라가기 때문에, 실행 자체만으로도 대량의 메모리가 필요하다. 프로세스의 경우 전환이 매우 빠르고 애플리케이션들이 대부분의 시간을 sleep 상태로 있기에, 여러 개의 VM들이 CPU를 경쟁해도 크게 문제가 되지 않는다. 하지만 메모리는 CPU 만큼 빠르게 전환(자원 회수) 할 수가 없기에 대량의 메모리를 가지고 있어야 한다. 가상화를 할 때, 적정 메모리를 산정하는 것은 상당히 까다로운 문제이다.
- 특정 OS는 라이센스가 필요한 경우도 있습니다.
이러한 단점을 보완하여 Host OS 위에서 프로세스 단위로 격리하는 컨테이너 기술이 등장하였다. 컨테이너와 VM 모두 다른 시스템과 격리되어 실행되기에 하이레벨에서는 유사하지만, 로우레벨에서는 게스트 OS 의 존재 유무로 인해 확장성과 이식성, 자원 효율성 등에 차이가 있어 장단점이 있습니다.
Container의 장점 ( 빠른 시작과 리소스 절감 )
- 컨테이너에 완전한 운영 체제(OS)를 포함하지 않고 호스트 운영체제 위에서 실행되어 호스트 운영 체제의 자원을 공유한다. 그래서 시작 시간이 매우 빠르며, 메모리 및 디스크 공간을 절약할 수 있다.
- VM을 시작하려면 운영 체제의 부팅 프로세스가 필요하며, 이는 시간이 오래 걸리고 비용이 많이 들 수 있다. 하지만 컨테이너는 가볍고 빠른 실행을 제공한다. 각 컨테이너는 애플리케이션을 실행하는 데 필요한 모든 것을 포함하는 패키지인 이미지를 사용하여 구동되기 때문이다.
- OS 패치 및 유지보수의 오버헤드를 줄여주며 VM의 문제를 해결할 수 있습니다.
Container의 단점 ( 네트워크 구성의 어려움과 단일 OS )
- 네트워크 구성이 어렵고 네트워킹 복잡성이 증가할 수 있다. 호스트 운영체제 레벨에서 네트워크를 한번 더 추상화 해야 해서 가상머신 보다 네트워크 구성에 신경을 써야 할게 많기 때문이다.
VM은 가상머신에다 IP 주소 붙여서 연결하면 된다. 하지만 컨테이너를 그럴 수 없다. 그냥 프로세스에 IP를 붙이는 것과 같은 개념이기 때문. 또한 컨테이너는 대량 생산이 장점인데 IP주소를 각각 붙이는 것이 더욱 낭비이다. - 컨테이너는 하나의 운영 체제만 구동할 수 있다.
- 보안에 취약하다. 컨테이너는 호스트 운영 체제의 리소스를 공유하므로, 보안 취약성이 발생할 경우 다른 컨테이너 및 호스트 시스템에 영향을 줄 수 있다. 악성 사용자나 프로세스가 호스트 운영 체제 또는 다른 컨테이너에 액세스하려는 시도를 할 수 있으며, 이를 방지하기 위해 추가적인 보안 조치가 필요할 수 있다.
즉, 단일 OS 커널에서 여러 애플리케이션을 실행하려는 경우에는 컨테이너가 유리하고, 서로 다른 OS 환경에서 애플리케이션을 실행해야 하는 경우에는 VM이 필요하다. 그리고 이런 컨테이너 기술을 구현한 것 중 가장 널리 쓰는 오픈소스는 Docker이다. 이전 게시물에서 얘기했던 LXC 기억을 끄집어내서 도커에 대해 자세히 알아보자.
컨테이너 기술인 LXC와 Docker는 애플리케이션을 격리된 환경에서 실행시키는데 중점을 두고 있다. 하지만 LXC는 주로 리눅스 운영 체제에 특화되어 있으며, 호스트 운영 체제가 리눅스인 경우에만 사용할 수 있다. 반면에 Docker는 Multi-Platform을 목표로 삼고 있어서 여러 운영 체제에서 동작하도록 설계되어있다. 이러한 이유로 Docker는 LXC를 대체하기 위해 libcontainer라는 독자적인 툴을 개발했으며, go 언어로 작성되어 platform-agnostic(불특정 플랫폼) 툴로 만들어졌다. 그래서 Docker 0.9부터 기본 실행 드라이버를 LXC에서 libcontainer로 대체되었다.이를 통해 container 실행 및 관리에 대한 더 많은 통제가 가능해졌다.
Docker 작동 원리
도커 컨테이너는 호스트 OS 의 커널을 공유한다. 각 컨테이너에 리눅스 커널을 할당하여 프로세스를 실행시키고 각 컨테이너 간의 격리를 구현한다. 이때 리눅스 커널의 Cgroup 과 네임스페이스 기능을 이용하여 독립된 공간을 구현하도록 한다. 이를 통해서 서로 다른 프로세스, 컨테이너 사이에 벽을 만든다.
호스트 시스템과 컨테이너가 하나의 커널을 공유하기 떄문에 호스트 시스템에서 컨테이너 내부의 프로세스를 볼 수 있다. (docker ps 명령어:컨테이너 내부에서 실행 중인 프로세스 확인 가능)
도커는 Docker Engine(도커 엔진)을 통해서 실행되고 관리된다. 도커는 리눅스 커널을 사용하기 때문에 호스트 시스템이 리눅스거나 리눅스 커널을 사용할 수 있는 OS 여야 한다. 도커 엔진은 또 하나의 VM 으로 리눅스를 게스트 OS 로 가지고 있다. 이 게스트 OS 인 리눅스의 커널을 컨테이너에 할당하여서 컨테이너가 실행되고, 이 커널을 통해서 각 컨테이너들이 격리된다. 그렇기 떄문에 호스트 OS 가 리눅스가 아니어도 도커를 사용할 수 있다. (윈도우에서 도커를 설치하고 사용할 수 있다는 것)
Docker는 Client-Server 아키텍처를 기반으로 작동한다. ( Docker Daemon & Docker Client )
Docker Client
- Docker Client는 사용자와 상호 작용하는 인터페이스 역할을 한다.
- 사용자는 Docker Client를 통해 커맨드 라인 또는 API를 사용하여 Docker Daemon에 명령을 전달할 수 있다.
- Docker Client는 사용자가 요청한 작업을 Docker Daemon에 전달하고, Docker Daemon이 해당 작업을 실행한 후 결과를 반환한다.
Docker Daemon
- Docker Daemon은 시스템의 실제 작업을 담당한다.
- Docker API 요청을 수신하고, 다른 데몬과 통신하여 도커 서비스를 관리
- 컨테이너의 생성, 실행, 관리, 이미지 다운로드 및 빌드 등과 같은 모든 작업을 처리한다.
- Docker Daemon은 백그라운드에서 실행되며, 호스트 시스템의 리소스를 관리하고 컨테이너의 상태를 관찰한다.
- 컨테이너의 상태를 변경하는 등의 작업을 수행한다.
요약 :
1) Docker Client 명령어 (docker cli)를 통해 dockerd에 요청
2) 입력한 명령어는 적절한 REST API payload로 변환되어 dockerd에 post 요청 (e.g. POST /containers/create HTTP/1.1)
3) 이 때, /var/run/docker.sock에 있는 유닉스 소켓을 통해 Docker Daemon의 API를 호출
(Linux에서 socket은 /var/run/docker.sock 이고, Windows에서는 \pipe\docker_engine)
여기서 도커 데몬과 통신하기 위한 소켓에 연결할 수 없을 경우 'Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?' 에러가 발생한다. 이 때 systemctl status docker 로 도커 상태 확인했던 과거가 살짝 지나가는 중..
containerd 의 등장
Docker 1.11 버전 이후부터는 Docker 엔진 아키텍처가 변경되었다. 이전에는 Docker Daemon(dockerd)이 직접적으로 컨테이너의 실행과 관리를 담당했지만, 이후에는 이러한 기능이 모듈화되어 컨테이너 실행과 관리를 별도의 프로세스인 containerd에 위임하게 되었다. 이를 통해 Docker 엔진의 안정성과 유연성을 향상시키고, 특정 기능에 대한 업그레이드 및 유지보수를 더욱 용이하게 되었다.
바뀐 이후 :
Docker Daemon(dockerd)은 컨테이너의 생성, 시작, 정지 등의 CRUD(Create, Read, Update, Delete) 작업을 수신하면 이를 containerd를 통해 처리한다. 이 때 Docker Daemon은 gRPC 기반의 CRUD 스타일 API를 사용하여 containerd와 통신합니다.
containerd의 역할 및 기능
containerd는 컨테이너의 실행과 관리를 담당하는 중요한 구성 요소로서, 컨테이너의 런타임 환경을 관리하고 컨테이너의 생명주기를 관리한다. 이러한 구조는 Docker 엔진의 내부를 단순화하고, 컨테이너 관리의 기능을 확장 가능한 모듈로 분리하여 관리 및 유지보수를 용이하게 하는 장점이 있다.
containerd는 컨테이너의 생명 주기를 관장하면서도 runc를 통해 컨테이너를 실제로 생성하고 실행한다. OCI(Open Container Initiative) 표준을 준수하여 Docker 이미지를 가져와서 OCI 표준에 따라 컨테이너 구성을 적용하고 OCI 번들 형식으로 변환한다. 이 때 runc라는 도구를 사용하여 OCI 번들을 실행할 수 있는 컨테이너로 변환한다.
containerd 와 runc의 상호 작용 방식
containerd는 컨테이너 런타임으로서 Docker 이미지를 가져와 컨테이너를 생성하고 관리한다. 이 때 runc를 사용하여 컨테이너의 런타임 환경을 관리합니다. -> containerd는 컨테이너의 생명주기를 관리하고 runc를 통해 컨테이너를 실행
- runc의 인스턴스 생성
각 컨테이너가 생성될 때마다 runc의 새로운 인스턴스가 fork된다. 이는 컨테이너의 런타임 환경을 구성하기 위한 과정으로, 각 컨테이너는 자체적인 runc 프로세스를 가진다. - 상위 runc 프로세스 종료
컨테이너가 생성되면 해당 컨테이너를 위한 상위 runc 프로세스가 fork되고 컨테이너의 런타임 환경을 설정한다. 그러나 이 상위 runc 프로세스는 컨테이너의 생성이 완료되면 종료된다. - containerd-shim 프로세스 (Docker 엔진과 컨테이너 런타임 간의 통신 담당)
상위 runc 프로세스가 종료되면, containerd는 해당 컨테이너를 관리하기 위해 containerd-shim 프로세스를 생성한다. 이 containerd-shim 프로세스는 컨테이너의 부모 프로세스가 되며, 컨테이너의 file descriptor 및 종료 상태를 관리하는 역할을 수행한다.
containerd-shim
- container의 부모 프로세스 역할을 하며, container와 Docker Engine 사이의 통신을 중개하고 컨테이너의 파일 디스크립터와 종료 상태 등을 관리한다.
- 컨테이너 런타임이 Docker Engine과 직접 통신할 수 있도록 인터페이스를 제공하며, 컨테이너 런타임의 실행과 관리를 위한 최소한의 코드를 메모리에 유지한다.
- 컨테이너가 종료되면 shim은 종료 상태를 확인하고 Docker Engine에게 해당 정보를 전달하여 컨테이너의 상태를 추적하고 적절한 조치를 취할 수 있도록 한다.
CRI 컨테이너의 런타임은 실제 컨테이너를 실행하는 저수준 컨테이너 런타임인 OCI 런타임과 컨테이너 이미지의 전송 및 관리, 이미지 압축 풀기 등을 실행하는 고수준 컨테이너 런타임으로 나뉜다. 이와 관련된 부분은 정리가 잘 되어 있는 참고 블로그 주소를 남길테니 이쪽으로 가셔서 한번 읽어보시길 ㅎㅎ 무려 샘숭 sds article이니 정보는 정확할 것이다.
https://www.samsungsds.com/kr/insights/docker.html
흔들리는 도커[Docker]의 위상 - OCI와 CRI 중심으로 재편되는 컨테이너 생태계 | 인사이트리포트 |
컨테이너에 대한 관심이 급격히 증가하면서 대부분의 주요 IT 벤더와 클라우드 공급자들은 컨테이너 기반의 솔루션을 발표했고 관련 스타트업 또한 급증해 컨테이너의 생태계를 넓혀왔습니다.
www.samsungsds.com