오늘은 쿠버네티스의 가장 작은 단위인 Pod의 생명주기를 이해하며 클러스터에서 애플리케이션이 어떻게 동작하고 관리되는지  이해해보려 합니다.


 

Pod는 여러 상태를 갖고 있습니다. 사실 Pod의 상태변화에 대한 일반적인 관심은 보통 '왜 장애가 났는가'로 귀결되고는 합니다. 이런 경우 kubectl describe 명령을 통해 원인에 대해 파악하려는 시도를 합니다. 하지만 왜 장애가 났는 지 이전에 어떤 과정으로 장애가 발생하는 지 이해한다면 장애지점에 대해 유추할 때 보다 넓은 시야를 가질 수 있을 것이라 생각합니다.

 

Pod의 상태

Pod는 생명주기 중 딱 한번만 스케줄링됩니다. Pod가 특정한 Node에 스케줄링되고 나면, 해당 Pod는 중지되거나 종료될 때까지 스케줄링되지 않고 해당 Node에서 실행됩니다. 만약 Pod가 실행 중인 Node가 죽거나 하는 상황이 되면, 똑같은 녀석이 다른 노드로 옮겨가는 것이 아니라 새로운 Pod가 생성되어 기존의 Pod를 대체하는 시스템입니다. 그럼 이제 Pod의 한번 뿐인 라이프사이클이 어떻게 구성되어있는 지 알아보겠습니다.

Pod는 크게 다섯 가지의 단계를 갖고 있습니다. 

Pod의 라이프사이클

 

1. Pending

Pod에 대한 생성 요청이 API 서버에 전달되었지만, 아직 노드에 스케줄링되지 않은 상태입니다. Pod가 특정 노드에 스케줄링 되기 전 시간은 물론 스케줄링 이후 Container 이미지를 다운로드 하는 시간도 포함됩니다.

컨테이너가 준비되면 ContainerCreating 상태로 돌입하여 컨테이너 생성을 하며 네트워크, 볼륨 마운트 등의 작업을 진행합니다.

2. Running

Pod가 Node에 바인딩되어 하나 이상의 컨테이너가 성공적으로 생성되고 실행 중인 상태입니다.

3. Succeeded

Pod가 정상적으로 종료된 상태입니다. 또한 완료된 상태이기 때문에 재시작되지 않습니다.

4. Failed

적어도 하나 이상의 컨테이너가 실패로 종료된 상태입니다. 그로 인해 재시작하지 않는 상태입니다.

5. Unknown

모종의 이유로 파드의 상태를 얻을 수 없는 상태입니다. 이런 경우 일반적으로 노드의 통신 오류로 인해 발생한다고 합니다.

 


컴포넌트 간 동작과정

위에서 살핀 각각의 단계에서 쿠버네티스의 컴포넌트들의 관심사는 어떻게 될까요? Pod의 각 단계에서 kube-apiserver, kube-scheduler, kubelet과 같은 Kubernetes 컴포넌트들이 서로 협력하여 Pod를 배치 및 관리합니다. 아래 링크에 도식화된 동작과정을 잘 그려놓아서 첨부합니다.(귀찮은 거 아님)

 

https://thebook.io/080241/0105/

 

컨테이너 인프라 환경 구축을 위한 쿠버네티스/도커: 3.1.5 파드의 생명주기로 쿠버네티스 구성

더북(TheBook): (주)도서출판 길벗에서 제공하는 IT 도서 열람 서비스입니다.

thebook.io

 

1. Pending

사용자가 kubectl create 명령을 통해 API 서버에 파드 생성을 요청합니다. API 서버는 이 정보를 etcd(Kubernetes 클러스터 상태 저장소)에 기록합니다. 그럼 kube-scheduler가 적절한 노드를 선택 해 API 서버에 전달합니다.

2. Running

스케줄러가 노드를 결정하면 API 서버는 kubelet이 노드에서 Pod를 생성할 수 있도록 정보를 제공합니다. 그럼 노드에 Pod를 생성하는 것은 kubelet의 관심사입니다. 이 때 컨테이너 런타임과 상호작용하여 컨테이너를 생성하고 시작합니다. kubelet이 Pod를 성공적으로 실행하면 이 Running 상태를 저장합니다. kubelet은 Pod의 상태를 모니터링하며, 주기적으로 API 서버에 이 정보를 업데이트 합니다.

 

3. Succeeded/Failed

만약 kubelet이 컨테이너가 종료되었다고 API 서버에 보고하면, Pod의 상태를 Succeded 혹은 Falied로 업데이트 합니다.

 

4. Unknown

이처럼 API 서버는 클러스터 상태를 주기적으로 노드와 동기화 합니다. 헌데 만약 네트워크 상의 문제와 같이 특정 노드와의 통신이 끊기면 해당 노드에서 실행 중이던 Pod의 상태가 Unknown으로 바뀝니다. 다시 말해 kubelet이 API 서버와 통신하지 못할 때 이런 상황이 발생합니다.

 

 

그럼 주요 컴포넌트들에 대해 간략하게 요약하고 코드로 넘어가겠습니다.

  • kube-apiserver는 Pod 생성, 상태 관리, 삭제 요청 등의 중심적인 역할을 담당합니다.
  • kube-scheduler는 Pod를 노드에 배치하는 스케줄링 역할을 합니다.
  • kubelet은 특정 노드 안에서 Pod를 모니터링하고 이를 지속적으로 보고하는 역할을 담당합니다.

바텀업 준비과정

최근 쿠버네티스 스터디를 진행하며 팀원 분이 바텀업하는 것을 보며 좋은 학습방법인 것 같아  Pod가 동작하는 과정을 코드 단에서 확인하고자 진행했습니다. 문제는... 너무 방대하더군요. 기본적인 이론을 정립했다고 생각했지만 어느 코드를 먼저 까봐야될지 알 수 없었습니다.

이에 따라 Pod를 하나 생성해서 로그를 따라가보고자 진행했습니다.

 

 

먼저 가벼운 이미지를 생성해줍니다.

apiVersion: v1
kind: Pod
metadata:
  name: busybox-pod
spec:
  containers:
    - name: busybox
      image: busybox
      command: ["sleep", "3600"]  # Pod가 1시간 동안 대기

 

로그 레벨을 조정하여 디버그 모드로 Pod의 로그를 확인하였습니다. k3s에서 디버깅 수준을 수정하는 과정에서 참고한 자료는 포스팅 하단에 첨부하겠습니다.

ubuntu@ip-172-31-89-213:~/pod$ kubectl logs busybox-pod -v=8
I1006 16:07:40.108312   14511 loader.go:395] Config loaded from file:  /etc/rancher/k3s/k3s.yaml
I1006 16:07:40.137446   14511 round_trippers.go:463] GET https://127.0.0.1:6443/api/v1/namespaces/default/pods/busybox-pod
I1006 16:07:40.137591   14511 round_trippers.go:469] Request Headers:
I1006 16:07:40.137606   14511 round_trippers.go:473]     User-Agent: kubectl/v1.30.4+k3s1 (linux/amd64) kubernetes/98262b5
I1006 16:07:40.137614   14511 round_trippers.go:473]     Accept: application/json, */*
I1006 16:07:40.162233   14511 round_trippers.go:574] Response Status: 200 OK in 24 milliseconds
I1006 16:07:40.162359   14511 round_trippers.go:577] Response Headers:
I1006 16:07:40.162391   14511 round_trippers.go:580]     Cache-Control: no-cache, private
I1006 16:07:40.162400   14511 round_trippers.go:580]     Content-Type: application/json
I1006 16:07:40.162437   14511 round_trippers.go:580]     X-Kubernetes-Pf-Flowschema-Uid: 0631fa81-1d9f-44b0-a3bf-383ca9ba96b5
I1006 16:07:40.162445   14511 round_trippers.go:580]     X-Kubernetes-Pf-Prioritylevel-Uid: 145e967e-a8ea-40e6-b6a1-aa4d4ef4436b
I1006 16:07:40.162468   14511 round_trippers.go:580]     Content-Length: 3896
I1006 16:07:40.162476   14511 round_trippers.go:580]     Date: Sun, 06 Oct 2024 16:07:40 GMT
I1006 16:07:40.162500   14511 round_trippers.go:580]     Audit-Id: fd18c30e-56da-4c07-abef-52bea63d933e
I1006 16:07:40.163731   14511 request.go:1212] Response Body: {"kind":"Pod","apiVersion":"v1","metadata":{"name":"busybox-pod","namespace":"default","uid":"993f6a18-4226-4500-b666-5aa25e259f23","resourceVersion":"338408","creationTimestamp":"2024-10-06T15:59:33Z","managedFields":[{"manager":"kubectl-create","operation":"Update","apiVersion":"v1","time":"2024-10-06T15:59:33Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:containers":{"k:{\"name\":\"busybox\"}":{".":{},"f:command":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}},{"manager":"k3s","operation":"Update","apiVersion":"v1","time":"2024-10-06T15:59:35Z","fieldsType":"FieldsV1","fieldsV1":{"f:status":{"f:conditions":{"k:{\"type\":\"ContainersReady\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Initialized\"}":{".":{},"f:lastProbeT [truncated 2872 chars]
I1006 16:07:40.166770   14511 round_trippers.go:463] GET https://127.0.0.1:6443/api/v1/namespaces/default/pods/busybox-pod/log?container=busybox
I1006 16:07:40.166834   14511 round_trippers.go:469] Request Headers:
I1006 16:07:40.166867   14511 round_trippers.go:473]     Accept: application/json, */*
I1006 16:07:40.166898   14511 round_trippers.go:473]     User-Agent: kubectl/v1.30.4+k3s1 (linux/amd64) kubernetes/98262b5
I1006 16:07:40.202746   14511 round_trippers.go:574] Response Status: 200 OK in 35 milliseconds
I1006 16:07:40.202760   14511 round_trippers.go:577] Response Headers:
I1006 16:07:40.202784   14511 round_trippers.go:580]     Audit-Id: dc489652-8f04-4d30-8574-d3e8f22f29ce
I1006 16:07:40.202789   14511 round_trippers.go:580]     Cache-Control: no-cache, private
I1006 16:07:40.202792   14511 round_trippers.go:580]     Content-Type: text/plain
I1006 16:07:40.202796   14511 round_trippers.go:580]     Date: Sun, 06 Oct 2024 16:07:40 GMT

 

꽤나 자세한 로그가 찍히긴 하지만 당연하게도 Pod 자체의 로그를 가져오는 것인 만큼 Pod의 동작과정에 대한 로그는 찍히지 않았습니다. 따라서 k3s 자체의 로그를 살펴봤습니다.

k3s 로그

kubelet이 부지런히 남긴 로그인 듯 합니다. 이제 슬슬 "kubelet_pods.go:1132" 쿠버네티스 특정 파일의 특정 라인에 명시된 보이고 있습니다.

ubuntu@ip-172-31-89-213:~/pod$ awk 'NR > 10931' go.txt > output.txt

생성된 시간 참고해서 로그 찾기

.go 가 명시된 라인만 가져오기 -> 적당적당한 키워드 -> pod생성시간 이후부터 살피기 등 아주 원초적인 방법으로 범위를 줄여가던 중 반가운 라인을 발견했습니다.

Pod생성 요청

 

Post요청으로 Pod 생성에 대한 HTTP요청과 응답값으로 201 을 던져주는 라인입니다. 이 로그를 기준으로 API 동작과정에 대해 살펴보고자 했습니다.

 

코드 분석

0. Pod 생성 요청

1. Pod 핸들링 - "Add event for unscheduled pod"

생성 요청 이후 가장 먼저 보이는 로그는 EventHandler입니다. 앞으로 나오는 코드들도 이처럼 찍힌 로그 순서를 보고 대조해가며 코드를 분석했습니다.

스케줄링 큐에 스케줄링되지 않은 Pod 추가

func (sched *Scheduler) addPodToSchedulingQueue(obj interface{}) {
	start := time.Now()
	defer metrics.EventHandlingLatency.WithLabelValues(framework.UnscheduledPodAdd.Label).Observe(metrics.SinceInSeconds(start))

	logger := sched.logger
	pod := obj.(*v1.Pod)
	logger.V(3).Info("Add event for unscheduled pod", "pod", klog.KObj(pod))
	sched.SchedulingQueue.Add(logger, pod)
}

 

스케줄링 되지 않은 파드를 큐에 추가하는 코드인가 봅니다. kube-scheduler 는 주기적으로 해당 스케줄링 큐를 확인하여 스케줄링 되어 있지 않은 파드들을 모니터링합니다.

 

2. 스케줄링 시도 - "Attempting to schedule pod"

Pod 스케줄링

 

func (sched *Scheduler) ScheduleOne(ctx context.Context) {
	logger := klog.FromContext(ctx)
	podInfo, err := sched.NextPod(logger)
	if err != nil {
		logger.Error(err, "Error while retrieving next pod from scheduling queue")
		return
	}
	// pod could be nil when schedulerQueue is closed
	if podInfo == nil || podInfo.Pod == nil {
		return
	}

	pod := podInfo.Pod
	// TODO(knelasevero): Remove duplicated keys from log entry calls
	// When contextualized logging hits GA
	// https://github.com/kubernetes/kubernetes/issues/111672
	logger = klog.LoggerWithValues(logger, "pod", klog.KObj(pod))
	ctx = klog.NewContext(ctx, logger)
	logger.V(4).Info("About to try and schedule pod", "pod", klog.KObj(pod))

	fwk, err := sched.frameworkForPod(pod)
	if err != nil {
		// This shouldn't happen, because we only accept for scheduling the pods
		// which specify a scheduler name that matches one of the profiles.
		logger.Error(err, "Error occurred")
		sched.SchedulingQueue.Done(pod.UID)
		return
	}
	if sched.skipPodSchedule(ctx, fwk, pod) {
		// We don't put this Pod back to the queue, but we have to cleanup the in-flight pods/events.
		sched.SchedulingQueue.Done(pod.UID)
		return
	}

	logger.V(3).Info("Attempting to schedule pod", "pod", klog.KObj(pod))

	// Synchronously attempt to find a fit for the pod.
	start := time.Now()
	state := framework.NewCycleState()
	state.SetRecordPluginMetrics(rand.Intn(100) < pluginMetricsSamplePercent)

	// Initialize an empty podsToActivate struct, which will be filled up by plugins or stay empty.
	podsToActivate := framework.NewPodsToActivate()
	state.Write(framework.PodsToActivateKey, podsToActivate)

	schedulingCycleCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	scheduleResult, assumedPodInfo, status := sched.schedulingCycle(schedulingCycleCtx, state, fwk, podInfo, start, podsToActivate)
	if !status.IsSuccess() {
		sched.FailureHandler(schedulingCycleCtx, fwk, assumedPodInfo, status, scheduleResult.nominatingInfo, start)
		return
	}

	// bind the pod to its host asynchronously (we can do this b/c of the assumption step above).
	go func() {
		bindingCycleCtx, cancel := context.WithCancel(ctx)
		defer cancel()

		metrics.Goroutines.WithLabelValues(metrics.Binding).Inc()
		defer metrics.Goroutines.WithLabelValues(metrics.Binding).Dec()

		status := sched.bindingCycle(bindingCycleCtx, state, fwk, scheduleResult, assumedPodInfo, start, podsToActivate)
		if !status.IsSuccess() {
			sched.handleBindingCycleError(bindingCycleCtx, state, fwk, assumedPodInfo, start, scheduleResult, status)
			return
		}
	}()
}

 

 

NextPod()메서드를 통해 스케줄링 큐에서 다음 스케줄링할 Pod를 찾아옵니다.

그 후, Pod정보에 대한 유효성을 검증하고 schedulingCycle()메서드를 통해 적절한 노드에 Pod를 스케줄링합니다. 

 

 

3. 노드 바인딩 - "Attempting to bind pod to node"

Pod에 Node Bind

// Bind binds pods to nodes using the k8s client.
func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) *framework.Status {
	logger := klog.FromContext(ctx)
	logger.V(3).Info("Attempting to bind pod to node", "pod", klog.KObj(p), "node", klog.KRef("", nodeName))
	binding := &v1.Binding{
		ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID},
		Target:     v1.ObjectReference{Kind: "Node", Name: nodeName},
	}
	err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})
	if err != nil {
		return framework.AsStatus(err)
	}
	return nil
}

 

Pod를 특정한 노드 이름과 바인딩합니다.

그 후, HTTP 요청을 통해 API Server로 파드의 바인딩을 요청합니다.

5. SyncLoop ADD

Kubelet이 API 서버에서 새로 추가된 포드를 감지했다는 것을 나타내며 이제 해당 Pod의 상태를 관리할 수 있도록 클러스터에 등록되기 위한 처리를 수행합니다.

func (kl *Kubelet) syncLoopIteration(ctx context.Context, configCh <-chan kubetypes.PodUpdate, handler SyncHandler,
	syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool {
	select {
	case u, open := <-configCh:
		// Update from a config source; dispatch it to the right handler
		// callback.
		if !open {
			klog.ErrorS(nil, "Update channel is closed, exiting the sync loop")
			return false
		}

		switch u.Op {
		case kubetypes.ADD:
			klog.V(2).InfoS("SyncLoop ADD", "source", u.Source, "pods", klog.KObjSlice(u.Pods))
			// After restarting, kubelet will get all existing pods through
			// ADD as if they are new pods. These pods will then go through the
			// admission process and *may* be rejected. This can be resolved
			// once we have checkpointing.
			handler.HandlePodAdditions(u.Pods)

 

 

6. Topology Admit Handler

7. Pod 상태 생성, CPU, 메모리, 이전에 생성된 unscheduleed pod 이벤트 삭제

8. Add event for scheduled pod

 


참고자료

(파드 라이프사이클)

https://kubernetes.io/ko/docs/concepts/workloads/pods/pod-lifecycle/

 

파드 라이프사이클

이 페이지에서는 파드의 라이프사이클을 설명한다. 파드는 정의된 라이프사이클을 따른다. Pending 단계에서 시작해서, 기본 컨테이너 중 적어도 하나 이상이 OK로 시작하면 Running 단계를 통과하

kubernetes.io

 

(로그 레벨 설정)

https://docs.k3s.io/kr/faq#k3s-%EB%A1%9C%EA%B7%B8%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%9E%88%EB%82%98%EC%9A%94

 

자주 묻는 질문 | K3s

자주 묻는 질문은 주기적으로 업데이트되며, 사용자가 K3s에 대해 가장 자주 묻는 질문에 대한 답변으로 구성되어 있습니다.

docs.k3s.io

 

 

 

훈