본문 바로가기
  • 오늘처럼
소프트웨어 아키텍처/Kubernetes

Kubernetes Pod Infra container (pause container)

by bluefriday 2023. 1. 10.
반응형

Container 와 Pod

쿠버네티스로 클러스터링 된 노드에서, docker 나 contained 의 명령어를 이용하여 컨테이너를 조회하면 파드에 대한 컨테이너외에 추가 다음과 같은 컨테이너들이 존재함을 알 수 있다.

[root@localhost]# ctr --namespace k8s.io containers list
CONTAINER          IMAGE                                  RUNTIME                  
b17bab258...b27    nginx:v1.11.2                          io.containerd.runc.v2    
c4e93ac4f...0e3    calico-kube-controllers:latest         io.containerd.runc.v2    
cc775ac0a...b8e    calico-node:latest                     io.containerd.runc.v2    
...
ea76c4e66...781    k8s.gcr.io/pause:3.6                   io.containerd.runc.v2    
eadc02066...8c8    k8s.gcr.io/pause:3.6                   io.containerd.runc.v2    
f4e2793c1...dcd    k8s.gcr.io/pause:3.6                   io.containerd.runc.v2    
fd2fb45f5...ee8    k8s.gcr.io/pause:3.6                   io.containerd.runc.v2    
[root@localhost]#

상단 명령어의 결과로 보이는 컨테이너들 중 일부는 k8s.gcr.io/pause:3.6 이란 이미지를 공통으로 사용하고 있다. 이 컨테이너들은 Pod Infra container 라고 불린다.

쿠버네티스는 워커 노드에, 쿠버네티스 배포의 기본 단위인 '파드(pods)' 를 배포하기 위해 Docker나 Containerd 와 같은 Container Runtime 을 사용한다. 여기서 파드는 하나 이상의 컨테이너의 모음인데 이를 바꿔 말하면 컨테이너를 조합하여 파드를 구성한다고도 표현할 수 있다. 

운영 환경 등의 여러 경우에서 프로세스들은 하나의 단일 프로세스가 단일 자원을 사용하는 것이 아니라, 어느 정도의 네트워크/스토리지 자원을 프로세스들이 공유하는 것이, 구성에 유리한 경우가 많다. 하지만 컨테이너는 기본적으로 모든 자원이 격리된(isolated) 어플리케이션의 실행 환경을 제공해준다.

이에 컨테이너를 사용하면서 프로세스 간의 네트워크/스토리지 공유를 위해, 쿠버네티스는 파드라는 추상화 된 디자인을 제공한다. 하나 이상의 컨테이너들이 외부적으로 네트워크 자원과 볼륨을 공유하게 하고, 이러한 설정 및 컨테이너 그룹을 감싼 파드라는 단위를 배포의 기본 단위로 사용함으로써, 사용자들이 파드 내부 컨테이너 간의 공유 설정을 고려하지 않아도 되게 하며, 나아가 containerd, rkt 등의 컨테이너 런타임의 차이 또한 가려준다.


Pod Infra container

이러한 추상화를 위해 인프라 컨테이너가 필요하다. 인프라 컨테이너는, 파드 안에 있는 다른 모든 컨테이너의 부모 컨테이너(parent container) 역할을 하며 다음의 두 가지 주요 기능을 수행한다.

 

1) Pod 내부에서의 Linux Namespace 공유

쿠버네티스가 컨테이너 런타임을 이용하여 파드를 생성하는 과정을, docker 명령어 레벨에서 구현해 볼 수 있다. 먼저 인프라 컨테이너를 생성한다. 

docker run -d --name infra-container -p 8080:80 k8s.gcr.io/pause:3.6

그리고 2개의 nginx 컨테이너를 이 인프라 컨테이너에 네트워크 적으로 연결한다.

docker run -d --name nginx1 \
  --net=container:infra-container --ipc=container:infra-container nginx:latest
  
 docker run -d --name nginx2 \
  --net=container:infra-container --ipc=container:infra-container nginx:latest

이렇게 되면 논리적으로 하나의 파드가 완성 된다. nginx1, nginx2 컨테이너는 infra container 와 네트워크 네임스페이스를 공유하기에 3개의 컨테이너가 서로 같은 네트워크를 공유하게 된다. 물론 실제 파드의 구성에서는 docker 명령어가 아닌, setns()와 같은 linux 명령어로 위와 같이 구현되어 있지만, 사용자들에게는 추상화 되어 보이지 않는다.

 

2) 좀비 프로세스에 대한 처리

이번에는 프로세스 아이디 (PID) 의 측면에서 인프라 컨테이너의 역할을 확인해본다. 컨테이너는 내부적으로 하나의 프로세스를 가지고 있으며, 이 프로세스는 PID 1 즉, Init 프로세스로 동작하게 된다. 그런데 위에서 설명한 부분처럼 네트워크 적으로 공유하기 위해 2개 이상의 컨테이너를 묶어서 논리적인 단위인 파드를 만들게 되면, 파드 안에는 2개 이상의 1번 PID 가 존재하게 된다. 위 예제에서는 '--net' 플래그를 사용하여 네트워크 네임스페이스를 공유하였는데, 여기에서는 '--pid' 플래그를 추가로 사용하여 하나의 컨테이너의 프로세스가 다른 컨테이너의 프로세스의 자식 프로세스가 되게 수정한다.

docker run -d --name nginx1 \
  nginx:latest
  
docker run -d --name nginx2 \
  --net=container:nginx1 --ipc=container:nginx1 --pid=container:nginx1 \ 
  nginx:latest

위 방식에서는 nginx2 컨테이너가 nginx1 컨테이너의 자식 프로세스가 되게 설정하였다. 이렇게 되면 파드 안에서 논리적으로 하나의 PID 1 프로세스가 nginx1 에 생성된다. nginx1 컨테이너의 프로세스가 nginx2 컨테이너의 부모 프로세스가 되므로, 자식 프로세스인 nginx2 의 자원을 회수해야 하는 책임 또한 nginx1 컨테이너의 프로세스에 있다. 그런데 nginx1 또한 처음부터, nginx2 를 자식 프로세스로 관리하고 자원을 회수하게 설계되지 않았기 때문에, 프로세스 충돌 등의 상황이 발생하면, 자원이 정상적으로 회수 되지 않아 좀비 프로세스가 발생하게 된다.

인프라 컨테이너는 이러한 좀비 프로세스를 회수하는 역할을 한다. 하단의 코드를 살펴보자.

/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#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;
}

인프라 컨테이너 내부에서 동작하는 짧은 프로그램이다. pause()를 수행하고 계속해서 잠자고 있다가, SIGINT 나 SIGTERM 을 수신할 때까지 대기했다가 좀비 프로세스를 삭제한다. 이렇게 하면 파드에 좀비 프로세스가 쌓이지 않게 된다.

 

*출처

댓글