도커를 처음 알게 된 건 2019년쯔음에 AI 쪽에 입문하면서 환경설정으로 애먹던 와중에 도커를 처음 듣고 적용해보았었다.
그러면서 도커에서 GPU를 쓰기 위해 nvidia-docker 부터 시작해서 쿠버네티스까지 이것저것 공부했었기에 나름 많이 안다고 생각했다. 하지만 최근에 다시 보니 표면적인 활용방법만 알았을 뿐, 도커가 어떻게 작동하는지에 대해 깊이 알지 못하단 걸 깨달았고 공부해서 이해해본 내용에 대해서 적어보려고 한다.
가상화(Virtualization)
도커는 리눅스 컨테이너 기반으로 하는 가상화 플랫폼이다.
도커 얘기가 왜 나왔냐부터 시작하면 하이퍼바이저부터 시작하는 가상화에 대한 얘기가 빠질 수 없는 것 같다.
초기의 가상화 모델에 대한 수요는 IBM의 운영체제 아이디어 테스트로부터 시작됐다고 한다.
당시에 IBM에서는 운영체제 테스트를 하기 위해 하이퍼바이저를 통해 안정성을 보장할 수 있었다고 하는데,
여기서 하이퍼바이저는 호스트 시스템이 여러 가상 시스템을 게스트로 작동시키는 가상화를 통해 운영체제와 어플리케이션을 분리시켜, 컴퓨팅 리소스를 효과적으로 사용할 수 있도록 돕는 프로세스다.
예전에는 컴퓨터가 매우 귀했기에 귀한 자원을 효율적으로 잘 쓰기 위해 이런 개념이 만들어진 것으로 보인다.
추가로 하이퍼바이저에는 하드웨어에 바로 접근하는 Type1과, 기존 OS 위에 추가로 OS를 올리는 Type2가 있다.
그러면 가상화로 인해 불편함이 어느정도 해소는 된 것 같은데, 도커는 왜 나오게 되었을까??
하이퍼바이저의 단점으로는 많은 리소스를 필요한다는 점이다. 하이퍼바이저는 운영체제를 새로 구축하는 개념이다보니, 그만큼 많은 자원을 가져가야 한다. 하지만 많은 OS보다 많은 어플리케이션 구동이 필요한 현대에서는 이보다 가볍게 운영할 수 있도록 하는 컨테이너 기술이 인기가 높아지면서 이를 기반으로 한 도커가 인기가 많아지게 되었다. 뿐만 아니라 이렇게 했을 때 원본 대비 성능이 거의 없기 때문에 도커가 대중적이게 된 데 한 몫 했다고 본다.
여담으로 나의 경험적으로도 학부생 때 리눅스 수업 시간에 윈도우 위에 Type2의 Hypervisor여서 더 그런 것도 있겠지만 VirtualBox를 설치하고 그 위에 리눅스를 설치해 실습했었는데 느렸었다.
윈도우 위에 윈도우 위에 윈도우 위에도 가능하다...
그렇다면 도커는 어떻게 하이퍼바이저와 다르고, 어떻게 작동할까?
위에서 언급했듯이 하이퍼바이저는 기본적으로 OS 전체를 다 올린 것과 같은 방식으로 작동한다.
반면에 도커는 리눅스의 namespace와 cgroup이라는 기능을 활용하는 컨테이너 기술을 기반으로 한다.
Namespace
리소스를 분리하는 가상화 방법에 있어서 하이퍼바이저는 리소스 자체를 가상화해서 독립적인 공간을 제공하여 서로가 충돌하지 않게 한다. 반면에 도커는 namespace라는 리눅스 내부의 기능을 통해 자원을 가상화하여 분리한다.
namespace는 6가지의 해당되는 flag를 옵션으로 받아 systemcall을 호출하여 생성하여 분리하는데, namespace가 분리하는 자원들은 다음과 같다.
/* * cloning flags */
CLONE_NEWNS /* New namespace group? */
CLONE_NEWUTS /* New utsname group? */
CLONE_NEWIPC /* New ipcs */
CLONE_NEWUSER /* New user namespace */
CLONE_NEWPID /* New pid namespace */
CLONE_NEWNET /* New network namespace */
출처: https://bluese05.tistory.com/11 [ㅍㅍㅋㄷ:티스토리]
- pid : 프로세스 ID를 분할
- ns: 파일시스템의 마운트 지점
- uts : hostname을 변경하고 분할
- ipc : Inter-process communication의 약자로 프로세스 간 통신 격리
- user : user와 group ID를 분할
- net : 네트워크 리소스와 관련된 정보
리눅스는 프로세스 1번으로부터 시작해 많은 fork를 통해 부모와 자식 프로세스로 계층이 나뉘면서 진행된다. 이를 그림으로 표현하면 트리 구조가 나오게 되는데, pid namespace는 이런 특정 부분의 프로세스를 루트로 가상화하여 1번으로 진행시켜 루트를 넘어가는 범위의 네임스페이스는 인식할 수 없도록 분리시킨다. 파일시스템도 마찬가지로 트리 구조를 갖고 있어 ns namespace도 동일한 방식으로 진행된다.
hostname은 얼핏 보면 계정명을 말하는 것 같지만, 사실은 API를 테스트하면서 많이 쓰이는 localhost가 여기서 쓰이는 내용으로, 네트워크 인터페이스의 별칭을 붙여 네트워크로 연결된 서버와 컴퓨터를 구분하는 용도이다. 새로운 hostname을 만들어 기존의 localhost와 분리한다. ipc도 같은 맥락으로 기존과 충돌을 방지하기 위해 새로운 값으로 생성하여 기존과 분리하는 방식으로 진행된다.
네트워크는 namespace와 같은 방식으로 인터페이스를 생성하여 기존과 분리한 뒤, veth(virtual ethernet) 타입의 인터페이스를 생성해 진행한다. 추가로 docker0 이라는 브릿지 네트워크를 생성하는데, 브릿지 네트워크는 사설망처럼 새로운 IP대로 연결해주는 NAT 역할을 한다. 기존의 veth 타입의 인터페이스는 컨테이너에서 새로 생성된 네트워크 인터페이스와 host의 인터페이스를 연결짓는 구조로 쌍을 짓는데, 컨테이너라는 가상의 PC와 docker0 의 네트워크 주소와 맞물려서 진행된다.
Cgroup
도커에서는 cgroup의 명령어를 이용해 host PC의 리소스를 허용하거나 제한하는데, Cgroup은 리눅스에서 태스크 단위의 프로세스에 대해 자원 할당을 제어하는 커널 모듈이다. 위의 내용들을 종합하여 namespace 를 통해 기존 OS에서의 리소와 프로세스를 독립시키고도 기존 리눅스 프로그램처럼 동작할 수 있게 되고, 스케쥴링할 수 있게 된다. 추가로 쿠버네티스에서도 컨테이너 배포 시에 cgroup 명령어로 리소스를 할당하고 모니터링을 진행한다.
오늘은 리눅스에서 도커가 하이퍼바이저와 어떤 점이 다르고, 어떻게 성능 감소가 거의 없이 진행되는지 원리에 대해서 공부해보았다.
참고
https://ko.wikipedia.org/wiki/%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80
https://www.networkworld.com/article/964890/what-is-a-hypervisor.html
https://loveinside79.tistory.com/245
https://tech.ssut.me/what-even-is-a-container/
https://selectel.ru/blog/en/2017/03/09/containerization-mechanisms-namespaces/
https://bluese05.tistory.com/11
https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/