Hacker News

Docker is weird

Docker는 기묘합니다

컨테이너는 별도의 가상 머신이 아니라 Linux 프로세스에 격리와 자원 제한을 적용한 형태입니다. 글쓴이는 이미지가 디렉터리에서 출발하고 runc가 Go의 스레드·fork 제약을 우회하는 방식 등을 직접 런타임을 만들며 배웠다고 설명합니다.

에디터 노트

Go가 컨테이너 런타임의 저수준 작업에 불편을 준다는 지적과, Docker가 당시 Go를 택한 판단은 함께 성립합니다. 창업자 Solomon Hykes도 단일 스레드 시스템 호출과 fork를 다루려고 C 초기화 코드와 재실행 우회를 넣었다고 인정하면서, Go의 배포 편의성과 기여자 확보가 성공에 보탬이 됐다고 설명합니다. 언어 선택을 현재 기준으로만 평가하면 당시의 채택 전략을 놓칩니다. root 권한 설명은 기본 Docker 데몬과 rootless 실행을 구분해야 합니다. 댓글에서는 rootless Docker가 이미 가능하며 현재 동작도 나아졌다는 보완이 나왔고, Podman은 데몬이 없어 설정 부담이 덜하다는 의견도 있었습니다. 컨테이너가 프로세스라는 설명은 Docker만의 특성이 아니라 Linux 컨테이너 전반에 해당합니다. Docker의 성취는 이 커널 기능들을 이미지 배포와 친숙한 도구로 묶었다는 데 있으며, 글은 그 구현을 직접 만들어보며 얻은 운영체제 관점의 입문 설명입니다.

AI 요약

직접 컨테이너 런타임을 만들기 시작한 글쓴이는 Docker의 내부가 익숙한 사용 경험과 다르게 구성됐다고 설명합니다. 컨테이너가 Linux에서 어떻게 작동하는지, Docker가 어떤 선택을 했는지 짚습니다.

컨테이너는 프로세스입니다

가상 머신(VM)은 하이퍼바이저 위에서 게스트 운영체제를 실행하지만, Linux 컨테이너는 호스트 커널이 실행하는 프로세스입니다. 프로세스에는 네임스페이스(namespaces)와 cgroups 같은 기능을 적용해 보이는 시스템 자원과 사용할 수 있는 자원을 제한합니다. 컨테이너라는 단일 Linux 기능이 따로 있는 게 아니라, 프로세스 격리와 자원 제어, 네트워크 설정 등 여러 커널 기능을 조합합니다. 실행 중인 컨테이너 프로세스도 호스트의 /proc에서 확인할 수 있습니다.

이미지는 디렉터리에서 출발합니다

Docker는 이미지를 저장한 뒤 컨테이너 프로세스의 루트 디렉터리를 이미지 쪽으로 바꿉니다. 글쓴이가 든 예에서는 docker run -it --rm ubuntu /bin/bash를 실행하면 Docker가 Ubuntu 이미지를 내려받고 저장한 다음, 이미지 안의 bash를 실행합니다. 이때 chroot나 pivot_root를 사용합니다. 실제 Docker 이미지에는 레이어와 저장소 드라이버가 관여하지만, 글은 컨테이너가 실행될 때 파일 시스템의 루트가 바뀐다는 기본 작동을 간단히 설명합니다.

runc와 Go의 제약

Docker의 컨테이너 런타임 runc는 Go로 작성됐습니다. 그러나 컨테이너 설정에 필요한 일부 Linux 시스템 호출은 단일 스레드 상태에서 실행해야 합니다. Go 런타임은 기본적으로 여러 운영체제 스레드를 사용하므로, runc는 Go 런타임 초기화 전에 동작하는 C 코드를 둬 이런 호출을 처리합니다.

또 Go에는 일반적인 fork 기능이 없습니다. fork는 호출한 프로세스의 스레드 하나만 복제하기 때문에 다중 스레드 런타임과 함께 쓰기 어렵습니다. 컨테이너 런타임은 제한을 설정한 자식 프로세스를 만든 뒤 실제 명령을 실행해야 하므로, runc는 /proc/self/exe를 특수한 인수와 함께 다시 실행해 자식 프로세스를 만드는 방식으로 fork를 흉내 냅니다.

root 권한과 네트워크

Docker를 macOS에서 주로 사용하면 Linux 호스트에서 Docker 데몬이 보통 root 권한으로 실행된다는 점을 놓치기 쉽다고 글쓴이는 말합니다. 컨테이너 런타임이 사용하는 Linux API 가운데는 root 권한이 필요한 기능이 있기 때문입니다. 다만 rootless 실행은 가능해졌고, 글쓴이는 네트워킹을 주요 예외로 꼽습니다. rootless 런타임은 같은 네트워크 기능을 제공하려고 사용자 공간에서 네트워크 스택을 다시 구현해야 할 수 있습니다.

Hacker News 반응

  • @shykes — Docker를 만든 사람으로서 다른 엔지니어가 시스템 설계를 어떻게 생각하는지 보는 일은 늘 즐겁습니다. 저는 여러 해 전에 Docker 개발에서 손을 뗐지만, 옛 기억을 떠올리게 하는 재미있는 글입니다. 궁금한 점이 있으면 답하겠습니다.
    • @julianeon — Go의 역사를 설명한 답변을 보면, Go가 나쁜 선택이었다는 글의 주장에 반박이 되는 것 같습니다.
    • @shykes — 커널과 맞닿는 일부 저수준 작업에 Go가 잘 맞지 않는다는 글의 지적은 정확합니다. 제대로 작동시키려면 여러 우회가 필요했습니다. 그래도 당시에는 Go가 옳은 선택이었고 Docker가 초기에 성공하는 데에도 도움이 됐습니다.
  • @suprjami — 복잡한 것을 구성 요소부터 배워가며 풀어내는 글은 늘 좋습니다. rootless Podman의 fork-exec 방식도 살펴보면 더 자연스럽다고 느낄 수 있습니다. Linux 네임스페이스를 더 배우고 싶다면 LWN의 관련 연재를 권합니다.
  • @j16sdiz — 글에 나온 내용 중 무엇이 기묘한지 모르겠습니다. 여러 해에 걸친 발전 과정을 알면 모두 자연스러운 선택처럼 보입니다.
    • @paxy — Docker를 블랙박스로 쓰며 가상 머신과 같은 구조라고 생각하다 내부를 들여다보면 기묘한 점들이 드러납니다.
  • @thayne — Go를 쓴 점은 확실히 낯섭니다. 네임스페이스를 쓰는 데 중요한 시스템 호출 clone을 Go에서 바로 호출하기 어렵기 때문입니다.
  • @0xbadcafebee — Docker가 root 권한을 요구한다고 반복해서 말하는데, 7년 전부터 root 없이 실행할 수 있습니다. 기본 설정에서 root 데몬을 쓰는 이유는 더 간단하기 때문입니다.
    • @fulafel — 예전 rootless Docker는 사용하기가 꽤 불편했습니다. 지금은 나아졌나요?
    • @hanumseous — 나아졌습니다. rootless dockerd는 실행 전에 newuidmap, newgidmap과 subuid 범위가 필요합니다. cgroups를 쓰려면 cgroup v2와 systemd도 필요합니다. 조건을 갖춘 호스트에서는 괜찮지만 Podman이 설정 부담은 더 적습니다.
  • @colinmarc — Docker와 컨테이너는 개발 경험 측면에서는 크게 성공했지만, 다중 테넌트 작업을 맡길 만큼 안전하지 않아 운영 환경에서는 KVM이나 gVisor 같은 별도 격리 계층과 함께 쓰이는 경우가 많았습니다. 그 결과 컨테이너와 가상 머신을 함께 운영하는 비효율도 생겼습니다.

원문: Code and Cake / 번역·요약: Trawling