쿠버네티스 전문가 양성과정 10주차 3일(2/22)

최수환·2023년 2월 22일
0

Kubernetes

목록 보기
44/75
post-thumbnail

Auto scailing

파드와 컨테이너의 리소스 제한

  • docker에서도 컨테이너를 생성할때 --memories나 --cpus옵션을 이용해서 컨테이너에 할당되는 리소스를 제한해서 생성했다.
  • 쿠버네티스에서도 request나 limit을 통해 컨테이너가 할당받는 리소스를 제한할 수 있다.
  • 만약 리소스를 제한하지 않는다면, 컨테이너는 호스트의 모든 리소스를 할당받기 때문에 리소스를 효율적으로 사용하지 못하게 된다.
  • request와 limit을 사용하려면 에드온으로 metrics-server를 설치해야 한다.

📒 리소스 제한 개념 참조

 kubectl describe pod 파드이름
 # 할당된 리소스를 확인할 수 있다.
 
 kubectl top pods
 # 현재 파드가 사용하고 있는 cpu,memory양을 알 수 있다.
 
 kubectl top nodes
 # 현재 노드가 사용하고 있는 cpu,memory양을 알 수 있다.
 
 kubectl describe node kube-node1
 # 노드에 할당된 리소스를 확인할 수 있다
 
 kubectl exec 파드이름 -- sha256sum /dev/zero
 # 해당 파드에 강제로 리소스의 부하를 준다  
 # 쓰레기 값을 계속 넣어서 용량을 계속해서 잡아먹는다

1 . 리소스 제한 예시

apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests: # 컨테이너가 할당 받을 수 있는 최소 리소스 
        memory: "64Mi"
        cpu: "250m"
      limits: # 컨테이너가 할당 받을 수 있는 최대 리소스
        memory: "128Mi"
        cpu: "500m"
  - name: log-aggregator
    image: images.my-company.example/log-aggregator:v6
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
  • request는 할당받을 수 있는 최소 리소스로, 컨테이너가 정의된 리소스를 다 사용하든 안하든 해당 용량만큼은 반드시 확보를 해둔다. 즉, request의 최소 리소스는 해당 컨테이너가 보장받는 리소스다. 용량을 확보를 못하게되면 해당 컨테이너는 실행하지 않는다.
  • limit은 최대 할당받을 수 있는 리소스로 컨테이너가 request만큼의 리소스를 할당 받고 이후 더 필요하면 커널에게 요청해 점차 리소스를 더 할당받으며, 최대 limit까지의 리소스를 할당받을 수 있다.

2 . limit만 설정한 예시

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod-lim
spec:
  containers:
  - name: myapp
    image: ghcr.io/c1t1d0s7/go-myweb:alpine
    resources:
      limits:
        cpu: 0.5
        memory: 20Mi

  • describe로 할당된 리소스를 보면 request를 설정하지 않았지만 자동으로 할당된것을 볼 수 있다.
  • limit만 설정하게 된다면 limit와 같은 용량으로 request가 설정된다

QOS (Quality of Service)

  • 서비스의 품질(QOS)을 높이기 위해서는 리소스에 제한을 두어야 한다.

Pod Quality of Service Classes

kubectl describe pod 파드이름
# 파드의 QOS의 클래스를 확인할 수 있다

1 . BestEffort

  • 가장 안좋은 것으로 limit나 request를 정의하지 않은 경우

2 . Burstable

  • request보다 limit이 큰 경우
  • request나 limit중 하나라도 있는 경우

3 . Guaranteed

  • 가장 좋은 경우로 request와 limit가 모두 존재해야한다.
  • request와 limit에 모두 cpu와 memory가 정의되어야 하며, request의 값과 limit의 값이 모두 같아야 한다.

kubelet이 리소스 부족으로 압박을 받기 시작하면 가장 먼저 종료하는 것은 BestEffort이고 그 다음은 Burstable을 종료시킨다.

제한된 리소스에 따라 종료시킬 파드의 우선순위를 고려해서 파드를 생성해야 한다.

📒 QOS 개념 참조

limit range

  • limit나 request와 다르게 별도의 리소스로 존재하는 것이다
  • 많이 사용하는 리소스는 아니다.

📒 리밋 레인지 개념 참조

kubectl get limits
# 생성한 리밋레인지 보기

kubectl describe limits 리밋레인지 이름
# 리밋레인지에 설정된 값 보기

1 . 리밋 레인지 예시

apiVersion: v1
kind: LimitRange
metadata:
  name: myapp-limitrange
spec:
  limits:
  - type: Pod # 파드 단위로 제한
    min:
      cpu: 50m
      memory: 5Mi
    max:
      cpu: 1
      memory: 1Gi
  - type: Container # 컨테이너 단위로 제한 
    defaultRequest: # 기본 request
      cpu: 100m
      memory: 10Mi 
    default: # 기본 limit 
      cpu: 200m
      memory: 100Mi
    min:
      cpu: 50m
      memory: 5Mi
    max:
      cpu: 1
      memory: 1Gi
    maxLimitRequestRatio:
      cpu: 4
      memory: 10
  - type: PersistentVolumeClaim # 볼륨 단위로 제한 
    min:
      storage: 10Mi
    max:
      storage: 1Gi

-> 리밋레인지에 리소스의 범위를 설정하고 해당 리소스를 생성하게되면 해당 범위에서만 생성이 가능하게 한다.
-> 범위를 넘어가는 리소스는 생성되지 않는다.
-> 컨테이너의 default 섹션은 만약 컨테이너를 생성할때 limit과 request를 설정하지 않으면 자동으로 해당 default의 값으로 limit과 request를 설정한다.

리소스 쿼터

  • 여러 사용자나 팀이 정해진 수의 노드로 클러스터를 공유할 때 한 팀이 공정하게 분배된 리소스보다 많은 리소스를 사용할 수 있다는 우려가 있다.
  • ResourceQuota 오브젝트로 정의된 리소스 쿼터는 네임스페이스별 총 리소스 사용을 제한하는 제약 조건을 제공한다. 유형별로 네임스페이스에서 만들 수 있는 오브젝트 수와 해당 네임스페이스의 리소스가 사용할 수 있는 총 컴퓨트 리소스의 양을 제한할 수 있다.

📒 리소스 쿼터 개념 참조

kubectl get quota # 생성한 쿼터 보기

1 . request와 limit을 지정한 쿼터

apiVersion: v1
kind: ResourceQuota
metadata:
  name: myapp-quota-cpumem
spec:
  hard:
    requests.cpu: 500m
    requests.memory: 200Mi
    limits.cpu: 1000m
    limits.memory: 1Gi

2 . 생성가능한 리소스의 개수를 제한한 쿼터

apiVersion: v1
kind: ResourceQuota
metadata:
  name: myapp-quota-object
spec:
  hard:
    pods: 10
    replicationcontrollers: 2
    secrets: 10
    configmaps: 10
    persistentvolumeclaims: 5
    services: 5
    services.loadbalancers: 1
    services.nodeports: 2
    nfs-client.storageclass.storage.k8s.io/persistentvolumeclaims: 2

HPA (Horizontal Pod Autoscaling)

  • 워크로드 리소스(예: 디플로이먼트 또는 스테이트풀셋)를 자동으로 업데이트하며, 워크로드의 크기를 수요에 맞게 자동으로 스케일링하는 것을 목표로 한다.


-> Deployment/RC/Sts등의 리소스를 통해 파드를 생성할 때 Replicas를 지정한다.
-> HPA가 이 Replicas의 수를 자동으로 조정해주는 구조이다.

-> HPA가 Replicas의 수를 조정하는 알고리즘

📒 HPA개념 참조

kubectl get hpa
# 생성한 hpa확인

1 . HPA사용 예시

apiVersion: autoscaling/v2 # 버전 유의 
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa-cpu
spec:
  scaleTargetRef: # replica수를 조정할 대상 설정 
    apiVersion: apps/v1
    kind: Deployment
    name: myapp-deploy-hpa
  minReplicas: 2 # replica 최소값
  maxReplicas: 10 # replica 최대값
  metrics: # replica수를 조정할 때 참조할 지표 설정 
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70 # 70퍼센트를 기준으로 스케일링 
  • CPU Utilization : CPU 평균 사용률을 퍼센트로 나타낸 것
  • HPA가 v2가 된지 얼마 안됬다.
    • v1은 metrics로 CPU Utilization만 참조 가능하다.
    • v2는 CPU Utilization 이외에도 다른 metrics들이 존재한다
  • 설정한 값인 70퍼센트를 넘어간다면 레플리카수를 증가시킨다.
  • CPU Utilization 70퍼센트는 request의 cpu값을 기준으로 한다. 절대 limit을 기준으로 하지 않는다

1-1 . 디플로이먼트 생성

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deploy-hpa
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp-deploy-hpa
  template:
    metadata:
      labels:
        app: myapp-deploy-hpa
    spec:
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine
        resources:
          requests: # metrics로 참조할 cpu사용률
            cpu: 50m
            memory: 5Mi
          limits:
            cpu: 100m
            memory: 20Mi
        ports:
        - containerPort: 8080
  • deployment로 파드 3개를 생성하면 각 파드는 최소 cpu 50m~ 최대 100m까지 사용할 수 있다.
  • HPA는 3개의 파드의 CPU 사용률의 평균을 계산한다.
  • HPA는 request의 cpu값을 기준으로 하기 때문에 3개의 파드의 CPU사용률이 총 150m라면 HPA는 CPU Utilization을 100%로 계산한다.
tmux # 여러 터미널을 띄우기 위해 tmux를 킨다
ctrl + b + " # 창을 세개로 분할한다

watch -n1 -d kubectl get hpa # hpa의 정보를 실시간으로 본다 
watch -n1 -d kubectl top pods # 파드의 cpu사용률을 실시간으로 본다 

kubectl exec 파드1 이름 -- sha256sum /dev/zero # 하나의 파드에 임의로 cpu용량 부하를 준다


-> 만약 임의로 한 파드에 용량의 부하를 줬을 경우 파드는 최대 limit값이 100까지 cpu를 사용할 것이고, 이것은 100/150 = 66%
이기 때문에 70%를 도달하지못해 scailing을 하지 않는다.

kubectl exec 파드2 이름 -- sha256sum /dev/zero
# 추가로 하나의 파드에 임의로 cpu용량 부하를 준다 


-> 만약 임의로 또 한 파드에 용량의 부하를 줬을 경우 3개의 파드 총 cpu사용률을 200이 될것이고, 200/150 = 133%이므로 replica의 수를 증가시킨다.

  • 위의 replica 증가 알고리즘 공식에 의해
    3*(133%/70%) = 5.7이므로 올림하여 replicas는 6이 된다.
  • 즉, 현재 3개의 파드에서 추가로 3개의 파드를 더 생성한다

만약 용량 부하가 걸린 파드를 삭제해서 cpu사용률을 낮추게 되면
autoscailing에 의해 파드의 개수를 최소개수까지 낮출것이다.
하지만 파드의 개수가 바로 변화되지 않는것을 알 수 있다.
이유는 바로 stabilizationWindowSeconds 때문이다.

  • 만약 cpu사용률이 69%~71%를 왔다갔다 한다고 가정하자.
    이때 만약 오토스케일링이 즉시 반응을 한다면 왔다갔다 함에 따라 계속해서 파드를 늘리거나 줄일것이다. 이런 scale과정이 오히려 리소스에 부하를 더 많이 주게 된다.
  • 따라서 stabilizationWindowSeconds를 설정해 오토스케일링이 바로 반응하지 않고 특정 시간만큼 기다렸다가 스케일을 진행한다.
    -> stabilizationWindowSeconds의 default값은 300초이다.
    -> 따라서 5분을 기다리면 파드가 최소개수로 낮춰지는 것을 볼 수 있다.

파드 스케줄러

파드를 노드에 배치하는 방법

📗 파드 스케줄러 개념 참조

nodename

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-rs-nn
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp-rs-nn
  template:
    metadata:
      labels:
        app: myapp-rs-nn
    spec:
      nodeName: kube-node1 # 특정 노드 지정
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine

-> 생성된 파드는 지정한 node1에만 배치된다
-> nodename은 특정 노드만 지정해서 사용해야하기 때문에 유연하지 않다. 따라서 보통 사용하지 않고 nodeselector를 사용한다.

nodeselector

kubectl label node kube-node1 gpu=lowend
kubectl label node kube-node2 gpu=highend
kubectl label node kube-node3 gpu=highend
# 각 노드에 레이블 설정 

kubectl get nodes -L gpu
# 설정한 gpu확인 

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-rs-ns
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp-rs-ns
  template:
    metadata:
      labels:
        app: myapp-rs-ns
    spec:
      nodeSelector: # label을 선택한다.
        gpu: highend # 해당 레이블을 가진 노드들을 선택 
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine

-> 노드 셀렉터에 의해 gpu: highend 레이블을 가진 node2,node3에만 파드가 배치된다.
-> nodename과 달리 노드이름을 지정하는 것이 아니라, 레이블을 통해 노드를 선택하기 때문에 유연하게 여러 노드를 선택할 수 있다.

어피니티

📗 어피니티 개념 참조

nodeAffinity

kubectl label node kube-node1 gpu-model=3080
kubectl label node kube-node2 gpu-model=2080
kubectl label node kube-node3 gpu-model=1660
# 노드에 레이블 설정 

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-rs-nodeaff
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp-rs-nodeaff
  template:
    metadata:
      labels:
        app: myapp-rs-nodeaff
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution: # 요청(hard)
            nodeSelectorTerms: # 노드의 레이블 선택, 반드시 하나를 선택해야하기 때문에 가중치가 없다 
            - matchExpressions:
              - key: gpu-model
                operator: In
                values:
                - '3080'
                - '2080'
          preferredDuringSchedulingIgnoredDuringExecution: # 요청(soft)
          - weight: 10 # 선호하는 정도, 여러개의 선호가 있다면 가중치로 순위를 매김  
            preference: 
              matchExpressions: # 노드의 레이블 선택 
              - key: gpu-model 
                operator: In
                values:
                - titan
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine
  • nodeaffinity를 실행하면 파드 두개는 node1 or node2에만 배치되는 것을 확인
    -> 만약 스케일을 통해서 파드 개수를 늘려도 마찬가지로 node1 or node2에만 배치
  • nodeselector랑 비슷하며, nodeaffinity는 복잡하기 때문에 보통 nodeselector를 사용한다.
  • 요청(soft) : 특정 노드를 선호하지만 되면 배치되고 아니여도 상관 x
  • 요청(hard) : 특정 노드에 반드시 배치되어야 한다.
    -> 요청한 노드가 더이상 파드를 받을 수 없거나, 해당 레이블을 가진 노드가 없다면 파드는 생성되지 않는다.

podAffinity / podAntiAffinity

1 . cache 파드 생성

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-rs-aff-cache
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp-rs-aff-cache
      tier: cache
  template:
    metadata:
      labels:
        app: myapp-rs-aff-cache
        tier: cache # 파드에 레이블 설정 
    spec:
      affinity:
        podAntiAffinity: # 파드가 서로 배척한다 
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: tier
                operator: In
                values:
                - cache
            topologyKey: "kubernetes.io/hostname"
            # 노드의 hostname으로 구역을 나눈다
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine

1-1 . front 파드 생성

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-rs-aff-front
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp-rs-aff-front
      tier: frontend
  template:
    metadata:
      labels:
        app: myapp-rs-aff-front
        tier: frontend # 파드에 레이블 설정 
    spec:
      affinity:
        podAntiAffinity: # 파드가 서로를 배척한다 
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: tier
                operator: In
                values:
                - frontend
            topologyKey: "kubernetes.io/hostname"
        podAffinity: # 파드가 붙어서 노드에 배치된다 
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: tier
                operator: In
                values:
                - cache 
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine

-> 하나의 노드에 cache파드와 front파드가 한 쌍으로 묶여서 배치된다. 다른 노드에도 마찬가지로 한 쌍으로 묶여서 배치된다.

  • 만약 파드 어피니티를 사용하지 않는다면 한 쌍으로 묶여서 배치되어야 할 파드들이 각 노드에 퍼져서 배치될 수 있다.
  • 물론, 노드간에도 네트워크 연결이 되어있기 때문에 떨어진 파드끼리 노드의 네트워크를 통해서 통신이 가능하다. 하지만 노드간 네트워크 통신은 네트워크 홉을 증가시키기 때문에 성능에 지대한 영향을 끼칠 수 있다.
  • 파드 어피니티를 통해 한 쌍으로 묶어서 하나의 노드에 배치하게되면 노드안에서 통신이 되기 때문에 노드 간 통신보다 성능이 좋다.

테인트(Taints)와 톨러레이션(Tolerations)

  • 지금까지 파드를 중심으로 생각했다면, 테인트와 톨러레이션은 노드를 중심으로 생각한다
  • 테인트와 톨러레이션은 쌍으로 작동한다.
  • 테인트는 국가의 입국 정책, 톨러레이션은 비자라고 생각하면 쉽다.
    = 테인트는 노드의 역할/정책, 톨러레이션은 파드에 부착되어서
    특정 톨러레이션이 있는 파드만이 노드의 정책에 의해 해당 노드에 배치된다.

📒 테인트와 톨러레이션 개념 참조

kubectl taint node kube-node1 env=production:NoSchedule
# taint 생성 
kubectl taint node kube-node1 env-
# taint 해제 
kubectl get nodes kube-control1 -o jsonpath='{.spec.taints}'
# 해당 노드의 taint 보기

1 . 위의 podAffinity / podAntiAffinity실습의 연장선이다.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myapp-rs-tol
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-rs-tol
      tier: backend
  template:
    metadata:
      labels:
        app: myapp-rs-tol
        tier: backend
    spec:
      affinity:
        podAntiAffinity: # cache 레이블을 가진 파드를 배척 
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: tier
                operator: In
                values:
                - cache
            topologyKey: "kubernetes.io/hostname"
      tolerations: # 톨러레이션 부여 
      - key: env 
        operator: Equal
        value: production
        effect: NoSchedule
      containers:
      - name: myapp
        image: ghcr.io/c1t1d0s7/go-myweb:alpine

-> 위의 podAffinity / podAntiAffinity실습에 의해서 node2,node3에 각각 cache,front 레이블을 가진 한쌍을 배치했었다.
-> podAntiAffinity설정에 의해 cache레이블을 가진 파드를 배척하기 때문에 node2,3에는 배치될 수 없다.
-> 따라서 node1에만 배치될 수 있는데 node1에 taint를 설정했기 때문에 taint가 가진 정책에 부합하는 tolerations를 가져야 된다.
-> taint정책에 부합하는 key,value를 가진 tolerations를 부여했기 때문에 node1에 배치되는 것을 볼 수 있다.

cordon과 drain

  • 유지보수나 서버증설을 위해 해당 노드를 재부팅 시켜야할때 해당 노드에는 파드가 존재하면 안된다. 따라서 커든을 통해 새로운 파드의 스케줄링을 막고 드레인을 통해 기존의 존재하는 파드들을 퇴거시킨다.
kubectl cordon kube-node3


-> 해당 노드에 스케줄링되는 것을 막는다.

kubectl drain kube-node3
kubectl drain kube-node3 --ignore-daemonsets

-> 해당 노드에 있는 모든 파드들을 퇴거(evict) 시킨다.
-> 하지만 데몬셋이 관리하는 파드들은 복제본 컨트롤러가 아니기 때문에 퇴거시키지 못한다. 따라서 --ignore옵션을 사용한다.
-> 다른 복제본 컨트롤러가 관리하는 파드들은 지워져도 다른 노드에 자동으로 생성되기 때문에 문제가 안된다 .

kubectl uncordon kube-node3 # 커든 해제 

💡 drain을 하게 되면 자동으로 커든이 되기때문에 작업이 끝나고나면 커든을 해제해야 한다.

📒 커든과 드레인 개념 참조

profile
성실하게 열심히!

0개의 댓글