Skip to content

Latest commit

 

History

History
205 lines (135 loc) · 13.6 KB

File metadata and controls

205 lines (135 loc) · 13.6 KB

컨테이너란 무엇일까?

컨테이너 기술의 본질을 이해하기 위해서는 단순히 가볍다, 빠르다는 결과가 아니라, OS 레벨에서 어떤 메커니즘으로 작동하는지를 알아보고자 한다.

요약

컨테이너는 호스트 OS의 커널을 공유하고, namespaces와 cgroups 같은 커널 기능으로 프로세스를 격리한다.

반면 가상 머신은 하드웨어를 가상화해 각 VM이 독립적인 커널을 실행하므로 격리 수준은 높지만 커널 부팅과 가상화 오버헤드로 상대적으로 무겁다.

왜 컨테이너가 필요했을까?

컨테이너 기술이 등장하기 전, 개발과 운영 환경에서는 고질적인 문제들이 있었다.

  • 환경 불일치
  • 자원 효율성과 격리의 딜레마

환경 불일치

가장 흔한 문제는 "내 컴퓨터에선 되는데 서버에선 안 되는 상황"이었다. 개발자의 PC와 서버의 OS 버전, 설치된 라이브러리, 런타임 버전이 미묘하게 달라서 발생하는 충돌이다.

자원 효율성과 격리의 딜레마

한 서버에서 여러 서비스를 실행하면 서로 간섭(포트 충돌, 라이브러리 버전 충돌)이 발생한다. 이를 막기 위해 가상 머신(VM)을 사용했지만, VM은 너무 무거웠다. 고성능 서버에서 작은 애플리케이션 하나를 돌리기 위해 수 GB짜리 OS를 통째로 띄우는 것은 매우 비효율적이었다.

"가상 머신처럼 격리되면서도, 일반 프로세스처럼 가볍고 빠른 기술은 없을까?"

이 질문에 대한 해답이 바로 컨테이너다.

컨테이너는 프로세스다

컨테이너를 이해하는 첫 번째 원칙은 명확하다. 컨테이너는 OS를 가상화하는 것이 아니라, 프로세스를 OS처럼 보이게 만드는 기술이다.

이 문장을 이해하지 못하면 컨테이너는 계속 마법 상자로 남는다.

일반적인 프로세스는 다른 프로세스와 같은 공간을 공유한다. 같은 프로세스 목록을 보고, 같은 네트워크 설정을 쓰고, 같은 파일 시스템에 접근한다. 하지만 컨테이너로 실행된 프로세스는 마치 독립된 시스템에서 돌아가는 것처럼 자기만의 세상을 가진다.

이게 가능한 이유는 리눅스 커널이 제공하는 두 가지 핵심 기능 때문이다.

  • 네임스페이스 (Namespaces)
  • cgroups (Control Groups)

네임스페이스는 프로세스가 인식하는 시스템의 범위를 분리하고,
cgroups는 그 프로세스가 사용할 수 있는 자원의 양을 제한한다.

격리의 실체: 네임스페이스

네임스페이스는 컨테이너가 격리된 환경을 가질 수 있게 만드는 커널 기능이다. 격리된 작업 공간이라는 추상적인 설명으로는 부족하다. 실제로는 커널이 제공하는 여러 종류의 네임스페이스가 조합되어 독립된 환경을 만든다.

주요 네임스페이스 종류

  • PID 네임스페이스: 프로세스 트리를 분리한다. 컨테이너 내부에서 실행된 프로세스는 자신이 PID 1번(관리자 프로세스)이라고 생각하지만, 호스트에서 보면 수천 번대 PID를 가진 일반 프로세스일 뿐이다. PID 1 프로세스는 시그널 전달과 자식 프로세스 수거를 담당하는데, 이 역할이 꼬이면 좀비 프로세스가 쌓여 장애의 원인이 된다.
  • NET 네임스페이스: 네트워크 스택을 분리한다. 각 컨테이너는 자신만의 네트워크 인터페이스, 라우팅 테이블, 포트 공간을 가진다. 그래서 여러 컨테이너가 내부적으로 같은 80번 포트를 사용해도 서로 충돌하지 않는다.
  • MNT 네임스페이스: 파일 시스템 마운트 포인트를 분리한다. 컨테이너는 자신만의 루트 파일 시스템을 가지며, 호스트의 파일 시스템과는 독립적으로 동작한다.
  • UTS 네임스페이스: 호스트명과 도메인명을 분리한다. 각 컨테이너는 고유한 호스트명을 가질 수 있다.
  • USER 네임스페이스: 사용자 ID(UID)를 분리한다. 컨테이너 내부에서 root(UID 0)처럼 보이는 사용자도 호스트에서는 권한이 없는 일반 사용자로 매핑될 수 있다.

이 네임스페이스들이 조합되어, 컨테이너는 하나의 프로세스지만 자기만의 시스템을 가진 것처럼 보이게 된다.

시스템 콜과 네임스페이스

컨테이너 내부에서 프로세스가 파일을 열거나, 네트워크 포트를 바인딩하는 등 시스템 콜이 발생한다. 이 시스템 콜은 호스트 커널로 전달되지만, 커널은 호출한 프로세스가 속한 네임스페이스를 기준으로 요청을 해석한다.

예를 들어, 두 개의 컨테이너에서 동시에 80번 포트 바인딩을 요청해도 충돌하지 않는다. (단, 호스트의 동일한 포트로 매핑하면 충돌한다.) 각 컨테이너가 서로 다른 NET 네임스페이스에 속해 있기 때문에, 커널은 이들을 완전히 다른 네트워크 스택으로 처리한다. 마찬가지로 /etc/passwd 파일을 열 때도 MNT 네임스페이스에 따라 각자 다른 파일 시스템을 참조한다.

같은 커널을 공유하지만 다른 환경을 보는 이유는, 시스템 콜이 네임스페이스 컨텍스트 안에서 해석되기 때문이다.

자원 제어의 실체: cgroups

네임스페이스가 보이는 것을 격리한다면, cgroups는 쓸 수 있는 것을 제한한다.

컨테이너가 가볍다는 말은 단순히 용량이 작다는 의미가 아니다. 진짜 핵심은 여러 컨테이너가 같은 하드웨어에서 안전하게 공존할 수 있다는 점이다.

cgroups는 프로세스 그룹에 대해 자원 사용량을 제한하고 추적한다.

  • CPU: 특정 컨테이너가 사용할 수 있는 CPU 코어 개수나 시간을 제한한다. 예를 들어 0.5 코어만 할당하거나, CPU 시간의 50%만 사용하도록 제한할 수 있다.
  • 메모리: 컨테이너가 사용할 수 있는 메모리 상한선을 설정한다. 512MB, 2GB 등으로 제한하면 컨테이너는 그 이상 메모리를 사용할 수 없다.
  • 디스크 I/O: 읽기/쓰기 속도를 제한하여 한 컨테이너가 디스크를 독점하지 못하게 막는다.
  • 네트워크 대역폭: 네트워크 사용량을 제어하여 다른 컨테이너의 통신을 방해하지 못하게 한다.

네임스페이스만 있고 cgroups가 없다면 컨테이너는 서로 자원을 뺏는 프로세스 덩어리일 뿐이다. cgroups가 있기에 하나의 서버에서 수십, 수백 개의 컨테이너가 안정적으로 공존할 수 있다.

메모리 제한과 OOM Killer

컨테이너가 cgroups로 설정된 메모리 제한을 초과하여 메모리 할당에 실패하면 커널의 OOM Killer가 작동한다. 커널은 시스템을 보호하기 위해 대개 해당 cgroups 내에서 메모리 점유율이 높은 프로세스를 강제로 종료시킨다.

이것이 컨테이너에서 Java 애플리케이션이 갑자기 죽는 이유다. JVM이 호스트의 전체 메모리를 기준으로 힙 크기를 설정했지만, 실제로는 cgroups 제한에 걸려 메모리 할당 실패로 이어지고, 결국 커널에 의해 종료된 것이다. 이건 Java 버그가 아니라 OS 레벨의 자원 관리 메커니즘이다.

커널 공유의 의미

컨테이너를 이해할 때 가장 중요한 전제가 있다.

"컨테이너는 호스트 OS의 커널을 공유한다."

커널은 하드웨어와 애플리케이션 사이에서 자원을 관리하고 시스템 콜을 처리하는 OS의 핵심이다. 프로세스 스케줄링, 메모리 관리, 파일 시스템 접근, 네트워크 통신 등 모든 저수준 작업은 커널을 통해 수행된다.

컨테이너는 하드웨어를 가상화하지도, 커널을 새로 띄우지도 않는다. 대신 하나의 호스트 커널을 공유한 채, 네임스페이스와 cgroups를 통해 각 프로세스가 서로 다른 환경에 있는 것처럼 보이게 만든다.

이 구조 때문에 컨테이너는 OS 자체를 포함하지 않는다. 컨테이너 이미지에는 애플리케이션과 실행에 필요한 라이브러리, 바이너리만 들어 있으며, 커널은 이미 호스트에 존재한다.

가상 머신과의 구조적 차이

가상 머신과 컨테이너의 차이는 성능 차이가 아니라 어디까지를 OS로 취급하느냐의 차이다.

가상 머신(하이퍼바이저 기반)은 하드웨어를 가상화한다. 가짜 하드웨어 위에 게스트 OS 커널이 다시 올라가며, 이로 인해 각 VM은 독립적인 커널을 가진다. 그 결과 디스크 사용량이 크고, 부팅 시 OS 초기화 과정을 거쳐야 하므로 실행 속도가 느리다.

반면 컨테이너는 OS 수준 가상화(OS-level virtualization) 방식이다. 커널은 공유하고, 사용자 공간만 분리한다. 그래서 컨테이너는 프로세스 실행에 가깝게 빠르게 시작되며, 상대적으로 훨씬 적은 자원을 사용한다.

요약하면, 가상 머신은 하드웨어를 가상화해 OS를 중복 실행하고, 컨테이너는 커널을 공유한 채 애플리케이션 실행 환경만 패키징한다.

격리의 한계와 보안 보완

커널을 공유한다는 것은 구조적인 보안 약점을 가진다는 뜻이기도 하다. 한 컨테이너가 커널의 취약점을 공격할 경우, 호스트와 다른 컨테이너까지 위험해질 수 있다. 컨테이너는 격리 메커니즘이지만 VM 수준의 보안 경계로 가정하면 위험하다.

이를 보완하기 위해 리눅스는 더 세밀한 보안 장치를 제공한다.

  • Capabilities: 루트(root) 권한을 잘게 쪼개어 관리한다. 컨테이너 내부의 root는 호스트의 진짜 root와 달리, 시스템 시각 변경이나 커널 모듈 로드 등 위험한 작업에 필요한 권한은 박탈된 제한된 root다.
  • Seccomp (Secure Computing mode): 프로세스가 사용할 수 있는 시스템 콜을 제한한다. 컨테이너가 굳이 호출할 필요가 없는 위험한 시스템 콜을 커널 차원에서 차단하여, 잠재적인 공격 표면을 최소화한다.

이러한 장치들이 있어도 완벽하지 않으므로, 실무에서는 권한 최소화 원칙을 지키고 런타임 보안 설정을 꼼꼼히 챙기는 것이 중요하다.

컨테이너의 한계

컨테이너는 호스트 OS의 커널을 공유하므로, 커널 의존성이 있는 프로그램에는 제약이 있다.

리눅스 호스트에서 윈도우 전용 애플리케이션을 컨테이너로 구동하는 것은 불가능하다. 윈도우 애플리케이션은 윈도우 커널의 시스템 콜을 사용하는데, 리눅스 커널은 그것을 이해하지 못한다.

Windows나 Mac에서 컨테이너를 쓸 때는 이 문제를 우회한다. 경량화된 가상 머신(WSL2, HyperKit 등)을 띄워 리눅스 커널을 확보한 뒤, 그 위에서 컨테이너 엔진을 구동하는 방식이다. 이 경우 가상 머신과 컨테이너를 함께 쓰는 셈이다.

Docker는 컨테이너가 아니다

Docker와 컨테이너를 동일시하면 오해가 생긴다.

  • 컨테이너: 리눅스 커널 기능(네임스페이스, cgroups)을 조합한 프로세스 격리 기술
  • Docker: 그 기술을 편하게 쓸 수 있게 만든 구현체

컨테이너 기술 자체는 리눅스 커널에 있다. Docker는 그것을 사용자 친화적으로 포장한 도구일 뿐이다.

Docker는 단일 바이너리가 아니라 여러 계층으로 구성되어 있다. 사용자가 docker 명령을 내리면 dockerd(데몬)가 이를 받고, 내부적으로 containerd에게 요청한다. containerd는 다시 runc를 호출하여 실제로 네임스페이스와 cgroups를 설정하고 프로세스를 실행한다.

그래서 Kubernetes 같은 시스템은 Docker 없이도 컨테이너를 실행할 수 있다. containerd나 CRI-O 같은 다른 컨테이너 런타임을 직접 사용하면 된다.

정리

컨테이너는 OS를 가상화하는 기술이 아니다. 프로세스를 OS처럼 보이게 만드는 기술이다.

컨테이너는 프로세스다. 프로세스가 종료되면 컨테이너도 종료된다. 그래서 컨테이너 안에서 백그라운드 데몬을 여러 개 돌리는 설계는 권장되지 않는다. 하나의 컨테이너는 하나의 책임을 가지는 것이 원칙이다.

네임스페이스로 격리하고, cgroups로 자원을 제어하며, 호스트 OS의 커널을 공유하여 가볍고 빠르게 작동한다.

Docker는 이 컨테이너 기술을 편리하게 사용할 수 있게 만든 도구일 뿐, 컨테이너 자체는 리눅스 커널에 있다.

참고 자료