본문 바로가기

Server Infra/Kubernetes

Pause Container를 분석해보자.

728x90

개요

Kubernetes를 사용하여 Pod를 구성하고 Node에서 Docker Image를 찾다 보면 아래와 같이 YAML이나 Dockerfile에 정의하지 않은 Container가 실행중인것을 확인할 수 있습니다.

CONTAINER ID   IMAGE                  COMMAND                 CREATED
c917a6f3c3f7   nginx                  "nginx -g 'daemon off"  4 seconds ago
98b8bf797174   gcr.io/.../pause:3.0   "/pause"                7 seconds ago

 

이번 게시글에서는 Nginx보다 3초 전에 생성된 pause:3.0 Container가 무엇인지 어떤 역할을 하는지 알아보고자 합니다.

Pause Container

Pause Container는 Pod의 모든 Container를 관리할수 있는 Container입니다. Pod는 모든 Container가 동일한 Network와 Linux의 Namespace를 공유하고 있습니다. Pause Container는 이러한 모든 Namespace를 공유받아 관리하는 것이 목적인 Continaer입니다. Pod의 사용자가 정의한 Container(예: Nginx Conatiner)는 Pod의 infrastructure container의 Namespace를 사용하여 관리합니다.

Kubernetes in Action

그러면 Pasue Container가 다른 Container를 어떻게 관리할까요? 프로세스를 어떻게 처리할까요? 이는 Pause Container의 Code를 분석해 보면 됩니다.

Pause Container 구성

Pasuse Container의 Dockerfile 구조는 아래와 같습니다.

# 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.

ARG BASE
FROM ${BASE}
ARG ARCH
ADD bin/pause-linux-${ARCH} /pause
USER 65535:65535
ENTRYPOINT ["/pause"]

 

굉장히 단순하게 구성되어 있습니다. 빌드된 결과물인 bin/내의 바이너리는 Makefile 에 의해 생성됩니다. Linux와 Windows 플랫폼에 따라 별도로 빌드되는 것을 확인할 수있습니다. 그러면 바이너리릐 코드를 분석해 보도록 하겠습니다. 이 게시글에서는 linux 플랫폼을 기준으로 코드를 분석해 보도록 하겠습니다.

orphan.c

/* 좀비 프로세스를 생성하여 init에 의해 정리되도록 하는 프로그램. */

#include <stdio.h>
#include <unistd.h>  // fork, getpid, getppid 함수를 사용하기 위한 헤더

int main() {
    pid_t pid;  // 프로세스 ID를 저장할 변수

    pid = fork();  // 현재 프로세스를 복제하여 자식 프로세스 생성

    if (pid == 0) {  // 자식 프로세스인 경우
        // 부모 프로세스가 종료될 때까지 대기
        while (getppid() > 1)
            ;
        // 부모 프로세스가 종료되면 자신의 정보를 출력하고 종료
        printf("Child exiting: pid=%d ppid=%d\n", getpid(), getppid());
        return 0;
    } else if (pid > 0) {  // 부모 프로세스인 경우
        // 자신의 정보를 출력하고 즉시 종료
        printf("Parent exiting: pid=%d ppid=%d\n", getpid(), getppid());
        return 0;
    }

    // fork 실패 시 오류 메시지 출력
    perror("Could not create child");
    return 1;
}

Pause Container를 위한 것으로, 좀비 프로세스를 생성하는 프로그램입니다. 주요 기능과 동작 방식은 다음과 같습니다.

  1. 프로그램은 fork()를 사용하여 자식 프로세스를 생성합니다.
  2. 자식 프로세스 (pid == 0):
    • 부모 프로세스가 종료될 때까지 대기합니다 (while (getppid() > 1)).
    • 부모가 종료되면 자신의 PID와 새로운 PPID(보통 1, init 프로세스)를 출력하고 종료합니다.
  3. 부모 프로세스 (pid > 0):
    • 자신의 PID와 PPID를 출력하고 즉시 종료합니다.
  4. 이 과정을 통해 자식 프로세스는 부모가 종료된 후에도 계속 실행되다가 종료되어 좀비 프로세스가 됩니다.
  5. 좀비 프로세스는 나중에 init 프로세스에 의해 정리됩니다.

위 코드를 통해서 Pause Container는 잔류하는 좀비 프로세스를 제거합니다.

pause.c

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)

#ifndef VERSION
#define VERSION HEAD
#endif

// SIGINT 또는 SIGTERM 시그널 처리 함수
static void sigdown(int signo) {
    psignal(signo, "Shutting down, got signal");
    exit(0);
}

// SIGCHLD 시그널 처리 함수 (좀비 프로세스 정리)
static void sigreap(int signo) {
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

int main(int argc, char **argv) {
    int i;

    // 버전 정보 출력 옵션 처리
    for (i = 1; i < argc; ++i) {
        if (!strcasecmp(argv[i], "-v")) {
            printf("pause.c %s\n", VERSION_STRING(VERSION));
            return 0;
        }
    }

    // PID 1 검사 (컨테이너의 init 프로세스여야 함)
    if (getpid() != 1)
        fprintf(stderr, "Warning: pause should be the first process\n");

    // SIGINT 시그널 핸들러 등록
    if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
        return 1;

    // SIGTERM 시그널 핸들러 등록
    if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
        return 2;

    // SIGCHLD 시그널 핸들러 등록 (자식 프로세스 종료 처리)
    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 container의 핵심적인 역할을 수행합니다. 주요 기능과 동작 방식은 다음과 같습니다.

  1. 초기화 프로세스 역할:
    • PID 1로 실행되어야 함을 권장합니다 (컨테이너의 init 프로세스 역할).
  2. 시그널 처리:
    • SIGINT와 SIGTERM: sigdown 함수로 처리하며, 프로그램을 종료합니다.
    • SIGCHLD: sigreap 함수로 처리하며, 좀비 프로세스를 정리합니다.
  3. 무한 대기:
    • pause() 함수를 무한 루프로 실행하여 프로세스를 계속 실행 상태로 유지합니다.

이 pause는 아래의 주요 목적을 가지고 코드를 실행합니다.

  • 컨테이너 내에서 init 프로세스 역할을 수행합니다.
  • 자식 프로세스들의 좀비 상태를 방지하고 정리합니다.
  • 시그널을 적절히 처리하여 컨테이너의 정상적인 종료를 보장합니다.
  • 최소한의 리소스를 사용하면서 컨테이너를 계속 실행 상태로 유지합니다.

Pause Container 주입

지금까지 pause가 어떻게 생기고 어떤 바이너리로 pod의 리소스를 관리하는지 알아 보았습니다. 결과적으로 namspace로 공유받은 pid와 network를 통하여 함께 동작하는 conatiner를 관리하였는데요. 그렇다면 pod를 생성할때 yaml에서 별도로 선언하지 않았는데 pause container가 뜨는 이유는 뭘까요? 어디서 주입하는걸까요?

https://kubernetes.io/blog/2017/11/containerd-container-runtime-options-kubernetes/

 

Pod는 대략적으로 위의 그림처럼 구동됩니다. 우선 kubelet이 master node와 통신하며 정보를 받아 worker node의 CRI와 통신하여 컨테이너를 구동 시킵니다.

 

여기서 저 sandbox가 중요합니다. 만약 kubectl run pod 형태로 뭔가를 생성하면 사실 제일 먼저 sandbox를 통해 container infra setting이 우선 수행됩니다. 이를 infra container라고 부릅니다.

https://github.com/containerd/containerd/blob/26b48a6b7aa7ff7886307aa22a3d1a75b6f5d248/internal/cri/server/sandbox_run.go#L52 코드에서 자세히 나옵니다.

 

containerd/internal/cri/server/sandbox_run.go at 26b48a6b7aa7ff7886307aa22a3d1a75b6f5d248 · containerd/containerd

An open and reliable container runtime. Contribute to containerd/containerd development by creating an account on GitHub.

github.com

위 함수 에서는 아래의 실행 순서를 가지고 있습니다.

  1. 샌드박스 ID 및 이름 생성 // 고유한 ID 생성 및 이름 예약
  2. 런타임 옵션 설정 // OCI 런타임 및 옵션 설정
  3. 내부 샌드박스 객체 생성 // 메타데이터 및 상태 정보로 샌드박스 객체 초기화
  4. 네트워크 네임스페이스 설정 // 호스트 네트워킹이 아닌 경우 네트워크 네임스페이스 생성 // 사용자 네임스페이스 활성화 여부에 따라 처리 방식 다름
  5. 샌드박스 생성 및 시작 // c.sandboxService.CreateSandbox() 및 StartSandbox() 호출
  6. 네트워크 설정 (사용자 네임스페이스 활성화된 경우) // OCI 런타임이 생성한 네트워크 네임스페이스 사용 // setupPodNetwork() 호출하여 네트워크 설정
  7. NRI (Node Resource Interface) 처리 // NRI RunPodSandbox 호출
  8. 샌드박스 상태 업데이트 // 상태를 'Ready'로 설정
  9. 샌드박스 스토어에 추가 // 생성된 샌드박스를 스토어에 저장
  10. 이벤트 생성 및 전송 // CONTAINER_CREATED 및 CONTAINER_STARTED 이벤트 전송
  11. 샌드박스 모니터링 시작 // 백그라운드에서 샌드박스 종료 모니터링 시작

이 함수는 Kubernetes CRI (Container Runtime Interface)의 RunPodSandbox 구현으로, 포드의 샌드박스 환경을 설정하는 복잡한 프로세스를 다루고 있습니다. 근데 잠깐! 여기서도 pause container는 보이지 않습니다. 그렇다면 kublet에서 containerD로 던지는 걸까요?

 

우선 kubeadm을 살펴보았습니다. 살펴보니 뭔가 인자값을 주입하는 코드가 보입니다.

func buildKubeletArgsCommon(opts kubeletFlagsOpts) []kubeadmapi.Arg {
	kubeletFlags := []kubeadmapi.Arg{}
	kubeletFlags = append(kubeletFlags, kubeadmapi.Arg{Name: "container-runtime-endpoint", Value: opts.nodeRegOpts.CRISocket})

	// This flag passes the pod infra container image (e.g. "pause" image) to the kubelet
	// and prevents its garbage collection
	if opts.pauseImage != "" {
		kubeletFlags = append(kubeletFlags, kubeadmapi.Arg{Name: "pod-infra-container-image", Value: opts.pauseImage})
	}

	if opts.registerTaintsUsingFlags && opts.nodeRegOpts.Taints != nil && len(opts.nodeRegOpts.Taints) > 0 {
		taintStrs := []string{}
		for _, taint := range opts.nodeRegOpts.Taints {
			taintStrs = append(taintStrs, taint.ToString())
		}
		kubeletFlags = append(kubeletFlags, kubeadmapi.Arg{Name: "register-with-taints", Value: strings.Join(taintStrs, ",")})
	}

	// Pass the "--hostname-override" flag to the kubelet only if it's different from the hostname
	nodeName, hostname, err := GetNodeNameAndHostname(opts.nodeRegOpts)
	if err != nil {
		klog.Warning(err)
	}
	if nodeName != hostname {
		klog.V(1).Infof("setting kubelet hostname-override to %q", nodeName)
		kubeletFlags = append(kubeletFlags, kubeadmapi.Arg{Name: "hostname-override", Value: nodeName})
	}

	return kubeletFlags
}

 

여기서  kubelet에 --pod-infra-container-image 플래그를 추가합니다. 이 플래그는 kubelet에게 어떤 이미지를 pause 컨테이너로 사용할지 알려줍니다. 여기서 확실한 것은 이 설정으로 kubelet이 pod를 실행할때 Sandbox를 통해 고유 자원을 할당받고 paus conatiner를 실행 시킨다는 것을 알았습니다.

 

그러면 kubelet의 코드를 한번 봐보겠습니다.(https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kuberuntime/kuberuntime_sandbox.go)

func (m *kubeGenericRuntimeManager) createPodSandbox(ctx context.Context, pod *v1.Pod, attempt uint32) (string, string, error) {
	podSandboxConfig, err := m.generatePodSandboxConfig(pod, attempt)
	if err != nil {
		message := fmt.Sprintf("Failed to generate sandbox config for pod %q: %v", format.Pod(pod), err)
		klog.ErrorS(err, "Failed to generate sandbox config for pod", "pod", klog.KObj(pod))
		return "", message, err
	}

	// Create pod logs directory
	err = m.osInterface.MkdirAll(podSandboxConfig.LogDirectory, 0755)
	if err != nil {
		message := fmt.Sprintf("Failed to create log directory for pod %q: %v", format.Pod(pod), err)
		klog.ErrorS(err, "Failed to create log directory for pod", "pod", klog.KObj(pod))
		return "", message, err
	}

	runtimeHandler := ""
	if m.runtimeClassManager != nil {
		runtimeHandler, err = m.runtimeClassManager.LookupRuntimeHandler(pod.Spec.RuntimeClassName)
		if err != nil {
			message := fmt.Sprintf("Failed to create sandbox for pod %q: %v", format.Pod(pod), err)
			return "", message, err
		}
		if runtimeHandler != "" {
			klog.V(2).InfoS("Running pod with runtime handler", "pod", klog.KObj(pod), "runtimeHandler", runtimeHandler)
		}
	}

	podSandBoxID, err := m.runtimeService.RunPodSandbox(ctx, podSandboxConfig, runtimeHandler)
	if err != nil {
		message := fmt.Sprintf("Failed to create sandbox for pod %q: %v", format.Pod(pod), err)
		klog.ErrorS(err, "Failed to create sandbox for pod", "pod", klog.KObj(pod))
		return "", message, err
	}

	return podSandBoxID, "", nil
}

 

위 코드는 Pod Sandbox를 생성하는 함수입니다.

  1. Pod Sandbox 설정 생성: generatePodSandboxConfig 함수를 호출하여 Pod Sandbox 설정을 생성합니다.
  2. 로그 디렉토리 생성: Pod의 로그를 저장할 디렉토리를 생성합니다.
  3. 런타임 핸들러 결정: Pod의 RuntimeClassName에 따라 적절한 런타임 핸들러를 결정합니다.
  4. Pod Sandbox 실행: m.runtimeService.RunPodSandbox를 호출하여 실제로 Pod Sandbox를 생성하고 실행합니다.

Pod Sandbox의 생성 과정을 관리합니다. Pod Sandbox는 Pod 내의 모든 컨테이너들이 공유하는 환경을 제공하며, 일반적으로 pause 컨테이너를 포함합니다. 이 상단의 인터페이스에서 위의 옵션을 받아 SandBox 실행시 pause container를 실행한다는 것을 알 수 있습니다.

 

그럼 실제 호출 부를 찾아보겠습니다. 우선 kubelet 서버가 동작할때 아래처럼 위에서 본 파라미터로 함게 세팅되며 실행됩니다.

			if cleanFlagSet.Changed("pod-infra-container-image") {
				klog.InfoS("--pod-infra-container-image will not be pruned by the image garbage collector in kubelet and should also be set in the remote runtime")
				_ = cmd.Flags().MarkDeprecated("pod-infra-container-image", "--pod-infra-container-image will be removed in 1.35. Image garbage collector will get sandbox image information from CRI.")
			}

 

자 그렇다면 kubelet이 pod-infra-container-image라는 변수 또는 설정에 pause container의 이미지 정보를 지니고 있을겁니다. 만약 pod 생성 호출이 오면 우선 sandbox 구성을 만들어라 라는 것을 kubelet에서 containerD로 호출합니다.

 

snadbox가 다 만들어지면 kubelet은 pause conatier를 실행시키고 함께 사용자 정의 conatiner를 실행시킵니다.

 

이상 코드로 보는 pause container였습니다 :)

728x90