Home [번역] Pid 네임스페이스에 대한 궁금증
Post
Cancel

[번역] Pid 네임스페이스에 대한 궁금증

The Curious Case of Pid Namespaces

[원본링크] https://hackernoon.com/the-curious-case-of-pid-namespaces-1ce86b6bc900

Pid 네임스페이스에 대한 궁금증

image

컨테이너들은 어떻게 pid들을 공유할까?

네임스페이스들은 리눅스 컨테이너들의 가장 중요한 컴포넌트 중 하나이다.

네임스페이스들은 공유 자원에 대해서 격리를 지원하는데, 각각의 어플리케이션이 자기 자신의 유니크한 시스템 뷰를 가질 수 있게 한다. 네임스페이스 덕분에 각각의 도커 컨테이너들은 자신의 파일시스템과 네트워크를 가질 수 있는 것이다. 리눅스는 수많은 배포를 통해서 네임스페이스를 더해왔다. 이러한 점진적인 변화 떄문에 네임스페이스의 각 타입들은 자신의 유니크한 도전들을 제공한다. 그 중에서도 Pid 네임스페이스는 특히 멀티 프로세스가 포함될 때 특별한 처리를 요구한다.

리눅스에서의 Pid

리눅스에서의 프로세스는 트리구조를 가지게 된다. 커널의 각 프로세스들은 유니크한 프로세스 식별자를 가지고 있는데, pid라고도 불린다. 각 프로세스의 발자취에 대한 기록은 직계 부모 프로세스로부터 물려받는다. Pid는 fork 시스템 콜로 생성되었을 때도 부모로부터 전달받는다. 커널은 새로운 자식 pid 를 생성하고 식별자를 호출한 프로세스로 반환해준다. 하지만 자식 pid를 수동으로 추적하는 것은 부모 프로세스에게 달려있다.

커널에 의해 시작되는 첫번째 프로세스는 pid를 1로 가진다. 이 프로세스는 init process라고도 불리며 쉽게 init이라 불린다. init 의 부모 프로세스의 pid는 0이며 그것이 커널을 의미한다는 것을 알 수 있다. Pid 1은 유저스페이스 프로세스 트리의 루트이다. 리눅스 시스템에서는 어떤 프로세스든 반복적으로 각자의 부모 프로세스를 따라가다 보면 pid 1 을 찾을 수 있다. 만약 pid 1 이 죽게되면, 커널은 panic에 빠지고 서버를 재부팅해야만 한다.

네임스페이스 훑어보기

리눅스 네임스페이스들은 unshare 시스템 콜에 어떤 네임스페이스를 생성할 것인지에 대한 플래그를 전달해서 만들어진다. 대부분의 케이스에서 unshare 는 새로운 네임스페이스로 당신을 던져버린다. 예를 들어, 프로세스가 네트워크 네임스페이스를 생성하자마자 어떠한 디바이스도 연결되지 않은 빈 네트워크를 즉각적으로 확인할 수 있다.

Pid 네임스페이스는 약간 다른데, 당신이 pid 네임스페이스를 unshare 했을때 프로세스는 즉각적으로 새로운 네임스페이스에 들어가지 않는다. 대신, fork를 해줘야한다. 자식 프로세스는 pid 네임스페이스에 들어가고 pid 1 이 된다. 이것을 통해서 특별한 성격이 가득 채워진다.

또한 pid 네임스페이스는 프로세스 계층에 대해서 분리되어진다는 것을 주목해야한다. 다시말해서 fork 된 프로세스는 실제로는 두개의 Pid를 가지고 있다는 것이다. 새로운 네임스페이스에서의 pid 1, 네임스페이스 바깥에서 봤을 때의 pid.

네임스페이스에서의 Pid 1

네임스페이스 안에서 init (Pid 1)은 다른 프로세스와 비교했을 때 3개의 특별한 특성을 가진다.

  1. 자동으로 디폴트 시그널 핸들러를 가지지 않기 때문에 해당 시그널에 대해서 시그널 핸들러를 등록해두지 않았다면 시그널을 받았을 때 무시한다. (왜 도커라이즈 된 수 많은 프로세스들이 ctrl+c 를 했을 때 강제 종료되지 않고 docker kill 을 해야만 죽는 이유이다!)
  2. 만약 네임스페이스에서 다른 프로세스가 자식보다 먼저 죽었다면, 그 프로세스의 자식은 pid 1을 부모로 다시 가지게 된다. init 이 exit 상태인 프로세스들을 거두면서 커널이 프로세스 테이블에서 지울 수 있게 된다.
  3. 만약 프로세스가 죽는다면, pid 네임스페이스에서의 모든 프로세스들은 강제로 종료가 되며 네임스페이스는 정리가 될 것이다.

init 프로세스는 컨테이너의 라이프타임과 매우 밀접하게 연관되어 있음을 알 수 있다.

Docker의 “실수”

도커(그리고 runc)는 새로운 pid 네임스페이스에서 지정된 프로세스를 컨테이너 엔트리 포인트로 pid 1 으로 실행시킨다. pid 1 로 실행하도록 디자인 된 부분은 어플리케이션 프로세스에 예기치 못한 행동을 불러일으킨다. 위에서 설명한 것처럼 pid 1로 프로세스가 실행되었을 때 만약 자신의 시그널 핸들러를 등록하지 않으면, 시그널이 먹히지 않을 것이다. 만약 포트한 자식 프로세스가 자식의 자식 프로세스가 죽기전에 먼저 죽어버리면, 좀비 프로세스가 컨테이너에 생성될 수 있고 잠재적으로 프로세스 테이블을 계속 채워갈 것이다.

도커는 이 내용에 대해서 거의 손을 떼어 왔다. 대신에 컨테이너에서 특별한 init 프로세스를 실행할 수 있는데 어플리케이션 프로세스가 fork-exec 이 되는 것이다.(pid 1이 되는 것이 아니라, 1의 자식 프로세스로 해당 어플리케이션 프로세스가 생성된다) 많은 컨테이너들이 위의 문제를 피하기 위해서 이렇게 수행해왔다. 이 방법에는 한가지 불운한 영향이 있는데 컨테이너가 좀 더 복잡해진다는 것이다. 컨테이너가 진짜 init 시스템을 가지고 있을 때는 사람들이 의존성 격리에 대한 이점을 희생시키는 멀티 프로세스를 컨테이너에 내장하는 경향이 있었다. 도커의 pod 에 대한 네이티브 지원 부족은 이 문제를 계속해서 악화시킬 뿐이다.

The Rkt “Solution”

Rkt 는 이 문제에 대해서 좀 더 깔끔한 해결책을 제시한다. 당신이 시작한 프로세스가 init 프로세스가 아니라고 가정하고 사용자(systemd)를 위한 init 프로세스를 생성하는데 이 때 systemd는 컨테이너 프로세스에 대한 파일 시스템 네임스페이스를 만들고 실행한다. Systemd는 네임스페이스에서 pid 1이 되고 컨테이너 프로세스는 pid 2로 실행된다. 즉, 컨테이너가 init 프로세스를 제공할 경우 pid 2로 실행될 것이지만, 실제로는 거의 문제가 발생하지 않는다는 것이다.

더 간단한 대안책

단일 프로세스에 대해서

This post is licensed under CC BY 4.0 by the author.