Kubernetes Operator – Reconciling resource

쿠버네티스 오퍼레이터를 개발하면서, 대상 리소스가 변경 되었는지 확인하는 경우가 있습니다. 리소스 내용이 변경되었으면, 어떠한 행위를 하기 위해서입니다.

두 리소스를 비교하기 위한 가장 쉬운 방법은 DeepEqual 을 사용하는 것입니다.

if !reflect.DeepEqual(expected, actual) {
	// need to update...
}

하지만 이 방법은 한계를 가지고 있습니다. 메타데이터나 기본값을 적용될 경우 제대로 된 비교가 안될 수 있기 때문입니다.

예를 들어 다음과 같은 hello-pod 를 생성했다고 가정해보겠습니다.

apiVersion: v1
Kind: Pod
metadata:
  name: hello-pod
spec:
  containers:
  - name: busybox
    image: busybox

쿠버네티스 API를 이용하여, hello-pod 의 정보를 조회해오면 다음과 같이 기본값들과 메타데이터 정보가 포함되어 있습니다.

apiVersion: v1
Kind: Pod
metadata:
  creationTimestamp: "2021-01-17T03:00:00Z"
  namespace: default
  name: hello-pod
  uid: 673795dc-595b-11eb-a633-fa163fad3156
spec:
  containers:
  - name: busybox
    image: busybox
    imagePullPolicy: Always
dnsPolicy: ClusterFirst

즉, 두 개의 Pod은 같은 것이지만, DeepEqual 을 이용하여 비교를 하면 다른것으로 판별됩니다.

두 개의 Pod가 진짜 다른것인지를 판별하기 위해서는 다음과 같이 할 수도 있습니다.

if sameName(expected, actual) &&
   sameContainers(expected, actual) && ...

필요한 부분만 직접 비교를 하는 것입니다. 비교할 필드가 1,2개 정도면 이 방법도 사용할 수 있습니다만, 현실세계에서는 대부분 많은 필드를 가지고 있기 때문에 효율적이지 않습니다.

두 리소스를 비교하는 가장 스마트한 방법은 리소스의 Hash값을 이용하는 것입니다. 다음은 HashObject 함수를 이용하여, 리소스의 해시값을 계산한 후 비교하는 코드입니다.

hash := HashObject(expected)
expected.Annotations["ResourceHash"] = hash

actualHash := actual.Annotations["ResourceHash"]
if actualHash != hash {
	// need to update...
}

다음은 HashObject 함수입니다.

// HashObject writes the specified object to a hash using the spew library
// which follows pointers and prints actual values of the nested objects
// ensuring the hash does not change when a pointer changes.
// The returned hash can be used for object comparisons.
//
// This is inspired by controller revisions in StatefulSets:
// <https://github.com/kubernetes/kubernetes/blob/8de1569ddae62e8fab559fe6bd210a5d6100a277/pkg/controller/history/controller_history.go#L89-L101>
func HashObject(object interface{}) string {
	hf := fnv.New32()
	printer := spew.ConfigState{
		Indent:         " ",
		SortKeys:       true,
		DisableMethods: true,
		SpewKeys:       true,
	}
	_, _ = printer.Fprintf(hf, "%#v", object)
	return fmt.Sprint(hf.Sum32())
}

출처 : https://github.com/elastic/cloud-on-k8s/blob/master/pkg/controller/common/hash/hash.go

참고 : https://github.com/kubernetes/kubernetes/blob/master/pkg/util/hash/hash.go

ARP cache

ARP 캐시는 로컬 네트워크에서, 패킷을 효율적으로 전환할 수 있도록 도와줍니다. 시스템이 어떤 IPv4 주소가 어떤 MAC 또는 하드웨어 주소를 가지는지를 파악하는 방법입니다. 예를 든다면, “10.233.134.150” 라는 IP는 “fe:16:48:a9:a6:5b”는 하드웨어 주소를 가지고 있는것입니다.

이 캐시의 내용은 arp -n 또는 /proc/net/arp 또는 ip -4 neigh 를 실행하면 확인할 수 있습니다.

명령어 실행 결과

다음 명령어를 실행하면 결과입니다.

arp -n

# arp -n

Address                  HWtype  HWaddress           Flags Mask            Iface
10.233.134.149           ether   fe:16:48:a9:a6:5b   C                     eth0
10.233.176.254           ether   fe:16:48:a9:a6:5b   C                     eth0
...
10.233.134.150           ether   fe:16:48:a9:a6:5b   C                     eth0
10.233.134.119           ether   fe:16:48:a9:a6:5b   C                     eth0
10.244.4.0               ether   8e:c4:59:6d:ee:83   CM                    flannel.1
...

/proc/net/arp

# cat /proc/net/arp

IP address       HW type     Flags       HW address            Mask     Device
10.233.134.149   0x1         0x2         fe:16:48:a9:a6:5b     *        eth0
10.233.176.254   0x1         0x2         fe:16:48:a9:a6:5b     *        eth0
...
10.244.4.0       0x1         0x6         8e:c4:59:6d:ee:83     *        flannel.1
...

ip -4 heigh

# ip -4 neigh

10.233.134.149 dev eth0 lladdr fe:16:48:a9:a6:5b REACHABLE
10.233.176.254 dev eth0 lladdr fe:16:48:a9:a6:5b STALE
...
10.233.134.119 dev eth0 lladdr fe:16:48:a9:a6:5b REACHABLE
10.244.4.0 dev flannel.1 lladdr 8e:c4:59:6d:ee:83 PERMANENT
...

캐시 크기 기본값

대부분의 경우 이 캐시의 크기는 중요하지 않습니다. 하지만, 많은 디바이스가 있는 네트워크 세그먼트의 경우, 이 옵션을 조정해줘야 원활한 서비스를 할 수 있습니다.

쿠버네티스 같은 컨테이너 환경에서 많은 컨티이너를 사용하거나, 많은 대상과 네트워크 통신이 필요할 때 이 값을 조정해줘야합니다.

(저의 경우 쿠버네티스에서 flannel.1 TX dropped 이 발생하여, 원인을 찾던 중에 발견하게 되었습니다.)

ARP 캐시의 기본값을 1024입니다. /proc/sys/net/ipv4/neigh/default/gc_thresh3 에서 확인할 수 있습니다.

캐시 크기 조정하기

다음은 캐시값을 4096으로 조정한 예입니다.

# echo "net.ipv4.neigh.default.gc_thresh3 = 4096" > /etc/sysctl.d/95-gc_thresh3.conf
# /sbin/sysctl -p /etc/sysctl.d/95-gc_thresh3.conf

프로메테우스 사용값 확인하기

Prometheus와 NodeExporter를 사용하고 있다면, ARP 테이블 관련 메트릭을 확인할 수 있습니다.

NodeExporter는 ARP 캐시의 항목 수를 디바이스별로 구분하여 표시합니다.

node_arp_entries{device="flannel.1"}

쿠버네티스 노드에 root 권한의 shell로 접속하기.

쿠버네티스 노드에 root 권한의 shell로 접속하기.

LENS(https://k8slens.dev/) 를 사용하다가, 쿠버네티스 노드에 root 권한의 shell로 접속할 수 있는 기능을 발견하였습니다.

이게 어떻게 가능한 일이지 궁금해서, 간단히 조사를 해보았습니다.

알고 보니 원리는 간단하였습니다.

권한이 있는 컨테이너(privileged containers)를 실행시키고, 그 컨테이너로 접속을 하는 것이었습니다.

CONTAINER ID        IMAGE                                                        COMMAND                  CREATED             STATUS              PORTS               NAMES
8f5f3c2b4c08        alpine                                                       "nsenter -t 1 -m -u …"   53 seconds ago      Up 52 seconds                           k8s_shell_node-shell-1fa79f9a-837e-4f46-a66a-c98e05fb5e5e_kube-system_2f3e6447-3046-11eb-951e-fa164857d866_0
docker inspect 8f5f3c2b4c08
[
    {
        "Id": "8f5f3c2b4c083d7f9c301ce06331dbc429927f5a8ee64a9969f24ceae4c7a3f9",
        "Created": "2020-11-26T12:19:20.51461897Z",
        "Path": "nsenter",
        "Args": [
            "-t",
            "1",
            "-m",
            "-u",
            "-i",
            "-n",
            "sleep",
            "14000"
        ],
...
]

nsenter 라는 명령어는 namepsace enter 의 약어입니다. 글자 그대로, 격리된 네임스페이스에 들어가는 명령어입니다.

kubectl node-shell

kubectl node-shell (https://github.com/kvaps/kubectl-node-shell) 을 이용해서. kubectl 에서도 똑같은 방법으로 노드에 접속할 수 있습니다.

Installation

using krew

brew install krew

kubectl krew index add kvaps <https://github.com/kvaps/krew-index>
kubectl krew install kvaps/node-shell

Usage

# Get standard bash shell
kubectl node-shell <node>

쿠버네티스 리소스 강제로 삭제하기

간혹 쿠버네티스의 리소스의 삭제가 불가능해지는 경우가 있습니다. 예를 들어, 커스텀 리소스를 등록하여 사용하다가, 의존성을 무시한채 컴포넌트를 제거해버려서 특정 리소스를 삭제하지 못하는 경우 입니다. 아무런 생각없이 다시 설치할 생각에, 네임스페이스를 제거해버렸다가, 영원히 삭제되지 않는 고통을 경험해 볼 수 있습니다.

이럴 경우 최후의 방법을 사용해야 하는데, 바로 etcd 의 리소스 데이터를 삭제해 버리는 것입니다. 정상적인 방법은 아니니, 꼭 필요한 경우가 아니면 사용하지 않는 것을 추천 드립니다.

다음은 etcdctl을 이용해서, 특정 리소스를 삭제하는 예제입니다.

tekton-pipelines 이라는 네임스페이스에 있는 echo-hello-world-task-run 라는 taskruns 커스텀 리소스를 삭제하는 것입니다.

ADVERTISE_URL="<https://10.203.163.116:2379>"
KEY="/registry/tekton.dev/taskruns/tekton-pipelines/echo-hello-world-task-run"

kubectl -n kube-system exec etcd-node001 -- sh -c \\
"ETCDCTL_API=3 etcdctl \\
--endpoints $ADVERTISE_URL \\
--cacert /etc/kubernetes/pki/etcd/ca.crt \\
--key /etc/kubernetes/pki/etcd/server.key \\
--cert /etc/kubernetes/pki/etcd/server.crt \\
del \\"$KEY\\"

etcd에 대한 자세한 사항은 “쿠버네티스의 etcd 살펴보기“를 참고하시기 바랍니다.

쿠버네티스의 Etcd 살펴보기

etcd는 쿠버네티스 클러스터의 중요한 컴포넌트로서, 클러스터의 상태를 저장하고 있습니다. 클러스터에 필요한 설정 정보와, 포드와 서비스 같은 각 리소스들의 명세와 상태 정보 등을 저장하고 있습니다. 이 글에서는 etcd가 이러한 정보들을 어떻게 저장하는지에 대해서 간단히 알아볼 것입니다.

etcd

etcd 는 분산 시스템에서 사용할 수 있는 분산형 키-값 (key-value) 저장소 입니다 CoreOS에서 클러스터를 관리하기 위해서 사용했으며, 요즘은 쿠버네티스의 기본 데이터 저장소로 많이 사용하고 있습니다. etcd는 고가용성을 위하여 클러스터로 설치됩니다. 여러 노드의 통신은 래프트(Raft) 알고리즘에 의해 처리합니다. 연결된 노드들 중 리더를 선정하여 클러스터를 관리합니다. 데이터는 분산되어 저장하기 때문에 시스템 오류에 대한 내성을 가지고 있습니다. 클러스터의 노드는 홀수개로 이루어져야 하며, 최소 3개 이상의 노드가 필요합니다.

http://thesecretlivesofdata.com/raft/ 에서 래프트 알고리즘의 작동 방식을 설명하는 애니메이션을 볼 수 있습니다.

쿠버네티스의 etcd

쿠버네티스는 etcd를 기본 데이터 저장소로 사용합니다. 그래서 쿠버네티스를 설치하기 위해서는 etcd 가 필요합니다. 별도의 etcd 클러스터를 쿠버네티스 외부에 설치한 후 쿠버네티스에서 사용할 수 있습니다. 또는 쿠버네티스를 설치할때 컨트롤플레인 노드에 etcd를 포드로 같이 설치할 수 있습니다.

etcd 클러스터를 별도로 설치한 경우

다음은 그림은 etcd 클러스터를 외부에 설치한 경우를 나타낸 것입니다. 쿠버네티스 외부에 etcd 클러스터가 존재하고 kube-apiserver에서 해당 etcd 클러스터에 접속하여 데이터를 저장하고 읽어옵니다.

etcd 클러스터를 컨트롤 플레인 노드에 포드를 설치한 경우

다음 그림은 etcd 클러스터를 쿠버네티스 컨트롤 플레인 노드에 같이 설치한 경우를 나타낸 것입니다. 컨트롤 플레인 노드에 스태틱 포드(static pod) 형태로 etcd가 실행됩니다.

etcd 데이터 흐름 살펴보기

다음 명령어를 실행하여 포드를 생성해 보겠습니다.

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.19.2
EOF

다음 그림은 포드를 생성했을 때 일어나는 일을 간단히 나타낸 것입니다.

  • 작성한 매니페스트는 kubectl 를 이용하여 kube-apiserver로 전달합니다.
  • kube-apiserver 는 포드를 생성하는 요청을 받은 후, 포드의 매니페스트를 etcd로 저장합니다.
  • kube-schedulerkube-apiserver 를 통해 포드 정보를 감시(watch)하고 있다가, 새로운 포드가 등록 된것을 감지하고, 포드가 실행 가능한 노드로 배치합니다. 포드의 정보에 배치될 노드의 정보를 추가한 후,kube-apiserver 를 통해 정보를 업데이트 합니다. kube-apiserver 는 업데이트 된 정보를 다시 etcd에 저장합니다.
  • kubelet 은 자신의 노드에 배치된 파드의 정보를 감지하고, 컨테이너를 실행시킵니다. 그리고, kube-apiserver 를 통해 상태 정보를 업데이트 합니다. kube-apiserver 는 업데이트 된 정보를 다시 etcd에 저장합니다.

쿠버네티스에서 etcd 정보 조회하기

쿠버네티스에서 사용되고 있는 etcd의 데이터를 조회하는 방법에 대해서 알아보겠습니다.

예제에 사용하는 쿠버네티스는 etcd가 컨트롤 플레인 노드에 스태틱 포드(static pod)로 같이 설치된 클러스터입니다.

쿠버네티스 클러스터가 정상적으로 설치되었다면, kube-system 네임스페이스에서 etcd 포드를 확인할 수 있습니다. 포드 이름은 일반적으로 etcd-노드명 입니다.

$ kubectl -n kube-system get pod
NAME                              READY   STATUS                   RESTARTS   AGE
...
etcd-node001                      1/1     Running                  0          229d
etcd-node002                      1/1     Running                  0          229d
etcd-node003                      1/1     Running                  1          229d
...

미러 포드(mirror pod)로 등록되어 있다면, etcd 포드를 확인할 수 있습니다.

etcdctl 사용하기

etcd 포드들 중 하나에 ps aux 명령어를 실행하여 etcd 서버의 주소를 확인해 보겠습니다.

$ kubectl -n kube-system exec -it etcd-node001 -- ps aux

PID   USER     TIME  COMMAND
    1 root      9d20 etcd --advertise-client-urls=https://10.203.163.116:2379,h
39283 root      0:00 ps aux

--advertise-client-urls 플래그의 값을 사용하여, etcd 클러스터에 명령을 실행해 보겠습니다. etcd 클러스터에 명령을 실행하기 위해서 etcdctl 이라는 유틸리티를 사용하겠습니다.

etcd의 데이터를 파일로 저장하기

etcdctl 실행할 때, v3 API를 사용하기 위해 ETCDCTL_API=3 을 붙입니다. 그리고, 접속 주소랑 인증을 위한 인증서 파일 경로를 플래그로 넘겨줍니다.

다음은 etcd에 저장된 데이터를 json 형식으로 저장하는 예제입니다.

$ ADVERTISE_URL="<https://10.203.163.116:2379>"

$ kubectl -n kube-system exec etcd-node001 -- sh -c \\
"ETCDCTL_API=3 etcdctl \\
--endpoints $ADVERTISE_URL \\
--cacert /etc/kubernetes/pki/etcd/ca.crt \\
--key /etc/kubernetes/pki/etcd/server.key \\
--cert /etc/kubernetes/pki/etcd/server.crt \\
get \\"\\" --prefix=true -w json" > etcd-data.json

저장된 데이터를 조회해 보기

다음은 etcd-data.json 파일의 일부분입니다. 파일을 살펴보면, 키와 값의 목록으로 이루어져 있는것을 알 수 있습니다. 키와 값들은 base64로 인코딩 되어 있기 때문에, 쉽게 알아 볼 수 없습니다.

{
  "header": {
    "cluster_id": 17306099881785348000,
    "member_id": 17264302005967186000,
    "revision": 66544117,
    "raft_term": 2
  },
  "kvs": [
    {
      "key": "L3JlZ2lzdHJ5L2FwaWV4dGVuc2lvbnMuazhzLmlvL2N1c3RvbXJlc291cmNlZGVmaW5pdGlvbnMvYWRhcHRlcnMuY29uZmlnLmlzdGlvLmlv",
      "create_revision": 6490,
      "mod_revision": 30436184,
      "version": 4,
      "value": "eyJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwiYXBpVmVyc2lvbiI6ImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxYmV0YTEi...생략..."
    },
    {
      "key": "L3JlZ2lzdHJ5L2FwaWV4dGVuc2lvbnMuazhzLmlvL2N1c3RvbXJlc291cmNlZGVmaW5pdGlvbnMvYXBpa2V5cy5jb25maWcuaXN0aW8uaW8=",
      "create_revision": 11406824,
      "mod_revision": 11406826,
      "version": 3,
      "value": "eyJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwiYXBpVmVyc2lvbiI6ImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxYmV0YTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoiYXBpa2V5cy5jb25maWcuaXN0aW8uaW8iLCJ1aWQiOiJhMWFjMGZhMi0yZWM1LTQ1MzMtOGViOC1mZDMwY2JlMjQ0ZDgiLCJn...생략..."
    },
...

다음 명령어를 실행하면, 디코딩된 키값들을 조회해 볼 수 있습니다.

$ for k in $(cat etcd-data.json | jq '.kvs[].key' | cut -d '"' -f2); do echo $k | base64 --decode; echo; done

쿠버네티스의 전체 리소스들을 확인할 수 있습니다.

...
/registry/pods/istio-system/istio-citadel-58c6fb56fb-fs2gp
/registry/pods/istio-system/istio-egressgateway-64787c7b7d-7fblz
/registry/pods/istio-system/istio-galley-67f97d7c8b-wgb2r
/registry/pods/istio-system/istio-grafana-post-install-1.2.10-sl54z
/registry/pods/istio-system/istio-ingressgateway-66cd7b9b8b-hbfsl
...
/registry/services/endpoints/istio-system/istio-citadel
/registry/services/endpoints/istio-system/istio-egressgateway
/registry/services/endpoints/istio-system/istio-galley
/registry/services/endpoints/istio-system/istio-ingressgateway
...

nginx 포드의 리소스

앞서 예제에 있는 포드를 생성한 다음, etcd 데이터를 저장하였다면 다음과 같은 리소스들의 키 정보도 확인할 수 있습니다. 해당 포드의 이벤트 리소스와 포드 리소스 입니다.

/registry/events/default/nginx.1630e320224081de
/registry/events/default/nginx.1630e320535d4dd0
/registry/events/default/nginx.1630e32218d6d691
/registry/events/default/nginx.1630e3221ea631fb
/registry/events/default/nginx.1630e322284c4b56
...
/registry/pods/default/nginx

포드의 이벤트는 kubectl describe pod 포드네임 명령어를 이용하여 확인해 볼 수 있습니다.

$ kubectl describe pod nginx

“Events:”라는 부분에 포드 관련 이벤트가 조회되는 것을 확인할 수 있습니다. 이 이벤트는 시간이 지나면 자동으로 삭제되기 때문에, 안 보일수도 있습니다. (이벤트 지속 시간의 기본 설정 값은 1시간입니다.)

Name:         nginx
Namespace:    default
Priority:     0
...
Events:
  Type    Reason     Age    From                 Message
  ----    ------     ----   ----                 -------
  Normal  Scheduled  2m37s  default-scheduler    Successfully assigned default/nginx to worker-001
  Normal  Pulling    2m36s  kubelet, worker-001  Pulling image "nginx:1.19.2"
  Normal  Pulled     2m28s  kubelet, worker-001  Successfully pulled image "nginx:1.19.2"
  Normal  Created    2m28s  kubelet, worker-001  Created container nginx
  Normal  Started    2m28s  kubelet, worker-001  Started container nginx

특정 키로 조회해보기

특정 키의 값을 바로 조회해 볼 수도 있습니다. etcdctl 의 get 플래그에 키를 지정하면 됩니다. default 네임스페이스에 있는 nginx 라는 포드의 정보를 조회해 보겠습니다.

KEY="/registry/pods/default/nginx"

kubectl -n kube-system exec etcd-node001 -- sh -c \\
"ETCDCTL_API=3 etcdctl \\
--endpoints $ADVERTISE_URL \\
--cacert /etc/kubernetes/pki/etcd/ca.crt \\
--key /etc/kubernetes/pki/etcd/server.key \\
--cert /etc/kubernetes/pki/etcd/server.crt \\
get \\"$KEY\\" -w json" | jq '.kvs[].value' | cut -d '"' -f2 | base64 --decode

다음은 조회된 결과 입니다. 포드의 정보를 가지고 있는것을 확인할 수 있습니다. 문자열 데이터가 아닌 부분은 깨져 보이지만, 대충 어떤 의미를 가지는지는 확인할 수 있습니다.

k8s

v1Pod�
�
nginxdefault"*$1ae7d990-d09e-443b-a419-0d8bbc0ece232���b�
0kubectl.kubernetes.io/last-applied-configuration�{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"nginx","namespace":"default"},"spec":{"containers":[{"image":"nginx:1.19.2","name":"nginx"}]}}
z�
1
default-token-pz9rt2
default-token-pz9rt��
nginx
     nginx:1.19.2*BJJ
default-token-pz9rt-/var/run/secrets/kubernetes.io/serviceaccount"2j/dev/termination-logr
                                                                                         IfNotPresent����FileAlways 2
                                                                                                                     ClusterFirstBdefaultJdefaultRworker-001X`hr���default-scheduler�6
node.kubernetes.io/not-readyExists"	NoExecute(��8
node.kubernetes.io/unreachableExists"	NoExecute(�����
Running#

InitializedTru���*2
ReadyTru���*2'
ContainersReadyTru���*2$

PodScheduledTru���*2"*
                      10.233.166.02
                                   172.32.8.8���B�
nginx

��� (2
      nginx:1.19.2:_docker-pullable://nginx@sha256:b0ad43f7ee5edbc0effbc14645ae7055e21bc1973aee5150745632a24a752661BIdocker://9e8f506939d41129f61720813ee9cda07569e6628cf385fa71b692425d4be40cJ
BestEffortZ"

이상으로 etcd에 대해서 간단히 살펴보았습니다.

도커 네트워크

도커 네트워크

도커에서 제공하는 대표적인인 네트워크 드라이버로는 호스트(host), 브리지(bridge), 사용안함(none) 등이 있습니다.

$ docker network ls

NETWORK ID          NAME                DRIVER              SCOPE
d4c7abefb75d        bridge              bridge              local
8c897fb9e7da        host                host                local
9fb15fe19162        none                null                local

도커의 기본 네티워크 모드는 bridge 입니다. 만약 다른 모드를 사용하여 컨테이너를 생성하고 싶다면 --net 을 이용하여 설정할 수 있습니다.

docker run --net=<NETWORK>

도커를 설치하면, 기본적으로 docker0 이라는 가상 브리지(bridge)가 생성 됩니다.

$ ifconfig

...
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:6c:3d:0f:04  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
$ ip addr

...
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:6c:3d:0f:04 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

이 브리지는 컨테이너를 기본 브리지 모드로 실행할 때 사용되면, CIRD 표기법으로 172.17.0.1/16 의 주소 범위를 가지고 있습니다. 172.17.0.1 부터 172.17.255.254 까지의 아이피를 사용할 수 있습니다. 그래서 컨테이너가 기본 브리지 모드로 실행될 때, 해당 범위에서 아이피를 할당받습니다.

만약 이 범위를 변경하고 싶다면, 도커 설정 파일인 /etc/docker/daemon.json"bip" 항목을 추가 하면 됩니다.


Bridge Mode Networking

Docker는 연결된 다른 네트워크 인터페이스 간에 패킷을 자동으로 전달하는 가상 이더넷 브리지인 docker0을 생성합니다. 기본적으로 호스트의 모든 컨테이너는 이 브리지를 이용하여 내부 네트워크에 연결이 됩니다. 이 모드는 컨테이너를 분리된 네트워크 네임스페이스에 배치하고, 네트워크 주소 변환을 사용하여 여러 컨테이너 간에 호스트의 외부 IP 주소를 공유합니다.

브리지 모드 네트워킹은 동일한 호스트에서 여러 컨테이너를 실행할 때 네트워크 포트 충돌을 일으키지 않습니다. 즉, 동일한 포트를 사용하는 다수의 컨테이너를 하나의 호스트에서 실행할 수 있습니다. 각 컨테이너는 호스트와 분리된 전용 네트워크 네임스페이스를 소유하고 있습니다. 그래서 이 모드는 NAT의 사용으로 인해 네트워크 처리량과 지연 시간에 영향을 미치고, 호스트와 컨테이너 간의 네트워크 포트 매핑을 제어해야하는 단점이 있습니다.

컨테이너가 생성되면, 해당 컨테이너를 위해서 페어 인터페이스(pair interfaces)가 생성됩니다. 이 인터페이스들은 두 개가 한 쌍으로 구성되어 있는데, 마치 직접 연결된 것 처럼 서로 패킷을 주고 받습니다.

컨테이너가 생성되면, 페어 인터페이스의 한쪽은 컨테이너 내부 네임스페이스에 eth0 이라는 이름으로 할당됩니다. 나머자 하나는 vethXXXX 라는 이름으로 docker0 브리지에 바인딩 됩니다.

컨테이너를 실행할 때 브리지 네트워킹 모드를 사용하려면 별다른 설정을 추가할 필요 없습니다. 기본값이 브리지 네트워킹 모드이기 때문입니다.

docker run -i -t --rm --name network_bridge ubuntu:18.04

정상적으로 실행되면, 쉘이 나타나고, 명령어를 입력할 수 있습니다. 우분투 이미지에서 네트워크 관련 도구가 설치되어 있지 않기 때문에, 필요한 도구들을 설치해 줍니다.

apt-get update
apt-get install net-tools
apt-get install iproute2
root@6a5f6efd7f52:/# ifconfig

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 3899  bytes 19574414 (19.5 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3318  bytes 224386 (224.3 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
root@6a5f6efd7f52:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

컨테이너 내부의 eth0 인터페이스의 번호가 4번인것을 확인 할 수 있습니다.

새로운 터미널을 열어서, 호스트에서 인터페이스를 조회해 보겠습니다.

root@magi:~# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:15:0e:00 brd ff:ff:ff:ff:ff:ff
    inet 192.168.21.39/24 brd 192.168.21.255 scope global dynamic eth0
       valid_lft 4714sec preferred_lft 4714sec
    inet6 fe80::215:5dff:fe15:e00/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:6c:3d:0f:04 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:6cff:fe3d:f04/64 scope link
       valid_lft forever preferred_lft forever
5: vethab05419@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 96:0e:66:36:cd:d0 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::940e:66ff:fe36:cdd0/64 scope link
       valid_lft forever preferred_lft forever

vethab05419 라는 새로운 인터페이스 생성된 것을 확인할 수 있습니다. vethab05419 와 컨테이너안의 eth0 인터페이스가 맺어져 있다는 것을 ethtool 을 이용하여 확인할 수 있습니다.

root@magi:~# ethtool -S vethab05419

NIC statistics:
     peer_ifindex: 4

peer_ifindex 가 4로 설정되어 있습니다. 앞서 컨테이너 안에서 확인한 eth0 인터페이스의 번호인 것을 알 수 있습니다.

컨테이너 게이트웨이

컨테이너의 게이트웨이를 확인해 보겠습니다. 컨테이너 안에서 route 명령어를 실행합니다.

root@6a5f6efd7f52:/# route

Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

출력된 결과처럼 컨테이너 내부의 모든 패킷은 default 인 172.17.0.1 로 가게 됩니다. 이 주소는 docker0 의 IP 입니다.

브리지 모드에 대한 자세한 정보를 얻고 싶다면, 다음과 같이 확인할 수 있습니다.

docker network inspect bridge

[
    {
        "Name": "bridge",
        "Id": "022cf8e85d00a623504732098d6c99f3a6bf74fbd632787b4d3bf70a1ad03256",
        "Created": "2020-07-28T11:56:57.739214Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "6a5f6efd7f5235d46d1ae332b003566494bb0e0255bf5edc05899b2e7c71191b": {
                "Name": "network_bridge",
                "EndpointID": "53666d16b8a932aaef1f241535ae2944758ad40cbbddb923d1a1621a286b3e2e",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

브리지 생성하기

브리지는 다음 명령어로 생성할 수도 있습니다.

docker network create --driver bridge <브리지 이름>

다음은 mybridge 라는 이름의 브리지를 생성하는 예제입니다.

docker network create --driver bridge mybridge 

생성한 브리지는 컨테이너를 실행할 때 --net 설정을 통해 사용할 수 있습니다.

docker run -i -t --name mybridge_container --net mybrdige ubuntu:18.04

Host Mode Networking

호스트 모드는 컨테이너가 호스트의 네트워킹 네임스페이스를 공유하고 있으며, 외부 네트워크에 직접 노출됩니다. 호스트의 IP 주소와 호스트의 TCP 포트 공간을 사용하여, 컨테이너 내부에서 실행 중인 서비스를 노출합니다.

컨테이너를 실행할 때 호스트 네트워킹 모드를 사용하려면 다음과 같이 --net=host 라고 설정하면 됩니다.

$ docker run -i -t --rm --net=host --name network_host ubuntu:18.04

이 네트워킹 모드는 간단하기 때문에, 개발자가 이해하기 쉽고, 사용하기 쉽습니다. 하지만 호스트 네트워크를 그대로 사용하기 때문에 동일한 네트워크 포트를 사용할 경우 충돌이 발생합니다. 동일한 포트를 사용하는 다수의 컨테이너를 하나의 호스트에서 실행할 경우, 포트 충돌이 발생하여 서비스가 시작되지 않을 수 있습니다.

apt-get update
apt-get install net-tools
apt-get install iproute2
root@docker-desktop:/# ifconfig
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:e3ff:fe89:ac76  prefixlen 64  scopeid 0x20<link>
        ether 02:42:e3:89:ac:76  txqueuelen 0  (Ethernet)
        RX packets 6303  bytes 257694 (257.6 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 13671  bytes 20016034 (20.0 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.65.3  netmask 255.255.255.0  broadcast 192.168.65.255
        inet6 fe80::50:ff:fe00:1  prefixlen 64  scopeid 0x20<link>
        ether 02:50:00:00:00:01  txqueuelen 1000  (Ethernet)
        RX packets 146158  bytes 147055882 (147.0 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 77952  bytes 6736462 (6.7 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 2  bytes 140 (140.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2  bytes 140 (140.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

호스트 모드에 대한 자세한 정보를 얻고 싶다면, 다음과 같이 확인할 수 있습니다.

docker network inspect host

[
    {
        "Name": "host",
        "Id": "8c897fb9e7da39ec5c0cbceaf25935d9fd32cd9cca5eea5a665085eb7a793070",
        "Created": "2019-01-04T16:21:19.013894618Z",
        "Scope": "local",
        "Driver": "host",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": []
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

None Mode Networking

none은 말 그대로, 네트워크를 사용하지 않는 다는것을 의미합니다. none 네트워크로 설정을 하면, 컨테이너에는 lo 인터페이스만 나타납니다. 이 모드로 설정된 컨테이너는 외부와 단절 됩니다.

docker run -i -t --rm --name network_none --net none ubuntu:18.04


Container Mode Networking

컨테이너 네트워크를 사용하면, 다른 컨테이너의 네트워크 환경을 공유할 수 있습니다.

--net container:<다른 컨테이너 이름 또는 아이디>
docker run -i -t --rm --name network_container_1 ubuntu:18.04

root@1fae52d25cbd:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 26111  bytes 38303216 (38.3 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 9567  bytes 526098 (526.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

다음과 같이 ifconfig 명령을 실행해보면, 동일한 네트워크를 사용하고 있다는 것을 확인할 수 있습니다.

docker run -i -t --rm --name network_container_2 --net container:network_container_1  ubuntu:18.04

root@1fae52d25cbd:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 13034  bytes 19150595 (19.1 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4542  bytes 250008 (250.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

도커 구조

도커 구조

도커 초기에는 LXC(LinuX Container)를 기반으로 구현하였습니다. 그리고 0.9 버전부터 LXC를 대신한는 libcontainer를 개발하여 사용하고 있습니다.

다음 그림은 도커 0.9 버전의 구조입니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c21284c-197c-4393-8448-a00d37533ecc/docker-execdriver-diagram.png

출처 : https://www.docker.com/blog/docker-0-9-introducing-execution-drivers-and-libcontainer/

도커 1.11 버전부터 containerdrunC 를 컨테이너 런타임으로 사용합니다. 앞서 사용한 libcontainer 프로젝트는 OCI에 기부되었고, runc 라는 이름으로 바뀌게 됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5af65736-5226-47ae-9c67-af23bc193744/docker-containerd.png

dockerd

dockerd 는 컨테이너를 지속적으로 관리하는 데몬 프로세스로서, docker CLI 같은 클라이언트가 사용할 수 있는 RESTful API를 제공하고 있습니다. 흔히 명령어로 사용하는 docker 실행 파일이 docker CLI 입니다. 사용자가 입력한 docker 명령어는, 이 dockerd 로 전달되고, 실행됩니다.

dockerd는 unix, tcp, fd의 세 가지 소켓 유형을 통해, 도커 API 요청을 수신할 수 있습니다.

containerd

containerd는 이미지를 push 하고 pull 하고, 스토리지를 관리하고, 네트워크 기능을 정의할 수 있는 독립 실행형 고수준(high-level) 컨테이너 런타임입니다. runc 같은 저수준(low-level)의 컨테이너 런타임에 해당 명령을 전달하여, 컨테이너를 실행하는 등의 라이프사이클을 관리합니다.

도커에 의해 빠르게 확산되고 있던 컨테이너 환경에서, 컨테이너 런타임을 특정 벤더에 의존하지 않고, 중립적인 입장에서 컨테이너 표준에 맞게 구현하는 것을 목적으로 만들어졌습니다.

Docker Inc는 2016 년 12 월 컨테이너 런타임 부분을 독립적인 오픈 소스 프로젝트인 containerd 로 분리하여, 마이크로 소프트, Google, AWS, IBM 등과 공동으로 개발하기로 발표하였습니다.

그리고, 2017년 3월에는 CNCF (Cloud Native Computing Foundation)에 기부되었고, 이후 이를 담당해왔습니다.

도커는 1.11 이후 버전부터 containerd를 컨테이너 런타임으로 사용하고 있습니다.

containerd-shim

containerd-shimrunc를 실행하고, 컨테이너 프로세스를 제어하는 경량 데몬입니다. 컨테이너와 containerd 의 모든 통신은 containerd-shim 을 통해서 이루어 집니다.

containerd-shim 은 보통 다음과 같은 역할을 담당합니다.

  • 컨테이너의 stdout 및 stderr의 스트림을 제공해 주고 있습니다. 그래서 containerd 가 재시작 중에도 문제가 발생하지 않습니다. containerd 는 stdout 및 stderr의 스트림을 받아서 로그 파일로 저장을 할 수 있습니다.
  • runc 는 컨테이너 프로세스를 실행(fork)한 다음, 포그라운드 프로세스를 종료하여, 컨테이너 프로세스를 의도적으로 데몬화 합니다. 이렇게 되면, 컨테이너 프로세스는 호스트의 init 프로세스가 담당하게 되어서, 컨테이너의 관리가 어려워집니다. 이 문제를 해결하기 위해 shim 프로세스를 subreaper로 만들어서, 컨테이너 프로세스를 shim 프로세스가 관리하도록 합니다.

runc

OCI 런타임 스펙을 구현하고 있는 저수준 컨테이너 런타임입니다. 저수준 컨테이너 런타임이라고 부르는 이유는, 오직 실행 중인 컨테이너 관리에만 그 범위를 집중시키고 있기 때문입니다. 리눅스 커널의 네임스페이스와 cgroups 을 사용하여 격리시키는 기능을 제공합니다. 컨테이너를 생성(spawning)과 실행(running) 할 수있는 CLI로 구현되어 있습니다.

runc 는 도커 프로젝트(이전 이름은 libcontainer)에서 나와, OCI에 기부되었고, 이후 이를 담당해 왔습니다.

dockerdcontainerd 확인해 보기

도커가 설치된 호스트에서 프로세스를 조회해 보면, dockerdcontainerd 가 작동중인 것을 확인할 수 있습니다.

다음은 우분투 18.04 버전에 설치한 도커 19.03.12 버전인 경우 조회해 본 결과입니다.

dockerd 를 조회해 보았습니다.

sudo systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-07-25 06:31:07 UTC; 3h 11min ago
     Docs: <https://docs.docker.com>
 Main PID: 3375 (dockerd)
    Tasks: 8
   CGroup: /system.slice/docker.service
           └─3375 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242010835Z" level=warning msg="Your kernel does not support swap memory lim
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242240538Z" level=warning msg="Your kernel does not support cgroup rt perio
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242342040Z" level=warning msg="Your kernel does not support cgroup rt runti
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242520743Z" level=info msg="Loading containers: start."
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.430146872Z" level=info msg="Default bridge (docker0) is assigned with an IP
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.580818585Z" level=info msg="Loading containers: done."
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.630878720Z" level=info msg="Docker daemon" commit=48a66213fe graphdriver(s)
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.636328410Z" level=info msg="Daemon has completed initialization"
Jul 25 06:31:07 magi systemd[1]: Started Docker Application Container Engine.
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.677294494Z" level=info msg="API listen on /var/run/docker.sock"

containerd 를 조회해 보았습니다.

sudo systemctl status containerd.service
containerd.service - containerd container runtime
   Loaded: loaded (/lib/systemd/system/containerd.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-07-25 06:31:05 UTC; 3h 11min ago
     Docs: <https://containerd.io>
 Main PID: 3118 (containerd)
    Tasks: 9
   CGroup: /system.slice/containerd.service
           └─3118 /usr/bin/containerd

Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738189803Z" level=info msg="loading plugin "io.containerd.grpc.v1.images
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738198104Z" level=info msg="loading plugin "io.containerd.grpc.v1.leases
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738208904Z" level=info msg="loading plugin "io.containerd.grpc.v1.namesp
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738217304Z" level=info msg="loading plugin "io.containerd.internal.v1.op
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744531308Z" level=info msg="loading plugin "io.containerd.grpc.v1.snapsh
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744556608Z" level=info msg="loading plugin "io.containerd.grpc.v1.tasks"
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744565509Z" level=info msg="loading plugin "io.containerd.grpc.v1.versio
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744574009Z" level=info msg="loading plugin "io.containerd.grpc.v1.intros
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.745122618Z" level=info msg=serving... address="/run/containerd/container
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.745135418Z" level=info msg="containerd successfully booted in 0.165150s"

dockerdcontainerd 가 분리되어 있기 때문에, 도커 버전을 올릴 때 재시작 하여도, 컨테이너의 재시작 없이 사용할 수 있습니다.

컨테이너 실행 과정 살펴보기

pstree 명령어를 이용하여, 프로세스를 트리 모양으로 확인해 보겠습니다.

sudo pstree

실행중인 컨테이너가 없다면, 다음과 같은 결과를 확인할 수 있습니다.

systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd───8*[{containerd}]
        ├─cron
...

nginx 컨테이너를 실행해 보겠습니다.

sudo docker run -d --name nginx nginx:latest

docker ps 명령어를 실행하여, 실행중인 컨테이너를 확인할 수 있습니다.

sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
79c0a9468509        nginx:latest        "/docker-entrypoint.…"   5 seconds ago       Up 4 seconds        80/tcp              nginx

pstree 명령어를 이용하여, 프로세스를 트리 모양으로 확인해 보겠습니다.

sudo pstree

containerd의 자식 프로세스로 containerd-shim 이 생성된 것을 알 수 있습니다.

systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd─┬─containerd-shim─┬─nginx───nginx
        │            │                 └─9*[{containerd-shim}]
        │            └─8*[{containerd}]
        ├─cron
        ├─dbus-daemon

앞서 살펴본, docker run 을 이용한 nginx 컨테이너의 실행 과정을 정리하면 다음과 같습니다.

  • docker 명령어, 즉 docker CLI 를 실행하면, dockerd 로 요청을 전달합니다.
  • dockerd는 gRPC를 통해 containerd 요청을 전달합니다.
  • containerdexec를 통해, containerd-shim 을 자식으로 생성합니다.
  • containerd-shimrunc 를 이용하여, 컨테이너를 생성하고 실행합니다.
  • runc 는 컨테이너가 정상적으로 실행되면 종료됩니다.
  • containerd-shim 은 컨테이너에서 실행되는 프로세스의 부모가 됩니다.

도커에는 컨테이너 안에 프로세스를 새로 실행할 수 있는 docker exec 라는 명령어가 있습니다.

다음 명령어를 실행하여, nginx 컨테이너에 bash 쉘을 추가로 실행해 보겠습니다.

sudo docker exec -it nginx bash

nginx 컨테이너 bash 쉘이 실행되고, -it 옵션을 사용했기 때문에 터미널에서 쉘을 이용할 수 있습니다.

다른 터미널을 열어서, pstree 를 실행해 보겠습니다.

pstree
systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd─┬─containerd-shim─┬─bash
        │            │                 ├─nginx───nginx
        │            │                 └─9*[{containerd-shim}]
        │            └─8*[{containerd}]
        ├─cron
...

containerd-shim 프로세스의 자식 프로세스로 bash 프로세스가 추가된 것을 확인할 수 있습니다.

참고 문서

우분투에 Docker 설치하기

Ubuntu Bionic 18.04 (LTS) 에 도커 설치하기

사전 확인

이전 버전 삭제하기

만약 이전 버전의 도커가 설치되어 있다면 먼저 삭제해야합니다. 패키지명은 docker, docker.io 또는 docker-engine 입니다

sudo apt-get remove docker docker-engine docker.io containerd runc

도커 설치하기

리포지토리를 이용하여 설치하기

새로운 호스트 시스템에, 처음으로 도커 엔진을 설치하기 위해서는, 도커 리포지토리를 설정해야 합니다. 설정 후후 리포지토리를 이용하여 도커를 설치하고 업데이트할 수 있습니다.

리포지토리 설정하기

https를 통해 리포지토리를 사용할 수 있도록, 적절한 패키지 인덱스 및 설치 패키지 업데이트 합니다.

sudo apt-get update

sudo apt-get install \\
    apt-transport-https \\
    ca-certificates \\
    curl \\
    gnupg-agent \\
    software-properties-common

도커의 공식 GPG key를 추가합니다

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

도커를 설치하기 위한 리포지토리를 추가합니다.

sudo add-apt-repository \\
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \\
   $(lsb_release -cs) \\
   stable"

도커 설치하기

apt 패키지 인덱스를 업데이트하고, 도커 엔진과, containerd 를 설치합니다.

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli 

도커 확인하기

docker 를 일반 사용자 계정에서 사용하려면, docker 그룹에 사용자 계정을 추가해 줘야합니다.

sudo usermod -aG docker <your-username>

로그 아웃을 한 다음, 다시 접속을 해야 설정이 적용됩니다.

docker info 명령어를 실행하여, 설치된 도커의 정보를 확인할 수 있습니다.

docker info

정상적으로 실행되면, 다음과 같은 결과를 확인할 수 있습니다.

Client:
 Debug Mode: false

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 0
 Server Version: 19.03.12
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
 init version: fec3683
 Security Options:
  apparmor
  seccomp
   Profile: default
 Kernel Version: 4.15.0-76-generic
 Operating System: Ubuntu 18.04.4 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 1
 Total Memory: 3.852GiB
 Name: magi
 ID: 53WF:R22P:ZLLX:5564:H5MV:2WDA:S7UK:CK5Y:G647:TH6W:D74Z:UPDZ
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: <https://index.docker.io/v1/>
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false

WARNING: No swap limit support

우분투 18.04에 네트워크 카드 추가하기

우분투 18.04 부터는 네트워크 카드를 설정하려면, netplan을 사용해야 합니다.

네트워크 인터페이스 추가

네트워크 인터페이스를 추가한 다음, ifconfig 명령어를 실행해 봅니다. 별다른 옵션을 주기 않았기 때문에, 추가된 네트워크 인터페이스가 보이지 않습니다.

ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 944  bytes 67408 (67.4 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 944  bytes 67408 (67.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

추가한 네트워크 인터페이스 확인

ifconfig -a 명령어를 이용하여, 추가한 네트워크 인터페이스를 확인할 수 있습니다.

ifconfig -a

eth0 이 추가된 것을 확인할 수 있습니다.

eth0: flags=4098<BROADCAST,MULTICAST>  mtu 1500
        ether 00:15:5d:15:0e:00  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 944  bytes 67408 (67.4 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 944  bytes 67408 (67.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

설정에 네트워크 인터페이스를 추가하기

/etc/netplan/*.yaml 파일에 네트워크 설정을 추가해 줘야 합니다

cat /etc/netplan/*.yaml
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets: {}
    version: 2

현재 설정된 네트워크 인터페이스가 없기 때문에, ethernets 필드에 아무런 값이 없습니다.

/etc/netplan/*.yaml 파일에 네트워크 설정을 추가합니다.

sudo vi /etc/netplan/*.yaml

ifconfig -a 명령어를 통해서 확인한 네트워크 인터페이스 이름을 ethernets 필드에 추가하고, 설정값을 지정해줍니다. 아래 예제에서는 dhcp 를 사용하기 때문에, IP 직접 명시하지 않았습니다.

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            addresses: []
            dhcp4: true
    version: 2

만약 고정 IP를 사용한다면 아래처럼 설정할 수 있습니다.

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            addresses: [192.168.21.100/24]
            gateway4: 192.168.56.1
            nameservers:
                    addresses: [8.8.8.8,8.8.4.4]
            dhcp4: no
            dhcp6: no
    version: 2

설정 적용하기

다음 명령어를 실행하여, 변경한 설정을 적용합니다.

sudo netplan apply

설정이 적용되면 ifconfig 명령어를 실행하여, 적용한 내용을 확인할 수 있습니다.

ifconfig

정상적으로 적용되었다면, 추가한 eth0 을 확인할 수 있습니다.

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.21.39  netmask 255.255.255.0  broadcast 192.168.21.255
        inet6 fe80::215:5dff:fe15:e00  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:15:0e:00  txqueuelen 1000  (Ethernet)
        RX packets 47668  bytes 70216376 (70.2 MB)
        RX errors 0  dropped 6  overruns 0  frame 0
        TX packets 9362  bytes 772780 (772.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 120  bytes 9592 (9.5 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 120  bytes 9592 (9.5 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Graceful Shutdown Pods in a Kubernetes Cluster

애플리케이션 서버를 직접 클라이언트에 노출하지 않는 것이 좋습니다. 로드 밸런서나, 리버스 프록시 같은 것을 중간에 둬서 서비스 트래픽을 제어할 수 있도록 합니다. 애플리케이션을 종료할 필요가 있을 경우, 로드 밸런서나 리버스 프록시에서 해당 애플리케이션을 제외 시켜버리면, 서비스 트랙이 해당 애플리케이션으로 전달되지 않으므로, 애플리케이션의 입장에서는 새로운 요청을 들어오지 않게됩니다.

로드 밸런서에 특정 서버를 제외할 수 있는 방법은 크게 두가지 있습니다.

  • 로드 밸런서에 서버를 제외 시키는 명령어를 보내기
  • 로드 밸런서에서 서버의 상태를 체크하여, 정상 상태가 아닐 경우 자동으로 제외하기

일반적으로는 두 번째 방법을 많이 사용합니다. 로드 밸런서에서 주기적으로 서버들을 체크하여, 정상 상태가 아닐 경우, 자동으로 해당 서버를 제외시켜버리는 것입니다. 그리고 정상 상태로 돌아올 경우 다시 로드 밸런서에 해당 서버를 추가 시킵니다.

쿠버네티스의 상태 프로브(Health Probe)

쿠버네티스는 컨테이너의 상태를 주기적으로 진단합니다. 각 노드에 설치된 kubelet 에 의하여 컨테이너의 상태를 진단하게 됩니다. 컨테이너의 상태를 진단하기 위하여, 다음과 같은 세가지 타입의 핸들러를 제공하고 있습니다.

  • HTTP : 지정한 경로와 포트에 HTTP GET 요청을 수행합니다. HTTP 응답 코드가 200 보드 크거나 같고, 400보다 작으면 진단이 성공한것으로 간주합니다.
  • TCP : 지정한 포트에 대하여 TCP 검사를 수행합니다. 포트가 활성화 되어 있으면, 진단이 성공한 것으로 간주합니다.
  • Exec : 컨테이너 안에서 지정한 명령어를 실행합니다. 명령어의 종료 코드가 0이면 진단이 성공한것으로 간주합니다.

라이브니스 프로브(Liveness Probe)

애플리케이션이 교착 상태 같은 비정상 상태에 빠졌을 경우, 단순히 프로세스의 상태 확인 만으로는 정상인지 아닌지 판단하기가 어렵습니다. 애플리케이션이 정상 작동 하지 않는 상태에서도, 프로세스의 상태는 정상으로 나올 수 있기 때문입니다. 이런 종류의 문제와, 비즈니스 로직에 따른 다른 형태의 장애를 감지가기 위해서, 쿠버네티스는 라이브니스 프로브를 사용합니다.

쿠버네티스의 노드에 설치된 kubelet이 정기적으로 파드 컨테이너의 라이브니스 상태를 점검하게 됩니다. 만약 비정상 상태가 감지된다면, 컨테이너를 재시작 시킵니다.

애플리케이션의 상태를 확인 할 수 있는 방법은 다음과 같습니다.

애플리케이션이 정상상태인지 아닌지를 판단하는 것은 개발자의 구현에 달려있습니다. 정상상태가 아닐 경우, 컨테이너가 다시 시작된다는 사실을 잘 기억하고 있어야 합니다. 컨테이너 재시작이 아무 소용 없는 경우라면, 해당 라이브니스의 상태값은 아무런 도움이 되지 않기 때문입니다.

레디니스 프로브(Readiness Probe)

쿠버네티스는 파드(Pod)를 이용하여, 애플리케이션을 실행 시킵니다. 그리고 이 파드에 주어진 주소를 이용하여 애플리케이션에 접근할 수 있습니다. 하지만 파드란 것은 상황에 따라 생성되고 없어지는 성질을 가지고 있습니다. 예를 들어, 새로운 배포에 의해 기존 파드가 제거되고, 새로운 파드가 생성될 수 있습니다. 그리고 스케일 아웃을 하기 위하여, 파드의 개수를 늘리 수도 있습니다.

이러한 이유로, 파드의 주소 만으로는 애플리케이션에 접근하는게 쉽지 않습니다. 그래서 쿠버네티스에서는 서비스(Service) 라는 개념을 만들어서, 파드의 애플리케이션으로 접근할 수 있게 하였습니다. 서비스는 레이블 셀렉터를 이용하여, 자신에게 속한 포드들의 목록을 가져올 수 있습니다. 그래서 파드들이 생성되고, 사라지더라도 대상 파드들 중 하나에게 트래픽을 보낼 수 있습니다. 하지만 파드들이 실행중에 있더라도, 무조건 트래픽을 보내면 문제가 생길 수 있습니다. 파드가 비정상 상태에 있을 수도 있기 때문입니다. 그래서 서비스는 파드의 상태를 체크할 필요가 있습니다. 이럴때 사용되는게 레니니스(Readniess) 프로브 입니다. 서비스는 파드의 레디니스(Readiness) 상태를 체크하고, 정상 상태일 때문에 해당 파드로 트래픽을 보냅니다. (정확히 말하면, 서비스가 트래픽을 보내는 것은 아니지만, 개념적으로는 그렇게 이해하시는게 편할것입니다.) 그리고 비정상 상황 파드일 경우, 트래픽을 보내는 대상에서 제외시켜버립니다.

정리하자면, 애플리케이션이 작업을 처리할 준비가 되어있는지를 판별하고, 준비가 되었다면 트래픽을 수신할 수 있도록 해줍니다.

파드 종료 생명주기

애플리케이션의 배포 단위는 파드입니다. 파드는 하나 또는 그 이상의 컨테이너로 이루어져 있습니다. 파드가 종료되면, 파드에 속한 컨테이너도 종료됩니다.

컨테이너가 종료될때 다음과 같은 절차를 따르게 됩니다.

  • 노드에 있는 kubelet이 파드에 정의한 preStop 훅(hook)을 호출합니다.
  • preStop 훅이 완료되면, kubelet 은 파드 컨테이너에서 실행중인 애플리케이션에게 SIGTERM 신호를 보냅니다.
  • kubelet 은 컨테이너가 종료될때 까지 유예 시간(기본값 30초) 동안 기다립니다. 그 후 SIGKILL 을 사용하여 프로세스를 강제로 종료 시킵니다. 이 유예 시간은 preStop 훅을 실행하는 시간까지 포함됩니다.
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5dc89ac3-b345-4f1f-aa3d-1a572ea8c46b/Container_Stop.png

이러한 흐름을 참조하여, preStop 훅이나, SIGTERM 신호를 이용하여, 애플리케이션이 정상적으로 종료될 수 있도록 정리 작업 같은 것을 할 수 있습니다. 예를 들면, 새로운 요청이 들어오지 못하게 하고, 이미 대기열에 있는 작업들의 처리가 완료될때까지 종료가 되지 않도록 기다리게 할 수 있습니다.

물론, preStop 훅이나, SIGTERM 신호를 이용하여 블록킹하더라도, 컨테이너가 종료되는것을 막을 수 없습니다. 유예 기간이 지나면 강제적으로 종료되기 때문입니다.

종료전 훅

preStop 훅은 컨테이너가 종료되기 전에 호출하는 블럭킹 호출입니다. SIGTERM 신호와 동일한 의미를 가지고 있으며, 컨테이너가 SIGTERM 신호를 처리하는 것이 불가능할 때 사용합니다.

preStop 훅 에서 사용할 수 있는 핸들러는 다음과 같습니다.

  • httpGet: 지정한 경로와 포트에 HTTP GET 요청을 수행합니다.
  • exec: 컨테이너 안에서 지정한 명령어를 실행합니다.

다음은 preStop 훅을 사용하는 컨테이너 예제입니다. 애플리케이션의 xxx 엔드포인트를 호출합니다.

apiVersion: v1
kind: Pod
metadata:
  name: pre-stop-hook
spec:
  containers:
  - name: demo-app
    image: xxxx
    lifecycle:
      preStop:
        httpGet:
          path: /shutdown
          port: 8080

SIGTERM 신호

컨테이너를 종료하려고 할때, kubelet 은 컨테이너에게 SIGTERM 신호를 보냅니다. SIGTERM 신호는 컨테이너한테, 종료할거라는 것을 미리 알려주는 역할을 합니다. SIGTERM 신호를 받은 애플리케이션은 가능한 한 빠르게 애플리케이션을 종료시켜야 합니다. 그리고 안전하고 깨끗하게 애플리케이션을 종료하기 위한 작업들도 진행할 수 있습니다.

SIGKILL 신호

SIGTERM 신호 후에도 컨테이너가 종료되지 않는다면, SIGKILL 신호를 발생시켜 강제로 종료시킵니다. SIGTERM 신호 발생 후, 지정된 유예 시간 동안 컨테이너가 종료되지 않으면, SIGKILL 신호를 발생시키는 것입니다. 이 유예 시간은 .spec.termicationGracePeriodSeconds 필드를 이용하여 설정할 수 있습니다.

종료 후 트래픽

파드의 정상적인 종료는, 종료 전에 들어온 트래픽을 처리하도록 보장할 수 있습니다. 하지만 파드가 종료된 후에도 트래픽을 계속 수신하여, 서비스에서 다운타임이 발생할 수 도 있습니다.

어떻게 해서 이런 문제가 발생하는지에 대해서 살펴보겠습니다. 다음 예제에서는 두 개의 포드가 실행되어 있고, 트래픽을 받기 위해서 하나의 서비스 가 있습니다. 트래픽은 서비스를 거처 두 개의 포드 중 하나로 전달됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6a050a05-fa7f-4b24-8141-7591171354c5/Kubernetes-service_(1).png

이 시점에서 파드 A1을 삭제한다가 가정하겠습니다. 파드 A1을 종료하기 위해서 kubectlpreStop 훅을 먼저 실행하게 되고, 파드 A1의 애플리케이션을 정상적인 종료 절차가 시작됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5217f1df-6adc-4491-9c7e-5c0ce1776eff/Kubernetes-preStop_(1).png

파드 A1의 종료를 위하여, preStop 훅을 호출 하는 시점에서도, 파드는 서비스에 등록되어 있을 수 있기 때문에 여전히 트래픽을 받을 수 있습니다.

파드 A1의 애플리케이션이 preStop 훅을 수신 받으면, 정상적인 종료를 위한 절차를 실행할 것입니다. 일반적인 종료 절차는, 이미 들어온 요청을 처리하고, 애플리케이션을 종료시키는 등의 작업일 것입니다. 그런데 이런 절차에 의해서, 애플리케이션이 종료되었는데, 새롭게 들어오는 추가 트래픽이 발생한다면, 오류를 발생시킬 수 있습니다.

그래서, 보다 안전하게 파드를 종료시키려면, 해당 파드를 서비스로부터 제거해야 하는 것입니다.

파드 종료 절차

API를 통해서 파드를 삭제한다고 가정해보겠습니다. API를 통해서 파드를 삭제하면, 메타데이터 서버에 파드가 삭제되길 원한다고 표시되기만 합니다. 이렇게 되면, 파드 삭제 통지가 전파가 됩니다. 그리고, 파드 삭제에 관련있는 시스템은 다음과 같은 처리를 하게 됩니다.

  • 파드를 실행하는 kubelet은 앞에 설명한 “파드 종료 생명주기”의 종료 절차를 시작하게 됩니다.
  • 엔드포인트 컨트롤러(endpoints controller) 가 유효한 엔드포인트 목록에서 파드를 제거하게 되고, 해당 서비스에서 파드가 제거됩니다.

여기서 중요한 점은, 여러 시스템이 관련이 있고, 서로 다른 노드에서 실행될 수도 있기 때문에, 이러한 절차가 병렬로 처리될 수도 있다는 것입니다. 그래서 종료 절차가 시작된 후에도 트래픽을 계속 수신할 수도 있는 것입니다.

문제 완화

불행히도 이러한 문제점을 완벽하게 해결할 수 있는 방법은 없습니다. 하지만 충분한 종료 절차 지연을 두어서, 대부분의 경우에는 이문제를 회피할 수 있습니다. 가장 간단한 방법은 preStop 훅에, sleep을 실행하여, 종료 절차를 지연시키는 것입니다.

다음은 preStop 훅에 지연을 추가한 매니페스트의 일부분입니다. 정확하게 몇초를 지연시키는것이 좋다는 것은 알 수 없습니다. 하지만, “Kubernete in Action”책에서 5-10초를 추천하였기 때문에, 예제에서는 5초를 사용하였습니다.

lifecycle:
  preStop:
    exec:
      command: [
        "sh", "-c",
        # Introduce a delay to the shutdown sequence to wait for the
        # pod eviction event to propagate. Then, gracefully shutdown
        "sleep 5",
      ]
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dcd36ceb-3ceb-4137-b11c-3c85e121b568/Kubernetes-sleep.png

preStop 훅에 의해서 5초간 지연을 시켰기 때문에, 유효한 엔드포인트 목록에서 파드를 제거하고, 해당 서비스에서도 파드를 제거하기 충분한 시간을 확보할 수 있게 됩니다.