LODY/정리

클라우드 네이티브 / 컨테이너와 리눅스 격리 원리

컨테이너와 리눅스 격리 원리

🟣 어디서나 똑같이 실행된다는 약속

마이크로서비스로 애플리케이션을 잘게 쪼개면 배포 단위가 폭증한다. 그 많은 단위를 개발자 노트북, 테스트 서버, 운영 서버에서 전부 똑같이 실행하려면 환경을 통일해야 한다. 그런데 서버마다 OS 버전, 설치된 라이브러리, 언어 런타임이 조금씩 다르다. "내 컴퓨터에서는 됐는데"는 이 차이에서 나온다.

가상 머신은 이 문제를 OS를 통째로 복제해서 풀었다. 하지만 서비스 하나 띄우자고 게스트 OS 전체를 올리는 것은 무겁다. 부팅에 수십 초가 걸리고 메모리도 기가 단위로 먹는다. 배포 단위가 수백 개인 세계에서는 감당하기 어렵다.

컨테이너는 다른 답을 낸다. OS를 복제하지 않고, 호스트 커널을 공유하되 프로세스가 보는 세계만 격리한다. 그래서 가볍고 빠르다. 이 "프로세스가 보는 세계만 격리"라는 말이 처음엔 모호했는데, 파고들어 보니 마법이 아니라 리눅스 커널의 두 기능을 조합한 것이었다.


🟣 컨테이너는 격리된 프로세스일 뿐이다

가장 중요한 오해부터 바로잡아야 한다. 컨테이너는 가벼운 가상 머신이 아니다. 컨테이너는 그냥 리눅스 프로세스다. 호스트에서 ps를 치면 컨테이너 안의 프로세스가 그대로 보인다. 다만 그 프로세스가 자기만의 격리된 세계에 있다고 믿을 뿐이다.

가상 머신게스트 OS 전체(무겁고 느림)컨테이너격리된 프로세스(호스트 커널 공유)

가상 머신은 게스트 OS라는 두꺼운 층을 두지만, 컨테이너는 호스트 커널을 그대로 쓰면서 프로세스만 격리한다. 이 차이가 부팅 시간과 메모리 사용량의 차이로 그대로 나타난다. 그렇다면 커널을 공유하는 프로세스를 어떻게 격리된 세계에 가둘까. 여기서 리눅스 시스템 프로그래밍에서 본 프로세스 개념이 커널 수준으로 한 발 더 내려간다.


🟣 네임스페이스: 보는 세계를 가른다

첫 번째 기능은 네임스페이스다. 네임스페이스는 프로세스가 볼 수 있는 시스템 자원의 범위를 가른다. 같은 커널 위에 있지만, 각 컨테이너의 프로세스는 자기 네임스페이스 안의 것만 본다.

앞 책에서 프로세스마다 독립된 주소 공간을 가진다고 배웠다. 네임스페이스는 그 격리를 메모리 너머로 확장한다. PID 네임스페이스는 프로세스 번호를 가른다. 컨테이너 안의 첫 프로세스는 자기가 PID 1이라고 믿지만, 호스트에서 보면 수천 번대의 평범한 프로세스다. 마운트 네임스페이스는 파일 시스템을 가른다. 컨테이너는 자기만의 루트 디렉터리를 보고 호스트의 다른 파일은 보지 못한다. 네트워크 네임스페이스는 네트워크 장치와 포트를 가른다. 그래서 여러 컨테이너가 각자 8080 포트를 충돌 없이 쓸 수 있다.

핵심은 이것이 격리처럼 보이는 착시가 아니라 커널이 강제하는 진짜 경계라는 점이다. 컨테이너 안 프로세스는 자기 네임스페이스 밖을 볼 방법이 아예 없다.


🟣 cgroup: 쓸 수 있는 양을 가른다

네임스페이스가 "무엇을 볼 수 있는가"를 가른다면, cgroup(control group)은 "얼마나 쓸 수 있는가"를 가른다. CPU, 메모리, 디스크 입출력을 프로세스 그룹별로 제한한다.

이게 없으면 컨테이너 하나가 메모리를 폭주시켜 호스트 전체를 마비시킬 수 있다. cgroup으로 "이 컨테이너는 메모리 512MB, CPU 0.5코어까지"라고 상한을 걸면, 한 컨테이너의 문제가 옆 컨테이너로 번지지 않는다. 격리가 자원 차원에서도 성립하는 것이다.

공유 호스트 커널네임스페이스: 보는 세계를 가른다(PID·마운트·네트워크)cgroup: 쓰는 양을 가른다(CPU·메모리·IO)커널 기능커널 기능

네임스페이스와 cgroup, 이 두 커널 기능의 조합이 컨테이너의 전부다. Docker는 이걸 발명한 게 아니라, 흩어져 있던 이 기능들을 이미지와 명령어로 쓰기 편하게 묶은 것이다. 격리의 원리가 커널에 있다는 걸 알고 나면 Docker가 마법처럼 보이지 않는다.


🟣 이미지: 레이어로 쌓는 파일 시스템

컨테이너가 실행 중인 프로세스라면, 이미지는 그 프로세스가 볼 파일 시스템을 통째로 담은 템플릿이다. 이미지의 특징은 레이어로 쌓인다는 점이다. 베이스 OS 위에 런타임을 얹고, 그 위에 라이브러리를, 그 위에 내 코드를 얹는 식으로 층층이 쌓인다.

각 레이어는 변경분만 담고 읽기 전용으로 공유된다. 그래서 같은 베이스 이미지를 쓰는 컨테이너 열 개를 띄워도 베이스 레이어는 한 벌만 디스크에 있다. 앞 책에서 본 공유 메모리가 같은 페이지를 여러 프로세스가 공유했듯, 이미지 레이어도 같은 층을 여러 컨테이너가 공유한다. 코드 한 줄만 바뀌면 맨 위 레이어만 새로 만들면 되니 빌드와 배포도 빨라진다.

FROM node:20-slim
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "server.js"]

Dockerfile의 각 줄이 하나의 레이어가 된다. 의존성 설치(npm install)를 코드 복사보다 위에 둔 이유가 여기 있다. 코드만 바뀌고 의존성이 그대로면, 무거운 설치 레이어는 캐시에서 재사용되고 가벼운 코드 레이어만 다시 만들어진다. 레이어 순서 하나가 빌드 속도를 좌우한다.


🟣 정리

  • 컨테이너는 가벼운 가상 머신이 아니라 격리된 리눅스 프로세스다. 호스트 커널을 공유하므로 가볍고 빠르다.
  • 네임스페이스는 프로세스가 보는 세계(PID·파일·네트워크)를 가른다. 앞 책의 주소 공간 격리가 커널 수준으로 확장된 것이다.
  • cgroup은 프로세스가 쓰는 자원(CPU·메모리·IO)의 양을 제한한다. 한 컨테이너의 폭주가 옆으로 번지지 않게 한다.
  • Docker는 격리를 발명한 게 아니라 네임스페이스와 cgroup을 쓰기 편하게 묶은 것이다.
  • 이미지는 읽기 전용 레이어로 쌓이고 공유된다. Dockerfile의 줄 순서가 캐시 재사용과 빌드 속도를 정한다.

컨테이너 하나의 격리 원리를 커널까지 내려가 봤다. 그런데 서비스가 커지면 이런 컨테이너가 수십, 수백 개가 된다. 사람이 일일이 어느 서버에 몇 개를 띄울지, 죽으면 어떻게 살릴지 관리할 수 없다. 다음 장은 이 컨테이너 무리를 대신 관리하는 마이크로서비스와 오케스트레이션이다.