Kubernetes/쿠버네티스 모범 사례 스터디

리소스 관리

막이86 2023. 11. 14. 15:23
728x90

쿠버네티스 모범 사례을 요약한 내용입니다.

  • 쿠버네티스 리소스를 관리하고 최적화하는 모범 사례를 살펴보기
  • 워크로드 스케줄링, 클러스터 관리, 파드 리소스 관리, 네임스페이스 관리, 애플리케이션 확장에 대해 논의
  • 어피니티(Affinity), 안티어피니티(AntiAffinity), 테인트(Taint), 톨러레이션(Toleration), 노드 셀렉터(NodeSelector)를 이용한 고급 스케줄링 기술을 알아보기
  • 리소스 제한과 요청, 파드 서비스 품질, PodDisruptionBuget, LimitRanger, 안티어피니티 정책을 알아보기

8.1 쿠버네티스 스케줄러

  • 스케줄러는 컨트롤 플레인에서 호스팅되는 핵심 컴포넌트
  • 스케줄러는 파드를 클러스터에 어떻게 배치할지를 결정
  • 클러스터와 사용자가 정의한 제약에 따라 리소스를 최적화
  • 논리 조건과 우선순위 기반의 스코어 알고리즘 사용

8.1.1 논리 조건

  • 스케줄링할 노드를 결정할 때 사용하는 첫 번째 기능은 논리 조건 함수
    • 강한 제약을 내포하고 있고 참 또는 거짓을 반환
  • ex) 파드가 4GB 메모리를 요청할 때 특정 노드가 요건을 만족하지 못하면 파드 스케줄링 후보에서 제거됨
  • 노드가 스케줄링 불가로 설정된 상태라면 후보에서 제거됨
  • 아래와 같은 논리 조건들이 있음

8.1.2 우선순위

  • 상대적인 값을 기반으로 모든 유효한 노드의 순위를 매김
  • 우선순위 점수를 합산하여 최종 우선순위 점수를 노드에 부여
    • ex) 파드 하나에 600밀리코어가 필요한 경우, 두 개의 노드중 하나는 900밀리코어, 하나는 1800밀리 코어라면 후자가 더 높은 우선순위를 가짐
  • 노드가 같은 우선순위를 반환한다면 스케줄러는 selectHost()함수를 사용하여 라운드 로빈 토너먼트 방식으로 노드를 선정
  • 아래와 같은 우선순위 조건들이 있음

8.2 고급 스케줄링 기술

  • 쿠버네티스는 스스로 파드 스케줄링을 최적화함
    • 넉넉한 리소스를 가진 노드에게만 파드를 배치
    • 균등한 리소스 사용률을 유지하면서 레플리카셋의 파드를 여러 노드에 분산시켜 가용성을 높임
  • 쿠버네티스의 리소스 스케줄링 방식을 변경할 수 있음
    • 영역 실패로 인한 장애를 막기 위해 가용한 영역 간에 파드를 분산 시킬 수 있음
    • 여러 파드를 단일 호스트에 배치하여 성능을 향상 시킬 수 있음

8.2.1 파드어피니티와 안티어피니티

  • 파드간의 배치 규직을 설정 할 수 있음
  • 스케줄링 방식을 변경하거나 스케줄러의 배치 결정을 오버라이드 할 수 있음
  • ex) 안티어피니티 규직으로 레플리카셋의 파드를 여러 데이터 센터 영역에 분산 시킬 수 있음
  • 파드어피니티 예제 - 동일한 노드에 파드를 스케줄링 할수 있음
  • 안티어피니티 예제 - 동일한 노드에 파드를 스케줄링 하지 못하게 할수 있음
  • 안티어피니티 규직을 설정하는 예제
    • 파드안티어피니티가 설정되어 있으면 스케줄러는 한 노드에 여러 레플리카를 배치하지 않음

8.2.2 노드 셀렉터

  • 특정 노드에 파드를 스케줄링하는 가장 간단한 방식
  • 키/값 쌍이 있는 레이블 셀렉터를 이용하여 스케줄링을 결정
    • ex) GPU같은 특수한 하드웨어를 가진 노드에 파드를 스케줄링할 수 있음
  • 노드 셀렉터는 노드 테인트도 함께 사용할 수 있음
    • GPU 워크로드 전용 노드를 예약하고 노드 테인트를 사용하여 GPU가 있는 노드를 자동으로 선택할 수 있음
    <aside> 💡 노드 테인트는 오직 GPU 워크로드를 위한 용도로 노드를 제한<aside> 💡 노드 셀렉터는 GPU가 가용한 노드를 요청할 때 사용
  • </aside>
  • </aside>
  • 노드에 레이블을 생성하고 파드 명세에서 노드 셀렉터를 사용하는 예제
    • 노드 셀렉터를 상요하면 disktype=ssd 레이블을 가진 도으에게만 파드를 스케줄링함
    kubectl label node <node_name> disktype=ssd
    
    • 파드 명세서

8.2.3 테인트와 톨러레이션

  • 테인트는 파드가 스케줄링되는 것을 거절하기 위해 노드에 사용됨
  • 안티어피니티와 같지만 다른 방식과 용도로 사용이 됨
    • ex) 특정 노드에 특정 성능 요건을 가진 파드만 필요하고 그 외의 다른 파드는 스케줄링 하지 않는 상황
  • 테인트는 톨러레이션과 함께 동작
    • 두 조합으로 안티어피니티 규칙을 세밀하게 조정할 수 있음
  • 일반적으로 다음과 같은 사례에서 테인트와 톨러레이션을 사용
    • 특수한 한드웨어를 가진 노드
    • 전용 노드 리소스
    • 성능이 낮은 노드 회피
  • 스케줄링과 실행 중인 컨테이너와 관련해 여러 테인트 타입이 있음
    • NoSchedule
      • 톨레이션이 일치하지 않는 파드가 스케줄링되는 것을 막는 강한 테인트
    • PreferNoSchedule
      • 다른 노드에 스케줄링될 수 없는 파드만 스케줄링
    • NoExecute
      • 노드에 이미 실행 중인 파드를 축출
    • NodeCondition
      • 특정 조건을 만족시키는 노드를 테인트
  • [그림 8-1]은 gpu=true:NoSchedule로 테인트된 노드의 예제
    • 파드 명세 1은 톨러레이션 키는 gpu이므로, 테인트된 노드의 스케줄 됨
    • 파드 명세 2의 롤러레이션 키는 no-gpu이므로 노드에 스케줄되지 않음

8.3 파드 리소스 관리

  • 파드 리소스를 절적하게 관리하는 것은 중요함
    • CPU와 메모리 사용률을 관리하여 클러스터의 활용도를 최적화 해야함
  • 리소스는 컨테이너 수준과 네임스페이스 수준으로 관리 할 수 있음
  • 네트워크와 스토리지 리소스도 있지만 쿠버네티스에서는 아직 요청과 한계를 설정할 방법이 없음
  • 스케줄러가 리소스를 촤적화하고 지능적으로 배치하려면 애플리케이션의 요구사항을 잘 파악해야함

8.3.1 리소스 요청

  • 리소스 요청에는 컨테이너가 스케줄링하기 위해 x크기의 CPU와 메모리를 필요로 한다고 정의
    • 만약 8GB를 요청했는데 노드에 7.5GB의 메모리만 존재하면 파드는 스케줄링되지 못함
    • 리소스가 가용해질 때까지 대기 상태가 됨
  • 클러스터의 가용한 리소스를 보기 위해 kubectl top을 사용
  • kubectl top nodes
  • 8000Mi의 메모리가 필요한 파드를 스케줄링 해보기
    • 가용한 노드가 없기 때문에 대기 상태라 되는 것을 볼수 있음
    • kubectl describe pods memory-request
  • resources: requests: memory: "8000Mi"

8.3.2 리소스 제한과 파드 서비스 품질

  • 리소스 제한으로 파드의 최대 CPU와 메모리 크기를 정의
  • 제한에 도달할 때 리소스마다 다른 일이 발생
    • CPU: 지정된 제한보다 사용되지 못하게 막힘
    • 메모리: 한계에 도달하면 파드가 재시작 됨
    • 파드는 동일한 호스트나 다른 호스트에서 재시작 될 수 있음
  • 컨테이너에 제한을 지정하는 것이 공정하게 리소스를 분배하기 위한 모벌 사례
  •  
  • 파드가 생성되면 QoS중 하나가 할당 됨
    • 보장
      • CPU와 메모리 모두 요청과 제한이 일치
    • 폭발
      • 제한이 요청보다 높게 할당될 때
      • 컨테이너는 요청을 보장 받지만 제한까지만 치솟을 수 있음
    • 최선의 노력
      • 요청 또는 한계를 설저앟지 않을 경우

<aside> 💡 보장 QoS이고 파드에 여러 컨테이너가 존재하는 경우, 컨테이너별로 CPU와 메모리 요청과 제한을 설정 해야함, 모든 컨테이너에 요청과 제한이 설정되지 않으면 보장 QoS가 할당되지 않음

</aside>

8.3.3 PodDisruptionBudget

  • 언젠가는 호스트에서 파드가 축출됨, 축출에는 두가지 유형이 있음
    • 자발적 중단
      • 클러스터의 유지보수, 클러스터 오토스케일러의 할당 해제, 파드템플릿 업데이트 등
    • 비자발적 중단
      • 하드웨어 장애, 네트워크 분할, 커널 패닉, 리소스 부족 등
  • 파드가 축출될 때 미치는 영향을 최소화 하기 위해서는 PodDisruptionBudget을 설정
    • 자발적 축출 이벤트 기간에 가용한 최소 파드와 불가용한 최대 파드 정책을 설정 할 수 있음
    • ex) 주어진 시간동안 특정 파드의 20%가 다운될 수 없도록 지정 가능
    • ex) 항상 가용해야할 레플리카 수 x를 정책에 정의 가능
  • 최소 가용
    • app:front-end의 최소 가용을 5로 PodDisruptionBudget에 설정
    • 항상 5개의 레플리가 파드가 가용하도록 설정
  • 최대 불가용
    • PodDisruptionBudget에 최대 20%의 레플리카가 불가용
    • 자발적 중단 과정에서 최대 20%의 파드를 축출

<aside> 💡 PodDisruptionBudget를 퍼센트로 지정하면 파드 수가 명확히 정해지지 않음, 예를 들어 7개의 파드를 가지고 있고 maxAvailable을 50%로 지정했다면 가장 가까운 정수로 반올림하므로 maxAvailable은 4개의 파드가 됨

</aside>

8.3.4 네임스페이스를 사용한 리소스 관리

  • 네임스페이스는 배포된 리소스를 논리적으로 구불할 수 있음
    • 리소스쿼터, RBAC, 네트워크 정책을 설정 할 수 있음
  • 멀티테넌시(Multitenancy)기능으로 팀이나 애플리케이션에 전용 인프라를 지정하지 않고도 워크로드를 분리할 수 있음
  • 네임스페이스를 설정하는 방법을 설계할 때는 특정 애플리케이션에 대한 접근을 제어하는 방법을 고려해야함
    • 단일 클러스터를 사용하는 여러 팀이 있을 경우 가장 나은 방법은 팀에 네임스페이스를 할당하는 것
    • 클러스터를 한 팀 전용으로 사용한다면 클러스터에 배포할 서비스별로 네임스페이스를 할당
    • 팀 조직과 역활에 맞춰 설계해야함
  • 쿠버네티스에 기본으로 설정되는 네임스페이스
    • kube-system
      • coredns, kube-proxy, metrics-server와 같은 쿠버네티스 내부 컴포넌트는 여기에 배포
    • default
      • 리소스 객체 안에 네임스페이스를 지정하지 않을때 사용되는 기본 네임스페이스
    • kube-public
      • 익명이나 인증되지 않은 콘텐츠, 예약된 시스템에 사용
  • 기본 네임스페이스를 사용하면 리소스 관리할 때 실수하기 쉬움으로 피하는 것이 좋음
  • kubectl을 이용해 네임스페이스 관련 작업을 할 때, —namespace 플래그 또는 줄여서 -n을 넣어야함
  • kubectl create ns team-1 kubectl get pods --namespace team-1
  • 특정 네임스페이스에 kubectl 컨텍스트를 설정하면, 모든 명령에 —namespace 플레그를 추가할 필요가 없어 편리함
  • kubectl config set-context my-contex --namespace-team-1

8.3.5 리소스쿼터

  • 여러 팀과 단일 클러스터를 공유할 때는 네임스페이스에 리소스쿼터를 설정 해야함
    • 단일 네임스페이스가 할당된 리소스 이상을 사용할 수 없도록
  • 계산 리소스
    • request.cpu: CPU 요청의 합은 이 값을 초과할 수 없음
    • limits.cpu: CPU 제한의 합은 이 값을 초과할 수 없음
    • request.memory: 메모리 요청의 합은 이 값을 초과할 수 없음
    • limit.memory: 메모리 제한의 합은 이 값을 초과할 수 없음
  • 스토리지 리소스
    • requests.storage: 스토리지 요청의 합은 이 값을 초과할 수 없음
    • persistentvolumeclaims: 네임스페이스에 존재할 수 있는 퍼시스턴트볼륨클레임의 합은 이 값을 초과할 수 없음
    • storageclass.request: 특정 스토리지클래스와 연관된 볼륨클레임은 이 값을 초과할 수 없음
    • storageclass.pvc: 네임스페이스에 존재하는 퍼시스턴트볼륨클레임 합은 이 값을 초과할 수 없음
  • 객체 카운트 쿼터의 예
    • 카운트/pvc
    • 카운트/서비스
    • 카운트/디플로이먼트
    • 카운트/레플리카셋
  • 네임스페이스별로 리소스쿼터를 정교하게 분할할 수 있음
  • 멀티테넌트 클러스터에서 리소스 사용을 보다 효율적으로 조절할 수 있음
  • 네임스페이스에서 리소스쿼터를 설정하는 YAML 파일
    • 리소스쿼터 적용
    kubectl apply auota.yaml -n team-1
    
    • 리소스쿼터를 초과하는 디플로이먼트 배포 해보기
      • 오류 발생: 2Gi 메모리 쿼터를 초과했기 때문
      kubectl run nginx-quotatest --image-nginx --restart=Never --replicas=1 --port=80 --requests='cpu=500m,memory=4Gi' --limits='cpu=500m,memory=4Gi' -n team-1
      
  • apiVersion: v1 kind: ResourceQuota metadata: name: mem-cpu-demo namespace: team-1 spec: hard: requests.cpu: "1" requests.memory: 1Gi limits.cpu: "2" limits.memory: 2Gi persistentvolumeclaims: "5" requests.storage: "10Gi"

8.3.6 LimitRange

  • 사용자가 request와 limits을 파드에 설정하지 않을 경우
    • 어드미션 컨트롤러를 제공해 설정이 되지 않을 경우 자동 설정을 할 수 있음
  • 쿼터와 LimitRange를 작업할 네임스페이스 생성
  • kubectl create ns team-1
  • LimitRange의 limits안의 defaultRequest를 네임스페이스에 적용
    kubectl apply -f limitranger.yaml -n team-1
    
  • apiVersion: v1 kind: LimitRange metadata: name: team-1-limit-range spec: limits: - default: memory: 512Mi defaultRequest: memory: 256Mi type: Container
  • LimitRange의 기본 제한과 요청이 적용되었는지 확인
  • kubectl run team-1-pod --image=nginx -n team-1
  • 파드의 요청과 제한이 설정되었는지 확인
    Limits:
    	memory: 512Mi
    Requests:
    	memory: 256Mi
    
  • kubectl describe pod team-1-pod -n team-1
  • 리소스쿼터를 사용할 때는 LimitRange를 사용해야 함
    • 요청과 제한이 명세에 설정되지 않으면 배포가 거절됨

8.3.7 클러스터 확장

  • 클러스터에 배포하기에 앞서 클러스터 안에서 사용할 인스턴스 크기를 결정 해야함
  • 클러스터에서의 좋은 시작점이 무엇일지 파악해야함
    • CPU와 메모리의 적절한 균형을 목표로 세우는 것도 하나의 방법
  • 인스턴스의 크기를 결정한 후 쿠버네티스의 핵심 기능을 사용하여 확장을 관리 할 수 있음
  • 수동 확장
    • 클러스터를 수동으로 확장한다는 것은 일반적으로 새로운 노드 수를 결정한다는 뜻
      • 서비스는 클러스터에 새로운 노드를 추가함
    • 도구로 노드 풀을 생성할 수 있음
      • 이미 실행 중인 클러스터에 새로운 인스턴스 유형의 풀을 추가할 수 있음
      • 단일 클러스터에 워크로드가 혼재되어 있을 때 매우 유용함
      • 노드 풀을 이용하면 단일 클러스터 내에서 여러 인스턴스 유형을 섞을 수 있음
        • ex) 많은 CPU를 사용하는 워크로드와 메모리를 많이 사용하는 워크로드를 섞을 수 있음
    • 클러스터 자동 확장을 위해서는 고려할 사항이 많음
      • 리소스가 필요한 때 사전 예방적으로 수동 확장하는 것부터 시작하는 것이 좋음
    • 워크로드의 변동성이 심한 경우라면 클러스터 자동 확장이 유용할 수 있음
  • 클러스터 자동확장
    • 쿠버네티스는 클러스터의 최소 가용 노드와 최대 가용 노드를 설정할 수 있는 부가 기능인 클러스터 오토스케일러를 제공
    • 오토스케일러는 대기 상태의 파드가 존재할 때 확장을 결정 함
      • 4000Mib 메모리 요청이 있을 경우 2000Mib만 가용하다면 파드는 대기상태가 됨
      • 새로운 노드가 클러스터에 추가되는 즉시 대기중인 파드가 스케줄링 됨
    • 오토스케일러의 단점은 파드가 대기 상태가 되어야 새로운 노드가 추가 된다는 점
      • 워크로드가 온라인 되려면 결국 새로운 노드를 대기 해야함
    • 쿠버네티스 v1.15 이후부터는 오토스케일러가 사용자 정의 메트릭 기반의 확장을 지원하지 않음
    • 오토스케일러가 더는 리소스가 필요 없는 클러스터의 크기를 줄일 수 있음
      • 노드를 드레인하고 파드를 새로운 노드에 다시 스케줄링

8.3.8 애플리케이션 확장

  • 쿠버네티스는 클러스터에서 애플리케이션을 확장할 수 있는 여러 방법을 제공
  • 디플로이먼트의 레플리카 수를 수동으로 변경하여 애플리케이션을 확장 할 수 있음
  • 레플리카셋이나 복제 컨트롤러를 통해 변경할 수 있지만 권장하진 않음
  • 수동 확장은 정적인 워크로드 또는 워크로드가 급증하는 시점을 알고 있을 때 유용
    • 예기치 않은 급증이 발생하거나 정적이지 않은 워크로드라면 수동확장은 적합하지 않음
  • 자동으로 워크로드를 확장할 수 있은 HPA를 제공
  • 디플로이먼트 매니패스트를 적용하여 디플로이먼트를 수동으로 확장하는 방법을 알아보자
    • kubectl scale 명령을 이용하면 디플로이먼트의 규모를 확장할 수 있음
    • kubectl scale deployment frontend --replicas 5
  • apiVersion: extensions/v1beta1 kind: Deployment metadata: name: frontend spec: replicas: 3 template: metadata: name: frontend labels: app: frontend spec: containers: - image: nginx:alpine name: frontend resources: requests: cpu: 100m

8.3.9 HPA를 이용한 확장

  • HPA는 CPU, 메모리, 사용자 정의 메트릭 가반으로 디플로이먼트를 확장할 수 있음
  • 디플로이먼트를 감시하여 metrics-server로부터 메트릭을 풀
  • 가용 파드의 최소 수, 최대 수를 설정
    • ex) 최소 수 3, 최대 수 10으로 HPA 정책을 정의하였다면 디플로이먼트가 80% CPU를 사용률에 도달했을 때 확장됨
  • 애플리케이션 버그나 이슈 때문에 HPA가 레플리카를 무한정 늘리지 않도록 하려면 최소 수와 최대 수를 설정하는 것이 중요
  • HPA는 메트릭 동기화, 레플리카 확장과 축소를 위한 기본 설정을 가짐
    • horizontal-pod-autoscaler-sync-period
      • 메트릭 동기화 시간은 기본 30초
    • horizontal-pod-autoscaler-upscale-delay
      • 두 확장 사이의 레이턴시는 기본 3분
    • horizontal-pod-autoscaler-downscale-delay
      • 두 축소 사이의 레이턴시는 기본 5분
  • 상대적인 플래그를 사용하여 기본값을 변경할수 있지만 신중해야함
  • 워크로드의 변동성이 큰 경우 특수한 사례에 맞게 워크로드를 최적화할 수 있도록 연습 해야함
  • 이전 예제에서 배포한 프론트엔드 HPA 정책을 설정 해보기
    • 80포트로 디플로이먼트를 노출
    • kubectl expose deployment frontend --port 80
    • 자동 확장 정책을 설정
      • 최소 1에서 최대 10까지 확장할 수 있도록 설정
      • CPU부하가 50%에 도달하면 확장 작업 수행
      kubectl autoscale deployment frontend --cpu-percent=50 --min=1 --max=10
      
    • 부하를 생성해 확장 되는 것을 확인
    • kubectl run -i 00tty load-generator --image=busybox /bin/sh while true; do wget -q -0- <http://frontend.default.svc.cluster.local>; done kubectl get hpa

8.3.10 사용자 정의 메트릭을 사용한 HPA

  • 메트릭 서버 API를 이용하면 사용자 정의 메트릭을 사용하여 확장할 수 있음
  • 사용자 정의 메트릭 API와 메트릭 애그리케이터는 서드파트 공급자가 플러그인으로 메트릭을 확장할 수 있도록 하며 HPA는 이러한 외부 메트릭을 기반으로 확장 가능
  • 사용자 정의 메트릭을 사용하여 자동으로 확장하면 애플리케이션에 특화된 메트릭이나 외부 서비스 메트릭으로 확장 가능

8.3.11 수직 파드 오토스케일러

  • VPA는 레플리카를 확장하지 않으므로 HPA와 다름
  • VPA는 자동으로 파드 요청을 늘리고 줄이기 때문에 이러한 요청을 수동으로 조절할 필요가 없음
  • 아키텍처로 인해 확장할 수 없는 워크로드의 경우 자동으로 리소스를 확장하는데 효과적임
    • MySQL를 스테이트리스 프론트엔드와 같은 방식으로 확장할 수 없음
    • MySQL의 마스터 노드가 워크로드에 따라 자동으로 확장되도록 설정할 수 있음
  • VPA는 HPA보다 더 복잡하며 다음 세개의 컴포넌트를 가짐
    • Recommender
      • 현재와 과거의 리소스 사용량을 모니터링하고 컨테이너의 CPU와 메모리 요청의 추천값을 제공
    • Updater
      • 파드가 정확한 리소스를 가지고 있는지 확인하고 그렇지 않으면 종료
      • 컨트롤러는 업데이트된 요청으로 컨트롤러를 다시 생성
    • Admission Plugin
      • 정확한 리소스 요청을 새로운 파드에 요청
  • v1.15부터는 운영 배포에서 VPA를 권장하지 않음

8.4 리소스 관리 모범 사례

  • 파드안티어피니티를 사용해 워크로드를 여러 가용 영역으로 분산하여 고가용성을 보장
  • 테인트를 사용해 특수한 하드웨어가 필요한 워크로드에만 해당 노드에 스케줄링되록 함
  • NodeCondition 테인트를 사용하여 노드 실패나 성능저하를 사전에 방지
  • 파드 명세에 노드 셀렉터를 적용하여 특수한 하드웨어를 가진 노드에 스케줄링 함
  • 운영으로 이동하기 전에 다양한 노드 크기를 실험하여 비용과 성능의 적절한 조합을 찾음
  • 다양한 성능 특성을 지닌 워크로드가 혼재되어 있다면 단일 클러스터에 노드 유형이 섞여 있는 노드 풀을 사용
  • 클러스터에 배포된 모든 파드에 대해 메모리와 CPU 제한을 설정
  • 여러 팀 또는 여러 애플리케이션이 고정한 리소스를 할당 받을 수 있도록 ResourceQuata를 사용
  • 제한과 요청이 설정되지 않은 파드 명세에 기본 제한과 요청을 설정하기 위해 LimitRange를 구현
  • 워크로드 프로필을 파악하기 전까지는 수동 클러스터 확장부터 시작
  • 자동 확장을 사용할 수 있지만 노드 가동 시간과 클러스터 축소에 대한 추가전인 고민이 필요
  • 변동성이 있거나 예상치 못한 정점이 있는 워크로드의 경우에는 HPA를 사용

8.5 마치며

  • 쿠버네티스에는 안정적이고 사용률이 높고 효과적인 클러스터 상태를 유지하기 위한 많은 기능이 내장 되어 있음
  • 클러스터 파드 크기를 설정하는 것은 처음엔 다소 어려울 수 있지만 모니터링하여 리소스를 최적화 하는 방법을 찾을 수 있음
  • 쿠버네티스 리소스를 관리하고 최적화하는 모범 사례를 살펴보기
  • 워크로드 스케줄링, 클러스터 관리, 파드 리소스 관리, 네임스페이스 관리, 애플리케이션 확장에 대해 논의
  • 어피니티(Affinity), 안티어피니티(AntiAffinity), 테인트(Taint), 톨러레이션(Toleration), 노드 셀렉터(NodeSelector)를 이용한 고급 스케줄링 기술을 알아보기
  • 리소스 제한과 요청, 파드 서비스 품질, PodDisruptionBuget, LimitRanger, 안티어피니티 정책을 알아보기8.1 쿠버네티스 스케줄러
    • 스케줄러는 컨트롤 플레인에서 호스팅되는 핵심 컴포넌트
    • 스케줄러는 파드를 클러스터에 어떻게 배치할지를 결정
    • 클러스터와 사용자가 정의한 제약에 따라 리소스를 최적화
    • 논리 조건과 우선순위 기반의 스코어 알고리즘 사용
    8.1.1 논리 조건
    • 스케줄링할 노드를 결정할 때 사용하는 첫 번째 기능은 논리 조건 함수
      • 강한 제약을 내포하고 있고 참 또는 거짓을 반환
    • ex) 파드가 4GB 메모리를 요청할 때 특정 노드가 요건을 만족하지 못하면 파드 스케줄링 후보에서 제거됨
    • 노드가 스케줄링 불가로 설정된 상태라면 후보에서 제거됨
    • 아래와 같은 논리 조건들이 있음
    8.1.2 우선순위
    • 상대적인 값을 기반으로 모든 유효한 노드의 순위를 매김
    • 우선순위 점수를 합산하여 최종 우선순위 점수를 노드에 부여
      • ex) 파드 하나에 600밀리코어가 필요한 경우, 두 개의 노드중 하나는 900밀리코어, 하나는 1800밀리 코어라면 후자가 더 높은 우선순위를 가짐
    • 노드가 같은 우선순위를 반환한다면 스케줄러는 selectHost()함수를 사용하여 라운드 로빈 토너먼트 방식으로 노드를 선정
    • 아래와 같은 우선순위 조건들이 있음
    8.2 고급 스케줄링 기술
    • 쿠버네티스는 스스로 파드 스케줄링을 최적화함
      • 넉넉한 리소스를 가진 노드에게만 파드를 배치
      • 균등한 리소스 사용률을 유지하면서 레플리카셋의 파드를 여러 노드에 분산시켜 가용성을 높임
    • 쿠버네티스의 리소스 스케줄링 방식을 변경할 수 있음
      • 영역 실패로 인한 장애를 막기 위해 가용한 영역 간에 파드를 분산 시킬 수 있음
      • 여러 파드를 단일 호스트에 배치하여 성능을 향상 시킬 수 있음
    8.2.1 파드어피니티와 안티어피니티
    • 파드간의 배치 규직을 설정 할 수 있음
    • 스케줄링 방식을 변경하거나 스케줄러의 배치 결정을 오버라이드 할 수 있음
    • ex) 안티어피니티 규직으로 레플리카셋의 파드를 여러 데이터 센터 영역에 분산 시킬 수 있음
    • 파드어피니티 예제 - 동일한 노드에 파드를 스케줄링 할수 있음
    • 안티어피니티 예제 - 동일한 노드에 파드를 스케줄링 하지 못하게 할수 있음
    • 안티어피니티 규직을 설정하는 예제
      • 파드안티어피니티가 설정되어 있으면 스케줄러는 한 노드에 여러 레플리카를 배치하지 않음
    8.2.2 노드 셀렉터
    • 특정 노드에 파드를 스케줄링하는 가장 간단한 방식
    • 키/값 쌍이 있는 레이블 셀렉터를 이용하여 스케줄링을 결정
      • ex) GPU같은 특수한 하드웨어를 가진 노드에 파드를 스케줄링할 수 있음
    • 노드 셀렉터는 노드 테인트도 함께 사용할 수 있음
      • GPU 워크로드 전용 노드를 예약하고 노드 테인트를 사용하여 GPU가 있는 노드를 자동으로 선택할 수 있음
      <aside> 💡 노드 테인트는 오직 GPU 워크로드를 위한 용도로 노드를 제한<aside> 💡 노드 셀렉터는 GPU가 가용한 노드를 요청할 때 사용
    • </aside>
    • </aside>
    • 노드에 레이블을 생성하고 파드 명세에서 노드 셀렉터를 사용하는 예제
      • 노드 셀렉터를 상요하면 disktype=ssd 레이블을 가진 도으에게만 파드를 스케줄링함
      kubectl label node <node_name> disktype=ssd
      
      • 파드 명세서
    8.2.3 테인트와 톨러레이션
    • 테인트는 파드가 스케줄링되는 것을 거절하기 위해 노드에 사용됨
    • 안티어피니티와 같지만 다른 방식과 용도로 사용이 됨
      • ex) 특정 노드에 특정 성능 요건을 가진 파드만 필요하고 그 외의 다른 파드는 스케줄링 하지 않는 상황
    • 테인트는 톨러레이션과 함께 동작
      • 두 조합으로 안티어피니티 규칙을 세밀하게 조정할 수 있음
    • 일반적으로 다음과 같은 사례에서 테인트와 톨러레이션을 사용
      • 특수한 한드웨어를 가진 노드
      • 전용 노드 리소스
      • 성능이 낮은 노드 회피
    • 스케줄링과 실행 중인 컨테이너와 관련해 여러 테인트 타입이 있음
      • NoSchedule
        • 톨레이션이 일치하지 않는 파드가 스케줄링되는 것을 막는 강한 테인트
      • PreferNoSchedule
        • 다른 노드에 스케줄링될 수 없는 파드만 스케줄링
      • NoExecute
        • 노드에 이미 실행 중인 파드를 축출
      • NodeCondition
        • 특정 조건을 만족시키는 노드를 테인트
    • [그림 8-1]은 gpu=true:NoSchedule로 테인트된 노드의 예제
      • 파드 명세 1은 톨러레이션 키는 gpu이므로, 테인트된 노드의 스케줄 됨
      • 파드 명세 2의 롤러레이션 키는 no-gpu이므로 노드에 스케줄되지 않음
    8.3 파드 리소스 관리
    • 파드 리소스를 절적하게 관리하는 것은 중요함
      • CPU와 메모리 사용률을 관리하여 클러스터의 활용도를 최적화 해야함
    • 리소스는 컨테이너 수준과 네임스페이스 수준으로 관리 할 수 있음
    • 네트워크와 스토리지 리소스도 있지만 쿠버네티스에서는 아직 요청과 한계를 설정할 방법이 없음
    • 스케줄러가 리소스를 촤적화하고 지능적으로 배치하려면 애플리케이션의 요구사항을 잘 파악해야함
    8.3.1 리소스 요청
    • 리소스 요청에는 컨테이너가 스케줄링하기 위해 x크기의 CPU와 메모리를 필요로 한다고 정의
      • 만약 8GB를 요청했는데 노드에 7.5GB의 메모리만 존재하면 파드는 스케줄링되지 못함
      • 리소스가 가용해질 때까지 대기 상태가 됨
    • 클러스터의 가용한 리소스를 보기 위해 kubectl top을 사용
    • kubectl top nodes
    • 8000Mi의 메모리가 필요한 파드를 스케줄링 해보기
      • 가용한 노드가 없기 때문에 대기 상태라 되는 것을 볼수 있음
      • kubectl describe pods memory-request
    • resources: requests: memory: "8000Mi"
    8.3.2 리소스 제한과 파드 서비스 품질
    • 리소스 제한으로 파드의 최대 CPU와 메모리 크기를 정의
    • 제한에 도달할 때 리소스마다 다른 일이 발생
      • CPU: 지정된 제한보다 사용되지 못하게 막힘
      • 메모리: 한계에 도달하면 파드가 재시작 됨
      • 파드는 동일한 호스트나 다른 호스트에서 재시작 될 수 있음
    • 컨테이너에 제한을 지정하는 것이 공정하게 리소스를 분배하기 위한 모벌 사례
    •  
    • 파드가 생성되면 QoS중 하나가 할당 됨
      • 보장
        • CPU와 메모리 모두 요청과 제한이 일치
      • 폭발
        • 제한이 요청보다 높게 할당될 때
        • 컨테이너는 요청을 보장 받지만 제한까지만 치솟을 수 있음
      • 최선의 노력
        • 요청 또는 한계를 설저앟지 않을 경우
    <aside> 💡 보장 QoS이고 파드에 여러 컨테이너가 존재하는 경우, 컨테이너별로 CPU와 메모리 요청과 제한을 설정 해야함, 모든 컨테이너에 요청과 제한이 설정되지 않으면 보장 QoS가 할당되지 않음8.3.3 PodDisruptionBudget
    • 언젠가는 호스트에서 파드가 축출됨, 축출에는 두가지 유형이 있음
      • 자발적 중단
        • 클러스터의 유지보수, 클러스터 오토스케일러의 할당 해제, 파드템플릿 업데이트 등
      • 비자발적 중단
        • 하드웨어 장애, 네트워크 분할, 커널 패닉, 리소스 부족 등
    • 파드가 축출될 때 미치는 영향을 최소화 하기 위해서는 PodDisruptionBudget을 설정
      • 자발적 축출 이벤트 기간에 가용한 최소 파드와 불가용한 최대 파드 정책을 설정 할 수 있음
      • ex) 주어진 시간동안 특정 파드의 20%가 다운될 수 없도록 지정 가능
      • ex) 항상 가용해야할 레플리카 수 x를 정책에 정의 가능
    • 최소 가용
      • app:front-end의 최소 가용을 5로 PodDisruptionBudget에 설정
      • 항상 5개의 레플리가 파드가 가용하도록 설정
    • 최대 불가용
      • PodDisruptionBudget에 최대 20%의 레플리카가 불가용
      • 자발적 중단 과정에서 최대 20%의 파드를 축출
    <aside> 💡 PodDisruptionBudget를 퍼센트로 지정하면 파드 수가 명확히 정해지지 않음, 예를 들어 7개의 파드를 가지고 있고 maxAvailable을 50%로 지정했다면 가장 가까운 정수로 반올림하므로 maxAvailable은 4개의 파드가 됨8.3.4 네임스페이스를 사용한 리소스 관리
    • 네임스페이스는 배포된 리소스를 논리적으로 구불할 수 있음
      • 리소스쿼터, RBAC, 네트워크 정책을 설정 할 수 있음
    • 멀티테넌시(Multitenancy)기능으로 팀이나 애플리케이션에 전용 인프라를 지정하지 않고도 워크로드를 분리할 수 있음
    • 네임스페이스를 설정하는 방법을 설계할 때는 특정 애플리케이션에 대한 접근을 제어하는 방법을 고려해야함
      • 단일 클러스터를 사용하는 여러 팀이 있을 경우 가장 나은 방법은 팀에 네임스페이스를 할당하는 것
      • 클러스터를 한 팀 전용으로 사용한다면 클러스터에 배포할 서비스별로 네임스페이스를 할당
      • 팀 조직과 역활에 맞춰 설계해야함
    • 쿠버네티스에 기본으로 설정되는 네임스페이스
      • kube-system
        • coredns, kube-proxy, metrics-server와 같은 쿠버네티스 내부 컴포넌트는 여기에 배포
      • default
        • 리소스 객체 안에 네임스페이스를 지정하지 않을때 사용되는 기본 네임스페이스
      • kube-public
        • 익명이나 인증되지 않은 콘텐츠, 예약된 시스템에 사용
    • 기본 네임스페이스를 사용하면 리소스 관리할 때 실수하기 쉬움으로 피하는 것이 좋음
    • kubectl을 이용해 네임스페이스 관련 작업을 할 때, —namespace 플래그 또는 줄여서 -n을 넣어야함
    • kubectl create ns team-1 kubectl get pods --namespace team-1
    • 특정 네임스페이스에 kubectl 컨텍스트를 설정하면, 모든 명령에 —namespace 플레그를 추가할 필요가 없어 편리함
    • kubectl config set-context my-contex --namespace-team-1
    8.3.5 리소스쿼터
    • 여러 팀과 단일 클러스터를 공유할 때는 네임스페이스에 리소스쿼터를 설정 해야함
      • 단일 네임스페이스가 할당된 리소스 이상을 사용할 수 없도록
    • 계산 리소스
      • request.cpu: CPU 요청의 합은 이 값을 초과할 수 없음
      • limits.cpu: CPU 제한의 합은 이 값을 초과할 수 없음
      • request.memory: 메모리 요청의 합은 이 값을 초과할 수 없음
      • limit.memory: 메모리 제한의 합은 이 값을 초과할 수 없음
    • 스토리지 리소스
      • requests.storage: 스토리지 요청의 합은 이 값을 초과할 수 없음
      • persistentvolumeclaims: 네임스페이스에 존재할 수 있는 퍼시스턴트볼륨클레임의 합은 이 값을 초과할 수 없음
      • storageclass.request: 특정 스토리지클래스와 연관된 볼륨클레임은 이 값을 초과할 수 없음
      • storageclass.pvc: 네임스페이스에 존재하는 퍼시스턴트볼륨클레임 합은 이 값을 초과할 수 없음
    • 객체 카운트 쿼터의 예
      • 카운트/pvc
      • 카운트/서비스
      • 카운트/디플로이먼트
      • 카운트/레플리카셋
    • 네임스페이스별로 리소스쿼터를 정교하게 분할할 수 있음
    • 멀티테넌트 클러스터에서 리소스 사용을 보다 효율적으로 조절할 수 있음
    • 네임스페이스에서 리소스쿼터를 설정하는 YAML 파일
      • 리소스쿼터 적용
      kubectl apply auota.yaml -n team-1
      
      • 리소스쿼터를 초과하는 디플로이먼트 배포 해보기
        • 오류 발생: 2Gi 메모리 쿼터를 초과했기 때문
        kubectl run nginx-quotatest --image-nginx --restart=Never --replicas=1 --port=80 --requests='cpu=500m,memory=4Gi' --limits='cpu=500m,memory=4Gi' -n team-1
        
    • apiVersion: v1 kind: ResourceQuota metadata: name: mem-cpu-demo namespace: team-1 spec: hard: requests.cpu: "1" requests.memory: 1Gi limits.cpu: "2" limits.memory: 2Gi persistentvolumeclaims: "5" requests.storage: "10Gi"
    8.3.6 LimitRange
    • 사용자가 request와 limits을 파드에 설정하지 않을 경우
      • 어드미션 컨트롤러를 제공해 설정이 되지 않을 경우 자동 설정을 할 수 있음
    • 쿼터와 LimitRange를 작업할 네임스페이스 생성
    • kubectl create ns team-1
    • LimitRange의 limits안의 defaultRequest를 네임스페이스에 적용
      kubectl apply -f limitranger.yaml -n team-1
      
    • apiVersion: v1 kind: LimitRange metadata: name: team-1-limit-range spec: limits: - default: memory: 512Mi defaultRequest: memory: 256Mi type: Container
    • LimitRange의 기본 제한과 요청이 적용되었는지 확인
    • kubectl run team-1-pod --image=nginx -n team-1
    • 파드의 요청과 제한이 설정되었는지 확인
      Limits:
      	memory: 512Mi
      Requests:
      	memory: 256Mi
      
    • kubectl describe pod team-1-pod -n team-1
    • 리소스쿼터를 사용할 때는 LimitRange를 사용해야 함
      • 요청과 제한이 명세에 설정되지 않으면 배포가 거절됨
    8.3.7 클러스터 확장
    • 클러스터에 배포하기에 앞서 클러스터 안에서 사용할 인스턴스 크기를 결정 해야함
    • 클러스터에서의 좋은 시작점이 무엇일지 파악해야함
      • CPU와 메모리의 적절한 균형을 목표로 세우는 것도 하나의 방법
    • 인스턴스의 크기를 결정한 후 쿠버네티스의 핵심 기능을 사용하여 확장을 관리 할 수 있음
    • 수동 확장
      • 클러스터를 수동으로 확장한다는 것은 일반적으로 새로운 노드 수를 결정한다는 뜻
        • 서비스는 클러스터에 새로운 노드를 추가함
      • 도구로 노드 풀을 생성할 수 있음
        • 이미 실행 중인 클러스터에 새로운 인스턴스 유형의 풀을 추가할 수 있음
        • 단일 클러스터에 워크로드가 혼재되어 있을 때 매우 유용함
        • 노드 풀을 이용하면 단일 클러스터 내에서 여러 인스턴스 유형을 섞을 수 있음
          • ex) 많은 CPU를 사용하는 워크로드와 메모리를 많이 사용하는 워크로드를 섞을 수 있음
      • 클러스터 자동 확장을 위해서는 고려할 사항이 많음
        • 리소스가 필요한 때 사전 예방적으로 수동 확장하는 것부터 시작하는 것이 좋음
      • 워크로드의 변동성이 심한 경우라면 클러스터 자동 확장이 유용할 수 있음
    • 클러스터 자동확장
      • 쿠버네티스는 클러스터의 최소 가용 노드와 최대 가용 노드를 설정할 수 있는 부가 기능인 클러스터 오토스케일러를 제공
      • 오토스케일러는 대기 상태의 파드가 존재할 때 확장을 결정 함
        • 4000Mib 메모리 요청이 있을 경우 2000Mib만 가용하다면 파드는 대기상태가 됨
        • 새로운 노드가 클러스터에 추가되는 즉시 대기중인 파드가 스케줄링 됨
      • 오토스케일러의 단점은 파드가 대기 상태가 되어야 새로운 노드가 추가 된다는 점
        • 워크로드가 온라인 되려면 결국 새로운 노드를 대기 해야함
      • 쿠버네티스 v1.15 이후부터는 오토스케일러가 사용자 정의 메트릭 기반의 확장을 지원하지 않음
      • 오토스케일러가 더는 리소스가 필요 없는 클러스터의 크기를 줄일 수 있음
        • 노드를 드레인하고 파드를 새로운 노드에 다시 스케줄링
    8.3.8 애플리케이션 확장
    • 쿠버네티스는 클러스터에서 애플리케이션을 확장할 수 있는 여러 방법을 제공
    • 디플로이먼트의 레플리카 수를 수동으로 변경하여 애플리케이션을 확장 할 수 있음
    • 레플리카셋이나 복제 컨트롤러를 통해 변경할 수 있지만 권장하진 않음
    • 수동 확장은 정적인 워크로드 또는 워크로드가 급증하는 시점을 알고 있을 때 유용
      • 예기치 않은 급증이 발생하거나 정적이지 않은 워크로드라면 수동확장은 적합하지 않음
    • 자동으로 워크로드를 확장할 수 있은 HPA를 제공
    • 디플로이먼트 매니패스트를 적용하여 디플로이먼트를 수동으로 확장하는 방법을 알아보자
      • kubectl scale 명령을 이용하면 디플로이먼트의 규모를 확장할 수 있음
      • kubectl scale deployment frontend --replicas 5
    • apiVersion: extensions/v1beta1 kind: Deployment metadata: name: frontend spec: replicas: 3 template: metadata: name: frontend labels: app: frontend spec: containers: - image: nginx:alpine name: frontend resources: requests: cpu: 100m
    8.3.9 HPA를 이용한 확장
    • HPA는 CPU, 메모리, 사용자 정의 메트릭 가반으로 디플로이먼트를 확장할 수 있음
    • 디플로이먼트를 감시하여 metrics-server로부터 메트릭을 풀
    • 가용 파드의 최소 수, 최대 수를 설정
      • ex) 최소 수 3, 최대 수 10으로 HPA 정책을 정의하였다면 디플로이먼트가 80% CPU를 사용률에 도달했을 때 확장됨
    • 애플리케이션 버그나 이슈 때문에 HPA가 레플리카를 무한정 늘리지 않도록 하려면 최소 수와 최대 수를 설정하는 것이 중요
    • HPA는 메트릭 동기화, 레플리카 확장과 축소를 위한 기본 설정을 가짐
      • horizontal-pod-autoscaler-sync-period
        • 메트릭 동기화 시간은 기본 30초
      • horizontal-pod-autoscaler-upscale-delay
        • 두 확장 사이의 레이턴시는 기본 3분
      • horizontal-pod-autoscaler-downscale-delay
        • 두 축소 사이의 레이턴시는 기본 5분
    • 상대적인 플래그를 사용하여 기본값을 변경할수 있지만 신중해야함
    • 워크로드의 변동성이 큰 경우 특수한 사례에 맞게 워크로드를 최적화할 수 있도록 연습 해야함
    • 이전 예제에서 배포한 프론트엔드 HPA 정책을 설정 해보기
      • 80포트로 디플로이먼트를 노출
      • kubectl expose deployment frontend --port 80
      • 자동 확장 정책을 설정
        • 최소 1에서 최대 10까지 확장할 수 있도록 설정
        • CPU부하가 50%에 도달하면 확장 작업 수행
        kubectl autoscale deployment frontend --cpu-percent=50 --min=1 --max=10
        
      • 부하를 생성해 확장 되는 것을 확인
      • kubectl run -i 00tty load-generator --image=busybox /bin/sh while true; do wget -q -0- <http://frontend.default.svc.cluster.local>; done kubectl get hpa
    8.3.10 사용자 정의 메트릭을 사용한 HPA
    • 메트릭 서버 API를 이용하면 사용자 정의 메트릭을 사용하여 확장할 수 있음
    • 사용자 정의 메트릭 API와 메트릭 애그리케이터는 서드파트 공급자가 플러그인으로 메트릭을 확장할 수 있도록 하며 HPA는 이러한 외부 메트릭을 기반으로 확장 가능
    • 사용자 정의 메트릭을 사용하여 자동으로 확장하면 애플리케이션에 특화된 메트릭이나 외부 서비스 메트릭으로 확장 가능
    8.3.11 수직 파드 오토스케일러
    • VPA는 레플리카를 확장하지 않으므로 HPA와 다름
    • VPA는 자동으로 파드 요청을 늘리고 줄이기 때문에 이러한 요청을 수동으로 조절할 필요가 없음
    • 아키텍처로 인해 확장할 수 없는 워크로드의 경우 자동으로 리소스를 확장하는데 효과적임
      • MySQL를 스테이트리스 프론트엔드와 같은 방식으로 확장할 수 없음
      • MySQL의 마스터 노드가 워크로드에 따라 자동으로 확장되도록 설정할 수 있음
    • VPA는 HPA보다 더 복잡하며 다음 세개의 컴포넌트를 가짐
      • Recommender
        • 현재와 과거의 리소스 사용량을 모니터링하고 컨테이너의 CPU와 메모리 요청의 추천값을 제공
      • Updater
        • 파드가 정확한 리소스를 가지고 있는지 확인하고 그렇지 않으면 종료
        • 컨트롤러는 업데이트된 요청으로 컨트롤러를 다시 생성
      • Admission Plugin
        • 정확한 리소스 요청을 새로운 파드에 요청
    • v1.15부터는 운영 배포에서 VPA를 권장하지 않음
    8.4 리소스 관리 모범 사례
    • 파드안티어피니티를 사용해 워크로드를 여러 가용 영역으로 분산하여 고가용성을 보장
    • 테인트를 사용해 특수한 하드웨어가 필요한 워크로드에만 해당 노드에 스케줄링되록 함
    • NodeCondition 테인트를 사용하여 노드 실패나 성능저하를 사전에 방지
    • 파드 명세에 노드 셀렉터를 적용하여 특수한 하드웨어를 가진 노드에 스케줄링 함
    • 운영으로 이동하기 전에 다양한 노드 크기를 실험하여 비용과 성능의 적절한 조합을 찾음
    • 다양한 성능 특성을 지닌 워크로드가 혼재되어 있다면 단일 클러스터에 노드 유형이 섞여 있는 노드 풀을 사용
    • 클러스터에 배포된 모든 파드에 대해 메모리와 CPU 제한을 설정
    • 여러 팀 또는 여러 애플리케이션이 고정한 리소스를 할당 받을 수 있도록 ResourceQuata를 사용
    • 제한과 요청이 설정되지 않은 파드 명세에 기본 제한과 요청을 설정하기 위해 LimitRange를 구현
    • 워크로드 프로필을 파악하기 전까지는 수동 클러스터 확장부터 시작
    • 자동 확장을 사용할 수 있지만 노드 가동 시간과 클러스터 축소에 대한 추가전인 고민이 필요
    • 변동성이 있거나 예상치 못한 정점이 있는 워크로드의 경우에는 HPA를 사용
    8.5 마치며
    • 쿠버네티스에는 안정적이고 사용률이 높고 효과적인 클러스터 상태를 유지하기 위한 많은 기능이 내장 되어 있음
    • 클러스터 파드 크기를 설정하는 것은 처음엔 다소 어려울 수 있지만 모니터링하여 리소스를 최적화 하는 방법을 찾을 수 있음
728x90