개발놀이터

쿠버네티스 이론 : 파드 (Label, Node Scheduler) 본문

배포/kubernetes

쿠버네티스 이론 : 파드 (Label, Node Scheduler)

마늘냄새폴폴 2024. 8. 31. 21:31

이번 포스팅에선 쿠버네티스의 파드에 대해서 공부한 내용을 정리해보도록 하겠습니다. 

 

파드 (Pod)

파드란 쿠버네티스가 관리하는 최소 배포 단위입니다. 최소 배포 단위라는 것은 파드를 기준으로 애플리케이션을 구동한다는 의미입니다. 

 

파드 안에는 여러개의 컨테이너가 들어갈 수 있고 이 컨테이너 하나하나가 애플리케이션이 됩니다. 

 

쿠버네티스가 파드를 이용해서 컨테이너를 묶음으로 관리하는 것은 사용자의 니즈를 파악한게 아닐까 싶네요. 어떤 애플리케이션이던 혼자서 단독으로 돌아가는 애플리케이션은 실 사용 서비스에선 있을 수 없는 일이죠. 

 

하다못해 스프링부트만 하더라도 단독으로 내장톰캣을 이용해서 사용할 수 있지만 결국 HTTPS를 사용하려면 NGINX같은 웹서버가 있어야하고 정상적인 서비스라면 데이터베이스와 연결이 되어있을테니 MySQL같은 데이터베이스도 필요하죠. 

 

 

쿠버네티스는 노드라는 더 큰 범위에 파드를 놓고 그 파드 안에 컨테이너를 두어 컨테이너들을 관리합니다. 

 

파드는 생성될 때마다 IP가 자동으로 부여되고 관리되는데요. 우리는 이 파드에 연결하기 위해 {pod IP}:{pod Port} 이렇게 들어갈 수 있습니다. 

 

하지만 문제가 있는 것이 파드는 언제든지 삭제되고 다시 재생성될 수 있다는 것입니다. 재생성되면 파드의 IP가 바뀌게 되어 저런 방법으로는 연결할 수 없습니다. 

 

또한, 파드의 IP로 들어갈 수 있는건 쿠버네티스 클러스터 내부에서만 해당합니다. 만약 외부 사용자들이 파드에 접근하게 하려면 어떻게 해야할까요? 

 

서비스 (Service)

여기서 나오는 것이 서비스입니다. 물론 이번 포스팅에선 서비스에 대해서 간략하게만 짚고 넘어갈것입니다. 다음 쿠버네티스 이론인 "쿠버네티스 이론 : 서비스 (A to Z) 에서 자세히 다루도록 하겠습니다. 

 

사실 위에서 만든 쿠버네티스 클러스터의 모습은 많은 것이 빠져있습니다. 파드만 강조해서 보여드리기 위해서 저렇게 만들었지만 사실 위에 서비스라는 오브젝트가 하나 더 있습니다. 

 

 

이렇게 되어있으면 서비스의 IP를 이용해서 노드에 접근할 수 있고 노드에 배포되어있는 파드에 연결할 수 있습니다. 

 

그럼 서비스가 어떤 파드를 사용자에게 보여줘야하는지 알아야합니다. 저 노드에 배포되어있다고 하여 모든 컨테이너를 외부로 노출할 수는 없으니까요. 

 

여기서 잠깐 파드 설정파일을 한번 보도록 하겠습니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-2
  labels:
    type: web
    log: dev
spec:
  containers:
  - name: container
    image: tmkube/init

 

우리가 주목해서 봐야할 부분은 metadata에 labels 부분입니다. 쿠버네티스는 이 라벨을 이용해서 각 오브젝트들을 연결합니다. 비단 서비스 - 파드 뿐만 아니라 컨트롤러 - 노드, 컨트롤러 - 파드, 파드 - 볼륨 등등 다양한 오브젝트들이 라벨을 이용해서 연결합니다. 

 

라벨은 key와 value로 되어있습니다. 이렇게 되어있는 이유는 키-벨류 쌍으로 지정할 수도 있지만 키만 가지고도 매칭할 수 있도록 하기 위해서이죠. 

 

아무튼 라벨을 이용해서 서비스와 파드를 연결한다는 것이 중요한 것이죠. 여기서 서비스 설정 파일을 한번 확인하겠습니다. 

 

apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    type: web
  ports:
  - port: 9000
    targetPort: 8080
  type: LoadBalancer

 

selector에 type: web 이렇게 적음으로써 key가 type, value가 web인 파드를 연결하겠다는 의미입니다. 

 

서비스와 관련된 내용은 다음 쿠버네티스 이론 서비스편에서 더 자세히 다루도록 하겠습니다. 

 

이제 파드에 대해서 조금 더 자세히 깊숙하게 들어가보도록 하겠습니다. 

 

Node Scheduler

파드의 한 부분을 차지하는 그리고 쿠버네티스가 오브젝트를 관리하는 라벨에 대해서는 위에서 언급했으니 다른 파드의 특징에 대해서 알아보도록 하겠습니다. 

 

Node Scheduler (개요)

노드스케줄러는 파드가 생성될 때 어느 노드에 들어갈 것인지 자동으로 정해주는 쿠버네티스의 오브젝트 중 하나입니다. 물론 자동으로 선택되게 하는 것이 개발자가 손을 덜 타게 하지만 수동으로 어떤 노드에 들어갈 것인지 정해줄 수도 있습니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-3
spec:
  nodeSelector:
    hostname: node1
  containers:
  - name: container
    image: tmkube/init

 

이렇게 nodeSelector 속성을 이용해서 어떤 노드에 들어갈지 골라주면 됩니다. hostname 속성에 노드를 만들 때의 노드 이름을 적어주면 해당 파드는 node1에만 생성됩니다. 

 

하지만 앞서 언급했듯이 이렇게 하는 것은 조금 번거롭죠. 때문에 자동으로 노드스케줄러를 이용해서 자동으로 넣는 방법도 쿠버네티스가 제공해줍니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-4
spec:
  containers:
  - name: container
    image: tmkube/init
    resources:
      requests:
        memory: 2Gi
      limits:
        memory: 3Gi

 

노드스케줄러에의해 자동으로 파드를 배포되게 하려면 resources에 대한 정보가 있어야합니다. 왜냐하면 이 정보를 바탕으로 들어갈 수 있는 노드를 찾기 때문이죠. 

 

위와 같은 파드를 배포하려고 하는데 요청된 메모리가 2기가입니다. 그럼 최소 2기가가 필요하다는 의미인데, 만약 어떤 노드에 메모리가 1기가밖에 남아있지 않다면 이 노드에는 넣을 수 없기 때문이죠. 

 

혹시라도 메모리가 초과되는 노드에 배치되면 어떤일이 벌어질까요? 

 

컴퓨터는 같은 메모리 주소를 프로세스가 참조하는 것을 절대로 허용하지 않습니다. 즉, requests에서 요청한 메모리가 초과되면 모든 프로세스가 종료됩니다. 하지만 반대로 CPU가 request를 초과하는 것은 허용하죠. 왜 이런 일이 벌어지는지 운영체제의 입장에서 생각해보겠습니다. 

 

CASE1. 메모리가 초과되는 상황

만약 프로세스가 같은 메모리 주소를 참조하게 된다면 데이터의 정합성이 깨질 수 있습니다. 또한, race condition 상태가 되어 데이터가 꼬여버릴 수도 있죠. 그리고 서로의 데이터를 덮어쓰면서 문제가 생기기 때문에 프로세스들은 같은 메모리 주소를 바라보지 못하도록 OS레벨에서 이를 막습니다. 

 

CASE2. CPU가 초과되는 상황

반대로 CPU가 초과되는 상황에서는 조금 다릅니다. 프로세스는 CPU자원을 할당받을 때 고유값인 CPU time이라는 것을 부여받고 프로세스 자신이 처리해야 하는 일에 따라 CPU레지스터들이 할당됩니다. 

 

이 말이 어떤 말이냐하면 프로세스가 실행될 때 CPU자원을 고유하게 받는다는 것입니다. 때문에, 프로세스는 CPU자원을 사용함에 있어서 충돌이라는 개념이 존재하지 않습니다. 

 

이런 케이스를 잘 보여주는 상황이 있는데 바로 메모리 스왑입니다. 

 

OS는 자신에게 장착된 메모리를 전부 다 써버리게 되면 HDD나 SSD에서 스토리지 자원을 끌어다가 메모리처럼 사용하게 됩니다. 

 

이때 실제로 메모리는 아니고 메모리처럼 동작하도록 하는 것이기 때문에 메모리에 비해 속도가 안나오는 것은 사실이지만 메모리가 초과되었다고 서로 다른 프로세스에게 같은 메모리 주소를 주면 위에서 언급한 문제가 발생할 수 있습니다. 

 

때문에 OS는 메모리 스왑으로 메모리를 할당해주는 특징이 있습니다. 

 

 

다시 노드 스케줄러로 돌아와서, 이때문에 노드 스케줄러가 자동으로 파드를 할당하는 기준은 바로 노드에 남아있는 자원의 양입니다. 여기서는 CPU와 메모리가 주된 자원이 되겠죠. 

 

메모리나 CPU가 많이 남아있는 노드에 더 우선적으로 파드를 배치하는 것이죠. 쿠버네티스는 자체적으로 노드에 점수를 매겨서 높은 점수인 노드에 파드를 배치한다는 특징이 있습니다. 

 

여기까지 노드스케줄러에 대한 개요였습니다. 이후 나올 내용들은 조금 딥한 내용입니다. 한번 더 깊이있게 알아보도록하죠. 

 

Node Scheduler (심화)

노드 스케줄러는 앞서 언급했듯이 아무런 설정도 되어있지 않으면 자원이 널널한 노드를 자동으로 선택해서 파드를 배치합니다. 하지만 다양한 설정으로 노드 스케줄러에게 어떤 노드를 선택해야하는지 지시할 수 있습니다. 

 

이런 설정이 필요한 이유는 파드가 노드에 배치될 때 아무 노드에나 배치되면 안되는 상황이 있을 수 있기 때문입니다. 

 

즉, 어떤 노드는 데이터베이스 노드이고 어떤 노드는 WAS노드인데 WAS와 관련된 파드가 데이터베이스 노드에 들어가면 관리하기가 힘들기 때문이죠. 

 

노드 스케줄링에는 세가지 정책이 있습니다. 노드 직접 선택, Pod간 집중 / 분산 선택, Node에 할당 제한 이렇게 세가지인데요. 한번 자세히 알아보도록 하겠습니다. 

 

Node 선택

노드를 직접 선택해야 하는 방법으로는 NodeName, NodeSelector, Node Affinity 이렇게 세가지로 또 나뉩니다. 하나씩 천천히 알아보죠. 

  • NodeName :  노드의 이름을 직접 선택하는 방법이지만 많이 사용되지는 않습니다. 왜냐하면 파드와 마찬가지로 노드 또한 삭제되고 생성되는 과정이 일어날 수 있고 생성되는 과정에서 노드의 이름이 바뀔 수 있기 때문입니다. 
  • NodeSelector : 노드의 라벨을 지정해줘서 해당 라벨이 붙어있는 노드에 파드를 배치하는 방법입니다. 이 방법이 다좋은데 하나 불편한 점이 key와 value가 정확히 일치해야한다는 점입니다. 만약 일치하는 노드가 없는 경우 파드가 생성되지 않습니다. 
  • NodeAffinity : 노드의 key값만 있으면 해당 키가 있는 노드로 파드를 자동으로 배치해주는 특징이 있습니다. 

NodeName과 NodeSelector는 간단하니 NodeAffinity에 대해서 자세히 알아보겠습니다. 

 

Node Affinity는 노드의 키를 보고 규칙에 의해 파드를 배치한다는 특징이 있습니다. 때문에 우리는 matchExpressions라는 속성으로 어떤 노드에 배치되어야하는지 쿠버네티스에게 알려줘야합니다. 

 

여기서 잠깐 관련된 속성 한번 보고 가시죠!

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-match-expressions1
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoreDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - {key: kr, operator: Exists}
  containers:
  - name: container
    image: tmkube/app
    terminationGracePeriodSeconds: 0

 

위의 설정에서 자세히 봐야하는 부분이 matchExpressions 부분입니다. 위의 설정은 노드의 key가 kr인 노드가 존재하면 그곳에 파드를 생성하라는 의미입니다. 

 

여기서 requiredDuringSchedulingIgnoreDuringExecution 속성을 넣으면 반드시 kr노드에 들어가게 되고 kr노드가 없으면 파드를 생성하지 않습니다. 

 

반대로 preferredDuringSchedulingIgnoreDuringExecution 속성을 넣으면 kr노드를 선호하지만 kr노드가 없으면 다른 노드에 생성하겠다는 의미입니다. 

 

matchExpressions에는 operator라는 속성이 있는데 이 속성은 규칙을 부여하는 것입니다. 여러가지 operator가 있지만 몇개만 소개하겠습니다. 

 

  • Exists : 키가 존재하는 경우에 라벨을 매치시켜줍니다. 위의 예제라면 kr노드에 배치해주는 것이죠. 
  • DosesNotExists : 키가 존재하지 않는 경우에 라벨을 매치시켜줍니다. 위의 예제에선 kr이라는 키가 없는 노드에 배치해줍니다. 이 경우엔 en, jp 등이 될 수 있겠네요. 
  • In : 키와 벨류가 모두 일치하는 경우에 매치시켜줍니다. 위의 예제에선 나오지 않았지만 예를 들어서 key: kr, value: seoul 이렇게 매치해준다는 의미입니다. 
  • NotIn : In속성과 완전 반대되는 특징을 가집니다. 

 

Pod간 집중 / 분산

이 정책은 파드를 배치할 때 여러개의 파드가 하나의 노드에 무조건 할당되어야하거나 여러개의 노드에 분산되어 배치되어야할 때 사용할 수 있습니다. 

 

Pod간 집중과 분산 두가지 관점에서 알아보고 어떤 상황에서 사용될 수 있는지 정리해봤습니다. 

 

  • Pod Affinity : 이 속성은 파드에 집중 배치되어야할 때 사용할 수 있습니다. 주로 HostPath로 설정된 볼륨과 연결된 파드들을 배치할 때 사용합니다. 

    여기서 HostPath로 설정된 볼륨이란, 쿠버네티스에는 여러가지 볼륨(저장소)의 형태가 있는데 HostPath로 설정된 볼륨은 노드에 국한된 볼륨입니다. 즉, 노드를 벗어나면 이 볼륨을 사용할 수 없다는 말입니다. 이외에도 모든 곳에서 사용할 수 있는 PV (Persistant Volumes) 가 있지만 다다음 포스팅 주제인 쿠버네티스 이론 : 볼륨 (A to Z) 에서 자세히 다루도록 하겠습니다. 

    즉, Pod Affinity의 경우 특정 노드에서만 사용 가능한 HostPath볼륨이 설정되어있는 노드에 파드를 배포해야 하는 경우에 사용할 수 있다는 것입니다. 
  • Pod Anti Affinity : 이 속성은 Pod Affinity와 완벽히 반대되는 속성으로서 서로 다른 노드들이 한 노드에 배포되면 안되는 경우에 사용할 수 있습니다. 

    보통 마스터 슬레이브 구조에서 마스터가 다운되면 슬레이브가 승격되어야 하는 구조에서 사용할 수 있습니다. 

    마스터 슬레이브 구조에서는 마스터가 죽을 수도 있는 특정한 상황을 비관적으로 가정하기 때문에 마스터 파드가 죽어버려 노드 전체를 사용할 수 없을 때 만약 슬레이브도 같은 노드에 배포되어있다면 문제가될 수 있습니다.

    때문에 마스터 슬레이브 구조에선 서로 다른 파드들이 서로 다른 노드에 들어가야 하는 것이죠. 

Pod Affinity는 Node Affinity와 마찬가지로 matchExpressions로 노드를 설정해주는데 보통 여러개의 파드를 동시에 하나의 노드에 배포되어야할 때 사용하기 때문에 In속성을 주로 사용하게 됩니다. 

 

Pod Anti Affinity는 Pod Affinity와 완전히 반대로 동작합니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: server1
spec:
  affinity:
    podAffinity: 
      requiredDuringSchedulingIgnoreDuringExecution:
      - topologyKey: a-team
        labelSelector:
          matchExpressions:
          - {key: type, operator: In, value: [web1]}
  containers:
  - name: container
    image: tmkube/app
    terminationGracePeriodSeconds: 0

 

여기서 주의해야 하는 점이 matchExpressions로 설정되는 key와 value들은 노드의 라벨이 아닙니다. 파드의 라벨입니다. 

 

노드 스케줄러는 이 속성을 보고 파드의 라벨을 확인합니다. 그리고 키와 벨류가 맞는 파드를 발견하면 그 파드가 있는 노드에 새로운 파드를 배치해줍니다. 

 

Pod Affinity에선 topologyKey라는 속성을 추가할 수 있는데 이 속성은 노드 스케줄러가 이 키값을 가지고 있는 노드에서 마땅한 노드를 찾는 것을 도와줍니다. 

 

Node에 할당 제한

이 정책은 어떤 노드에 특정 파드만 들어가야 하는 경우가 있는 경우에 사용합니다. 예를 들어서 GPU를 많이 필요로하는 파드만 모아서 배치하는 경우가 그러한데 이럴 때 사용할 수 있는 속성입니다. 

 

Node에 할당을 제한하기 위해서는 Taint와 Toleration에 대해서 짚고넘어가야합니다. 

 

먼저 특정 파드만 들어와야하는 노드에 Taint속성을 집어넣습니다. 이렇게 하는 경운 해당 노드에는 파드를 정상적인 방법으로는 배치할 수 없습니다. 

 

노드 스케줄러에 의해 자동으로 배치되지도 않고, 심지어 노드의 이름을 직접 지정하는 경우도 배치되지 않습니다. 

 

이 노드에 배치되기 위해서는 Toleration이라는 속성이 추가된 파드만 해당 노드에 위치할 수 있습니다. 

 

한번 설정파일을 한번 보겠습니다!

 

그 전에 노드에 라벨을 설정하고 Taint설정을 해보겠습니다. 

 

kubectl label nodes k8s-node1 hw=gpu:NoSchedule

kubectl taint nodes k8s-node1 hw=gpu:NoSchedule

 

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-toleration
spec:
  nodeSelector:
    gpu: no1
  tolerations:
  - effect: NoSchedule
    key: hw
    operator: Equal
    value: gpu
  containers:
  - name: container
    image: tmkube/app
    terminationGracePeriodSeconds: 0

 

그리고 위와같이 노드를 Taint로 설정하면 어떤 방법으로도 파드를 배치할 수 없습니다. 

 

위와같이 tolerations 속성을 달아주면 Taint속성이 부여된 노드에 파드가 배치됩니다. 

 

이때 effect, key, operator, value 모든 값이 일치하여야 Taint속성의 노드에 배치됩니다. 이때 effect에 대해서 잠깐 짚고 넘어가겠습니다. 

 

  • NoSchedule : 우리가 노드를 만들 때 hw=gpu:NoSchedule 이렇게 설정할 때의 속성을 말합니다. hw=gpu 는 키와 벨류인 라벨을 말하는 것이고 NoSchedule은 effect를 말하는 것입니다. 

    NoSchedule은 해당 노드가 노드스케줄러에 의해 자동으로 배치되지 않는 것을 의미합니다. 
  • NoExecute : NoSchedule의 특징은 해당 노드에 NoSchedule이 적히면 노드스케줄러에 의해 파드가 배치되지 않긴 하지만 이미 해당 노드에 파드가 배치되어 있는 경우에는 이 파드들은 계속 돌아갑니다. 

    하지만 NoExecute를 설정하면 이 설정이 부여되는 즉시 그 노드에 존재하는 toleration이 없는 파드들이 전부 삭제됩니다.

    모든 파드가 삭제되긴 하지만 모든 파드들이 삭제되고 다른 노드에 배치되기 때문에 아예 이용이 불가능한 것은 아닙니다. 

 

마치며

원래 파드에 대한 모든 내용을 한 포스팅에 담으려고 했는데 너무 많아졌네요... 노드 스케줄링만 거의 모든 포스팅을 차지하네요. 다음 포스팅은 이번 포스팅과 이어지는 내용입니다. 파드에 대해서 계속 자세히 다뤄보도록 하겠습니다.