PRO DBDATA PRO DBDATA

Linux cgroup v2와 PSI 분석: CPU·메모리·I/O 병목 진단 및 해결 가이드

읽는 시간 약 14분
Linux cgroup v2와 PSI를 이용한 CPU 메모리 I/O 병목 분석

서버나 컨테이너의 응답 시간이 갑자기 길어지면 가장 먼저 CPU 사용률과 메모리 사용량을 확인하게 됩니다. 그런데 모니터링 화면에서는 CPU 사용률이 50% 이하이고 사용 가능한 메모리도 남아 있는데, 애플리케이션 응답 시간만 수백 밀리초에서 수 초까지 증가하는 경우가 있습니다.

이런 장애는 단순 사용량만 확인해서는 원인을 찾기 어렵습니다. 프로세스가 실제로 CPU를 기다렸는지, 메모리 회수 때문에 멈췄는지, 디스크 I/O 완료를 기다렸는지를 함께 확인해야 합니다.

Linux cgroup v2와 PSI(Pressure Stall Information)를 사용하면 제한된 자원을 얼마나 사용했는지를 넘어, 자원을 기다리느라 작업이 멈춘 시간을 확인할 수 있습니다. 이번 글에서는 증상 확인부터 내부 동작 원리, 명령어를 이용한 진단, 복구 절차와 재발 방지 방법까지 운영 관점에서 정리해 보겠습니다.

CPU 사용률은 낮은데 응답이 느려지는 증상

운영 환경에서 자주 만나는 유형은 노드 전체의 CPU 사용률은 높지 않지만 특정 컨테이너나 서비스의 처리량이 급격히 떨어지는 상황입니다. 애플리케이션 로그에는 명확한 오류가 없고 데이터베이스 응답도 정상인데, API의 상위 응답 시간이 평소보다 몇 배 이상 증가합니다.

예를 들어 8코어 서버에서 애플리케이션 cgroup에 CPU 1코어 수준의 제한이 적용되어 있다고 가정해 보겠습니다. 애플리케이션이 할당량을 모두 사용하면 노드 전체 CPU 사용률은 약 12.5%에 불과해도 해당 cgroup 안에서는 CPU Throttling이 발생할 수 있습니다.

메모리도 비슷합니다. 서버에 여유 메모리가 남아 있더라도 특정 cgroup의 memory.high 또는 memory.max에 가까워지면 해당 그룹의 프로세스는 반복적인 메모리 회수나 OOM 종료를 경험할 수 있습니다.

I/O 제한이 적용된 환경에서는 디스크 사용률이 높지 않은데도 특정 서비스의 쓰기 작업만 지연될 수 있습니다. 로그 기록, 임시 파일 생성, 데이터 압축 작업이 동시에 실행되면 이러한 현상이 더욱 뚜렷하게 나타납니다.

개인적으로 이 유형을 분석할 때는 CPU 사용률이나 메모리 사용량 하나만 보고 자원을 증설하지 않습니다. 먼저 지연이 발생한 시간대의 PSI와 cgroup 이벤트를 확인한 뒤 CPU, 메모리, I/O 중 어느 자원에서 대기 시간이 늘었는지를 구분하는 편입니다.

cgroup v2와 PSI의 내부 동작 원리

cgroup은 프로세스를 그룹으로 묶어 CPU, 메모리, I/O 등의 자원을 제한하고 사용량을 기록하는 Linux 커널 기능입니다. cgroup v2는 자원 컨트롤러를 하나의 통합 계층 구조에서 관리한다는 점이 cgroup v1과 가장 크게 다릅니다.

Kubernetes에서도 Pod와 컨테이너의 CPU·메모리 요청 및 제한을 실제 Linux 수준에서 적용하기 위해 cgroup을 사용합니다. cgroup v2는 통합 계층 구조, 강화된 메모리 관리와 PSI 지원 등의 장점을 제공합니다. Kubernetes cgroup v2 공식 문서

현재 시스템에서 cgroup v2가 활성화되어 있는지는 다음 명령어로 확인할 수 있습니다.

stat -fc %T /sys/fs/cgroup

정상적인 cgroup v2 환경에서는 다음과 같이 출력됩니다.

cgroup2fs

마운트 상태도 함께 확인하는 것이 좋습니다.

mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

PSI는 프로세스가 CPU, 메모리 또는 I/O 자원을 기다리면서 실행하지 못한 시간을 측정합니다. 시스템 전체 지표는 /proc/pressure에서 확인할 수 있고, cgroup별 지표는 각 cgroup 디렉터리의 cpu.pressure, memory.pressure, io.pressure에서 확인할 수 있습니다.

PSI 출력에는 somefull이 표시됩니다. some은 하나 이상의 작업이 자원을 기다린 시간의 비율이고, full은 실행 가능한 모든 비유휴 작업이 동시에 멈춘 시간의 비율입니다.

avg10, avg60, avg300은 각각 최근 10초, 60초, 300초 동안의 평균 압력 비율입니다. total은 누적된 지연 시간을 마이크로초 단위로 보여줍니다. 단, 시스템 전체 CPU의 full 값은 의미가 정의되지 않아 호환성을 위해 0으로 표시될 수 있습니다. Linux Kernel PSI 공식 문서

확인 지표와 진단 명령어

먼저 시스템 전체의 PSI를 확인합니다.

cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

CPU 압력이 발생한 서버에서는 다음과 같은 결과를 볼 수 있습니다.

some avg10=18.40 avg60=11.72 avg300=4.81 total=384729115
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cpu some avg10=18.40은 최근 10초 동안 적어도 하나의 작업이 CPU를 기다린 시간의 비율이 높았다는 의미입니다. 순간적인 배치 작업으로 잠깐 상승할 수 있으므로 avg10만 보지 말고 avg60, avg300과 애플리케이션 응답 시간을 함께 비교해야 합니다.

메모리 PSI가 다음과 같다면 메모리 회수나 직접 회수 과정에서 작업 지연이 발생했을 가능성이 있습니다.

some avg10=6.40 avg60=4.12 avg300=1.35 total=82411390
full avg10=1.20 avg60=0.75 avg300=0.18 total=19528440

특히 memory full이 지속적으로 증가한다면 모든 작업이 동시에 메모리 자원을 기다리는 구간이 발생한 것입니다. 이때는 단순한 메모리 사용량보다 페이지 캐시 회수, Swap, cgroup 한도와 OOM 이벤트를 함께 확인해야 합니다.

I/O PSI 결과는 다음과 같이 나타날 수 있습니다.

some avg10=12.55 avg60=8.31 avg300=3.27 total=218371992
full avg10=4.75 avg60=2.61 avg300=0.92 total=57198341

io full이 높으면 그룹 내 모든 실행 가능한 작업이 I/O 완료를 기다리는 구간이 있다는 의미입니다. 디스크 장치의 지연뿐 아니라 cgroup에 설정된 IOPS 또는 대역폭 제한도 확인해야 합니다.

특정 서비스의 cgroup 경로는 systemd에서 다음과 같이 확인할 수 있습니다.

systemctl show myapp.service --property=ControlGroup
ControlGroup=/system.slice/myapp.service

해당 경로를 변수로 지정한 뒤 cgroup별 PSI를 확인합니다.

CG=/sys/fs/cgroup/system.slice/myapp.service

cat "$CG/cpu.pressure"
cat "$CG/memory.pressure"
cat "$CG/io.pressure"

CPU 제한과 Throttling 발생 여부는 cpu.maxcpu.stat으로 확인합니다.

cat "$CG/cpu.max"
cat "$CG/cpu.stat"
100000 100000

usage_usec 984235128
user_usec 746818443
system_usec 237416685
nr_periods 48219
nr_throttled 12837
throttled_usec 238495112

cpu.max의 첫 번째 값은 사용할 수 있는 CPU 시간이고 두 번째 값은 기간입니다. 위 설정은 100,000마이크로초마다 최대 100,000마이크로초를 사용할 수 있으므로 약 1코어에 해당합니다.

nr_throttledthrottled_usec가 빠르게 증가하면서 cpu.pressure도 함께 상승한다면 CPU 제한이 응답 지연의 직접적인 원인일 가능성이 높습니다. 누적값 자체보다 10초 또는 1분 간격의 증가량을 비교하는 것이 중요합니다.

메모리 상태는 다음 파일을 확인합니다.

cat "$CG/memory.current"
cat "$CG/memory.high"
cat "$CG/memory.max"
cat "$CG/memory.events"
987541504
805306368
1073741824

low 0
high 584
max 238
oom 4
oom_kill 2
oom_group_kill 0

현재 메모리 사용량은 약 942MiB이고 memory.high는 768MiB, memory.max는 1GiB로 설정된 상태입니다. high 값이 계속 증가하면 메모리 사용량이 memory.high를 넘어 회수와 제한이 반복된 것이며, oom_kill은 실제 프로세스가 종료된 횟수를 나타냅니다.

I/O 사용량과 제한은 io.statio.max에서 확인할 수 있습니다.

cat "$CG/io.stat"
cat "$CG/io.max"
8:0 rbytes=238182400 wbytes=498417664 rios=18244 wios=39215 dbytes=0 dios=0
8:0 rbps=max wbps=10485760 riops=max wiops=500

위 결과에서는 장치 8:0에 초당 10MiB의 쓰기 대역폭과 500 IOPS 제한이 적용되어 있습니다. 로그나 임시 파일 쓰기가 증가하면 장치 전체가 여유로운 상황에서도 해당 cgroup의 io.pressure가 상승할 수 있습니다.

원인별 복구 절차와 해결 방법

CPU 압력이 높고 nr_throttled가 빠르게 증가한다면 먼저 불필요하게 높은 스레드 수와 동시 처리량을 줄여야 합니다. 애플리케이션이 실제로 더 많은 CPU를 필요로 하고 호스트에 여유가 있다면 CPU 할당량을 조정할 수 있습니다.

systemd 서비스는 다음과 같이 CPU 한도를 변경할 수 있습니다.

systemctl set-property myapp.service CPUQuota=200%

이 설정은 최대 약 2코어를 사용할 수 있게 합니다. 변경 후에는 nr_throttled 증가 속도, CPU PSI와 애플리케이션 응답 시간이 함께 감소하는지 확인해야 합니다. 한도만 높였는데 노드 전체 CPU 압력이 증가한다면 수평 확장이나 코드 최적화가 더 적절한 해결책입니다.

메모리 문제는 memory.events를 기준으로 접근하는 것이 좋습니다. high만 증가한다면 메모리 회수로 인한 지연일 가능성이 높고, max, oom, oom_kill까지 증가한다면 하드 한도에 도달한 것입니다.

호스트 메모리에 여유가 있고 애플리케이션의 정상 Working Set이 기존 한도보다 크다면 다음과 같이 조정할 수 있습니다.

systemctl set-property myapp.service MemoryHigh=1536M MemoryMax=2G

다만 메모리 누수나 무제한 캐시가 원인이라면 한도 증가는 장애 발생 시간을 늦출 뿐입니다. Heap Dump, 프로세스 RSS, 페이지 캐시와 객체 캐시 정책을 먼저 확인해야 합니다.

I/O 압력이 높다면 iostat으로 장치 자체가 포화되었는지 확인합니다.

iostat -xz 1

장치의 지연시간과 사용률은 낮은데 특정 cgroup의 io.pressure만 높다면 io.max 제한을 우선 점검합니다. 반대로 장치 전체의 대기시간과 큐가 증가했다면 스토리지 성능 확장, 로그 쓰기량 감소, 임시 파일 경로 분리 또는 배치 작업 시간 조정이 필요합니다.

복구 후에는 반드시 동일한 명령어를 다시 실행해 PSI와 누적 이벤트의 증가 속도를 비교해야 합니다. 개인적으로는 설정 변경 직후 CPU 사용률만 확인하기보다 10분 이상 avg10, avg60, 응답 시간과 처리량을 함께 관찰하는 방식을 권장합니다.

재발 방지와 마치며

PSI에는 모든 환경에 공통으로 적용할 수 있는 절대 임계치가 없습니다. 짧은 CPU 대기는 정상적인 동시 처리 과정에서도 발생할 수 있기 때문에 평상시 기준선을 수집하고 애플리케이션 지연과의 상관관계를 확인해야 합니다.

재발을 막으려면 cpu.stat, memory.events, io.stat과 각 PSI 파일을 시계열 지표로 수집하는 것이 좋습니다. 경보도 PSI 하나만 기준으로 만들기보다 응답 시간, 오류율, Throttling 증가량과 조합해야 오탐을 줄일 수 있습니다.

컨테이너의 CPU와 메모리 제한은 평균 사용량이 아니라 부하 테스트에서 확인한 상위 사용량과 순간적인 Burst를 고려해 결정해야 합니다. Kubernetes 환경이라면 requests와 limits의 차이, QoS Class, 노드의 실제 수용량도 함께 검토해야 합니다.

또한 cgroup v2와 PSI가 정상적으로 활성화되어 있는지 운영 표준에 포함하는 것이 좋습니다. PSI는 커널의 CONFIG_PSI 옵션이 필요하며, 배포판에 따라 부팅 매개변수 psi=1이 필요할 수 있습니다.

Linux cgroup v2와 PSI 분석의 핵심은 사용량과 대기 시간을 구분하는 것입니다. CPU 사용률이 낮다는 사실만으로 CPU 병목이 없다고 판단할 수 없으며, 여유 메모리가 보인다고 해서 특정 cgroup이 메모리 압력을 겪지 않는 것도 아닙니다.

장애 시간대의 PSI, cgroup 제한값과 이벤트 증가량을 함께 분석하면 CPU Throttling, 메모리 회수, OOM 종료와 I/O 제한을 더 빠르게 구분할 수 있습니다. 이 과정을 운영 절차로 정착시키면 무조건적인 자원 증설을 줄이고 실제 병목에 맞는 해결책을 적용할 수 있습니다.

PRODB

함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.