포화
포화는 자원 하나가 감당할 수 있는 한계에 얼마나 몰렸는지 알려 주는 값입니다. 한계에 닿은 자원은 들어온 일을 제때 처리하지 못합니다. 서비스가 멀쩡한지 볼 때 맨 먼저 보는 신호 넷, 곧 골든 시그널 가운데 하나입니다.
쉽고 빠른 이해
무슨 일을 하는 값인가 — 자원 하나가 얼마나 찼는지 보여 줍니다. 커넥션 풀에서 커넥션을 기다리는 스레드가 늘기 시작하면 포화가 오르고 있다는 신호입니다.
왜 재나 — 많은 시스템은 자원이 다 차기 전부터 느려집니다. 응답이 늦거나 실패한 것만 봐서는 어느 자원이 원인인지 알기 어렵습니다.
어떻게 재나
- 서비스에서 가장 빠듯한 자원을 고릅니다
- 그 자원을 얼마나 쓰고 있는지, 또는 그 앞에 일이 얼마나 밀렸는지를 잽니다
- 문제가 되기 직전에 알림을 보냅니다
대가 — 자원마다 볼 값이 다릅니다. 같은 포화라는 이름 아래 사용률을 보는 쪽과 밀린 일을 보는 쪽이 갈립니다. 긴 구간의 평균으로 재면 잠깐 찼다 빠지는 포화를 놓칠 수 있습니다.
잘 안 맞을 때 — 디스크 용량처럼 다 차면 바로 오류가 나는 자원에서는 포화를 어떻게 볼지부터 갈립니다.
상세
마트 계산대에 계산원이 한 명 있습니다. 손님이 드문드문 오면 오는 대로 바로 계산을 받습니다. 손님이 계산하는 속도보다 빨리 몰려오면 계산대 앞에 줄이 생깁니다. 손님이 계속 오면 줄은 점점 길어집니다.
서버의 자원도 한 번에 처리할 수 있는 양이 정해져 있습니다. 포화는 자원이 이 한계에 얼마나 몰렸는지를 나타내는 값입니다.
그 정도를 무엇으로 재느냐는 둘로 나뉩니다. 한쪽은 자원이 가진 것 가운데 얼마를 쓰고 있는지, 곧 얼마나 찼는지를 봅니다. 다른 쪽은 한계에 닿아 처리하지 못한 채 밀린 일이 얼마나 되는지를 봅니다. 두 쪽이 누구인지는 「이견」 절이 다룹니다.
예를 들어 CPU(Central Processing Unit, 중앙 처리 장치)가 쉬지 않고 돌고 있습니다. 그런데도 실행 차례를 기다리는 작업이 넷 더 줄 서 있습니다. 앞쪽은 CPU 가 다 찼다는 것을 봅니다. 뒤쪽은 줄 선 작업 넷을 봅니다.
포화를 따지는 자원
포화를 따질 수 있는 자원은 처리할 수 있는 양에 위쪽 한계가 있는 것입니다. 서버를 이루는 물리 부품이 먼저 꼽힙니다. CPU · 메모리 · 디스크 · 네트워크 인터페이스가 그렇습니다.
소프트웨어 안에도 한계가 정해진 자원이 있습니다. 그 가운데 일부는 같은 방식으로 따질 수 있습니다. 대개 애플리케이션 전체가 아니라 그 안의 작은 부품입니다.
뮤텍스 락은 한 번에 한 스레드만 쥐는 잠금입니다. 다른 스레드는 락이 풀리기를 기다리며 줄을 섭니다. 스레드 풀은 스레드 수가 정해져 있습니다. 남는 요청은 처리를 기다립니다. 이런 자원에서는 기다리는 스레드나 요청의 수를 포화로 볼 수 있습니다.
한계를 넘은 일의 두 갈래
자원에 여유가 있으면 들어온 일은 바로 처리됩니다. 한계에 닿은 자원은 일을 더 받지 못합니다. 그 일은 흔히 대기열에 들어가 차례를 기다립니다. 대기열 대신 거절되어 오류로 돌아가기도 합니다.
flowchart TD
A[들어온 일] --> B{자원에 여유가 있나}
B -->|있다| C["바로 처리 · 사용률에 잡힘"]
B -->|없다| D["대기열에서 차례를 기다림 · 줄 길이와 기다린 시간에 잡힘"]
B -->|없다| E["거절되어 오류로 돌아감 · 오류 수에 잡힘"]
갈래마다 잡히는 값이 다릅니다. 사용률로 재는 쪽은 바로 처리되는 일이 자원을 얼마나 채웠는지를 봅니다. 밀린 일로 재는 쪽은 기다리는 갈래를 봅니다. 그쪽에서는 줄 선 일의 수나 그 줄에서 기다린 시간으로 포화를 잴 수 있습니다. 거절된 일은 두 쪽 모두 오류로 따로 셉니다.
가장 빠듯한 자원
서비스 하나에는 자원이 여럿입니다. 포화를 볼 때는 그중 가장 빠듯한 자원에 무게를 둡니다. 메모리가 먼저 모자라는 시스템이면 메모리를 봅니다. 디스크 입출력이 먼저 막히는 시스템이면 입출력을 봅니다.
자원 하나가 한계에 닿아 전체 흐름을 붙잡고 있으면 그 자원을 병목이라 부릅니다. 포화가 오른 자원은 병목 후보입니다.
배경
서비스가 느려지거나 실패하면 요청 쪽에서 먼저 보입니다. 응답이 늦어지는 지연, 실패한 응답의 비율인 오류율, 들어오는 요청의 양인 트래픽이 그런 값입니다. 이 셋은 요청이 겪은 결과를 잽니다. 요청을 붙잡은 것이 어느 자원인지는 이 셋만으로 드러나지 않습니다. 데이터베이스 같은 하위 시스템의 포화는 흔히 그 하위 시스템에서 직접 재야 합니다.
자원이 다 찬 것을 확인한 뒤에 움직이면 늦을 수 있습니다. 많은 시스템은 사용률이 100퍼센트에 닿기 전부터 성능이 떨어집니다. 그래서 자원마다 한계와의 거리를 따로 재는 값이 필요했습니다. 사용률에 목표선을 두는 것이 꼭 필요한 까닭도 같습니다.
두 곳이 이 값을 포화라는 이름의 감시 신호로 세웠습니다. 서비스를 감시할 때 맨 먼저 볼 신호 넷을 묶은 골든 시그널에서 넷째가 포화입니다. 성능 병목을 빨리 찾는 절차인 USE(Utilization·Saturation·Errors, 사용률·포화·오류) 방법은 자원마다 세 가지를 묻습니다. 그 가운데 S 가 포화입니다.
예시
포화는 제품마다 다른 이름의 값으로 드러납니다. 아래 넷은 커널 · 명령행 도구 · 애플리케이션 라이브러리 · 컨테이너 플랫폼에서 하나씩 고른 것입니다.
Linux 커널의 PSI
여러 작업이 같은 자원을 두고 다투는 것을 경합이라 합니다. Linux 커널은 CPU · 메모리 · 입출력 세 자원의 경합을 PSI(Pressure Stall Information, 압력 정지 정보)로 내보냅니다. 작업이 자원을 기다리며 멈춘(stall) 시간을 잽니다.
값은 /proc/pressure/ 아래 cpu · memory · io 파일에 있습니다. 형식은 이렇습니다.
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
두 줄은 멈춘 작업이 얼마나 많은지로 나뉩니다. 칸은 재는 창의 길이로 나뉩니다.
| 칸 | 뜻 |
|---|---|
some 줄 |
적어도 일부 작업이 그 자원을 기다리며 멈춰 있던 시간의 비율 |
full 줄 |
쉬지 않는 작업 전부가 동시에 멈춰 있던 시간의 비율. 이 상태에서는 CPU 시간이 헛되이 흐릅니다 |
avg10 · avg60 · avg300 |
10초 · 60초 · 300초 창에서 잰 비율(퍼센트) |
total |
멈춰 있던 시간의 누계(마이크로초). 평균에 반드시 흔적을 남기지는 않는 짧은 지연 급등을 잡는 데 씁니다. 직접 정한 구간으로 평균을 내는 데도 씁니다 |
full 상태에 오래 머무는 워크로드는 스래싱 중이라고 봅니다.
CPU 의 full 줄은 시스템 전체 수준에서는 뜻이 정해져 있지 않습니다. 그래도 이 줄은 커널 5.13 부터 출력에 들어 있었습니다. 하위 호환을 위해 값을 0 으로 둡니다. 그래서 이 0 은 CPU 앞에서 멈춘 작업이 없었다는 뜻이 아닙니다.
vmstat 의 r 칸
vmstat 은 프로세스 · 메모리 · 스왑 상태를 주기마다 한 줄씩 보여 주는 명령입니다. 출력의 Procs 묶음 첫 칸 r 은 실행할 수 있는 프로세스의 수입니다. 지금 도는 것과 실행 차례를 기다리는 것을 함께 셉니다. 그래서 r 에서 CPU 개수를 넘는 몫은 CPU 앞에 줄을 선 프로세스로 읽습니다.
같은 출력의 Swap 묶음에는 si 와 so 가 있습니다. si 는 디스크에서 메모리로 스왑해 들인 양입니다. so 는 메모리에서 디스크로 스왑해 내보낸 양입니다. 둘 다 초당 값입니다.
메모리 용량이 포화하면 시스템이 감당할 수 있는 것보다 많은 메모리를 요구받습니다. 그러면 페이징과 스와핑이 일어납니다. 이 두 값이 그때 움직입니다.
HikariCP 커넥션 풀
커넥션 풀 라이브러리 HikariCP 는 2.2.0 부터 지표 라이브러리 Dropwizard Metrics 를 지원합니다. 지표 저장소(MetricRegistry)를 설정해 두면 풀 상태를 지표로 모읍니다. 그중 넷이 포화와 맞닿습니다.
| 지표 | 뜻 |
|---|---|
<pool name>.pool.PendingConnections |
풀에서 커넥션이 나오기를 기다리는 스레드 수 |
<pool name>.pool.Wait |
getConnection() 을 부른 스레드가 커넥션을 받기까지, 또는 타임아웃 예외가 나기까지 기다린 시간 |
<pool name>.pool.ActiveConnections |
지금 쓰이고 있는 커넥션 수 |
<pool name>.pool.TotalConnections |
풀 안의 커넥션 총수 |
PendingConnections 는 풀 앞에 선 줄의 길이입니다. Wait 는 그 줄에서 보낸 시간입니다. 밀린 일로 포화를 재는 두 방식이 지표 둘로 나뉘어 있습니다.
ActiveConnections 를 TotalConnections 에 견주면 풀이 얼마나 찼는지가 나옵니다. 사용률로 재는 쪽이 보는 값입니다. PendingConnections · ActiveConnections · TotalConnections 세 값은 지표를 읽을 때 새로 고칩니다. 새로 고치는 단위는 1초입니다.
Kubernetes 컨테이너의 CPU 한도
여러 사용자가 한 시스템을 나눠 쓰는 클라우드 환경에서는 한 사용자가 쓸 수 있는 양에 한도를 걸 수 있습니다. 이 한도도 물리 자원처럼 포화를 따질 수 있습니다.
Kubernetes 에서는 컨테이너에 CPU 한도(cpu limit)를 걸 수 있습니다. Linux 노드에서는 Linux 커널이 cgroup(control group, 컨트롤 그룹)으로 이 한도를 지킵니다. 컨테이너가 한도에 다가가면 커널이 그 컨테이너의 CPU 사용을 조절(throttling)합니다.
커널은 스케줄링 주기마다 한도를 넘었는지 봅니다. 넘었으면 그 cgroup 이 다시 돌기 전에 기다리게 합니다. 한도에 막힌 일은 거절되지 않고 기다립니다.
메모리 한도는 다르게 지켜집니다. 컨테이너가 메모리 한도를 넘으면 커널이 OOM(Out Of Memory, 메모리 부족) 킬로 그 컨테이너를 끝낼 수 있습니다. 커널은 메모리 압력을 감지했을 때만 끝냅니다. 그래서 한도를 넘은 컨테이너가 바로 죽지 않을 수도 있습니다.
flowchart TD
A[컨테이너가 한도에 닿음] --> B{어느 한도인가}
B -->|CPU 한도| C["커널이 조절 · 다시 돌기 전에 기다림"]
B -->|메모리 한도| D{커널이 메모리 압력을 감지했나}
D -->|예| E["OOM 킬로 끝낼 수 있음"]
D -->|아니오| F["끝내지 않음 · 한도를 넘어 쓸 수 있음"]
CPU 한도는 상세의 기다리는 갈래로 갑니다. 메모리 한도는 끝내는 쪽으로 갈 수 있습니다.
cgroup v2 에서 CPU 한도는 cpu.max 파일에 적힙니다. 꼴은 $MAX $PERIOD 두 값입니다. $PERIOD 라는 시간이 한 번 지날 때마다 그 cgroup 이 CPU 시간을 $MAX 만큼까지 쓸 수 있다는 뜻입니다. $MAX 와 $PERIOD 에 적는 수가 어느 단위인지는 자료에 없습니다(미확인).
기본값은 max 100000 입니다. $MAX 자리의 max 는 한도가 없다는 뜻입니다. 그래서 한도를 따로 걸지 않은 cgroup 은 CPU 한도 없이 돕니다.
같은 cgroup 의 cpu.pressure 파일은 위 PSI 와 같은 형식으로 그 cgroup 의 CPU 압력을 보여 줍니다.
이견
포화의 정의는 둘로 나뉩니다. 한쪽은 SRE(Site Reliability Engineering, 사이트 신뢰성 엔지니어링) 책입니다. 다른 쪽은 USE 방법입니다. 갈림의 축은 사용률과 포화를 한 값으로 보느냐, 둘로 가르느냐입니다.
SRE 책의 정의
Google 의 책 『Site Reliability Engineering』 6장 「Monitoring Distributed Systems」(Rob Ewaschuk 씀)는 포화를 골든 시그널의 넷째로 둡니다. 정의는 서비스가 얼마나 「찼는지」입니다. 시스템이 가진 것 가운데 얼마를 쓰고 있는지를 잽니다. 그중 가장 빠듯한 자원에 무게를 둡니다.
이 책은 대부분의 서비스가 CPU 사용률이나 네트워크 대역폭처럼 위쪽 한계가 알려진 간접 신호를 써야 한다고 적습니다. 이 정의에서는 사용률이 포화를 재는 한 방법입니다. 이 책은 사용률에 목표선을 두는 것이 꼭 필요하다고도 적습니다.
USE 방법의 정의
Brendan Gregg 는 2012년 글 「Thinking Methodically about Performance」에서 USE 방법을 소개했습니다. 이 글은 사용률과 포화를 가릅니다.
사용률은 정해진 시간 동안 자원이 일하느라 바빴던 시간의 비율입니다. 바쁜 동안에도 자원은 일을 더 받을 수도 있습니다. 더 받지 못하는 정도가 포화입니다. 그 남는 일은 흔히 대기열에서 기다립니다.
적는 방식도 다릅니다. 사용률은 「CPU 하나가 90퍼센트로 돈다」처럼 적습니다. 포화는 「CPU 실행 대기열의 평균 길이가 4」처럼 적습니다. Gregg 는 포화가 0 만 아니면 문제일 수 있다고 봅니다. 포화는 대기열의 길이로 잴 수도, 대기열에서 기다린 시간으로 잴 수도 있다고 적습니다.
메모리처럼 용량으로 재는 자원에서는 Gregg 도 사용률을 쓰고 있는 용량의 비율로 정의합니다. 이 사용률이 100퍼센트에 닿으면 더는 일을 못 받습니다. 그때 일이 대기열에 쌓이면 포화입니다. 거절되면 오류입니다.
| SRE 책 | USE 방법 | |
|---|---|---|
| 포화의 정의 | 서비스가 얼마나 찼나 | 자원이 처리 못 하고 밀린 일의 정도 |
| 사용률과의 관계 | 사용률을 포화의 간접 신호로 씀 | 사용률과 포화를 따로 잼 |
| 대표 값 | CPU 사용률 · 네트워크 대역폭 | 실행 대기열 길이 · 대기열에서 기다린 시간 |
| 문턱 | 사용률 목표선을 둔다 | 0 이 아니면 문제일 수 있다 |
디스크 용량을 두고
갈림은 디스크 용량에서 다시 보입니다. SRE 책은 「데이터베이스가 네 시간 뒤 하드 디스크를 다 채울 것 같다」를 포화를 미리 내다보는 예로 듭니다.
Gregg 는 USE 방법을 Linux 에 맞춘 점검표도 냈습니다. 자원마다 사용률 · 포화 · 오류를 어느 명령의 어느 값으로 보는지 표로 적은 문서입니다. ENOSPC(No space left on device, 장치에 남은 공간 없음)는 다 찬 장치에 쓰려 할 때 나는 오류입니다. 이 표에서 저장장치 용량 줄의 포화 칸에는 「이게 말이 되는지 확실하지 않다. 다 차면 ENOSPC 다」라고 적혀 있습니다.
같은 표의 파일 서술자 줄에도 같은 물음이 있습니다. 포화 칸에 「이게 말이 되나? 메모리 할당 말고는 줄을 서거나 막히는 일이 없는 것 같다」라고 적혀 있습니다.
같은 현상의 다른 이름
PSI 문서는 포화라는 낱말을 쓰지 않습니다. 같은 현상을 압력(pressure) · 정지(stall)라고 부릅니다. 자원을 두고 작업이 다투는 것은 경합한다(contended)고 적습니다.
운영
자원별로 보는 포화 지표
Linux 서버에서 자원마다 포화를 볼 값은 이렇습니다. 볼 값은 명령 이름과 그 출력의 칸 이름입니다.
| 자원 | 볼 값 | 무엇을 세나 |
|---|---|---|
| CPU | vmstat 1 의 r 이 CPU 개수보다 큼 · sar -q 의 runq-sz 가 CPU 개수보다 큼 |
실행 대기열의 길이 |
| 메모리 용량 | vmstat 1 의 si · so · sar -B 의 pgscank + pgscand · dmesg | grep killed |
스와핑 · 페이지 스캔 · OOM 킬러 기록 |
| 저장장치 입출력 | iostat -xnz 1 의 avgqu-sz 가 1 보다 큼, 또는 await 가 높음 |
대기열의 길이 · 기다린 시간 |
| 네트워크 인터페이스 | ifconfig 의 overruns · dropped |
인터페이스나 운영체제가 남긴 넘침 · 버림 사건 |
메모리 칸의 페이지 스캔은 메모리가 모자랄 때 도는 페이지 스캐너의 활동입니다.
떨어진 패킷(dropped)은 포화 신호로도, 오류 신호로도 셉니다. 두 원인 모두로 생길 수 있기 때문입니다.
위 표는 uptime 의 load average 를 CPU 칸에 넣지 않습니다. Linux 의 load average 는 깨울 수 없는 상태로 기다리는 작업까지 셉니다. 그 기다림은 대개 입출력입니다.
사용률 문턱
사용률 100퍼센트는 대개 병목이라는 신호입니다. 확인은 포화와 그 영향으로 합니다. 대략 60~70퍼센트를 넘는 높은 사용률도 문제가 되기 시작할 수 있습니다.
하드 디스크는 작업 도중에 대개 끼어들 수 없습니다. 더 급한 작업이 와도 지금 작업이 끝나기를 기다려야 합니다. CPU 는 거의 언제든 끼어들 수 있습니다(선점).
디스크 사용률이 60퍼센트를 넘으면 대기열 꼬리가 길어질 수 있습니다. 그러면 대기 지연이 더 자주, 더 눈에 띄게 생길 수 있습니다.
재는 창의 길이
긴 구간의 평균 사용률이 낮아도 짧게 몰린 사용률이 포화를 낳을 수 있습니다. 감시 도구 가운데는 사용률을 5분 평균으로 보고하는 것이 있습니다. 5분 평균은 몇 초씩 100퍼센트를 찍은 구간을 가릴 수 있습니다.
이런 사례가 있습니다. 한 고객의 감시 도구는 CPU 사용률이 80퍼센트를 넘은 적이 없다고 보여 줬습니다. 그런데도 그 서비스는 CPU 포화로 지연을 겪었습니다. 도구가 5분 평균을 보고하는 동안 CPU 사용률은 몇 초씩 100퍼센트를 찍고 있었습니다.
지연도 이른 신호가 됩니다. 지연이 오르는 것은 흔히 포화의 선행 지표입니다. 응답 시간을 짧은 것부터 늘어놓았을 때 99퍼센트 지점의 값을 99번째 백분위 응답 시간이라 합니다. 이 값을 1분 같은 짧은 창으로 재면 포화의 신호를 아주 일찍 받을 수 있습니다.
알림과 예측
포화는 사람을 부르는 문턱도 다릅니다. 골든 시그널 넷을 감시할 때 다른 신호는 문제가 된 때 사람을 부릅니다. 포화는 문제가 거의 된 때 부릅니다.
앞 소절의 선행 지표는 값이 오르는 순서의 이야기입니다. 이 소절의 문턱은 사람을 부르는 시점의 이야기입니다.
포화 감시는 곧 찰 것도 내다봅니다. 하드 디스크가 몇 시간 뒤 다 찰지 미리 셈하는 것이 그런 예측입니다.
복잡한 시스템에서는 더 높은 수준의 부하 측정으로 포화를 보충할 수 있습니다. 지금 트래픽의 두 배를 제대로 받아 낼 수 있나, 10퍼센트 더만 받을 수 있나, 지금 받는 양조차 다 감당하지 못하나를 묻는 식입니다.
커널에 임계 걸기
PSI 에는 임계를 넘으면 감시 프로그램을 깨워 주는 트리거가 있습니다. 트리거는 정해진 창 안에서 멈춘 시간의 누계가 얼마를 넘으면 깨울지를 정합니다.
등록하려면 /proc/pressure/ 아래 감시할 자원의 파일을 엽니다. 그 파일에 <some|full> <멈춘 시간 µs> <창 길이 µs> 꼴로 씁니다. 그다음 연 파일 서술자를 select() · poll() · epoll 로 기다립니다.
sequenceDiagram
participant 감시 as 감시 프로그램
participant 커널
감시->>커널: /proc/pressure/memory 를 연다
감시->>커널: some 150000 1000000 을 쓴다
감시->>커널: poll() 로 기다린다
Note over 커널: 1초 창 안에서 일부 작업의 메모리 정지가 150밀리초를 넘는다
커널-->>감시: 깨운다
/proc/pressure/memory 에 some 150000 1000000 을 쓰면 1초 창 안에서 일부 작업의 메모리 정지가 150밀리초를 넘을 때 깨웁니다. 창 길이는 500밀리초에서 10초까지 받습니다.
관련 항목
포화와 함께 재는 감시 신호
골든 시그널 · 지연 · 트래픽 · 오류율 · 응답 시간 · 백분위수 · 꼬리 지연
포화를 재는 방법론과 지표
USE 방법 · 사용률 · PSI · vmstat · 실행 대기열 · 로드 애버리지 · iostat
포화가 드러나는 자원
CPU · 메모리 · 디스크 · 대역폭 · 커넥션 풀 · 스레드 풀 · 뮤텍스 · 파일 서술자
포화한 자원이 일으키는 현상
스와핑 · 스로틀링 · OOM 킬러 · 스래싱 · 큐 · 백프레셔
포화를 풀거나 미리 막는 수단
수평 확장 · 오토스케일링 · 부하 분산 · 로드 밸런서 · 캐싱 · 용량 계획 · 부하 테스트
포화를 설명하는 이론과 법칙
큐잉 이론 · 리틀의 법칙 · 병목 · 처리량 · 초당 요청 수
포화 지표를 내보내는 운영체제와 플랫폼
Linux · cgroup · Kubernetes · HikariCP
포화를 지켜보는 운영 체계
다른 이름: saturation · 새추레이션 · 포화 상태 · 자원 포화