Kubernetes Pod Resources: CPU Limit

Seonsoo Choi
Backpackr Team (idus, Tumblbug, Steadio)
15 min readJan 2, 2024

안녕하세요. 아이디어스 DevOps 셀 최선수입니다.

오늘은 DevOps 셀에서 간헐적으로 순간 요청이 급증하면서 서비스 요청 처리 지연이 발생하고, 이로 인해 다른 서비스도 영향을 받는 문제를 인프라 레벨에서 해결하고자 했던 과정과 경험을 공유하고자 합니다.

들어가기 전에

아이디어스는 Kubernetes(EKS)에서 서비스를 운영하고 있습니다. Kubernetes 서비스를 운영할 때 우리는 수많은 Pod를 정의합니다. Pod는 하나 이상의 컨테이너로 이뤄져 있고 각 컨테이너 별로 리소스(CPU, Memory)를 정의합니다. 리소스 명세는 Requests와 Limits으로 나누어집니다. 어떻게 리소스를 정의했느냐에 따라 사용 가능한 자원은 물론, Pod scheduling 및 Eviction, QoS 등 실제 운영에 전반적인 영향을 주게 됩니다.

DevOps 셀은 image 서비스의 지표 모니터링 중에 특정 패턴을 발견하고, CPU Limit 설정에 대해 의문을 갖게 되었습니다. 관련하여 조사 및 테스트를 진행했으며 도출된 결과를 바탕으로 Production 환경에 일부 적용 했습니다. 이 과정들과 결과를 공유하고 서로의 경험과 의견을 나누고자 합니다.

왜 CPU Limit을 의심해?

Memory와 달리 CPU는 압축 가능한 리소스입니다.[1] 이는 Pod 간 CPU 사용을 위한 경합이 생기더라도 CPU Throttling이 생길지언정 시간을 나눠 분배 가능하다는 뜻입니다.

CPU Throttling

그럼에도 불구하고 저희는 Memory처럼 CPU 역시 Limit을 미리 정의하여 운영하고 있었습니다. CPU Limit을 지정하지 않으면,

  • 시끄러운 이웃이 되어 다른 서비스에 영향을 주면 어떡하지?
  • 심지어 kubelet 같이 Kubernetes를 운영하는 핵심 system에게도 시끄럽게 굴어서 노드가 Unready 상태로 변한다면?

와 같은 우려(두려움)가 있기 때문이었습니다. 아마 많은 팀이 비슷한 근거로 비슷한 운영을 하고 있을 것이라 생각됩니다.

어느 날 셀에서 특이한 지표를 확인했습니다. image 서비스에 순간적으로 요청이 몰렸습니다. 그리고 HPA가 반응하기도 전에 응답 속도가 급속히 증가하였습니다. image 서비스 Pod들은 꾸역꾸역 받아둔 요청들을 처리하다가 결국 요청 처리를 못하는 상태에 빠졌습니다. 그렇게 늦어진 요청 처리는 다른 일부 서비스 지표에도 영향을 주었습니다. 재밌는 점은 노드의 전체 CPU 사용률이나 다른 이웃(Pod)들의 CPU 사용률은 여유 있었다는 것이었습니다. image 서비스는 혼자 애쓰고 있었고 결국 얼럿을 통해 도움을 외치고 있었던 것입니다.

image 서비스의 언어상 이슈든 예외적인 순간 요청 대응이 안 되는 구조든, 근본 원인이 무엇이든 간에 인프라 레벨에서 이 현상을 빠르게 완화하고자 했습니다. 컨테이너 리소스를 늘리거나 HPA 임계값을 낮추는 등 가장 쉬운 방법을 먼저 적용해 두고 다음 방법을 고민했습니다(조금 완화되었나 싶었을 때도 동일 패턴은 나타나고 있었습니다.).

고민 중에 앞서 언급했던 재밌는 점이 다음 아이디어로 이어졌습니다. 다른 노드의 여유 CPU 자원이 있었음에도 image 서비스는 우리가 정해준 CPU Limit 때문에 여유분을 온전히 사용하지 못했음을 지표로 확인했습니다. 노드가 가지고 있던 CPU 자원 여유분을 사용할 수 있었다면 어땠을까요? 그러니까 CPU Limit을 설정하지 않는다면? 혹시 관련해서 다른 팀 혹은 엔지니어는 어떻게 생각하고, 운영하고 있을까요?

조사

먼저 조사부터 시작했습니다. 관심을 갖고 살펴보기 시작하니 CPU Limit(앞으로 언급하는 Limit은 CPU Limit입니다.)은 생각보다 의견이 뜨뜻하게? 대립 중이었습니다.

두 주장 모두 자세히 살펴보고 비교하면 좋겠지만, 우리의 관심사는 CPU Limit 안 함!이었기 때문에 해당 주장 위주로 살펴보았습니다. 그들은 다음과 같은 근거로 주장하고 있었습니다.

  • CPU는 압축 가능한 자원이다.
  • Kubernetes에서 system이 필요로 하는 CPU는 따로 보장받기 때문에 노드 상태에 영향을 주지 않는다.
  • CPU Limit이 없는 두 pod 간 CPU 경합이 발생한다면 Request 비율(정확히는 CPU Share)대로 CPU를 분배받는다.
  • 즉 Request 정의를 해당 컨테이너가 최소로 보장받아야 하는 만큼의 값으로 정의되어 있다면 성능을 바라보고 Limit을 해제해도 된다.

Limit 설정이 오히려 유연하고 효율적인 자원 사용에 도움이 되지 않는다고 해석되었습니다. 일리 있는 주장인 것 같습니다. 위 내용들이 사실이라면 특히 우리 image 서비스 상황을 타개할 좋은 방법일 것 같습니다. 검증하기 위해 먼저 테스트를 진행해 보았습니다.

테스트 진행

테스트 환경 및 방법

  • c5.large 노드 한 대
    - 2 vCPU / 4Gi Memory
    - 시스템 예약 및 daemon들을 제외하고 CPU 여유분은 약 1370m
    - 테스트 Pod 3개를 배포하고 나면 370m 정도 여유[2]
  • 테스트 Pod
    - image 서비스 Pod: 500m / 1024Mi
    - 더미 1(other-0) Pod 100m(200m) / 128Mi
    - 더미 2(other-1) Pod 100m(200m) / 128Mi

이렇게 구성하고 스트레스(image pod는 실제 요청으로, others는 코드로)를 주며 지표를 케이스별로 확인해 보았습니다.

관찰하려는 지표는 아래와 같습니다.

  • Pod CPU 사용량
  • 노드의 여유분 대비 CPU 사용률
  • 노드의 전체 CPU 사용률
  • image Pod 요청량 및 응답 코드

이제 크게 세 가지 케이스로 나눈 뒤,

  1. 모든 Pod가 Limit 정의됨
  2. image Pod만 Limit 없음
  3. 모든 Pod가 Limit 없음

각각 세부적으로 또 두 가지 케이스로 나눕니다.

  1. 일부 Pod idle, 나머지 Pod full-load
  2. 모든 Pod full-load

총 여섯 가지 케이스의 지표를 확인했습니다.

케이스 1: 모든 Pod가 Limit 설정되어 있음

케이스 1–1: image full-load | others idle

케이스 1–1

더미 Pod(이후 other-0, other-1으로 지칭)이 모두 idle 상태이고 노드의 CPU 여유분이 더 있어도 image Pod는 자신의 Limit만큼만 CPU를 사용합니다.

케이스 1–2: 모든 Pod full-load

케이스 1–2

역시 노드 CPU 여분이 있음에도 각자 Limit만큼만 CPU를 사용할 수 있음을 확인할 수 있습니다.

케이스 2: image Pod Limit 없음 | others Pod Limit 있음

케이스 2–1: Image full-load | others idle

케이스 2–1

image Pod가 요청 처리를 위해 노드의 여유분 CPU를 100%를 사용하는 것을 확인할 수 있습니다.
그럼에도 노드 전체 CPU 자원을 다 쓰는 것은 아닙니다.

케이스 2–2: 모든 Pod full-load

케이스 2–2: other-0 full-load
케이스 2–2: others full-load
케이스 2–2: 모든 Pod full-load

other-0, other-1, image 순서로 스트레스를 주었습니다.

other-0을 보면 자신이 사용해야 할 CPU는 뒤에 어떤 스트레스 Pod가 들어오든 꾸준한 사용률을 보임을 확인할 수 있습니다.

마지막으로 image에 스트레스를 주자 other-0, other-1이 사용하고 있는 여유분 CPU를 제외하고 나머지를 사용하는 것을 지표로 통해 확인할 수 있습니다.

특이사항이 있다면 일부 지표가 끊기는 현상을 확인할 수 있는데 이에 관해선 마지막 테스트 결과에서 다시 언급하겠습니다.

케이스3: 모든 Pod가 Limit 없음

케이스 3–1: image idle | others full-load

케이스 3–1

케이스 2–1에서 image Pod 혼자 스트레스일 때와 비슷한 양의 여유분 대비 및 전체 대비 CPU 사용률을 others가 나누어 사용하고 있음을 확인할 수 있습니다.

(지표 끊김 현상은 이번에도 발생하고 있습니다.)

케이스 3–2: 모든 Pod full-load

케이스 3–2

케이스 3–1에서 image Pod에도 스트레스 주자 CPU 사용률 비율이 바뀌었습니다.
마치 케이스 2–2와 비슷한 형태를 최종적으로 보여 줍니다.
세 Pod 중에 CPU를 할당받지 받지 못해 LivenessProbe를 실패하여 Restart 되는 Pod는 없었습니다.
노드가 Unready 되는 상황도 없었습니다.

(지표 끊김 현상은 이번에도 발생하고 있습니다.)

테스트 결과

테스트를 진행하면서 다음 내용들을 확인할 수 있었습니다.

CPU Limit이 없어도,

  1. 노드 상태에 영향을 주지 않습니다; kubelet 등 system이 사용할 CPU에 영향을 주지 않습니다.
    1) 노드 상태 중에 CpuPressure는 없습니다. 처음에 언급했듯이 압축 가능한 자원이기 때문이라 생각됩니다.
    2) Node-pressure Eviction 문서를 살펴보면 CPU 사용량 기준으로 Pod를 Eviction 시키는 일도 없습니다.
  2. Limit 없는 Pod가 Limit 있는 다른 Pod를 굶겨 죽이진 않습니다.
  3. 2번과 조금 다른 상황인데 BestEffort QoS(좀 더 정확하게는 CPU Request가 없는) Pod는 영향을 받을 수 있습니다. 위에 지표 끊김 현상이 그것입니다. deamon 중에 node-exporter가 지속적으로 LivenessProbe를 실패하며 죽었기 때문입니다.
    1) 이는 CPU Request가 없을 때 CPU Share(Request * 1024) 기본 값이 2이기 때문입니다. CPU Request가 100m(0.1 * 1024)인 others Pod만 해도 CPU Share는 102이니까 꽤 차이가 납니다.
    2) 사실 꾸준히 Request에 비율대로 CPU를 분배한다고 언급했지만, 사실 원론적으론 CPU Share 비율대로 우선순위를 선정하여 분배합니다. 기본값인 2로는 CPU Share가 매우 낮기 때문에 아사할 수 있습니다.
    3) Helm chart repo에 포함된 DaemonSet은 BestEffort인 경우[3]가 있습니다. 만약 Limit 해제를 고려하신다면 꼭 한 번 살펴보시길 추천드립니다.
    4) 기술적인 상세한 내용은 하단 참고에 Kubernetes resources under the hood 시리즈에 상세히 설명되어 있습니다.
  4. 모든 Pod가 Limit이 없고 스트레스 상태일 땐 CPU Share 비율대로 분배받습니다.

그리고 덤으로 image Pod의 (부분적이지만) 현상 재현과 개선 사항도 지표로 확인할 수 있었습니다.

Locust Response TImes

Run#1: 케이스 1–2 | Run#2: 케이스 3–2

노드의 여유 자원을 모두 사용함으로써 향상되고 안정적인 응답속도를 보여줍니다.

요청 수 자체는 Run#2가 Run#1보다 약 2배 정도 많습니다.

Production 적용 및 결과

테스트 결과의 3번 사항만 조심하면 큰 사전 준비 없이 Production에 적용할만하다고 판단 했습니다. 사실 빠른 롤백이 언제든 가능하므로 그걸 믿고 진행한 부분도 있습니다.

그럼 적용 결과를 공유 하겠습니다. (비교 지표를 잘 정리해주신 동현님께 감사드립니다.)

먼저 응답 속도 P99는 적용 이후 24시간 기준 지표로 봤을때 감소 효과가 있었습니다.

CPU Throttling도 사라졌습니다.

궁극적으로 원했던 순간 트래픽 급증 때 응답 속도도 편안해졌습니다.

1분당 요청 수: 1.5k > 55.99K 일때도 응답 속도는 편안 합니다.
가용한 CPU, 호로록 합니다.

결론

고백하자면 CPU Limit 해제 쪽의 주장에 좀 더 집중하여 조사하고, 테스트를 진행 후 빠르게 Proudction에 적용했던 것 같습니다. 특히 Kubneretes resources under the hood 글을 통해 Kubernetes와 Linux에서 CPU라는 자원을 어떻게 관리하는지 좀 더 자세히 이해할 수 있었습니다. 기술적 부분 없이 테스트와 결과만 있는 이 글을 보고 나서 호기심이 생겼다면 꼭 한 번 정독해 보는 것을 추천합니다.

반대 측 주장을 좀 더 자세히 들여다봐야 보다 정확히 판단하겠지만, CPU Limit을 두고 양측의 주장하는 바는 서로 정반대지만 그 이유는 조금 성격이 다르다고 판단되었습니다. 우리 셀 내부에서는 이렇게 결론 내렸습니다.

워크로드 성격에 따라 순간적인 요청이 몰리는 패턴을 보인다던가, 응답 속도가 중요할 땐 CPU Limit 해제.
응답성보다는 안정적인 자원 활용(Batch 형태의 Pod들이 배포되어 있는 환경)이 중요하다. 혹은 순간 치고 오는 패턴 없이 항상 꾸준한 처리를 하는 워크로드라면 CPU Limit을 설정.

지금까지 공유드린 내용을 다른 엔지니어 혹은 팀들은 어떻게 생각하는지 궁금합니다. 함께 의견을 나눌 수 있으면 좋겠습니다.

요약

  • 아래 조건을 충족한다면 CPU Limit을 설정하지 않은 Pod가 노드 및 이웃 Pod를 좀 먹지 않는다.
    - 모든 컨테이너는 필요로 하는(보장받아야 하는) CPU만큼 Request로 정의되어야 한다.
    - CPU Request가 없는 경우 CPU Share 기본값이 너무 낮아 CPU를 분배받는 우선순위가 매우 낮다.
  • CPU Limit이 없는 Pod 끼리 CPU 경합이 생기면 CPU Share 비율 대로 분배받는다.
    - 비율이라 함은 결국 상대적이라는 뜻이다.
  • 모든 상황을 관통하는 CPU Limit 설정 정답은 없다. 상황에 맞게 판단하자.
  • 우린 불을 껐다.

이어서 더 생각해 보면 좋을 주제들

연관하여 아래 주제들도 좀 더 생각해보거나 이미 경험 있다면 나누면 좋을 것 같습니다.

  • 반대 주장(CPU Limit 유지)과 이유
  • 컨테이너의 적정 CPU Request 측정 — 특히 Java Application
  • 컨테이너의 Memory 리소스 설정 및 cgroup v2, QoS for Memory

참고

주장: CPU Limit 해제!

Kubernetes resources under the hood — Part 1 (Part 3까지 쭉 이어집니다.)

Kubernetes의 CPU requests와 limits :: Outsider’s Dev Story (위 글을 잘 정리한 우리말 문서. 감사합니다!)

Kubernetes Throttling Doesn’t Have To Suck. Let Us Help! | Netdata Blog

For the Love of God, Stop Using CPU Limits on Kubernetes (Updated) | Robusta

주장: CPU Limit 필수!

CPU limits and requests in Kubernetes

Why You Should Keep Using CPU Limits on Kubernetes (For the Love of God, Stop Using CPU Limits On Kubernetes 글에 대한 반박)

각주

[1] Kubernetes 1.25에서 GA된 cgroup v2를 통해 1.27부터 Memory 관리에도 변화가 생깁니다.

[2] Pod CPU Requests와 배포 후 여유 CPU 자원 차이는 Sidecar(istio) 때문입니다.

[3] Burstable QoS DaemonSet 예시

# prometheus-node-exporter의 경우 이런 식으로 되어있습니다.
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 200m
# memory: 50Mi
# requests:
# cpu: 100m
# memory: 30Mi

Written by idus DevOps Cell

Content writer
Seonsoo Choi

백패커팀과 함께 할 인재를 영입 중입니다.
지금 여기에서 지원해 보세요!

--

--