13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] CPU Throttling

  • [k8s] Kubernetes CPU Limit, 정말 사용해야 할까? 요청 중심 최적화 전략

    [k8s] Kubernetes CPU Limit, 정말 사용해야 할까? 요청 중심 최적화 전략

    Kubernetes CPU Limit, 정말 사용해야 할까?

    Kubernetes CPU Limit 얘기는 설정 한 줄처럼 보이지만, 실제 운영에선 지연 시간, 처리량, 오토스케일링, 멀티테넌시 정책이 한꺼번에 얽힙니다. 저도 예전엔 CPU limit를 일단 넣어두는 편이 더 안전하다고 봤거든요. 그런데 운영을 오래 해보면 패턴이 꽤 선명합니다. CPU limit는 성능 최적화 장치라기보다, 클러스터 통제 장치에 더 가깝더라고요.

    핵심은 메모리와 CPU를 같은 감각으로 다루면 안 된다는 점입니다. 메모리는 한계를 넘으면 OOMKill 같은 형태로 비교적 분명하게 드러납니다. 반면 CPU는 압축 가능한 자원이라서, limit를 넘는다고 바로 죽지 않고 리눅스 커널의 CFS quota 기반 throttling으로 서서히 막힙니다. 겉으로는 Pod가 멀쩡하고 readiness도 통과하는데, 실제 사용자 경험만 나빠지는 식이죠. 이게 운영에서 진짜 까다롭습니다.

    이번 글에서는 단순히 “limit를 빼세요” 같은 구호로 끝내지 않고, Kubernetes CPU Limit를 언제 유지해야 하는지, 언제 제거하는 편이 나은지, 판단 기준을 어디에 둬야 하는지 실무 기준으로 정리해보겠습니다. 특히 아래 세 가지는 꼭 분리해서 보셔야 합니다.

    • 노드가 진짜로 바쁜 상황: 클러스터 용량 부족 문제입니다.
    • 컨테이너 limit에 막힌 상황: 설정한 ceiling이 병목입니다.
    • request가 너무 낮아 경쟁에서 밀린 상황: throttling이 거의 없어도 성능이 흔들릴 수 있습니다.

    이 셋을 한데 묶어 보면 원인이 계속 엇나갑니다. 현업에서 CPU 문제를 오래 끄는 팀들은 대개 여기서 많이 헷갈리더라고요.

    CPU request와 CPU limit, 스케줄러와 kubelet, Linux cgroup이 어떻게 연결되는지 한눈에 보여주는 개요 이미지입니다.

    1. CPU request와 CPU limit, 정말 다르게 읽어야 합니다

    request는 스케줄링 기준이고, Linux 노드에서는 혼잡 시 상대적인 CPU 배분 기준에도 영향을 줍니다. 반면 limit는 커널이 강제로 거는 상한선입니다. 둘 다 resource 설정이지만 역할은 완전히 다릅니다. 스케줄러는 request를 보고 “이 Pod를 어느 노드에 올릴 수 있는가”를 판단하고, kubelet과 컨테이너 런타임은 limit를 받아 cgroup quota를 설정합니다. 즉, request는 배치 계획에 가깝고, limit는 실행 중 브레이크에 가깝습니다.

    이 차이가 중요한 이유는 운영상 신호가 완전히 다르게 나타나기 때문입니다.

    구분 CPU Request CPU Limit 운영에서 보이는 증상
    주요 역할 스케줄링 기준, 혼잡 시 상대적 배분 기준 절대 상한선 문제 원인이 다르게 드러납니다
    값이 너무 낮을 때 혼잡 시 경쟁에서 밀릴 수 있음 짧은 burst도 바로 잘릴 수 있음 전자는 처리량 저하, 후자는 지연 급증으로 자주 보입니다
    노드 CPU 여유와의 관계 여유가 있으면 영향이 완화될 수 있음 노드가 한가해도 limit에 걸리면 throttling 발생 노드 지표만 보면 놓치기 쉽습니다
    오토스케일링 영향 HPA의 utilization 계산 기준과 직접 연결됨 직접적인 스케일 기준은 아니지만 사용 패턴을 왜곡할 수 있음 request가 비현실적이면 HPA 판단도 흔들립니다
    정책적 의미 예약 통제 성능보다 거버넌스 목적일 때 limit가 더 설득력 있습니다

    그리고 의외로 자주 놓치는 사실이 하나 있습니다. 컨테이너에 limit만 주고 request를 생략하면, admission 단계에서 다른 기본값이 들어오지 않는 한 Kubernetes는 request를 limit와 동일하게 복사합니다. 여기에 namespace의 LimitRange까지 얹히면 의도하지 않은 request/limit 조합이 생깁니다. 그래서 manifest만 보고 판단하면 자꾸 틀립니다. 실제 Pod에 주입된 값을 봐야 합니다.

    2. Kubernetes CPU Limit가 안티패턴이 되는 순간

    제가 Kubernetes CPU Limit를 경계하는 이유는 단순합니다. 짧게 몰아서 끝낼 수 있는 일을, 굳이 길게 끌도록 만들기 때문입니다. 웹 API, gRPC 서버, 메시지 소비자, 배치 워커, 압축/암호화/직렬화가 섞인 애플리케이션은 CPU를 늘 높게 쓰는 게 아니라 순간적으로 확 씁니다. 이때 노드에 여유가 있어도 limit가 낮으면 그 순간만 잘립니다.

    현장에서 제일 많이 보는 패턴은 이렇습니다.

    1. 컨테이너에 requests.cpu: 250m, limits.cpu: 500m를 둡니다.
    2. 평소 평균 CPU 사용량은 낮아서 대시보드상 멀쩡해 보입니다.
    3. 트래픽 급증, TLS handshake 증가, JSON serialization, 압축, GC 같은 CPU 이벤트가 같은 시점에 겹칩니다.
    4. 잠깐 500m를 넘겨야 빨리 끝날 일을 quota가 막아버립니다.
    5. Pod 상태는 정상인데 P95, P99 latency와 queue lag가 튑니다.

    여기서 운영자들이 자주 잘못 읽는 지점이 있습니다. 노드 CPU 사용률이 낮다는 사실은 throttling이 없다는 뜻이 아닙니다. throttling은 노드 포화가 아니라, 내가 걸어둔 컨테이너 상한선 때문에 발생할 수 있기 때문입니다. 다시 말해, “클러스터가 부족해서 느린 것”과 “설정이 과하게 보수적이라 느린 것”은 완전히 다른 문제입니다.

    저는 CPU 이슈를 볼 때 아래 세 가지 실패 모드로 먼저 분류합니다.

    실패 모드 대표 신호 근본 원인 주된 처방
    Quota ceiling 노드 여유가 있는데 throttling 증가 CPU limit가 burst를 잘라냄 CPU limit 제거 또는 상향, request 재산정
    Share starvation throttling은 적은데 혼잡 시 처리량 저하 request가 너무 낮아 경쟁에서 밀림 request 상향, HPA 기준 재검토
    Actual node saturation 노드 전체 CPU도 높고 여러 Pod가 함께 흔들림 클러스터 용량 부족 또는 bin-packing 과밀 replica, node size, autoscaling, 배치 정책 조정

    실무에서 중요한 건 “CPU limit를 쓸지 말지”보다, 지금 내가 맞닥뜨린 문제가 셋 중 무엇인지 먼저 판별하는 것입니다. 이걸 건너뛰면 limit를 지워도 해결이 안 되고, request만 올려도 엉뚱한 방향으로 갑니다.

    3. 실전 구현 1: 먼저 현재 리소스 설정부터 확인합니다

    CPU는 감으로 만지면 거의 틀립니다. 제가 제일 먼저 보는 건 애플리케이션 코드가 아니라 실제 Pod에 주입된 request/limit와 namespace 정책입니다. 특히 헬름 차트, Kustomize, 정책 엔진, LimitRange가 섞여 있으면 manifest 원본과 실제 실행 상태가 다를 수 있습니다.

    kubectl get deploy -n prod api-server -o yaml
    kubectl describe pod -n prod api-server-xxxxx
    kubectl get limitrange -n prod -o yaml
    kubectl get resourcequota -n prod -o yaml

    kubectl describe pod의 Requests, Limits, QoS Class를 같이 보세요. 여기서 저는 세 가지를 체크합니다.

    • 의도하지 않은 기본값 주입: LimitRange가 CPU limit와 defaultRequest를 넣었는지
    • request=limit 강제 여부: Guaranteed QoS가 의도된 것인지
    • 컨테이너별 편차: 사이드카가 더 타이트한 limit를 가져 병목이 되는지

    사용량 확인은 kubectl top으로 시작해도 괜찮습니다. 다만 여기 값은 추세를 보는 용도지, throttling 자체를 증명하는 용도는 아닙니다. 저는 kubectl top을 “지금 누구부터 의심할지 정하는 1차 필터” 정도로 씁니다.

    kubectl top pod -n prod --containers
    kubectl top node
    kubectl get --raw /apis/metrics.k8s.io/v1/namespaces/prod/pods
    # 클러스터에 따라 아직 v1beta1만 열려 있을 수도 있습니다.
    # kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/prod/pods

    여기서 평균만 보고 끝내면 거의 반드시 놓칩니다. CPU limit 이슈는 평균 CPU가 아니라 짧은 burst의 절단 여부가 핵심이기 때문입니다. 1분 평균 그래프는 멀쩡한데 P99만 무너지는 서비스가 딱 이 부류예요.

    Prometheus가 있다면 저는 바로 throttling 비율도 같이 봅니다. 다만 아래 메트릭 이름은 보통 cAdvisor 계열 수집 기준이라, 배포 환경이나 런타임에 따라 이름이 다를 수 있습니다.

    sum by (namespace, pod, container) (
      rate(container_cpu_cfs_throttled_periods_total{namespace="prod",container!=""}[5m])
    )
    /
    sum by (namespace, pod, container) (
      rate(container_cpu_cfs_periods_total{namespace="prod",container!=""}[5m])
    )

    이 비율은 절대값 하나로 좋다 나쁘다를 단정하기보다, 배포 전후 추세와 latency 변화를 같이 보셔야 의미가 있습니다. 비율이 조금 보여도 성능에 영향이 없을 수 있고, 반대로 특정 서비스만 유독 증가하면서 응답 시간이 같이 튄다면 꽤 강한 신호입니다.

    Kubernetes CPU Limit 점검을 위해 Pod 리소스와 사용량을 확인하는 운영 화면

    실제 운영 점검 흐름처럼, Pod 사용량과 리소스 필드를 함께 보는 장면을 넣으면 이해가 훨씬 빨라집니다.

    4. Kubernetes CPU Limit를 줄이고 request 중심으로 바꾸는 방법

    지연 시간에 민감한 서비스라면 저는 보통 메모리 limit는 유지하고 CPU는 request 중심으로 먼저 바꿔봅니다. 여기서 중요한 건 limit만 지우고 끝내는 게 아니라, request를 실제 소비 패턴에 맞춰 다시 올리는 것입니다. request를 너무 낮게 두면 throttling은 사라져도 경쟁 상황에서 CPU 배분이 부족해질 수 있거든요. 그러면 병목이 모양만 바뀌어서 다시 돌아옵니다.

    예를 들어 기존 Deployment가 아래처럼 되어 있었다고 해보겠습니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: api-server
      namespace: prod
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: api-server
      template:
        metadata:
          labels:
            app: api-server
        spec:
          containers:
          - name: api-server
            image: nginx
            resources:
              requests:
                cpu: "250m"
                memory: "256Mi"
              limits:
                cpu: "500m"
                memory: "512Mi"

    제가 지연 민감 서비스에서 먼저 시도하는 형태는 이런 쪽입니다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: api-server
      namespace: prod
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: api-server
      template:
        metadata:
          labels:
            app: api-server
        spec:
          containers:
          - name: api-server
            image: nginx
            resources:
              requests:
                cpu: "500m"
                memory: "256Mi"
              limits:
                memory: "512Mi"

    이때 request를 어떻게 잡느냐가 훨씬 중요합니다. 저는 보통 아래 순서로 판단합니다.

    1. 평시 사용량이 아니라 혼잡 시간대의 안정적 최소치를 봅니다.
    2. 짧은 burst는 request가 아니라 노드 여유가 흡수하게 둡니다.
    3. Pending 증가나 bin-packing 악화가 보이면 request를 다시 낮추거나 replica 전략을 손봅니다.
    4. HPA가 CPU utilization을 쓰고 있다면 request 변경이 스케일 판단에도 영향을 준다는 점을 같이 봅니다.

    즉, request는 단순한 “희망 사용량”이 아니라 스케줄링 비용과 오토스케일링 민감도를 동시에 결정하는 숫자입니다. 그래서 저는 limit보다 request를 더 신중하게 다룹니다.

    적용과 확인은 기본기가 중요합니다.

    kubectl apply -f deployment.yaml
    kubectl rollout status deployment/api-server -n prod
    kubectl describe deployment api-server -n prod
    kubectl get pod -n prod -l app=api-server -o wide

    그리고 namespace 정책상 CPU limit가 강제되는 환경이면 Deployment만 바꿔서는 소용이 없습니다. 아래처럼 LimitRange도 같이 봐야 합니다.

    apiVersion: v1
    kind: LimitRange
    metadata:
      name: default-cpu-memory
      namespace: prod
    spec:
      limits:
      - type: Container
        default:
          memory: "512Mi"
          cpu: "500m"
        defaultRequest:
          memory: "256Mi"
          cpu: "250m"

    이런 정책이 있으면 새 Pod가 뜰 때 CPU limit가 다시 들어갑니다. 운영에서는 이걸 배포 파이프라인 문제나 헬름 템플릿 버그로 오해하는 경우가 많습니다. 실제로는 admission 단계 정책 때문인 경우가 적지 않습니다.

    기존 설정과 개선 후 설정이 어떻게 달라지는지, request 중심 전략이 어떤 의미인지 보여주는 비교 이미지 자리입니다.

    5. Kubernetes CPU Limit와 CPU throttling을 어떻게 읽을까

    CPU 튜닝은 수치 하나만 보고 판정하면 자꾸 빗나갑니다. 저는 항상 애플리케이션 지표와 커널 신호를 묶어서 봅니다. 순서는 대체로 이렇습니다.

    1. 사용자 체감 지표: 응답 시간, 작업 처리 시간, 큐 적체, timeout 증가 여부
    2. throttling 지표: container_cpu_cfs_throttled_periods_total, container_cpu_cfs_periods_total, container_cpu_cfs_throttled_seconds_total
    3. 노드 여유: 실제로 클러스터가 바쁜지, 아니면 컨테이너만 막히는지
    4. request/limit 설계: 현재 값이 워크로드의 burst 패턴과 맞는지

    컨테이너 내부에서 cgroup 통계를 직접 확인할 수 있는 환경이라면 이런 식의 점검도 유용합니다. 다만 cgroup v1/v2에 따라 파일 경로는 달라질 수 있습니다.

    kubectl exec -n prod api-server-xxxxx -- sh -c 'cat /sys/fs/cgroup/cpu.stat 2>/dev/null || cat /sys/fs/cgroup/cpu/cpu.stat'
    kubectl exec -n prod api-server-xxxxx -- sh -c 'grep . /sys/fs/cgroup/cpu.max 2>/dev/null || grep . /sys/fs/cgroup/cpu/cpu.cfs_*'

    여기서 보고 싶은 건 멋진 숫자가 아니라 패턴의 일치입니다. 배포 이후 nr_throttled가 빠르게 늘고, 같은 시간대에 API latency가 같이 튄다면 limit가 유력한 원인입니다. 반대로 throttling은 거의 없는데 혼잡 시 처리량이 떨어진다면 request 과소설정이나 노드 포화 쪽을 의심하는 편이 맞습니다.

    제가 현장에서 자주 쓰는 실무 기준은 이렇습니다.

    • 노드 CPU는 한가한데 특정 Pod만 느리다: CPU limit부터 의심합니다.
    • 여러 Pod가 동시에 흔들리고 노드도 바쁘다: 클러스터 용량 또는 배치 문제일 가능성이 큽니다.
    • throttling은 낮은데 HPA가 늦게 반응한다: request 값과 HPA 목표치 조합을 다시 봅니다.
    • 배치 워커가 정시성 트래픽에만 흔들린다: 평균 CPU가 아니라 burst 패턴이 원인일 가능성이 높습니다.

    6. 자주 겪는 함정과 트러블슈팅

    운영에서 반복해서 나오는 함정은 대체로 비슷합니다. 그런데 겉으로 보이는 증상은 비슷해도 근본 원인은 꽤 다릅니다. 저는 아래 네 가지를 별개로 다룹니다.

    • LimitRange 자동 주입: manifest에서 CPU limit를 지워도 실제 Pod에는 다시 생깁니다.
    • limit만 넣었더니 request도 같이 올라감: 의도치 않게 request가 커져 스케줄링이 빡빡해집니다.
    • 평균값 함정: 짧은 burst형 워크로드는 1분 평균 CPU로는 거의 안 잡힙니다.
    • CPU와 메모리를 같은 정책으로 처리: 메모리 보호 논리를 CPU에 그대로 적용하면 성능 비용이 크게 생깁니다.

    제가 실제로 여러 번 본 재현 시나리오를 하나 들어보겠습니다. 백그라운드 워커가 평소에는 조용한데, 매시 정각에 누적된 메시지를 한꺼번에 소모하는 구조였습니다. 대시보드 평균 CPU는 높지 않았고 노드도 한가했습니다. 그런데 정각 직후에만 queue lag와 처리 지연이 치솟았어요. 겉으로 보면 외부 API 지연이나 브로커 문제처럼 보이는데, 실제로는 워커 컨테이너의 CPU limit가 burst를 잘라내고 있던 경우가 꽤 있었습니다.

    이럴 때 흔한 실수는 consumer 개수만 늘리거나 replica만 추가하는 겁니다. limit가 병목이면 replica를 늘려도 각 Pod가 똑같이 잘립니다. 결국 CPU limit 제거, request 상향, 워커 동시성 재조정을 함께 해야 풀리더라고요.

    또 하나, Guaranteed QoS가 항상 성능상 유리한 것은 아닙니다. 모든 컨테이너에 CPU와 메모리의 request/limit를 동일하게 맞추면 eviction 관점에서는 이점이 있을 수 있습니다. 하지만 일반 웹 서비스나 이벤트 소비자처럼 순간 burst가 필요한 워크로드에선 CPU burst 여지를 줄여 응답성에 손해가 날 수 있습니다. 반대로 정수 CPU request를 가진 Guaranteed Pod에 static CPU Manager를 조합하는 초저지연 워크로드는 예외입니다. 이런 경우는 아예 목표가 다릅니다. 멀티테넌시 유연성보다 코어 고정성과 지터 최소화가 우선이니까요.

    7. 검증: 바꾼 뒤 무엇이 좋아져야 정상인가

    설정을 바꿨다면 확인 항목도 분명해야 합니다. 저는 변경 후 아래 네 가지가 같이 움직이는지 봅니다.

    1. P95/P99 지연 시간 또는 작업 처리 시간이 개선되는가
    2. throttling 관련 메트릭 증가 속도가 눈에 띄게 낮아지는가
    3. 노드 CPU 사용량은 다소 올라가더라도 서비스 응답성이 좋아지는가
    4. Pending, FailedScheduling, HPA 이상 반응이 새로 생기지 않는가

    여기서 중요한 건 목표를 헷갈리지 않는 겁니다. CPU 사용률을 낮추는 것이 목적이 아니라, 필요한 순간에 일을 빨리 끝내게 만드는 것이 목적입니다. 그래서 CPU limit를 제거한 뒤 노드 CPU가 조금 더 올라가더라도, 응답 시간과 완료 시간이 좋아졌다면 그건 오히려 정상 반응일 수 있습니다.

    반대로 아래처럼 나오면 다시 조정해야 합니다.

    변경 후 관찰 결과 해석 다음 액션
    latency 개선, throttling 감소, 노드 CPU 소폭 상승 원인이 limit였을 가능성이 큼 현재 전략 유지, request만 미세 조정
    throttling 감소했는데 Pending 증가 request를 공격적으로 올린 상태 request 재산정, replica 또는 node capacity 검토
    latency 변화 거의 없음, 노드 CPU만 상승 limit가 핵심 병목이 아닐 수 있음 애플리케이션 lock, DB, 외부 API, GC 확인
    HPA 스케일 패턴이 갑자기 달라짐 request 변경이 autoscaling 기준에 영향 CPU target utilization과 min/max replica 재검토

    운영에서는 “좋아졌다”를 감각으로 말하면 안 됩니다. 변경 전후 1~2개 배포 주기라도 묶어서 보면서, 애플리케이션 지표와 throttling 신호가 같은 방향으로 개선되는지 확인하셔야 합니다.

    Kubernetes CPU Limit 조정 후 throttling 감소와 응답 시간 개선을 보여주는 대시보드 이미지

    변경 전후를 한눈에 보여주는 대시보드 이미지가 들어가면, 독자가 무엇을 검증해야 하는지 바로 이해할 수 있습니다.

    8. Kubernetes CPU Limit, 상황별 추천은 이렇게 보시면 됩니다

    이 주제는 열린 결말로 끝내면 현업에서 바로 쓰기 어렵습니다. 저는 아래처럼 꽤 명확하게 가져가는 편입니다.

    • 일반 웹 API, gRPC 서버, 이벤트 소비자, burst형 워커: CPU limit는 기본값처럼 넣지 마시고, request 중심으로 먼저 설계해 보세요. 메모리 limit는 유지하고, CPU는 관측 기반으로 조정하는 쪽이 대체로 덜 아픕니다.
    • 공유 클러스터에서 특정 팀이나 테넌트의 폭주를 강하게 막아야 하는 경우: CPU limit를 유지할 이유가 충분합니다. 다만 이때는 성능 비용을 인정하고, throttling 메트릭을 운영 기본 대시보드에 올려두는 편이 좋습니다.
    • HPA를 CPU utilization 기준으로 쓰는 경우: request가 곧 스케일 판단의 분모가 됩니다. request를 너무 높이거나 낮추면 autoscaling 동작이 왜곡될 수 있으니 limit보다 request 튜닝을 더 신중히 보셔야 합니다.
    • 초저지연, 코어 고정성이 중요한 특수 워크로드: request=limit, Guaranteed QoS, static CPU Manager 같은 전용 전략이 맞을 수 있습니다. 이건 일반 웹 서비스의 기본 전략과 분리해서 보셔야 합니다.

    제 판단 기준을 한 줄로 압축하면 이렇습니다. Kubernetes CPU Limit는 성능 향상을 위한 기본 옵션이 아니라, 통제 비용을 감수하고 쓰는 정책 옵션입니다. 서비스 응답성과 처리량이 우선이면 request를 먼저 제대로 잡고, CPU limit는 정말 필요한 경우에만 추가하는 편이 낫습니다. 반대로 클러스터 거버넌스와 테넌트 격리가 우선이라면 limit를 유지하되, 그 대가로 생기는 throttling을 보이는 지표로 관리해야 합니다.

    저는 실무에서 이걸 이렇게 정리합니다. 성능 문제를 풀 때는 CPU limit를 의심하고, 거버넌스 문제를 풀 때는 CPU limit를 설계한다. 이 구분만 분명해져도 불필요한 튜닝 시행착오가 꽤 줄어듭니다.

    HPA나 VPA, ResourceQuota까지 함께 보는 글도 이어서 읽어보시면 흐름이 더 잘 잡힙니다. 다음 글에서는 request 튜닝과 autoscaling을 어떻게 엮어야 하는지, 그리고 LimitRange와 ResourceQuota를 운영 정책으로 쓸 때 어디서 성능 비용이 생기는지 더 깊게 다뤄보겠습니다.

    마무리 직전에 들어갈 요약 인포그래픽 자리입니다. 독자가 상황별 권고안을 빠르게 다시 확인할 수 있게 해줍니다.