IL-0175 Hyperconnect · 2022-07-19 (게시일)kubernetes
Hyperconnect 는 대부분의 마이크로서비스를 쿠버네티스에 배포한다. DevOps 팀이 낭비되는 자원을 줄이는 작업을 하다가 아직 운영에서 쓰지 않는 notification-preferences 서비스의 CPU requests 를 2에서 1로 줄였다. 이 서비스는 레플리카 2개에 메모리 requests 와 limits 를 1Gi 로 주었고, JVM 에는 `-XX:+UseContainerSupport` 와 `-XX:MaxRAMPercentage=60` 같은 옵션을 넣었다. Java 는 시작할 때와 초기 요청에서 CPU 를 많이 쓰기 때문에 CPU limit 은 일부러 두지 않았다.
서비스는 Spring WebFlux 를 쓰고, Reactor Netty 는 쓸 수 있는 CPU 수에 맞춰 I/O 스레드 수를 정한다. requests 를 1로 줄이자 `availableProcessors()` 의 값이 컨테이너에 준 CPU 가 아니라 노드 전체 CPU 수인 36이 됐고, reactor-http-epoll 스레드가 36개 생겨 메모리 사용량이 올라갔다.
원인은 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 수를 쓰게 된다.
팀은 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 를 명시해야 한다.
DevOps 팀이 아직 운영 트래픽을 받지 않는 notification-preferences 마이크로서비스의 `resources.requests.cpu` 를 2에서 1로 줄였다. 작업을 마치고 몇 분 뒤 메모리 사용량이 90% 를 넘었다는 경보가 왔다. 메모리 할당량이나 JVM 의 GC 관련 인자는 건드리지 않은 상태였다.
처음에는 GC 를 의심했다. CPU 가 줄어 GC 스레드가 줄고 GC 를 덜 하게 됐다고 생각했지만, JMX 와 APM 으로 본 GC 지표에는 별 변화가 없었다. 대신 JMX 로 본 JVM 지표에서 스레드 수가 두 배 넘게 늘어난 것을 찾았다. 1GB 메모리를 준 컨테이너에서 스레드 20개가 늘면 100MB 정도 차이가 날 만했다.
VisualVM 으로 CPU 할당을 바꾸기 전과 후를 비교하자 reactor-http-epoll 스레드가 늘어 있었다. Reactor Netty 는 `Math.max(Runtime.getRuntime().availableProcessors(), 4)` 로 I/O 워커 수를 정한다. CPU 가 2일 때 이 값은 4였는데, 1로 줄이자 36이 됐다.
`kubectl exec` 로 파드에 들어가 jshell 에서 `Runtime.getRuntime().availableProcessors();` 를 실행하니 requests 가 2일 때는 `$1 ==> 2` 가 나왔다. requests 를 1로 바꾸자 `$1 ==> 36` 이 나왔고, 36은 파드가 뜬 노드 전체의 CPU 코어 수였다.
증상 ? 메모리 압박
원인 ? 소프트웨어 결함
CPU requests 를 줄이고 몇 분 뒤 메모리 사용량이 90% 를 넘었다는 경보가 울렸다.
cgroup v1 환경에서 JVM 은 `cpu.shares` 값이 정확히 1024 일 때 이를 설정 없음으로 보고 노드의 CPU 수를 쓴다. CPU limit 없이 requests 만 1로 준 JVM 파드는 노드가 클수록 스레드를 많이 띄우고 메모리를 더 쓴다. 이 문제는 쓸 수 있는 CPU 수로 스레드 수를 정하는 WebFlux 같은 프레임워크에서 특히 크게 드러난다. 이런 설정이라면 `-XX:ActiveProcessorCount` 나 CPU limit 으로 CPU 수를 명시해야 한다.
Hyperconnect 는 원래 운영 JVM 애플리케이션에 CPU requests 를 2 이상 주었고, 마이크로서비스가 늘어 자원을 아끼려다 이 문제를 만났다.
같은 원인 · 공통 컨테이너 CPU 한도
같은 원인 · 공통 컨테이너 CPU 한도