42서울/Inception

컨테이너 빌드에 대한 권장사항 [PID 1] [42 inception 과제 개념11]

뜨거운 개발자 2023. 6. 24. 17:59

시작하며

이전의 글들을 모두 읽고 이 글을 읽기를 권장합니다.

이 글은 구글의 컨테이너 빌드에 대한 권장사항을 기반으로 하고 있고, 그 권장사항들에 대해서 정리합니다.

컨테이너 빌드에 대한 권장사항  |  클라우드 아키텍처 센터  |  Google Cloud
https://cloud.google.com/architecture/best-practices-for-building-containers?hl=ko#signal-handling

이전에 도커파일에서 다뤘던 CMDENTRYPOINT 와 깊은 연관이 있으니 그 부분과 함께 공부하면 좋습니다.

이 글은 컨테이너를 실행과 동시에 꺼지는 상황을 해결하는데 해결책을 제시할 수 있습니다.

그 중 중요한 부분인 PID1 에 대한 내용도 함께 다뤄보겠습니다.

컨테이너 빌드에 대한 권장사항

컨테이너 작업을 시작할 때 사용자가 자주 범하는 실수는 컨테이너를 여러 가지 작업을 동시에 실행할 수 있는 가상 머신(VM)으로 처리하는 것입니다. 컨테이너는 이러한 방식으로 작동할 수 있지만 그렇게 하면 컨테이너 모델의 장점이 상당 부분 줄어듭니다.

예를 들어 기본 Apache/MySQL/PHP 스택을 선택해 단일 컨테이너에서 모든 구성요소를 실행하려고 할 수 있습니다.

하지만 PHP-FPM을 실행하는 경우 Apache용 컨테이너 하나, MySQL용 컨테이너 하나, PHP용 하나 등과 같이 2개나 3개의 컨테이너를 사용하는 것이 가장 좋습니다.

이전 글에서 몇 번 언급한 적이 있는데, 컨테이너는 한번에 하나의 앱만을 패키징 하는 것이 좋습니다.

오늘은 그 이유에 대해서 먼저 이야기 해보겠습니다.

💡
일단 컨테이너는 컨테이너가 호스팅하는 앱과 같은 수명 주기를 사용하도록 설계되어있습니다. 그 이유는 처음부터 컨테이너의 설계는 앱을 하나만을 호스팅하는 것으로 가정하고 설계 되었기 때문입니다.

따라서 초기 설계의도를 고려해서, 각 컨테이너에는 하나의 앱만을 포함하여야만 합니다.

컨테이너가 시작될 때, 앱이 시작되어야만 하고, 앱이 중단되면, 컨테이너도 중단되어야만 합니다.

이것은 컨테이너를 사용하는데 강력한 권고사항입니다.

다음 그림에서 왼쪽 그림이 컨테이너에서 권장사항을 지키는 프로세스의 모습입니다.

👨🏻‍💻
가장 상위 부모 프로세스가 있고, 그 밑에 자식 프로세스들이 실행되는 구조가 권장사항을 지키는 즉 하나의 앱만을 컨테이너에서 실행하는 모습이라고 볼 수 있습니다.

그에 반해 오른쪽의 안 좋은 예시로는 부모 프로세스가 두 개여서, 두 개의 앱을 실행하고 있습니다.

왜 하나의 컨테이너에는 하나의 앱, 즉 하나의 부모 프로세스 만을 가지는 것이 좋다고 할까요?

👨🏻‍💻
하나의 컨테이너에 여러 개의 앱이 있는 경우, 수명 주기가 다를 수도 있고, 다른 상태일 수도 있기 때문입니다.

이것을 더 쉽게 위의 그림을 보면서 예시를 들어 설명해보겠습니다.

컨테이너에서 실행 중인 프로세스 중 하나에서 갑자기 오류가 발생해서 응답이 없어졌습니다. 그런데 오류가 난 프로세스가 컨테이너에서 상당히 중요한 역할을 하는 프로세스여서, 더이상 컨테이너는 제 역할을 하지 못합니다.

이런 상황이라면 당연하게도, 컨테이너가 오류가 난 상태를 확인해서 컨테이너의 오류를 처리를 해야만 합니다.

🚨
그러나, 여러개의 앱이 돌아가고 있는 컨테이너는 특별한 설정을 해주지 않는 한, 컨테이너를 관리하는 주체(도커 또는 쿠버네티스)는 컨테이너에서 오류가 났는지 확인할 수가 없습니다.
주의사항 : 단, 프로세스 관리 시스템인 supervisor를 사용하거나, bash 스크립트를 컨테이너 진입점으로 해서 백그라운드로 앱을 실행하는 방법을 통해 조치할 수 있지만, 이 방법을 권장되지 않습니다.

이것을 보게 되면 왜 inception 서브젝트에서 위와 같이 명시했는지 볼 수가 있습니다.

🚨
무한 루프를 실행하는 명령으로 컨테이너를 시작해서는 안된다. 이는 엔트리 포인트 스크립트에 사용되는 모든 명령에 포함된다. tail -f, bash, sleep 명령은 사용 불가하다”

왜 이 멘트가 있는지 알 수가 있는 부분이었습니다.

그런데 왜 여러개의 어플리케이션을 실행하면, 컨테이너가 오류가 났다는 사실을 감지하지 못하는 것일까요?

이것에 대한 이해를 하기 위해서는 유닉스의 프로세스 구조에 대해서 알아야만 합니다.

유닉스의 프로세스 구조 (트리구조, 데몬프로세스, PID1)

👨🏻‍💻
기본적으로 Unix 의 프로세스 구조는 루트가 되는 PID 1부터 시작하여, 자식을 만들어가는 형태로 트리 구조를 가집니다.

트리 구조로 유지되는 프로세스들은 부모 프로세스가 죽으면 모두 종료됩니다.

그렇기 때문에 서비스로 유지해야 하는 프로세스들을 별도의 루트를 만들어 동작하는 방식을 사용합니다.

이때 사용하는 루트 프로세스데몬 프로세스라고 합니다. (여기서 루트 프로세스PID1은 같은 맥락으로 사용합니다.)

일반적인 유닉스 구조는 PID 1데몬 프로세스를 실행합니다.

하지만, 도커 컨테이너 내부에서는 따로 직접적으로 설정해서 데몬 프로세스 가 설정되어있지 않다면, 기본적으로 PID 1로써 실행하지 않습니다.

도커 컨테이너PID1로써 컨테이너를 실행할 때 처음 사용하는 명령으로 설정합니다.

즉 도커파일의 ENTRYPOINT 또는 CMD를 이용해서 설정해줄 수 있습니다.

이전에 도커파일 게시글에서 ENTRYPOINT 파트에서 설명 했듯, 어플리케이션컨테이너인 경우 CMD보다는 ENTRYPOINTPID1번을 설정해주는 것을 더 권장했습니다.

🚧
만약 직접적으로 설정을 해주지 않았다면 당연히 Docker Container는 종료된 상태로 판단하여 컨테이너가 제대로 실행되지 않고 종료되는 이슈가 발생합니다.

그렇기 때문에 컨테이너를 제대로 관리하기 위해서는 PID1번에 데몬 프로세스 또는 그에 맞는 기능을 하는 프로세스를 지정해주어야만 합니다.

그렇다면 어떻게 컨테이너를 설정하는 것이 더 잘 관리할 수 있는 방법일까요?

컨테이너 관리 권장사항

1. 리눅스 signal 처리

첫째로, 리눅스 signal을 제대로 처리 해야 합니다. 리눅스에서 시그널은 컨테이너 내부의 프로세스 수명 주기를 제어하는 주요 방법입니다.

앱의 수명 주기를 앱이 포함된 컨테이너와 동일하게 연결하려면 앱이 Linux 신호를 올바르게 처리하도록 해야만 합니다.

리눅스 시그널 중 가장 중요하게 처리해야하는 시그널은 프로세스를 종료하는 SIGTERM입니다.

그리고 앱은 프로세스를 비정상적으로 종료하는 데 사용되는 SIGKILL 신호 또는 Ctrl+C를 입력할 때 전송되는, SIGINT 신호를 수신하도록 할 수 있습니다.

💡
실제로 도커쿠버네티스 등 컨테이너를 관리하는 주체는 시그널을 사용해서 컨테이너 내부 프로세스와 통신합니다. 시그널은 주로 컨테이너를 종료하기 위해서 사용 합니다.
💡
여기서 도커와 쿠버네티스는 시그널을 컨테이너 내부에 PID 1 인 프로세스에만 신호를 보낼 수 있습니다.

기본적으로 도커에서 시그널로 PID1과 통신을 하기 때문에, 만약 PID1 프로그램을 설정해야한다면, 시그널을 제대로 처리하는 프로세스인지 확인할 필요가 있습니다.

[문제 1] 리눅스 커널의 시그널 처리 방법

Linux 커널이 신호를 처리하는 방법은 PID 1을 가진 프로세스와 그렇지 않은 프로세스에서 차이가 있습니다. 신호 핸들러가 PID1을 가진 프로세스에 자동으로 등록되지 않으므로 SIGTERM 또는 SIGINT 같은 신호는 기본적으로 아무런 영향을 미치지 않습니다.

따라서 PID1을 가진 프로세스를 종료하려면 기본적으로, 단계적 종료를 방지하는 SIGKILL을 사용하여 프로세스를 강제 종료해야 합니다.

다만, 앱에 따라 SIGKILL을 사용하면 모니터링 시스템에 사용자 표시 오류, 쓰기 중단(데이터 저장용), 원치 않는 알림이 발생할 수 있는 문제가 있습니다.

2. PID의 개념과 활용

둘째로, PID1을 활용해야 합니다.

계속해서 PID1에 대해서 설명했지만 아주 중요한 개념이기 때문에 PID의 개념에 대해서 더 자세히 설명하고, 문제점을 해결하는 방법에 대해서 설명하도록 하겠습니다.

3.1. PID Namespace
Linux Process 관리 PID Namespace는 Process의 격리를 담당하는 Namespace이다. PID Namespace를 완전히 이해하기 위해서는 Linux의 Process Tree를 이해할 필요가 있다. [그림 1]은 Linux의 Process Tree를 나타내고 있다. 네모는 하나의 Process를 나타내며 각 Process에는 Process에는 이름과, PID (Process ID)가 기제되어 있다. Process Tree의 Root에 존재하는 Process는 반드시 PID 1번을 갖으며 Init Process라고 불린다. Process는 fork() System Call을 호출하여 자식 Process를 생성할 수 있다. fork() System Call을 호출한 Process는 부모 Process가 된다. 예를들어 [그림 1]에서 B Process는 fork() System Call을 2번 호출하여 통해서 C Process와 D Process를 생성하였다. B Process는 C Process와 D Process의 부모 Process가 되며, C Process와 D Process는 B Process의 자식 Process가 된다. 모든 Process는 fork() System Call을 통해서 자유롭게 자식 Process를 생성할 수 있다. 따라서 [그림 1]처럼 Linux의 Process는 Init Process를 Root로하는 Tree를 구성하게 된다. PID Namespace를 완전히 이해하기 위해서 또하나 알고 있어야 하는 배경지식은 고아 Process와 Zombie Process의 정의와, Linux에서 고아 Process와 Zombie Process의 처리하는 방법이다. 고아 Process는 의미 그대로 부모 Process가 죽어 고아가된 Process를 의미한다. Linux는 고아 Process가 발생하면 고아 Process의 부모를 Init Process로 설정한다. [그림 2]에서는 B Process가 종료되어 C Process와 D Process가 고아 Process가 되었기 때문에, Init Process인 A Process가 C Process와 D Process의 새로운 부모 Process가 되는 과정을 나타내고 있다. Zombie Process는 죽지 않는 Process를 의미한다. Zombie Process가 죽지 않는 이유는 Process는 실제로 죽어 종료된 상태이지만 Process의 Meta 정보가 Kernel에 남아 있어 Process가 존재하는것 처럼 보이는 상태이기 때문이다. Zombie Process를 제거하기 위해서는 Kernel에서 Zombie Process의 Meta 정보를 제거하는 방법밖에 존재하지 않는다. Linux에서 Zombie Process의 Meta 정보를 제거하는 방법은 부모 Process가 wait() System Call을 호출하여 Zombie Process의 Meta 정보를 회수해야 제거된다. 따라서 부모 Process는 fork() System Call을 통해서 생성한 자식 Process를 wait() System Call을 통해서 회수해야 한다. 하지만 만약 부모 Process가 wait() System Call을 호출하지 않는다면, 종료된 자식 Process는 Zombie Process가 된다. 이러한 Zombie Process는 부모 Process가 죽어야 Init Process에 의해서 제거된다. [그림 3]은 Linux에서 부모 Process가 wait() System Call을 호출하지 않았을 경우 Zombie Process의 처리 과정을 나타내고 있다. C Process가 종료되었지만 B Process가 wait() System Call을 호출하지 않았기 때문에 C Process는 Zombie Process가 된다. 이후 B Process가 종료되면 C Process의 부모 Process는 Init Process가 되기 때문에 Init Process는 wait() System Call을 호출하여 C Process를 제거한다. 이와 같은 이유 때문에 Init Process는 반드시 wait() System Call을 호출하여 Zombie Process를 제거하는 역할을 수행해야 한다. 가장 많이 이용되는 Init Process인 systemd는 Zombie Process 제거 역할을 수행한다. # sleep process을 생성하는 bash Process를 생성 (host)# bash -c "(bash -c 'sleep 60')" & (host)# ps -ef root 1 0 0 Apr22 ? 00:00:03 /sbin/init ... root 29756 28207 0 22:06 pts/24 00:00:00 bash -c (bash -c 'sleep 60') root 29758 29756 0 22:06 pts/24 00:00:00 sleep 60 root 29764 28207 0 22:06 pts/24 00:00:00 ps -ef # bash Process를 종료시킨 다음 sleep Process의 부모 Process가 Init Process가 되는것을 확인 (host)# kill -9 29756 (host)# ps -ef root 1 0 0 Apr22 ? 00:00:03 /sbin/init ... root 29758 1 0 22:06 pts/24 00:00:00 sleep 60 root 29779 28207 0 22:07 pts/24 00:00:00 ps -ef # 60초 뒤에 sleep Process가 종료가 된다음 sleep Process가 Zombie Process가 되지 않고 제거되는것을 확인 (host)# ps -ef root 1 0 0 Apr22 ? 00:00:03 /sbin/init ... root 29779 28207 0 22:07 pts/24 00:00:00 ps -ef [Shell 1] Linux Host에서 고아 Process 확인 [Shell 1]은 Linux Host에서 고아 Process 생성 및 상태를 확인하는 과정을 나타내고 있다. [Shell 1]에서 sleep Process는 Bash Process가 부모 Process인데, Bash Process가 종료된 다음 sleep Process의 새로운 부모 Process는 init Process가 되는것을 확인할 수 있다. 60초 후에 sleep Process가 종료된 다음 /sbin/init Process는 자식 Process인 sleep Process의 Meta 정보를 회수하여 sleep Process가 Zombie Process가 되는것을 방지하여 sleep Process를 제거한다. PID Namespace # nginx Container를 Daemon으로 실행하고 exec을 통해서 nginx Container에 bash Process를 실행 (host)# docker run -d --rm --name nginx nginx:1.16.1 (host)# docker exec -it nginx bash # nginx Container의 Process를 확인한다. (nginx)# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 Apr10 ? 00:00:00 nginx: master process nginx -g daemon off; nginx 6 1 0 Apr10 ? 00:00:00 nginx: worker process [Shell 1] nginx Container Process # httpd Container를 Daemon으로 실행하고 exec을 통해서 httpd Container에 bash Process를 실행 (host)# docker run -d --rm --name httpd httpd:2.4.43 (host)# docker exec -it httpd bash # httpd Container의 Process를 확인한다. (httpd)# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 Apr10 ? 00:00:05 httpd -DFOREGROUND daemon 7 1 0 Apr10 ? 00:00:00 httpd -DFOREGROUND daemon 8 1 0 Apr10 ? 00:00:00 httpd -DFOREGROUND daemon 9 1 0 Apr10 ? 00:00:00 httpd -DFOREGROUND [Shell 2] httpd Container Process # host에서 nginx Container와 httpd Container의 Process를 확인 (host)# ps -ef UID PID PPID C STIME TTY TIME CMD ... root 20997 20969 0 Apr10 ? 00:00:00 nginx: master process nginx -g daemon off; systemd+ 21042 20997 0 Apr10 ? 00:00:00 nginx: worker process ... root 25759 25739 0 Apr10 ? 00:00:05 httpd -DFOREGROUND daemon 25816 25759 0 Apr10 ? 00:00:00 httpd -DFOREGROUND daemon 25817 25759 0 Apr10 ? 00:00:00 httpd -DFOREGROUND daemon 25818 25759 0 Apr10 ? 00:00:00 httpd -DFOREGROUND ... [Shell 3] Host Process [Shell 1]은 nginx Container 내부에서 본 Process를 나타내고 있고 [Shell 2]는 httpd Container 내부에서 본 Process를 나타내고 있다. 마지막으로 [Shell 3]은 nginx Container와 httpd Container를 구동한 Host에서 본 Process를 나타내고 있다. NGNIX Container와 httpd Container는 서로의 Process를 확인할 수 없지만, Host는 두 Container의 Proces를 모두 확인할 수 있다. 이러한 현상은 PID Namespace의 특징 때문에 발생한다. PID Namespace는 의미 그대로 PID를 격리하는 Namespace이다. PID를 격리한다는 의미는 좀더 확장되면 Process를 격리한다는 의미와 동일하다. [그림 4]는 Host가 이용하는 Host PID Namespace, Container A가 이용하는 Container A PID Namespace, Container B가 이용하는 Container B PID Namespace, 3개의 PID Namespace를 나타내고 있다. 또한 [그림 4]의 왼쪽에는 PID Namespace 사이의 관계도 나타내고 있다. PID Namespace는 계층을 갖는 Namespace이다. 가장 상위에 존재하는 PID Namespace는 Init PID Namespace라고 명칭한다. Host PID Namespace는 Init PID Namespace이며, 자식 PID Namespace로 Container A와 Container B의 PID Namespace를 갖는다. 각 Namespace에서 Process Tree의 가장 높이 위치하는 Proess는 Namespace의 Init Process라고 명칭한다. [그림 4]에서 A Process는 Host PID Namespace의 Init Process이고, D Process는 Container B PID Namespace의 Init Process이다. 각 Process는 오직 자신이 소속되어 있는 PID Namespace의 Process들 및 자신이 소속되어 있는 PID Namespace의 하위 PID Namespace들에게 소속되어 있는 Process들에게만 접근할 수 있다. 따라서 Host PID Namespace에 소속되어 있는 Process는 Container A와 Container B의 Process에 접근할 수 있지만, Container에 소속되어 있는 Process들은 Container의 Process에게만 접근할 수 있다. 동일한 Process라도 각 PID Namespace마다 다른 PID를 갖는다. 각 Namespace의 Init Procesess는 해당 Namespace에서 1번 PID를 갖는다. [그림 4]에서는 PID Namespace마다 다르게 보이는 PID도 나타내고 있다. E Process는 A Namespace에서는 5번 PID로 보이지만 B Namespace에서는 6번 PID로 보인다. B Process는 B Namespace의 Init Process이기 때문에 1번 B Namespace에서는 1번 PID로 보인다. Container A를 nginx Container라고 간주하고 Container B를 httpd Container라고 간주한다면 [Shell 1~3]의 동작 과정을 이해할 수 있게 된다. 그렇다면 Container 내부에서 PID Namespace를 생성하면 어떻게 될까? [그림 5]는 Container B PID Namespace에서 Container를 생성하여 Nested Container PID Namespace가 생겼을때를 표현하고 있다. Container B PID Namespace 하위에 Nested Container PID Namespace가 생성된다. Docker Contaier 안에서 Docker Container를 구동하는 (Docker in Docker)의 경우에 [그림 5]와 같은 PID Namespace의 관계가 생성된다. 마지막으로 PID Namespace의 중요한 특성은 바로 PID Namespace의 Init Process가 죽으면, Linux Kernel에 의해서 PID Namespace의 나머지 Process도 SIGKIll Signal을 받고 강제로 죽는다는 점이다. 따라서 Container의 Init Process가 죽으면 Container의 모든 Process가 죽고, Container는 사라지게 된다. [그림 6]은 PID Namespace의 Init Process가 죽었을때의 과정을 나타내고 있다. Container PID Namespace의 Init Process인 C Process가 죽으면, D Process 및 E Process도 죽게된다. D Process와 E Process는 Container PID Namespace의 부모 Namespace인 Host Namespace의 Init Process인 A Process의 자식 Process가 된다. 이후에 A Process는 D Process, E Process의 Meta 정보를 회수하여 Zombie Process가 되는것을 방지하고 D Process, E Process를 제거한다. Container Process 관리 Container Process중에서 고아 Process 및 Zombie Process가 발생하면 어떻게 될까? [그림 6]은 PID Namespace까지 고려된 상태에서 고아 Process와 Zombie Process의 처리 과정을 나타내고 있다. Host PID Namespace에서의 고아 Process 및 Zombie Process의 처리 과정은 Host의 Init Process와 Host PID Namespace의 Init Process가 동일하다고 간주하면 쉽게 이해할 수 있다. B Process가 종료될 경우 고아 Process가 된 D Process와 E Process의 새로운 부모 Process는 Host PID Namespace의 Init Process인 A Process가 된다. D Process가 종료될 경우 A Process는 D Process의 Meta 정보를 회수하여 D Process가 Zombie Process가 되는것을 방지한다. 하지만 Container PID Namespace에서 고아 Process가 발생한다면 고아 Process의 새로운 부모 Process는 Host PID Namespace의 Init Process가 아닌 Container PID Namespace의 Init Process가 새로운 부모 Process가 된다. [그림 6]에서 F Process가 종료된 다음 고아 Process가된 G Process, H Process의 새로운 부모 Process는 C Process가 된다. 따라서 Container PID Namespace의 Init Process인 C Process도 Host PID Namespace의 Init Process인 A Process 처럼 wait() System Call을 호출하여 Zombie Process를 제거하는 역할을 수행해야 한다. C Process는 Container PID Namespace의 Init Process이기 때문에, 이후 C Process가 죽으면 Container PID Namespace의 G Process, H Process도 죽게된다. C Process가 존재하지 않기 때문에 A Process는 고아가된 G Process, H Process의 새로운 부모 Process가 된다. A Process는 G Process, H Process의 Meta 정보를 회수하여 G Process, H Process를 제거한다. 만약 C Process가 Zombie Process를 제거하는 역할을 수행하지 않아 F Process가 Zombie Process 상태였어도, C Process가 죽으면 A Process가 F Process의 새로운 부모 Process가 되기 때문에, A Process가 F Process의 Meta 정보를 회수하여 F Process는 제거된다. 즉 Container의 Zombie Process가 Container의 Init Process에 의해서 제거되지 않더라도, Container가 죽으면 Host의 systemd와 같은 Init Process에 의해서 Container의 Zombie Proces는 제거된다는 의미이다. Container의 Init Process로 많이 이용되는 supervisord, dumb-init, tini 같은 명령어들은 모두 Child Process의 Meta 정보를 회수하는 기능을 갖고 있기 때문에 Container의 Zombie Process를 예방하는 역할을 수행한다. # Init Process가 sleep infinity인 ubuntu Container를 Daemon으로 실행하고 exec을 통해서 ubuntu Container에 bash Process를 실행한다. (host)# docker run -d --rm --name ubuntu ubuntu sleep infinity (host)# docker exec -it ubuntu bash # ubuntu Container에서 bash Process를 부모 Process로 갖는 sleep 60 Process를 생성 (ubuntu)# bash -c "(bash -c 'sleep 60')" & (ubuntu)# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 13:33 ? 00:00:00 sleep infinity root 6 0 1 13:45 pts/0 00:00:00 bash root 15 6 0 13:46 pts/0 00:00:00 bash -c (bash -c 'sleep 60') root 16 15 0 13:46 pts/0 00:00:00 sleep 60 root 17 6 0 13:46 pts/0 00:00:00 ps -ef # sleep 60 Process의 부모 Process인 bash Process를 강제로 종료시킨다음 sleep 60 Process의 부모 Process를 확인 (ubuntu)# kill -9 15 (ubuntu)# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 13:33 ? 00:00:00 sleep infinity root 6 0 0 13:45 pts/0 00:00:00 bash root 16 1 0 13:46 pts/0 00:00:00 sleep 60 root 18 6 0 13:46 pts/0 00:00:00 ps -ef # 60초가 지난후에 sleep 60 process가 종료되면서 Zombie Process가 되는것을 확인 (ubuntu)# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 13:33 ? 00:00:00 sleep infinity root 6 0 0 13:45 pts/0 00:00:00 bash root 16 1 0 13:46 pts/0 00:00:00 [sleep] <defunct> root 19 6 0 13:47 pts/0 00:00:00 ps -ef [Shell 5] Container의 고아 Process, Zombie Process 확인 [Shell 5]는 Container에서 고아 Process와 Zombie Process를 확인하는 과정을 나타내고 있다. [Shell 5]에서 ubuntu Container의 Init Process는 sleep infinity Process로 설정하였다. 그 후 ubuntu Container에 bash Process를 생성하여 sleep 60 Process를 고아 Process로 만들었다. sleep 60 Process의 Parant는 Container의 Init Process인 sleep inifinity가 되는것을 확인할 수 있다. sleep infinity Process는 wait() System Call 호출하지 않기 때문에 죽은 Child Process의 Meta 정보를 회수하는 기능을 수행하지 못한다. 따라서 60초 후에 sleep 60 Process가 종료되면 sleep 60 Process는 Zombie Process가 된다. defunt는 Zombie Process가 되었다는걸 의미한다. # Host에서 ubuntu Container의 Zombie Process 확인 (host)# ps -ef root 12552 12526 0 22:33 ? 00:00:00 sleep infinity root 18319 12526 0 22:45 pts/0 00:00:00 bash root 18461 12552 0 22:46 pts/0 00:00:00 [sleep] <defunct> root 20908 28207 0 22:51 pts/24 00:00:00 ps -ef # ubuntu Container를 제거한 다음 ubuntu Container의 Zombie Process가 제거된것을 확인 (host)# docker rm -f ubuntu (host)# ps -ef root 22783 28207 0 22:55 pts/24 00:00:00 ps -ef [Shell 6] Container Zombie Process 확인 및 제거 [Shell 6]은 Host에서 ubuntu Container의 Zombie Process를 확인하고, ubuntu Container를 제거하여 Container의 Zombie Process도 제거된것을 확인하는 과정을 나타내고 있다. Host에서는 Container의 모든 Process를 볼수 있기 때문에 ubuntu Container의 Zombie Process는 Host Process에서도 확인할 수 있다. ubuntu Container를 제거한 다음 ubuntu Container의 Zombie Process도 제거된것을 확인할 수 있다.
https://ssup2.github.io/onebyone_container/3.1.PID_Namespace/
프로세스 식별자(PID)는 Linux 커널이 각 프로세스에 제공하는 고유한 식별자입니다.

컨테이너에는 호스트 시스템의 PID에 매핑되는 고유한 PID 세트가 있습니다.

💡
Linux 커널을 시작할 때 실행된 첫 번째 프로세스에는 PID 1이 할당이 됩니다.

일반적으로 정상적인 운영체제의 경우 이 프로세스는 init 시스템(예: systemd 또는 SysV)입니다.

💡
마찬가지로 컨테이너도 같은 맥락으로 실행된 첫 번째 프로세스는 PID 1을 얻습니다.

[문제 2] 기본 init 시스템의 고아 프로세스 관리

👨🏻‍💻
좀비 프로세스 : 프로세스가 종료 됐으나, 회수되지 않은 프로세스 고아 프로세스 : 아직 실행 중이지만, 부모가 죽은 프로세스

systemd와 같은 기본 init 시스템은 분리된 좀비 프로세스를 제거(회수)하는 데에도 사용됩니다.

상위 프로세스가 사라져 분리된 프로세스 즉 ,고아 프로세스PID 1이 있는 프로세스를 다시 부모 프로세스로 가지게 됩니다. PID 1은 프로세스가 사라질 때 다시 자원을 회수할 책임이 있습니다. 정상적인 init 시스템은 그렇게 작동합니다.

따라서 컨테이너에서도 PID 1을 갖고 있는 프로세스가 이러한 책임을 다해야만합니다. 이러한 제거를 제대로 처리하지 못하면 메모리나 다른 리소스가 부족해질 수 있습니다.

문제 해결 솔루션

솔루션 1: PID 1으로 실행하고 시그널 핸들러로 등록

이 방법은 첫번째 문제인 리눅스 커널의 신호 처리 문제만을 해결할 수 있습니다.

다만, 앱이 제어를 해서 하위 프로세스를 생성하면 두 번째 문제를 방지할 수 있습니다.

이 솔루션을 구현하는 가장 쉬운 방법은 Dockerfile에서 CMD 또는 ENTRYPOINT 안내를 사용하여 프로세스를 실행하는 것입니다.

예를 들어 다음 Dockerfile에서 nginx는 실행할 수 있는 최초이자 유일한 프로세스입니다.

FROM debian:11

RUN apt-get update && \
    apt-get install -y nginx

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]

다만, 때로는 프로세스가 제대로 실행될 수 있도록 컨테이너에서 환경을 준비해야 할 수 있습니다. 이 경우 컨테이너를 시작할 때 셸 스크립트를 실행하는 것이 가장 좋습니다. 이 셸 스크립트는 환경을 준비하고 기본 프로세스를 실행하는 작업을 담당합니다.

하지만 이 방법을 사용하는 경우 셸 스크립트는 프로세스가 아닌 PID 1을 가지므로 기본 exec 명령어를 사용하여 셸 스크립트에서 프로세스를 실행해야 합니다. exec 명령어로 스크립트를 원하는 프로그램으로 바꿉니다. 그 다음 프로세스에서 PID 1을 상속합니다

솔루션 2: 특수한 init 시스템 사용

기본적인 Linux 환경에서와 마찬가지로 init 시스템을 사용하여 이러한 문제를 처리할 수도 있습니다.

👨🏻‍💻
하지만 systemd 또는 SysV 등의 일반 init 시스템은 단지 이 용도로 사용하기에는 너무 복잡하고 크기 때문에 컨테이너용으로 특별히 제작된 tini와 같은 init 시스템을 사용하는 것이 좋습니다.

특수한 init 시스템을 사용하는 경우 init 프로세스PID 1을 가지며 다음과 같은 역할을 수행합니다.

  • 올바른 신호 핸들러를 등록합니다.
  • 앱에서 신호가 작동하는지 확인합니다.
  • 최종 모든 좀비 프로세스를 수거(reap)합니다.
👨🏻‍💻
docker run 명령어의 --init 옵션을 사용하면 Docker 자체에서 이 솔루션을 사용할 수 있습니다.

그외의 솔루션으로 쿠버네티스의 프로세스 네임스페이스 공유 사용 설정 등의 방법이 있습니다.

https://github.com/krallin/tini
💡
저는 권장사항에서 나온 tini를 사용해서 init시스템을 설정해줬습니다.

또는 dumb-init 을 사용하는 방법도 존재합니다.

Introducing dumb-init, an init system for Docker containers
Introducing dumb-init, an init system for Docker containers Chris K., Software Engineer Jan 6, 2016 At Yelp we use Docker containers everywhere: we run tests in them, build tools around...
https://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html
https://github.com/Yelp/dumb-init

컨테이너 실행에 대한 권장사항

💡
이 파트의 내용은 한번 읽어보는 느낌으로 가시면 좋습니다.

컨테이너를 관리하는 로깅, 모니터링

  • 컨테이너의 로그를 관리하는 것은 어플리케이션 관리의 필수적입니다. 따라서 권장되는 사항으로는 컨테이너의 로그를 다른 파일시스템에 저장하는 것을 권장합니다.
  • 다만 저희 개발 단계에서는 의미가 있다고 생각했으나 현재 과제와는 맞지 않다고 생각해 굳이 설정을 해주지는 않았습니다.

모니터링

로깅과 마찬가지로 모니터링은 애플리케이션 관리의 필수적인 부분입니다.

컨테이너화된 애플리케이션 모니터링은 많은 측면에서 컨테이너화되지 않은 애플리케이션 모니터링에 적용되는 것과 동일한 원칙에 따릅니다.

그러나 컨테이너화된 인프라는 일반적으로 매우 동적이고 컨테이너가 자주 생성 또는 삭제되므로 매번 모니터링 시스템을 다시 구성하기는 어렵습니다.

모니터링은 크게 블랙박스 모니터링과 화이트박스 모니터링, 두 가지로 분류됩니다.

블랙박스 모니터링은 최종 사용자처럼 외부에서 애플리케이션을 살펴보는 것을 나타냅니다. 블랙박스 모니터링은 제공하고자 하는 최종 서비스가 사용 가능하고 작동하는 경우 유용합니다. 블랙박스 모니터링은 인프라 외부에서 작동하므로 기존 인프라와 컨테이너화된 인프라를 구분하지 않습니다.

화이트박스 모니터링은 일종의 높은 권한 액세스를 사용하여 애플리케이션을 살펴보고 최종 사용자가 볼 수 없는 애플리케이션 행동에 대한 측정항목을 수집하는 것을 나타냅니다. 화이트박스 모니터링은 인프라의 가장 깊은 레이어를 살펴봐야 하므로 기존 인프라와 컨테이너화된 인프라의 차이가 큽니다. 주로 쿠버네티스에서 많이 사용됩니다.

보안문제 권장사항

  • 컨테이너에게 권한을 너무 많이 주지 않는 것을 권장합니다.
  • 너무 높은 권한을 가진 컨테이너는 호스트 머신의 모든 디바이스에 엑세스 할 수가 있게 되는데 이것은 보안적으로 상당히 위험한 행동입니다.
  • 컨테이너 내에서 루트로 프로세스를 실행하지 않는 것을 추천합니다. 왜냐하면 도커와 리눅스 커널 자체에서 취약점이 있어서 컨테이너 밖으로 나가서 로컬으로 접속하는 사고가 발생할 수 있기 때문입니다.
  • 하지만 루트로 실행하지 않는 것은 현실적으로 어렵습니다. 왜냐하면 유명 소프트웨어 패키의 상당수가 루트로 프로세스를 실행하기 때문입니다.

이미지 버전 권장사항

FROM debian:latest 보다는 FROM debian:11.6 이렇게 특정 버전을 정확하게 지정해주는 것이 두가지 버전이 나오지 않는 방법입니다.


Uploaded by N2T

728x90