도커는 무엇이고 어떻게 동작할까요?

윤학·2023년 4월 3일
0

Docker

목록 보기
1/2
post-thumbnail

이제는 거의 필수가 되어버린 컨테이너 기술에 빠질 수 없는 도커

도커가 무엇인지 어떻게 동작하는지 정리해 보았다.

그럼 얼른 시작합시다!

컨테이너

일단 컨테이너가 뭘까?

컨테이너는 하나의 OS 안에서 커널을 공유하여 개별적인 실행 환경을 제공하는 격리된 공간이다.

여기서 개별적인 실행 환경이란 CPU, 네트워크, 메모리와 같은 시스템 자원을 독자적으로 사용하도록 할당된 환경을 말한다.

여기서 우리가 이전에 사용했던 가상 머신(Virtual Machine)과 차이가 난다.

VM은 OS를 공유하는 것이 아닌 VM에서 사용하는 OS까지 포함하고 있기 때문이다.

장점

그럼 컨테이너 기술이 가지는 장점이 뭐가 있을까?

1. 높은 집적도

여러 개의 컨테이너를 만들어서 실행 중이라 해도 OS는 하나이므로 고밀도화가 가능하며 컨테이너에서는 실행되는 프로세스를 위한 메모리만 필요하기 때문에 낮은 사양의 환경에서도 활용할 수 있다.

2. 작은 이미지 크기

VM은 가상머신마다 OS가 필요하다고 한 반면 컨테이너는 호스트의 OS를 직접 사용하면서 여러 컨테이너가 공유한다.

따라서 OS를 포함하지 않는 만큼 이미지 크기가 더 작다.

3. 다양한 운영 환경

컨테이너는 Windows, Mac OS, Linux, 퍼블릭 클라우드등 어느 환경에서나 실행 가능해 개발과 배포가 쉬워진다.

4. 빠른 시작

OS의 입장에선 컨테이너를 실행한다는게 프로세스를 시작한다는 것과 같기 때문에 일반적인 프로세스를 시작하는 것과 차이가 없어 빠르게 시작할 수 있다.

도커

도커는 앞서 알아보았던 격리된 컨테이너 환경에서 애플리케이션을 패키징하고 실행할 수 있는 플랫폼이다.

그럼 도커 개체는 어떻게 이루어져 있을까

1. Dockerfile

명령문들이 포함된 간단한 텍스트 파일로 명령문들이 연속적으로 실행되어 새 이미지를 만든다.

이미지를 만들 때 명령문들은 단계별로 실행이 되고 실행이 되면서 단계별로 레이어가 만들어진다.

2. Image

Dockerfile에서 작성된 명령문들을 통해 빌드하는 읽기 전용 템플릿으로 애플리케이션 프로그램에 필수적인 파일들을 하나로 묶어 구성한다.

한번 이미지가 만들어지면 동일한 이미지에 대해서 수정은 불가능 하고 삭제하고 다시 만들어야 한다.

3. Container

이미지를 실행해 만들어진 환경으로 이미지를 제외하고도 컨테이너의 네트워크, 스토리지와 같은 다른 구성 옵션들도 포함하고 있어 애플리케이션을 실행하는데 필요한 전체 패키지를 보관하고 있다고 생각하면 된다.

4. Volume

볼륨은 컨테이너 내의 디렉터리를 호스트의 디렉터리와 연결해 컨테이너 데이터의 지속성공유를 가능하게 해주며 도커가 직접 관리한다.

5. Network

도커의 네트워크 드라이버를 통해 컨테이너 별로 격리 환경을 구성할 수 있다.

  • bridge: 기본 네트워크 드라이브로 동일한 bridge 네트워크에 연결된 컨테이너는 통신이 가능하지만 서로 다른 bridge 네트워크의 컨테이너는 직접 통신 할 수 없다.
  • host: 호스트와의 네트워크 격리를 제거하고 호스트로 들어오는 요청을 컨테이너가 직접 받는다.
  • overlay: 여러 도커 데몬 호스트 간에 분산 네트워크를 생성하여 서로 다른 도커 호스트에 있는 컨테이너들끼리 같은 서버에 있는 것처럼 통신 할 수 있는 네트워크

아하! 요약하자면 내 애플리케이션을 어디에서든 실행할 수 있게 하려면 Dockerfile로 이미지를 만들어서 가지고 있으면 되는군요!

어차피 내부 네트워크나 스토리지는 이미지로 컨테이너를 실행할 때 도커에서 구성해 주니까요!

근데 만든 이미지는 어디에 저장이 되는거에요...?

Dockerfile을 build해 이미지를 생성하면 기본적으로 로컬에 존재하는 도커 디렉토리에 저장되는데 스토리지 드라이버에 따라 다르다고 한다.

나의 우분투를 예로 들면 overlay2라는 스토리지 드라이버를 사용하고 있고 /var/lib/docker 디렉터리를 도커의 루트 디렉터리로 사용하고 있는 것을 볼 수 있다.

docker info

그럼 여기가면 제 이미지 볼 수 있나요?

ubuntu@ip-172-31-4-90:~$ sudo ls -l /var/lib/docker/overlay2
total 540
drwx--x--- 4 root root  4096 Feb 24 08:32 00c8436787ce716b83d2be4d79385f9ae239d237e7c197de8884f6562fa3a9a0
drwx--x--- 4 root root  4096 Feb 24 07:43 00wwn1ffrw5pa6vfvccl4k708
drwx--x--- 4 root root  4096 Feb 24 09:24 0158de691342a0c21a2924d7dd607bc4b4002a5931d083c52c2e9ad174e1789c
drwx--x--- 4 root root  4096 Feb 24 09:24 03d308202c4ee20abe0ca9a9718d14dd71c643be8a92347f7961af4d79015c89
drwx--x--- 4 root root  4096 Apr  3 16:59 0b229490cf8770c16b4df6295d81d9d273218c808934fa5289eafc398675f469
drwx--x--- 4 root root  4096 Apr  3 16:35 0b229490cf8770c16b4df6295d81d9d273218c808934fa5289eafc398675f469-init

이미지를 빌드할 때 생성된 레이어들이 디렉터리별로 저장되어 있는 것을 볼 수 있다.

여기에 있는 디렉터리의 구조는 여기를 통해서 읽어만 보고 추후 학습내용으로 남겨두었다.

다만 /var/lib/docker은 도커가 만지지 말라고 한다.

동작원리

그럼 내가 이미지를 가지고 실행하면 도커는 어떻게 컨테이너를 생성할까?

먼저 도커에서 제공하는 전체적인 아키텍처를 보면서 과정을 하나씩 살펴보자.

1.Docker Client

사용자가 도커와 상호 작용하는 기본 방법으로 Docker CLI를 생각하면 된다.

도커가 컨테이너를 빌드하고 실행하는 무거운 작업들은 Docker Daemon이 수행하므로 만약 우리가 docker ps라는 명령을 CLI에 입력하면 Client가 해당 명령을 Docker API를 통해 Daemon한테 전송해주는 것이다.

둘 사이의 통신은 docker.sock이라는 UNIX SOCKET을 통해 이루어진다.

2.Docker Daemon

dockerd는 Docker API를 수신해서 처리하고 이미지, 컨테이너, 네트워크와 같은 Docker 객체를 관리하는 역할을 하며 도커 서비스를 관리하기 위해 다른 데몬 프로세스랑 통신도하는 데몬 프로세스이다.

3.Docker Registry

도커의 이미지들을 저장하고 가져오는 저장소로 별다른 설정을 하지 않으면 Docker Hub라는 공개 레지스트리로 설정되어 있으며 개인 레지스트리를 사용할 수도 있다.

그럼 dockerd(Docker Daemon)가 이미지까지 가져오는거죠?

아니다. 이미지를 가져오거나 다운받는 것은 다른 데몬 프로세스인 containerd가 해준다.

이게 무슨 말인지 이해하기 위해 아래 그림의 구성요소에 대해 간단히만 알아보자.

아래의 좋은 그림은 여기에서 가져왔다.

4.Containerd

dockerd와 통신하는 데몬프로세스인 containerd는 고수준 컨테이너 런타임에 해당한다.

고수준 컨테이너 런타임은 이미지를 전송 및 다운로드, 압축해제, 마지막으론 컨테이너의 실행을 위한 저수준 런타임에게 전달하는 역할을 하는데

이렇게 실제 이미지를 가져오는 것은 dockerd가 아닌 containerd가 수행하며 해당 이미지를 OCI 런타임 번들로 압축을 풀어 저수준 런타임에게 전달한다고 한다.

또한 containerd는 추후에 생성된 컨테이너들을 관리하는 역할을 하고 직접 컨테이너를 생성하진 않고 실제 컨테이너를 생성하고 실행하기 위해 containerd-shimrunc를 이용한다.

5.Containerd-Shim

containerd-shim(shim)runc를 이용해서 컨테이너를 생성하며 컨테이너당 하나씩 존재한다.

shim의 주요 역할은 runc가 컨테이너를 생성만 하고 종료되기 때문에 runc가 죽고 난 후 해당 컨테이너 프로세스의 부모 역할을 한다

6.runc

컨테이너는 리눅스의 kernel기능인 namespacecgroup을 사용하여 구현이되는데

namespace를 사용해 각 컨테이너에 대해 파일 시스템이나 네트워킹과 같은 시스템 리소스를 가상화 하며

cgroup를 통해 각 컨테이너가 사용할 수 있는 CPU나 메모리같은 리소스를 제한한다.

저수준 컨테이너 런타임에 해당하는 runc는 OCI Runtime Spec을 준수하면서 OCI 번들을 통해 컨테이너를 실행시킨다.

여기서 OCI 번들이란 컨테이너를 생성하고 실행하는데 필요한 모든 정보를 말하며 config.json에 작성하며 루트 파일 시스템도 설정해 해당 경로를 config.json에 같이 작성한다.

7.그럼 간단한 실험 해보자

개인적인 의견이므로 잘못된 내용일 수도 있습니다!

일단 도커를 개인 이미지를 가지고 실행해봤다.

1) 3개의 프로세스들이 존재할까?

root       34737  0.1  5.5 1392632 55052 ?       Ssl  09:10   0:22 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root       35773  0.0  3.6 1283212 36212 ?       Ssl  09:47   0:06 /usr/bin/containerd
root       35929  0.0  0.8 720756  8440 ?        Sl   10:18   0:01 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 1fdd510611de766686ce3dffd126f6dde45bf4c5a84f4f07cb71098ae1a83a04 -ad

일단 dockerd, shim, containerd가 다 존재한다.

2) 왜 dockerd(도커서비스)를 중지시켜도 docker 명령어를 입력하면 자동으로 dockerd가 시작될까?

기본적으로 도커 client와 dockerd가 docker.sock이라는 UNIX SOCKET을 통해 통신하는데 따로 막지 않으면 항상 docker.sock이 docker API를 수신하고 있다.

만약 docker cli client를 통해 docker 명령을 입력한다면 client는 docker api를 호출하고 아래와 같이 dockerd는 docker.socket이 사용될 때마다 실행되도록 설정되어 있기 때문에 항상 dockerd도 실행이 되는 것이다.

만약 docker api 수신을 막는다면

ubuntu@ip-172-31-4-90:~$ sudo systemctl stop docker.socket
ubuntu@ip-172-31-4-90:~$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

위와 같이 dockerd와 통신하지 못한다.

3) containerd가 컨테이너들을 관리한다 했는데 containerd를 중지시키고 도커 컨테이너를 종료시키면 어떻게 될까?

dockerd log

Error response from daemon: cannot stop container: 1fdd510611de: tried to kill container, but did not receive an exit event

그럼 위와 같이 종료 이벤트를 받지 못했다는 에러와 함께 컨테이너를 중지시키지 못하는데 일단 종료 이벤트를 받지 못했다는 것만 기억해두자.

그리고 정상적으로 containerd가 실행중일 때 중지시키는 상황과 비교해보자.

dockerd log

Apr 03 15:25:48 ip-172-31-4-90 dockerd[37452]: time="2023-04-03T15:25:48.738629154Z" level=info msg="ignoring event" container=abafa0c08230eef5153550993719d7392360d85b3eae98387bae08f604744eca module=libcontainerd namespace=moby topic=/tasks/delete type="*events.TaskDelete"

containerd log

Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.728042003Z" level=info msg="shim disconnected" id=2fd65806cd2d361c6cbcb39c92cbc123d39984180fa5f5041a245dc1e07cdea7
Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.728581763Z" level=warning msg="cleaning up after shim disconnected" id=2fd65806cd2d361c6cbcb39c92cbc123d39984180fa5f5041a245dc1e07cdea7 namespace=moby
Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.729401265Z" level=info msg="cleaning up dead shim"
Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.740072206Z" level=warning msg="cleanup warnings time=\"2023-04-03T06:14:18Z\" level=info msg=\"starting signal loop\" namespace=moby 

위처럼 dockerd는 TaskDelete라는 이벤트를 받았지만 무시한다는 log와 함께 containerd에서는 컨테이너를 깔끔하게 삭제한다.(위에는 시간이 맞지 않지만 엄청나게 근소한 차이로 dockerd가 먼저 찍힌다.)

여기서 그럼 dockerd가 무시했던 이벤트는 누구로부터 발생되는 걸까?

만약 shim이 컨테이너가 종료되면서 직접 dockerd에게 종료 사실을 알린다면 내가 containerd를 중지시키고 shim의 프로세스를 kill명령어로 죽였을 때 dockerd log에 로그가 찍혔을 거라고 생각한다.

결국 containerd가 컨테이너 종료 signal을 정상적으로 받으면 TaskDelete이벤트를 발생시켜서 dockerd는 이 이벤트를 받는것일테고

dockerd log에 있는 did not receive an exit event는 containerd를 중지시키면 dockerd와 containerd가 통신에 사용했던 containerd.sock파일이 없어져서 어떤 event도 수신할 수 없는 것이다. (실행시키면 다시 파일이 생긴다)

이 때 컨테이너를 종료하는 client는 무엇이든 상관없다.

containerd의 client인 ctr로도 수행해 본 결과 정상적인 상황에선 dockerd에게 똑같은 log가 dockerd에 찍혔다.

4) 그럼 containerd를 중지시킨 상황에서 dockerd까지 중지시키면 실행중이던 컨테이너는 어떻게 될까?

컨테이너(shim)가 덩그러니 남게 된다.

하지만 이 상황에서 containerd를 다시 실행시켜면

containerd log

Apr 03 05:57:14 ip-172-31-4-90 systemd[1]: Starting containerd container runtime...
-- Subject: A start job for unit containerd.service has begun execution
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
--
-- A start job for unit containerd.service has begun execution.
--
-- The job identifier is 11751.
Apr 03 05:57:14 ip-172-31-4-90 systemd[1]: containerd.service: Found left-over process 21866 (containerd-shim) in control group while starting unit. Ignoring.
Apr 03 05:57:14 ip-172-31-4-90 systemd[1]: This usually indicates unclean termination of a previous run, or service implementation deficiencies.

위와 같이 남겨져 있던 컨테이너를 발견하고 이후 dockerd를 다시 시작하면 아래와 같이 삭제 된다.

Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.728042003Z" level=info msg="shim disconnected" id=2fd65806cd2d361c6cbcb39c92cbc123d39984180fa5f5041a245dc1e07cdea7
Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.728581763Z" level=warning msg="cleaning up after shim disconnected" id=2fd65806cd2d361c6cbcb39c92cbc123d39984180fa5f5041a245dc1e07cdea7 namespace=moby
Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.729401265Z" level=info msg="cleaning up dead shim"
Apr 03 06:14:18 ip-172-31-4-90 containerd[22472]: time="2023-04-03T06:14:18.740072206Z" level=warning msg="cleanup warnings time=\"2023-04-03T06:14:18Z\" level=info msg=\"starting signal loop\" namespace=moby 

마치면서..

사실 조금 어려운 내용이라 생각해서 잘못 이해한 부분도 있을 수 있지만 컨테이너가 생성되기까지 client부터 dockerd의 뒤 과정까지 전체적으로 이해하고 그림이 그려진다면 그래도 성공했다고 생각한다.

처음 보시는 분들도 이 글이 잘 이해가 된다면 기쁠 것 같다.

참고

Docker architecture
Container Runtimes Part 1: An Introduction to Container Runtimes
Docker and OCI Runtimes
흔들리는 도커(Docker)의 위상 - OCI와 CRI 중심으로 재편되는 컨테이너 생태계

profile
해결한 문제는 그때 기록하자

0개의 댓글