본문 바로가기
IT/Kubernetes

[Kubernetes] Internals - Pause Container

by 물통꿀꿀이 2019. 5. 16.

이번 포스팅에서는 Pod 내부의 Pause Container에 대해 알아보려고 한다.


그림 1. Node Overview


그림 1을 살펴보면 Node에 여러 구성 요소들이 존재하는 것을 확인 할 수 있다. 특히 그림 1에서 kubelet과 Docker를 주목해보자.

Node가 생성될 때 기본 구성처럼 kubelet이 만들어지는데 이는 kubelet이 Node에서 Pods 및 Containers 관리를 하기 때문이다. 추가로 볼 수 있는 Docker는 k8s에서 기본적으로 사용하는 Container Runtime이다. (rkt 와 같은 다른 Container Runtime이 사용될 수 있다.)


그럼 여기서는 Docker를 기본 Container Runtime으로 가정하고 Pods가 실행할 때 내부에서 어떤 작업들이 일어나는지 알아보자. (외부 자료를 바탕으로 알아보겠다.)

apiVersion: v1

kind: Pod

metadata:

  name: myapp-pod

  labels:

    app: myapp

spec:

  containers:

  - name: myapp-container

    image: busybox

    ... 

Pods는 위의 매니페스트 파일처럼 Container의 Spec 및 개수를 결정할 수 있다. 그리고 실행하면 항상 그렇듯이 Pod 및 Container가 동작하는 것을 확인 할 수 있다.

그런데 실제 Node에서 Container의 개수를 확인하면 매니페스트 파일에 정의되어 있는 개수만큼 동작하고 있지 않다.


Pause Container

그림 2. Container list on Node


Node에서 Container 목록을 확인 하기 위해 "docker ps" 명령어를 입력하면 그림 2와 같이 여러 개의 Container가 동작하고 있는 것을 확인 할 수 있다. (그림 2에서는 사용자가 매니페스트 파일을 통해 직접 만든 Container를 제외하였다.) 또한 좀 더 면밀히 살펴보면 메니페스트 파일에서 만든적이 없는 "/pause" COMMAND를 사용하는 Container가 여럿있다.


바로 Pause Container가 모든 Container의 Parent Container의 역할을 한다. (Pods에 필수적인 요소이다.) 아래에서 좀 더 자세히 알아보자.

(들어가기에 앞서 Pods의 등장 배경을 아는 것이 Pause Container를 좀 더 이해하기 쉽다.)

Docker를 통해 생성된 Container는 말 그대로 격리된 공간에서 리소스 사용을 할 수 있고 application의 운영 및 관리를 쉽게 해준다. 그런데 적은 개수의 Container를 관리하기는 쉬운 반면에 점점 개수가 많아지면서 복잡성이 점점 증가하게 되었다. 때문에 개발자들은 부분적으로 환경을 공유할 수 있는 Container의 그룹을 만드는 것이 보다 유용하다는 것을 알게 되었다.

이러한 배경을 이유로 등장한 것이 Pod로 Container의 추상화된 개념이다. 실제로 Docker Container를 만들때 여러 옵션들이 필요하고 (Volume 등을 추가할 때), k8s는 Docker 뿐 만아니라 rkt와 같은 다른 Container Runtime을 선택할 수도 있기 때문에 사용자들이 동일한 인터페이스를 통해 사용 할 수 있도록 Clean Abstraction인 Pod가 등장했다.


다시 돌아가서, Pause Container는 Pod에 속해있는 모든 Container의 Parent Container로 역할은 크게 2가지로 구분된다.

1) Linux namespace sharing in Pod

리눅스에서는 일반적으로 프로세스가 생성될 때, Parent Process로 부터 namespace를 상속받는다. (물론, 새로운 Process가 Parent Process와 namespace를 끊으면 새로운 namespace를 만들 수 있다.) 때문에 새로운 Process가 추가되었을 때에도 동일한 namespace를 가질 수 있다. 


이와 같은 방식으로 Pod를 구성하는 Process의 namespace에 다른 Process를 추가 할 수 있다. 즉, Pod에 있는 모든 Container는 서로 간에 namespace를 공유할 수 있다.

이 과정을 Docker에서 (약간)자동화할 수 있다. 아래에서 예시를 통해 확인해보자.

docker run -d --name pause -p 8080:80 gcr.io/google_containers/pause-amd64:3.0

먼저 Docker로 pause 이름을 갖는 Container를 생성한다.


docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf --net=container:pause --ipc=container:pause --pid=container:pause nginx

docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost

그리고 2개의 Container(nginx, ghost)를 추가햔다. 다만, 추가 Container의 net, ipc, pid는 모두 Parent Container인 pause 값을 상속한다.

이런식으로 구성을 하면서 각 Container는 namespace를 공유하고 Pod 안에 있는 Container 간의 통신은 localhost로 가능해진다.

그림 3. Shared Container


그림 3을 통해 전체 Overview를 확인 할 수 있다. 물론 각각 복잡하지만 k8s에서는 이 모든 작업을 자동화해주기 때문에 Pod 안에 있는 모든 Container는 서로 간에 namespace를 공유하고 있다.


2) Reaping Zombies

일반적으로 Zombie 또는 Defunct Process는 실행이 완료되었지만 아직 Process Table에 목록에 남아 있는 Process(자원을 사용한채 Process가 종료되지 않음)를 의미한다.

Container 경우에는 Process는 init Process로 각 PID namespace를 가지고 있다. 때문에 Docker에서 Container를 생성할 때마다 각 Container는 자신만의 PID namespace를 가지고 있다. 

Pod로 넓혀서 보면, Pod에 속하는 모든 Container 중에 하나가 init Process 역할을 하고 나머지는 init Process로 부터 namespace를 상속받는다. 즉, 나머지 Container는 Child Container가 된다.


예를 들어 아래와 같이 보자

docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf -p 8080:80 nginx docker run -d --name ghost --net=container:nginx --ipc=container:nginx --pid=container:nginx ghost

위에서 nginx는 init Process의 역할을 하고 ghost는 nginx의 Child Process의 역할을 한다. 그렇지만 nginx는 Zombie Process를 수거 할 수 있는 기능을 수행 할 수 없다.

의미적으로는 init Process의 역할을 수행하긴 하지만 실질적인 init Process의 역할을 하지 못한다. (다시 말해 Zombie Process를 수거 할 수가 없다.)


때문에 k8s Pod는 Pause Container에서 init Process 역할을 대신 수행한다. 

/#include <signal.h>

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/wait.h>

#include <unistd.h>


static void sigdown(int signo) {

  psignal(signo, "Shutting down, got signal");

  exit(0);

}


static void sigreap(int signo) {

  while (waitpid(-1, NULL, WNOHANG) > 0);

}


int main() {

  if (getpid() != 1)

    /* Not an error because pause sees use outside of infra containers. */

    fprintf(stderr, "Warning: pause should be the first process\n");


  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)

    return 1;

  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)

    return 2;

  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,

                                             .sa_flags = SA_NOCLDSTOP},

                NULL) < 0)

    return 3;


  for (;;)

    pause();

  fprintf(stderr, "Error: infinite loop terminated\n");

  return 42;


위의 코드가 실제 k8s에서 동작하는 Pause Container의 실행 코드이다. init Process의 역할로써 Child Process가 Zombie가 되었을 때 wait signal을 호출하여 Zombie Process를 수거한다.


그림 4. Network Namespace


전체 내용을 바탕으로 그림 4를 확인해보면, 각 Pod의 Container 및 Pause 구성이 Namespace에 의해 묶여 있고 이를 바탕으로 Networking도 마찬가지로 동작한다는 것을 확인 할 수 있다. (모든 것은 init Process인 Pause Container의 환경 값을 공유하기 때문) 


Reference

https://kubernetes.io/docs/tutorials/kubernetes-basics/explore/explore-intro/

https://www.ianlewis.org/en/almighty-pause-container

https://www.slideshare.net/ZvikaGazit/kubernetes-networking-in-aws

'IT > Kubernetes' 카테고리의 다른 글

Docker, Pod 인자 전달 비교  (0) 2019.05.27
[Kubernetes] ConfigMap  (0) 2019.05.27
[Kubernetes] Internals - Init Pod  (0) 2019.05.16
[Kubernetes] Namespace  (0) 2019.05.14
[Kubernetes] Networking - Pods  (0) 2019.04.27

댓글