도커는 어떻게 컨테이너 단위의 격리된 프로세스 환경을 구성할 수 있을까?

Posted by , March 11, 2023
Docker

컨테이너 단위의 격리된 프로세스 환경

가상머신(Virtual Machine) 을 사용해 각 마이크로서비스의 환경을 격리하는 대신 개발자들은 리눅스 컨테이너 기술로 눈을 돌렸습니다. 컨테이너에서 실행되는 프로세스는 다른 프로세스와 마찬가지로 동일한 호스트 OS 에서 실행됩니다. 그러나 컨테이너내의 프로세스는 여전히 다른 프로세스와 격리되어 있습니다.

컨테이너 vs 가상머신

컨테이너는 가상머신에 비해 경량화된 것으로써 동일한 환경에서 더 많은 소프트웨어 구성 요소를 실행할 수 있습니다. 가상머신은 구성 요소 프로세스뿐만 아니라 시스템 자체의 프로세스도 실행해야 하기 때문에 리소스가 많이 요구됩니다. 반면 컨테이너는 호스트 OS에서 실행되는 격리된 프로세스에 지나지 않으며, 각 컨테이너에 들어있는 애플리케이션을 실행할 때 소비되는 리소스외에는 추가 프로세스에 대한 오버헤드는 없습니다.

위와 같이 호스트에서 가상머신 3개를 실행하면, 3개는 각각 완전히 분리된 OS가 실행도고 동일한 베어메탈 하드웨어를 공유하게 됩니다. 또 이런 가상머신 단위의 격리환경의 구성요소는 호스트 OS 와 하이퍼바이저가 존재하죠.

하이퍼바이저 & Guest OS

하이퍼바이저는 각 가상머신에 대해 게스트 OS를 생성하고, 각 게스트 OS에게 자원을 적절히 분배합니다. 또한 각 게스트 OS 는 각각 다른 게스트 OS 와 완전히 독립된 공간과 시스템 자원을 할당받고 사용합니다.

특정 가상머신 내에서 실행되는 애플리케이션이 가상머신 게스트 OS 커널에 대한 시스템 콜을 호출하면, 커널은 하이퍼바이저로 호스트의 물리적 CPU 에서 명령을 수행합니다.


컨테이너를 활용시 동일한 호스트 OS 에서 실행된다

반면 컨테이너는 호스트 OS 에서 실행되는 동안 동일한 커널에서 시스템 콜을 수행합니다. 직전에 언급한 가상머신 환경은 하이퍼바이저를 활용해서 각기 다른 게스트 OS 커널에 대한 시스템 콜을 수행하는 방식이였죠.

동일한 시스템에서 더 많은 수의 격리된 프로세스를 사용하려면 컨테이너의 오버헤드가 낮기 때문에 컨테이너를 사용하는 것이 좋습니다. 각 가상머신은 각기 다른 자체 시스템 서비스를 실행하지만, 컨테이너는 모두 동일한 OS 에서 실행되므로 컨테이너는 시스템 서비스를 실행하지 않는다는 점을 기억합시다.

즉 컨테이너를 실행하면 가상머신처럼 부팅할 필요없이, 컨테이너에서 실행되는 프로세스는 즉시 시작됩니다.


컨테이너 격리는 어떻게 가능할까?

앞서 언급했듯이, 각 컨테이너는 동일한 OS 커널을 통해 호출되며, 각 컨테이너에는 제각각의 격리된 프로세스를 지니고 있습니다. 그렇다면 컨테이너가 동일한 OS 에서 실행중인 경우 어떻게 프로세스를 격리시킬 수 있는 환경을 구성할 수 있을까요? 바로 리눅스 네임스페이스와 cgroup(리눅스 컨트롤 그룹) 을 통해 격리가 가능합니다.

리눅스 네임스페이스

각 리눅스 시스템은 기본적으로 하나의 네임스페이스라는 것을 보유하고 있습니다. 파일시스템, 프로세스 ID, 사용자 ID, 네트워크 등과 같은 모든 시스템 리소스는 하나의 네임스페이스에 속합니다.

각 프로세스를 실행시 여러 네임스페이스중 하나에서 프로세스를 실행하며, 프로세스는 동일한 네임스페이스 내에 있는 리소스만 조회 가능합니다. 이러한 특징을 활용하여, 각 네임스페이스는 특정 리소스 그룹을 격리하는데 사용합니다. 두 프로세스를 마치 2개의 다른 시스템에서 실행중인 것 처럼 보이게 할 수 있는것입니다.

각 컨테이너는 고유한 각 네임스페이스를 사용하므로, 각 컨테이너는 고유한 네임스페이스를 활용해 격리 가능합니다.

cgroup

또 컨테이너가 사용할 수 있는 시스템 리소스의 양을 제한하는 방법도 활용해서 격리합니다. 리소스 사용을 제한하는 리눅스 커널 기능인 cgroup 을 활용합니다. 각 프로세스는 설정된 양 이상의 CPU, 메모리 등을 사용할 수 없어서, 프로세스는 다른 프로세스용으로 예약된 리소스를 사용할 수 없으며, 이는 각 프로세스가 별도의 시스템에서 실행될 때와 비슷합니다.


도커 컨테이너 플랫폼

도커로 패키징된 애플리케이션을 실행시 함께 제공된 파일시스템 (메타데이터) 내용을 정확히 볼 수 있습니다. 각 컨테이너의 애플리케이션은 실행중인 서버의 내용은 볼 수 없으므로, 서버에 개발 컴퓨터와 다른 설치 라이브러리가 있는지는 중요하지 않습니다.

도커는 애플리케이션을 전체 환경과 함께 패키지화 할 수 있습니다. 애플리케이션에서 필요한 몇가지 라이브러리나 OS의 파일시스템에 설치되는 모든 파일을 포컨테이너안에 포함시킬 수 있습니다.

도커 이미지

애플리케이션과 해당 환경을 패키지화 한 것입니다. 이 이미지에는 애플리케이션에서 사용할 수 있는 파일시스템과, 이미지가 실행될 때 실행되야하는 실행파일 경로와 같은 메타데이터가 포함되어 있습니다.

도커 컨테이너

도커 이미지로부터 생성된 일반적인 리눅스 컨테이너입니다. 컨테이너안에는 프로세스가 존재하며, 컨테이너는 프로세스의 생명을 관리하는 툴이라 보면 됩니다. 각 컨테이너의 프로세스는 동일한 호스트에서 실행되는 프로세스이지만, 프로세스들은 각각 다른 프로세스들과 모두 격리되어 있습니다.

또 프로세스는 리소스 사용이 제한돼 있으므로 리소스의 지정된 양(CPU, RAM 등) 만 엑세스하고 사용할 수 있습니다.


이미지 레이어

그런데 사실 동일한 부모 도커 이미지를 통해 실행된 컨테이너의 애플리케이션들은 같은 파일을 공유할 수 있습니다. 각 컨테이터는 격리된 자체 파일시스템이 존재한다고 했는데, 어떻게해서 애플리케이션들이 같은 파일을 공유할 수 있을까요?

컨테이너들이 함께 공유하는 부모 이미지 레이어는 읽기 전용이다

도커 이미지는 래이어로 구성돼있으며, 2개의 다른 이미지를 기본 이미지로 동일한 부모 이미지를 사용할 수 있으므로 동일한 레이어가 포함될 수 있습니다. 각 레이어는 동일 호스트에 1번만 저장됩니다. 따라서 동일한 기본 레이어를 기반으로 한 2개의 이미지에서 생성된 컨테이너는 동일한 파일을 읽을 수 있지만, 그 중 하나가 해당 파일을 덮어쓰면 다른 해당 변경 사항을 읽을 수 없습니다. 따라서 파일을 공유하더라도 여전히 서로 격리돼있는데 이것은 컨테이너 이미지 레이어가 읽기 전용이기 때문입니다.

각 컨테이너마다 별도의 쓰기 가능한 레이어가 생긴다

각 컨테이너가 실행될 때 이미지 레이어 위에 각각 쓰기 기능한 레이어가 생깁니다. 컨테이너 프로세스가 기본 부모 레이어 중 하나에 있는 파일에 쓰면 전체 파일의 복사본의 최상위 레이어에 만글어지고 프로세스는 복사본에 씁니다.


결론

도커는 컨테이너를 주류로 만든 최초의 컨테이터 플랫폼입니다. 각 컨테이너는 격리된 프로세스가 실행되고 있으며, 동일한 부모 이미지 레이어에서 생성된 컨테이너들은 동일한 파일시스템을 공유하며, 각 컨테이너마다 별도의 쓰기 가능한 레이어가 생깁니다.

이때 도커 자체가 프로세스 격리를 제공하지 않는다는 것을 유의합시다.컨테이너의 격리는 리눅스 네임스페이스와 cgroup 과 같은 커널 기능으로 리눅스 커널 수준에서 수행되는 것입니다. 도커는 이런 기능을 사용하기 쉽게 하는것이고요.


참고

Kubernetes in Action, Second Edition