{"id":"IL-0028","org":"Temporal","date":"2023-06-13","docs":["https://github.com/prometheus-operator/kube-prometheus#readme","https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container","https://twitter.com/thockin/status/1134193838841401345?s=20","https://home.robusta.dev/blog/stop-using-cpu-limits","https://medium.com/directeam/kubernetes-resources-under-the-hood-part-3-6ee7d6015965","https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/","https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#max-surge","https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#max-unavailable","https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#min-ready-seconds"],"title":{"en":"Temporal History service latency rose because its pods were CPU-throttled while CPU usage read below 60 percent","ko":"쿠버네티스에서 Temporal History 파드가 CPU 사용률이 60% 아래인데도 CPU 스로틀링을 받아 요청 지연이 불안정했다"},"domain":["kubernetes","observability"],"impact":{"scope":"request latency of the History service of a self-hosted Temporal Server on Kubernetes","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A CPU usage graph averaged over 15 seconds or more can sit far below 100 percent while a container is being throttled, so throttling needs its own metric. A Go program in a container should have GOMAXPROCS matched to its CPU limit, or to its CPU request when it has no limit. The author recommends not using CPU limits at all.","ko":"CPU 사용률이 낮게 나와도 스로틀링은 일어날 수 있으므로 스로틀링 지표를 따로 관찰해야 한다. Go 서비스에서는 GOMAXPROCS 를 CPU 한도의 정수 코어 수에 맞춘다. GOMAXPROCS 는 CPU 요청값으로는 자동 설정되지 않으므로, CPU 한도를 쓰지 않는다면 CPU 요청값과 같거나 조금 큰 값으로 직접 설정한다. 가능하면 CPU 한도를 쓰지 말고 CPU 요청값을 적절히 잡은 뒤 노드 CPU 사용량을 지켜본다. 노드 CPU 가 자주 바닥나면 파드가 요청값을 넘어 자주 버스트한다는 뜻이므로 요청값을 다시 잡는다."},"related":[],"sources":[{"url":"https://temporal.io/blog/tuning-temporal-server-request-latency-on-kubernetes","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"The post looks at request latency of a self-hosted Temporal Server on Kubernetes, using the History service as the example. Kubernetes turns a container's CPU limit into a quota of CPU time per 1/10th of a second; once the container uses the quota up, its execution waits for the next period. That delay adds latency, most of all to CPU-heavy work such as taking locks. On Temporal's own Kubernetes cluster the nodes had 8 cores and the history pods were limited to 2.\n\nTemporal Server is written in Go, and GOMAXPROCS, the Go runtime setting that controls how many processes Go may use for concurrent work, defaults to the number of cores on the machine. Each History container therefore ran work across up to 8 cores while only 2 cores' worth of time was allowed per period. Garbage collection and other housekeeping ran on cores the container could not really use, and the container's processes starved each other of the allowed CPU time. Setting the GOMAXPROCS environment variable to 2 made the throttling disappear, made latency steadier and slightly lower, and reduced CPU usage across all Temporal services. From release 1.21.0 Temporal sets GOMAXPROCS from the Kubernetes CPU limit when a limit exists and the variable is unset; it does not look at CPU requests, so without limits GOMAXPROCS should be set by hand to the CPU request or slightly above.\n\nThe author also recommends dropping CPU limits and sizing CPU requests carefully while watching node CPU, or, where limits are required for reasons such as customer isolation, putting Temporal on dedicated nodes with taints and tolerations. For the latency spikes during rollouts: with more than 10 history pods, roll one pod at a time with low maxSurge and maxUnavailable and give each pod time to rebalance with minReadySeconds or a startupProbe; with fewer pods, roll quickly. Temporal had good results with 50 percent maxSurge and maxUnavailable at 4 pods or fewer. The Matching service, which balances task queue partitions, behaves the same way.","ko":"Temporal 블로그에 실린 글로, 쿠버네티스에 자체 호스팅한 Temporal Server 의 요청 지연을 낮추고 안정시키는 방법을 다룬다. 예시로 든 것은 History 서비스로, 워크플로 실행을 시작하고 워크플로 상태를 갱신하는 호출을 처리한다. 클러스터 모니터링은 kube-prometheus 스택으로 했다.\n\n쿠버네티스는 컨테이너의 CPU 한도를 0.1초마다 쓸 수 있는 CPU 사이클 한도로 바꾸고, 컨테이너가 이 한도를 넘으면 실행을 늦춘다. 이것이 CPU 스로틀링이고, 락을 잡는 것처럼 CPU 를 많이 쓰는 요청일수록 지연이 커진다. Go 는 기본적으로 머신의 코어마다 프로세스 하나를 쓸 수 있다고 가정한다. 노드는 8코어, history 파드의 CPU 한도는 2코어였으므로 파드는 프로세스를 8개까지 만들 수 있었지만, 스로틀링 주기마다 쓸 수 있는 사이클은 2코어 몫뿐이었다. 프로세스들이 허용된 CPU 사이클을 놓고 서로 다퉜고, 가비지 컬렉션 같은 관리 작업까지 그 좁은 몫 안에서 돌아야 했다.\n\nGOMAXPROCS 환경 변수를 CPU 한도와 같은 2로 설정하자 CPU 스로틀링이 완전히 사라졌고 지연이 안정됐다. CPU 사용량은 줄었고 요청 지연은 오히려 조금 낮아졌다. Temporal 의 모든 서비스에서 CPU 사용량이 함께 내려갔다. 글은 더 나아가 CPU 한도를 아예 쓰지 말고 CPU 요청값과 노드 CPU 사용량을 꼼꼼히 관찰하라고 권한다. 고객 격리 같은 업무상 이유로 한도를 없앨 수 없다면, Temporal 전용 노드를 두고 테인트와 톨러레이션으로 Temporal 배포를 그 노드에 고정한 뒤 Temporal 에서만 한도를 빼라고 한다. 또 History 서비스는 파드가 늘거나 줄 때마다 샤드를 다시 나누는데, 옮겨지는 샤드는 그동안 쓸 수 없어서 배포 중에 지연이 튄다. 파드가 10개보다 많으면 maxSurge 와 maxUnavailable 을 낮게 잡아 하나씩 천천히 교체하고, 그보다 적으면 빠르게 교체하라고 권한다. 4개 이하에서는 두 값을 50%로 두었을 때 결과가 좋았다."},"trigger":{"en":"Normal load on History pods that had a CPU limit of 2 cores on 8-core nodes, with the Go runtime's GOMAXPROCS left at its default of one per node core.","ko":"노드는 8코어였고 history 파드의 CPU 한도는 2코어였는데, Go 로 작성된 Temporal Server 는 GOMAXPROCS 기본값대로 노드의 코어 8개를 모두 쓸 수 있다고 보고 동작했다."},"severity":"unknown","symptoms":["cpu-throttling","high-latency"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"A Prometheus query for CPU throttling showed the history pods throttled although their CPU usage read under 60 percent.","ko":"kube-prometheus 대시보드에서 history 파드의 CPU 사용률은 60% 아래였지만 스로틀링 비율은 0보다 컸다. 15초 이상 간격으로 수집하는 CPU 사용률 지표로는 0.1초 단위로 일어나는 스로틀링이 보이지 않으므로, 스로틀링 지표를 따로 보고서야 문제를 찾았다."},"components":["cpu-limit","language-runtime","prometheus"],"root_cause":"capacity","remediation":[{"text":{"en":"set the GOMAXPROCS environment variable to 2 to match the 2-core CPU limit","ko":"history 파드 배포에 GOMAXPROCS 환경 변수를 CPU 한도와 같은 2로 설정했다."}}],"action_items":[{"text":{"en":"Set GOMAXPROCS automatically from the Kubernetes CPU limit, from Temporal release 1.21.0","ko":"Temporal 1.21.0 부터는 CPU 한도가 있고 GOMAXPROCS 환경 변수가 따로 설정되지 않았으면 서버가 GOMAXPROCS 를 CPU 한도에 맞춰 자동으로 설정한다."}}],"contributing":["default-config-unchanged","monitoring-blind-spot"],"recurrence_of":null,"symptoms_text":{"en":"Request latency from the Temporal Server History service, the service that starts workflow executions and records their progress, was higher and less steady than it should be on a self-hosted Kubernetes deployment. The CPU dashboard showed the history pods using less than 60 percent of their CPU, yet the same pods were being throttled.\n\nThe CPU usage graph hid the problem because of a gap in time scales. Kubernetes enforces a container's CPU limit as a budget of CPU time in every 1/10th-second period, while Prometheus in the kube-prometheus stack scrapes aggregated CPU metrics every 15 seconds or more. A container can run out of its budget in many short periods and still average far below 100 percent over 15 seconds. The throttling only showed up in a query that divides container_cpu_cfs_throttled_periods_total by container_cpu_cfs_periods_total for the temporal-history workload in the temporal namespace.\n\nSeparately, latency could spike when the History deployment was rolled out, because each pod added or removed makes the service move history shards between pods, and a shard cannot be written while it is being reassigned. Metrics emitted by the History service itself are unreliable during a rollout, since Prometheus may not scrape new pods for a few intervals after they start.","ko":"자체 호스팅한 Temporal Server 의 History 서비스에서 요청 지연이 일정하지 않았다. 대시보드에서 history 파드의 CPU 사용률은 60% 아래로 나왔지만 파드는 CPU 스로틀링을 받고 있었다.\n\nCPU 사용률 지표만으로는 이 문제가 보이지 않았다. 쿠버네티스는 컨테이너의 CPU 한도를 0.1초마다 쓸 수 있는 CPU 사이클 한도로 바꿔 적용한다. 반면 Prometheus 는 집계된 CPU 지표를 15초 이상의 간격으로 수집한다. 그래서 사용률이 100%에 한참 못 미쳐도 스로틀링이 일어날 수 있다.\n\n스로틀링은 kube-prometheus 의 cAdvisor 지표로 확인했다. container_cpu_cfs_throttled_periods_total 의 증가량을 container_cpu_cfs_periods_total 의 증가량으로 나누고, namespace=\"temporal\", workload=\"temporal-history\" 로 걸러 이 비율이 0보다 큰지 보았다."},"updated_at":"2026-11-10T00:00:37.875399Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0028","org_slug":"temporal"}