{"id":"IL-0175","org":"Hyperconnect","date":"2022-07-19","docs":["https://bugs.openjdk.org/browse/JDK-8146115","https://visualvm.github.io/","https://github.com/reactor/reactor-netty/blob/v1.0.20/reactor-netty-core/src/main/java/reactor/netty/resources/LoopResources.java#L41","https://github.com/corretto/corretto-11/","https://bugs.openjdk.org/browse/JDK-8281571","https://bugs.openjdk.org/browse/JDK-8281181","https://github.com/openjdk/jdk18u/commit/a5411119c383225e9be27311c6cb7fe5d1700b68"],"title":{"en":"Lowering a JVM pod's CPU request from 2 to 1 pushed memory past the 90% alert because the JVM treats a cgroup v1 cpu.shares value of exactly 1024 as unset and sized Reactor Netty threads from the node's 36 CPUs","ko":"JVM 파드의 CPU requests 를 1코어로 줄이자 JVM 이 노드의 CPU 36개를 인식해 Reactor Netty 스레드가 늘고 메모리 사용량이 경보 기준을 넘었다"},"domain":["kubernetes"],"impact":{"scope":"the notification-preferences microservice (two replicas), not yet serving user traffic","users":"none; the service did not yet receive user requests","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"On cgroup v1, a JVM in a pod with a CPU request of exactly one core and no CPU limit sees every CPU of the node, because cpu.shares 1024 is read as unset. Thread pools sized from availableProcessors(), such as Reactor Netty's I/O workers, grow accordingly and use more memory.\n\nWhen lowering requests for JVM services without a CPU limit, set -XX:ActiveProcessorCount or a CPU limit explicitly. Later JDKs ignore CPU requests for this calculation altogether.","ko":"cgroup v1 환경에서 JVM 은 `cpu.shares` 값이 정확히 1024 일 때 이를 설정 없음으로 보고 노드의 CPU 수를 쓴다. CPU limit 없이 requests 만 1로 준 JVM 파드는 노드가 클수록 스레드를 많이 띄우고 메모리를 더 쓴다. 이 문제는 쓸 수 있는 CPU 수로 스레드 수를 정하는 WebFlux 같은 프레임워크에서 특히 크게 드러난다. 이런 설정이라면 `-XX:ActiveProcessorCount` 나 CPU limit 으로 CPU 수를 명시해야 한다.\n\nHyperconnect 는 원래 운영 JVM 애플리케이션에 CPU requests 를 2 이상 주었고, 마이크로서비스가 늘어 자원을 아끼려다 이 문제를 만났다."},"related":[],"sources":[{"url":"https://hyperconnect.github.io/2022/07/19/suspicious-jvm-memory-in-container.html","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"Hyperconnect runs most of its microservices on Kubernetes. The service's deployment ran with -XX:+UseContainerSupport, heap set to 60% of RAM and a 1Gi memory limit, and deliberately without a CPU limit, because a regular JVM uses a lot of CPU at start-up and while it compiles classes for the first requests, and a CPU limit makes start-up slower and early latency spiky.\n\nThe JVM's container support (Corretto 11 in this case) computes the processor count from the cgroup's CPU quota, period and shares. With no CPU limit, the quota was -1, so only cpu.shares counted, which is 1024 with one requested core and 2048 with two. The HotSpot function CgroupV1Subsystem::cpu_shares() treats a value of exactly 1024 as no shares set and returns -1, so with one requested core the JVM fell back to the CPU count of the node, 36. Spring WebFlux then started 36 reactor-http-epoll threads instead of 4, and the extra thread memory pushed the pod over the alert threshold.\n\nRaising the request again, setting a CPU limit, or requesting 1001m or 999m each had its own drawbacks, so the team fixed the processor count explicitly with -XX:ActiveProcessorCount=1 in JAVA_OPTIONS and wrote an internal notice. The issue is tracked in JDK-8281571 and JDK-8281181; the fix, merged in OpenJDK 18 and planned for JVM 19 with a UseContainerCpuShares option for compatibility, stops CPU requests from affecting availableProcessors() at all, so a CPU limit or ActiveProcessorCount has to be set explicitly.","ko":"Hyperconnect 는 대부분의 마이크로서비스를 쿠버네티스에 배포한다. DevOps 팀이 낭비되는 자원을 줄이는 작업을 하다가 아직 운영에서 쓰지 않는 notification-preferences 서비스의 CPU requests 를 2에서 1로 줄였다. 이 서비스는 레플리카 2개에 메모리 requests 와 limits 를 1Gi 로 주었고, JVM 에는 `-XX:+UseContainerSupport` 와 `-XX:MaxRAMPercentage=60` 같은 옵션을 넣었다. Java 는 시작할 때와 초기 요청에서 CPU 를 많이 쓰기 때문에 CPU limit 은 일부러 두지 않았다.\n\n서비스는 Spring WebFlux 를 쓰고, Reactor Netty 는 쓸 수 있는 CPU 수에 맞춰 I/O 스레드 수를 정한다. requests 를 1로 줄이자 `availableProcessors()` 의 값이 컨테이너에 준 CPU 가 아니라 노드 전체 CPU 수인 36이 됐고, reactor-http-epoll 스레드가 36개 생겨 메모리 사용량이 올라갔다.\n\n원인은 JVM HotSpot 코드에 있었다. 이 파드의 런타임인 Corretto 11 에서 `availableProcessors()` 는 `CgroupSubsystem::active_processor_count()` 를 거쳐 cgroup 의 `cpu.cfs_quota_us`, `cpu.cfs_period_us`, `cpu.shares` 를 읽는다. 기준이 되는 CPU 수는 `sysconf(_SC_NPROCESSORS_CONF)` 로 얻은 노드 코어 수 36이다. limit 이 없으니 quota 는 -1 이고, `cpu.shares` 는 1코어일 때 1024, 2코어일 때 2048 이다. 그런데 cgroup v1 구현의 `cpu_shares()` 는 값이 정확히 1024 이면 shares 를 설정하지 않은 것으로 보고 -1 을 낸다. 그래서 1코어를 준 파드만 컨테이너 CPU 를 인식하지 못하고 노드의 CPU 수를 쓰게 된다.\n\n팀은 requests 를 1001m 과 999m 으로 바꿔 확인했다. 1001m 에서는 `cpu.shares` 가 1025 이고 `availableProcessors()` 는 2 였다. 999m 에서는 `cpu.shares` 가 1022 이고 `availableProcessors()` 는 1 이었다. 스레드와 메모리 지표도 예상대로 움직였다. 같은 문제가 JDK-8281571 과 JDK-8281181 로 보고되어 있었다. JDK-8281571 은 JVM 19 에서 이 문제를 고치고 하위 호환을 위해 UseContainerCpuShares 옵션을 넣는다는 내용이고, 고친 코드는 OpenJDK 18 에도 들어갔다. 고친 구현에서는 requests 가 `availableProcessors()` 값에 더 이상 관여하지 않으므로 CPU limit 이나 ActiveProcessorCount 를 명시해야 한다."},"trigger":{"en":"The team lowered resources.requests.cpu for the notification-preferences deployment from 2 to 1 as part of resource optimisation, with no CPU limit set.","ko":"리소스 최적화 작업 중 notification-preferences 디플로이먼트의 `resources.requests.cpu` 를 2에서 1로 줄였다. CPU limit 은 설정하지 않은 상태였다."},"severity":"unknown","symptoms":["memory-pressure"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"A memory usage alarm above 90% fired a few minutes after the CPU request was lowered.","ko":"CPU requests 를 줄이고 몇 분 뒤 메모리 사용량이 90% 를 넘었다는 경보가 울렸다."},"components":["language-runtime","cpu-limit"],"root_cause":"software-bug","remediation":[{"text":{"en":"set -XX:ActiveProcessorCount=1 in JAVA_OPTIONS so the JVM uses one processor regardless of the cgroup values","ko":"JVM 옵션에 `-XX:ActiveProcessorCount=1` 을 넣어 JVM 이 쓸 CPU 수를 강제로 지정했다. 자원을 다시 늘리거나, CPU limit 을 걸거나, requests 를 1001m 이나 999m 으로 두는 방법은 각각 문제가 있어 고르지 않았다."}}],"action_items":[{"text":{"en":"Write an internal notice about the issue","ko":"이 문제를 사내 공지로 알렸다."}}],"contributing":["no-resource-limits"],"recurrence_of":null,"symptoms_text":{"en":"During a DevOps review of resource usage across microservices, the team lowered resources.requests.cpu from 2 to 1 for notification-preferences, a Spring WebFlux service not yet used in production. A few minutes later an alarm fired because memory usage was above 90%. Neither the memory allocation nor the JVM's GC arguments had been changed.\n\nGC metrics collected through JMX showed no change. The JVM metrics did show that the thread count had more than doubled; about 20 extra threads seemed enough to make a difference of around 100 MB in a pod with 1 GB of memory. VisualVM comparisons before and after the change showed that the extra threads were named reactor-http-epoll. Reactor Netty sizes this pool as the larger of availableProcessors() and 4, so it was 4 with two CPUs requested but 36 with one.\n\nIn jshell inside the pod, Runtime.getRuntime().availableProcessors() returned 2 with requests.cpu set to 2, but 36 with requests.cpu set to 1. 36 is the CPU count of the whole node. With requests.cpu at 1001m, /sys/fs/cgroup/cpu/cpu.shares read 1025 and availableProcessors() returned 2; at 999m, cpu.shares read 1022 and it returned 1.","ko":"DevOps 팀이 아직 운영 트래픽을 받지 않는 notification-preferences 마이크로서비스의 `resources.requests.cpu` 를 2에서 1로 줄였다. 작업을 마치고 몇 분 뒤 메모리 사용량이 90% 를 넘었다는 경보가 왔다. 메모리 할당량이나 JVM 의 GC 관련 인자는 건드리지 않은 상태였다.\n\n처음에는 GC 를 의심했다. CPU 가 줄어 GC 스레드가 줄고 GC 를 덜 하게 됐다고 생각했지만, JMX 와 APM 으로 본 GC 지표에는 별 변화가 없었다. 대신 JMX 로 본 JVM 지표에서 스레드 수가 두 배 넘게 늘어난 것을 찾았다. 1GB 메모리를 준 컨테이너에서 스레드 20개가 늘면 100MB 정도 차이가 날 만했다.\n\nVisualVM 으로 CPU 할당을 바꾸기 전과 후를 비교하자 reactor-http-epoll 스레드가 늘어 있었다. Reactor Netty 는 `Math.max(Runtime.getRuntime().availableProcessors(), 4)` 로 I/O 워커 수를 정한다. CPU 가 2일 때 이 값은 4였는데, 1로 줄이자 36이 됐다.\n\n`kubectl exec` 로 파드에 들어가 jshell 에서 `Runtime.getRuntime().availableProcessors();` 를 실행하니 requests 가 2일 때는 `$1 ==> 2` 가 나왔다. requests 를 1로 바꾸자 `$1 ==> 36` 이 나왔고, 36은 파드가 뜬 노드 전체의 CPU 코어 수였다."},"updated_at":"2026-11-10T00:00:37.333512Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0175","org_slug":"hyperconnect"}