
💡
모든 글은 가급적 미괄식으로 작성되어, 독자도 같은 상황을 겪게 설계했습니다.따라서 B.1. ~ B.12. / C.1. ~ C.5. 에서 PromQL은 지속적으로 수정됩니다. 각 챕터의 내용을 읽고 이해하여 노드 매트릭을 이해하면 좋을 것 같습니다.
🚩
매트릭(metrics)은 시스템 성능을 표시하는 고수준 지표입니다. 시간에 따라 변화하며 CPU, MEM, Disk, Network 사용량 등을 의미합니다.
지난 시간에는 모니터링 시스템[1]을 배포하였습니다.
오늘은 노드 모니터링 대시보드를 구축하며 PromQL을 배우고 이해하고자 합니다.
사실 지금은
ChatGPT 등을 사용하면 누구나 손쉽게 PromQL을 작성할 수 있습니다.
그리고 많은 블로그에서는 설명과 함께 Best Practice를 전달하고 있습니다.
하지만 그렇게 구축한 대시보드를 얼마나 이해하고 있나요?
가설을 세우고 증명할 수 없다고 그 대시보드가 목적 적합하다고 말할 수 있을까요?
저는 이 가이드를 통해서 3가지 질문의 답을 스스로 찾아가고자 합니다.
왜 그 쿼리를 사용해야 하나요?
CPU, MEM, Disk, Network은 어떻게 측정되고 기록되나요?
1, 2를 기반으로 시나리오 테스트를 통해서 이를 입증할 수 있나요?
실습을 진행하면서 고민을 했던 부분에는 깃발 🚩과 구분자로 표시해두었습니다.
여러분들도 깃발 🚩을 마주하면 스스로 고민하고 답을 찾으면 좋을 것 같습니다.
⚠️
이 문서는 쿼리를 이해하고 작성하고 검증하는 과정을 중점적으로 다룹니다. Grafana 기능에 대해 설명하지 않으며 완성품은 [D. 정리]에서 공유합니다.
A. 실습 환경
A.1. 부하 테스트 도구 설치
apt 패키지 업데이트
apt update -y
stress 툴 설치[A.1.1] — CPU, MEM 부하 테스트 용
apt install stress
hey 툴 설치 [A.1.2] — Network, HTTP(s) Request 부하 테스트 용
apt install hey
iperf 툴 설치 [A.1.3] — Network Bandwidth 측정용 (신뢰도 낮음)
apt install iperf
B. 노드 모니터링 구축하기
엔지니어링의 시작은 정의(definition)을 내리는 것에서 시작합니다.
따라서 노드 모니터링에서 중요한 두 가지 질문을 스스로에게 던져보겠습니다.
노드(node)란 무엇인가?
노드(node)를 모니터링 하는 이유는 무엇인가?
설명의 편의를 위해
쿠버네티스에서 노드[B.1]는 컴포넌트와 워크로드가 실행되는 곳으로 해석했습니다.
📖
컴포넌트(component)는 쿠버네티스로서 작동하기 위한 핵심 부품이며 워크로드(workload)는 실행하기 위한 작업(API, 배치)로 이해하겠습니다.
일반적으로
모든 컴포넌트와 워크로드는 모두 노드(node)를 통해 실행됩니다.
따라서 노드의 상태를 보장할 수 없다면 쿠버네티스와 서비스 모두 보장할 수 없습니다.
📖
클라우드 네이티브(Cloud Native) 의 핵심은 계층화(layer)입니다. - 클라우드 > 클러스터(노드) > 컨테이너 > 코드 이런 관점은 온프레미스에서도 유사하게 사용할 수 있습니다. - 물리적 환경 > 클러스터(노드) > 컨테이너 > 코드 계층화를 통해 관심사를 분리하고 중요도를 배분하기 용이해집니다. - 클라우드(물리적 기계)가 있어야 클러스터가 있을 수 있습니다. - 클러스터(노드)가 있어야 컨테이너가 실행될 수 있습니다. - 컨테이너가 있어야 코드가 실행될 수 있습니다. 상위 계층을 우선적으로 모니터링 하는 것이 합리적일 것입니다. 따라서 통제 가능한 최상위 계층인 클러스터(노드)를 먼저 모니터링 합니다.
따라서
관측 가능한 최상위 계층인 노드를 모니터링 하는 법을 우선적으로 배울 것입니다.
완성된 대시보드의 json 파일은 이 경로[B.1]에 존재합니다.
B.1. 노드 수
💡
각 노드는 node-exporter를 통해서 매트릭을 수집 및 저장해둡니다. 프로메테우스 서버는 node-exporter의 데이터를 별도의 TSDB에 저장합니다.
node-exporter가 설치된 노드들의 수를 집계합니다.
이 경우, 한번 감지된 이후 비활성화(다운)된 노드들도 집계됩니다.
sum(node_exporter_build_info) or vector(0)
정상 가동 중인 노드들의 수를 집계합니다.
가장 기본적인 up 매트릭[B.1.1]을 사용하며 그 값은 1(정상) 혹은 0(다운) 입니다.
sum(up{job="kubernetes-nodes"}) or vector(0)
B.2. vCPU 수량
💡
CPU라고 부르는 연산 장치는 다양한 요소들의 결합체입니다. 기본적으로 소켓(socket)이라는 곳에 물리적 코어(pCPU)가 있습니다. 연산처리 능력을 극대화하기 위해 HyperThreading(or SMT) 기술을 통해 일정한 비율에 따라 논리적 코어(vCPU)를 연산처리 단위로 사용하게 됩니다. 동일한 CPU여도 HyperThreading 활성화 유무에 따라 node_cpu_seconds_total의 수가 달라질 수 있으며 따라서 이 지표의 숫자가 100% 노드의 성능을 나타내지 않으며 노드가 얼마만큼 연산 처리를 많이 하는지의 지표로 이해하는 것이 옳습니다.
각 노드별 연산 처리 단위(일반적으로 vCPU)의 수량을 표기합니다.
node_cpu_seconds_total 매트릭 [B.2.1]의 수량을 카운트하고 노드별로 집계합니다.
count(node_cpu_seconds_total{mode="idle"}) by (node)
실제로 cp-k8s은 아래의 시스템 스펙을 가지고 있습니다.
이름 | socket | pCPU | vCPU | CPU Over |
---|---|---|---|---|
cp-k8s | 1 | 4 | 8 | 2 |
해당 수치는 아래 스크립트로 확인이 가능합니다.
Linux System에서 사용 가능한 컴퓨팅 유닛의 수 ( := vCPU 혹은 pCPU )
nproc
8
Linux System에서 pCPU 수량 조회
lscpu | grep "Core(s) per socket"
Core(s) per socket: 4
Linux System에서 vCPU 수량 조회
lscpu | grep "CPU(s)" | grep -v "NUMA"
CPU(s): 8 On-line CPU(s) list: 0-7
Linux System에서 CPU Topology 확인
lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE MAXMHZ MINMHZ MHZ 0 0 0 0 0:0:0:0 yes 3900.0000 400.0000 800.081 1 0 0 1 1:1:1:0 yes 3900.0000 400.0000 800.247 2 0 0 2 2:2:2:0 yes 3900.0000 400.0000 800.438 3 0 0 3 3:3:3:0 yes 3900.0000 400.0000 400.000 4 0 0 0 0:0:0:0 yes 3900.0000 400.0000 800.409 5 0 0 1 1:1:1:0 yes 3900.0000 400.0000 800.233 6 0 0 2 2:2:2:0 yes 3900.0000 400.0000 800.033 7 0 0 3 3:3:3:0 yes 3900.0000 400.0000 799.991
B.3. MEM 총량
💡
MEM은 데이터를 일시적으로 저장하는 역할을 담당합니다.
시스템에서 사용 가능한 총 메모리를 집계합니다.
이 메모리는 커널과 하드웨어에서 예약된 메모리 용량을 제외합니다.
아래는 노드(cp-k8s)에서 집계된 총 메모리를 볼 수 있습니다.
node_memory_MemTotal_bytes
8210173952
그렇다면 해당 지표는 노드(cp-k8s)에서 어떤 지표와 일치할까요?
그리고 시스템에 등록된 총 메모리는 어떻게 볼 수 있을까요?
노드(cp-k8s) 내부에서 사용 가능한 총 메모리 확인하기
커맨드 사용
PromQL과 Command 모두 같은 값으로 기록됨을 알 수 있습니다.
free -b
total used free shared buff/cache available Mem: 8210173952 2675949568 361148416 505159680 5173075968 4708040704 Swap: 0 0 0
물리적인 파일 참조
KB을 Byte로 변환하면 8210173952 이라는 값이 나옵니다.
cat /proc/meminfo | grep MemTotal
MemTotal: 8017748 kB
노드(cp-k8s)의 총 메모리 확인하기
커맨드 사용
GB를 Byte로 변환하면 8589934592 Byte가 나옵니다. (8 * 1024^3)
즉, 커널/하드웨어 예약 메모리는 362 MB입니다. (379760640 / 1024^2)sudo dmidecode -t memory | grep -i size
Size: 4 GB Size: 4 GB
B.4. 디스크 총량
💡
N 개의 디스크는 M 개의 노드에 마운트되어 사용됩니다. 따라서 이런 경우 디스크 총량을 판별하기 위해서는 필터링이 필요합니다. 하지만 여기서는 1개의 디스크를 1개의 노드의 '/' 경로에 마운트합니다. 따라서 단순한 필터링을 사용하여 실습을 진행했습니다.
시스템에 마운트된 디스크의 총량을 집계합니다.
node_filesystem_size_bytes{mountpoint="/"}
250375106560
이 지표도 노드(cp-k8s)에서 아래와 같이 마운트 경로를 확인할 수 있습니다.
df -B1 /
Filesystem 1B-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 250375106560 15311716352 222270500864 7% /
B.5. 노드 업타임
💡
노드가 사용 가능한 상태인지를 나타내는 지표입니다. 프로메테우스의 metric endpoint를 통해서 up을 1 혹은 0으로 기록합니다.
일정 기간[5m] 동안 노드가 사용가능한 비율을 0.0 ~ 1.0으로 집계합니다.
sum_over_time(up{job="kubernetes-nodes"}[5m]) / count_over_time(up{job="kubernetes-nodes"}[5m])
B.6. 시간대 CPU 사용율
💡
B.2. vCPU 수량 [B.6.1]에서 node_cpu_seconds_total을 처음 봤습니다. 이 지표는 각각의 vCPU 들에 대한 지표를 병렬적으로 수집되고 있습니다. 따라서 적절한 그룹화, 필터링을 통해 원하는 지표를 얻을 수 있습니다.
일정 시간[5m] 동안 노드의 CPU 사용율을 0.0 ~ 1.0으로 집계합니다.
node_cpu_seconds_total은 각 시스템 시작 이후 CPU 코어가 각 모드(idle, user, …)에서 소비한 총 시간을 표기합니다.
(
1
- avg by(node)
(irate(node_cpu_seconds_total{mode="idle"}[5m]))
)
설명만 들으면 이해(공감)이 되지 않기 때문에
아래 각 지표를 직접입력하면서 세부 결과를 비교하는게 좋습니다.
지표 | 설명 |
---|---|
node_cpu_seconds_total{mode=“idle”} | 유휴(idle) 누적 시간 |
irate( | 5m 간 유휴 누적 시간 증가치 |
avg by (node) | 노드(node) 별 |
( | 노드(node) 별 |
Prometheus에서 해당 매트릭을 조회하면 아래와 같은 형태입니다.
여기서 주요한 값으로는 node, cpu, mode과 누적 시간 356018.39과 같습니다.
node_cpu_seconds_total{
app_kubernetes_io_component="metrics",
app_kubernetes_io_instance="prometheus",
app_kubernetes_io_managed_by="Helm",
app_kubernetes_io_name="prometheus-node-exporter",
app_kubernetes_io_part_of="prometheus-node-exporter",
app_kubernetes_io_version="1.9.0",
cpu="0",
helm_sh_chart="prometheus-node-exporter-4.44.0",
instance="172.30.1.48:9100",
job="kubernetes-service-endpoints",
mode="idle",
namespace="kube-monitor",
node="cp-k8s",
service="prometheus-prometheus-node-exporter"
}
356018.39
이 지표는 노드(cp-k8s)에서 /proc/stat 값 [B.6.2]과 일치합니다.
해당 지표는 실시간으로 누적되므로 완벽한 일치를 찾기 어렵습니다.
다만 어떤 cpu, mode 들로 구성되는지를 확인할 수 있습니다.
cat /proc/stat | grep cpu
cpu 5802762 13489 3286836 285729508 430316 0 185856 0 0 0
cpu0 729347 2008 423017 35591418 52866 0 25783 0 0 0
cpu1 723699 1507 416220 35695526 53641 0 23746 0 0 0
cpu2 725307 1269 402979 35734282 54477 0 22767 0 0 0
cpu3 727540 1748 408720 35731832 53453 0 22904 0 0 0
cpu4 722202 1363 405020 35739788 54192 0 22920 0 0 0
cpu5 722854 1478 413442 35744169 54681 0 22384 0 0 0
cpu6 722979 2171 407505 35752651 54043 0 22520 0 0 0
cpu7 728831 1942 409930 35739840 52960 0 22828 0 0 0
첫 줄은 전체 CPU의 mode 별 누적값을 의미합니다.
이후로는 각 CPU 별로 mode 별 누적값을 의미합니다.
mode [B.6.2]는 user, nice, system, idle, iowait, irq, softirq로 구성됩니다.
따라서
1.0 - 1.0 초당 유휴 시간(idle)을 CPU 사용율이라고 해석할 수 있을 것입니다.
B.7. 시간대별 MEM 사용율
💡
B.3. MEM 총량 [B.7.1]에서 node_memory_* 지표를 처음봤습니다. 이 지표는 모든 MEM 들의 지표를 일괄적으로 기록하고 있습니다. 따라서 간단한 그룹화, 필터링 만으로도 원하는 지표를 얻을 수 있습니다. 단, 메모리 수집 도구에 따라 사용 중 메모리 계산 방식이 달라질 수 있습니다. 따라서 Linux RSS, CAdvisor 등에 대한 기본적인 이해가 필요합니다.
각 시점의 MEM 사용율을 0.0 ~ 1.0로 집계합니다.
대부분의 수집 도구에서는 메모리 미사용분을 기준으로 사용량을 측정합니다.
(
1
- (
(
node_memory_MemFree_bytes
+ node_memory_Cached_bytes
+ node_memory_Buffers_bytes
) / node_memory_MemTotal_bytes
)
)
완성된 PromQL만 보면 이해(공감)가 되지 않기 때문에,
아래 각 지표들을 직접 입력하면서 세부 결과를 확인할 수 있습니다.
지표 | 설명 |
---|---|
node_memory_MemFree_bytes | 미사용 메모리 |
node_memory_Cached_bytes | Page Cache 등 |
node_memory_Buffers_bytes | Filesystem I/O 용 Buffer |
node_memory_MemFree_bytes | 사용 중 메모리 |
( | 메모리 미사용 비율 (0.0 ~ 1.0) |
1 - ( | 메모리 사용 비율 (0.0 ~ 1.0) |
위 MemFree, Cached, Buffers는 노드(cp-k8s)의 /proc/meminfo와 일치합니다.
cat /proc/meminfo | grep -E "(MemTotal|MemFree|Buffer|^Cached)"
MemTotal: 8017748 kB
MemFree: 382000 kB
Buffers: 756820 kB
Cached: 4139280 kB
위 계산식에 따르면
전체 메모리의 34.16%을 사용하며 65.83% 가량 여유로운 것으로 보입니다.
하지만 이런 계산식에는 큰 오류가 포함되어 있어서 장애를 늦게 식별할 수 있습니다.
💡
Cached/Buffers Memory는 회수 가능한 부분과 아닌 부분이 있습니다. 이를 회수 가능(Reclaimable), 회수 불가능(Unreclaimable)이라 부릅니다. 사용한 아래 매트릭은 이런 회수 가능성을 구분하지 않고 기록하고 있습니다. - node_memory_Cached_bytes - node_memory_Buffers_bytes 즉, 회수가능한 Cached/Buffers과 Free를 더한 아래 지표를 사용해야 합니다. - node_memory_MemAvailable_bytes
따라서 회수가능한 부분으로 다시 계산하면
전체 메모리의 42.01%을 사용하며 57.99% 가량 여유로운 것을 볼 수 있습니다.
여유 메모리 오차는 수치상 7.84%이며 용량으로는 613.85 MB 임을 알 수 있습니다.
(SpringBoot 파드가 300~400MB 정도 소모하니, 오차 크기가 생각보다 크다…)
(
1
- (
(
node_memory_MemAvailable_bytes
) / node_memory_MemTotal_bytes
)
)
B.8. 시간대별 디스크 사용율
avg by (node) (
1 - (
node_filesystem_avail_bytes{mountpoint="/"}
/ node_filesystem_size_bytes{mountpoint="/"}
)
)
현재 시점의 디스크 사용율을 0.0 ~ 1.0으로 집계합니다.
지표 | 설명 |
---|---|
node_filesystem_avail_bytes{mountpoint="/"} | 사용 가능 디스크 |
node_filesystem_size_bytes{mountpoint="/"} | 전체 디스크 |
node_filesystem_avail_bytes{mountpoint="/"} | 사용 가능 비율 |
1 - ( | 사용 비율 |
avg by (node) ( | 노드별 사용 가능 비율 |
B.9. 노드별 네트워크 수신
💡
네트워크 수신을 external/internal/unkown으로 구분하였습니다. 다만 만약에 외부 인터페이스 카드의 숫자가 많다면, 개별적으로 봐야하지 않을까 싶습니다.
sum by (node, group) (
label_replace(
rate(
node_network_receive_bytes_total{
device=~"wlo.*|en.*|eth.*|ens.*|enp.*"
}[5m]
),
'group',
'external',
'device',
'.*'
) or
label_replace(
rate(
node_network_receive_bytes_total{
device=~"docker.*|cni.*|veth.*|flannel.*|br-.*|lo"
}[5m]
),
'group',
'internal',
'device',
'.*'
) or
label_replace(
rate(
node_network_receive_bytes_total{
device!~"wlo.*|en.*|eth.*|ens.*|enp.*|docker.*|cni.*|veth.*|flannel.*|br-.*|lo"
}[5m]
),
'group',
'unkown',
'device',
'.*'
)
)
지표 | 설명 |
---|---|
node_network_receive_bytes_total | 수신 크기 (bytes) |
node_network_receive_bytes_total[5m] | 기간 당 수신 크기 (bytes) |
rate( | 기간 당 수신 크기 평균 (bytes/s) |
node_network_receive_bytes_total{ | device가 아래에 해당하는 경우 |
sum by (node) ( | device가 아래에 해당하는 경우 |
sum by (node) ( | device가 아래에 해당하는 경우 |
sum by (node, group) ( | device가 아래에 해당하는 경우 |
sum by (node, group) ( | device가 아래에 해당하지 않는 경우 |
B.10. 노드별 네트워크 송신
💡
네트워크 송신을 external/internal/unkown으로 구분하였습니다. 전체적으로 B.9. 노드별 네트워크 수신과 동일하여 쿼리만 기록하였습니다.
sum by (node, group) (
label_replace(
rate(
node_network_transmit_bytes_total{
device=~"wlo.*|en.*|eth.*|ens.*|enp.*"
}[5m]
),
'group',
'external',
'device',
'.*'
) or
label_replace(
rate(
node_network_transmit_bytes_total{
device=~"docker.*|cni.*|veth.*|flannel.*|br-.*|lo"
}[5m]
),
'group',
'internal',
'device',
'.*'
) or
label_replace(
rate(
node_network_transmit_bytes_total{
device!~"wlo.*|en.*|eth.*|ens.*|enp.*|docker.*|cni.*|veth.*|flannel.*|br-.*|lo"
}[5m]
),
'group',
'unkown',
'device',
'.*'
)
)
B.11. 노드별 디스크 조회 시간/크기
💡
기본적으로 디스크 조회 시간(Latency)이 핵심 지표로 설정하고 이후 디스크 조회 크기를 병목 지점 파악을 위해서 사용하려고 합니다.
노드별 디스크 조회 시간
avg by(node) (
rate(node_disk_read_time_seconds_total[5m]) /
clamp_min(rate(node_disk_reads_completed_total[5m]), 0.001)
) * 1000
노드별 디스크 조회 크기
rate(node_disk_read_bytes_total[5m])
B.12. 노드별 디스크 쓰기 시간/크기
💡
전체적으로 B.11. 노드별 디스크 조회/조회시간과 동일하여 쿼리만 기록하였습니다.
노드별 디스크 쓰기 시간
avg by(node, device) (
rate(node_disk_write_time_seconds_total[5m]) /
clamp_min(rate(node_disk_writes_completed_total[5m]), 0.001)
) * 1000
노드별 디스크 쓰기 크기
rate(node_disk_written_bytes_total[5m])
C. 스트레스 테스트
B.1 ~ B.12의 시나리오에 대한 스트레스 테스트를 진행하고자 합니다.
C.1. [테스트] 시간대별 CPU 사용율
💡
현재는 global.scrape_interval을 [5m]에서 [1m]으로 줄였습니다. 하지만 요구사항에 맞춰서 각 job 별로 scrape_interval을 수정해야 합니다. 그렇지 않으면 과도하게 많은 metric 수집으로 인한 부하량이 발생합니다.
vCPU 1 ~ 8 구간에서 30초 테스트, 30초 휴식을 진행했습니다.
for i in {1..8}; do stress --cpu $i --timeout 30s; sleep 30; done
노드(cp-k8s)는 vCPU가 8개 이므로,
예상 사용율은 12.5, 25.0, …, 87.5, 100.0 (± 2.5)으로 증가해야 했으나
실제 사용율은 5.0, 10.0, …, 45.0, 50.0 (± 2.5)으로 증가하는 버그가 보였습니다.
실제로 노드(cp-k8s) 내부에서
top으로 보았을 때에는 12.5, 25.0, …, 87.5, 100.0 (± 2.5)으로 증가했습니다.
즉,
작성한 쿼리가 CPU 사용율을 반영하는데에는 1~3분의 지연시간이 발생했습니다.
이 지연시간을 쿼리 Range를 [5m]에서 [1m], [30s] 등으로 줄여 해결 가능합니다.
하지만 현재 설정은 scrape_interval이 5m, 1m이므로 더이상 줄일 수 없습니다.
kubernetes-service-endpoints-slow, kubernetes-pods-slow : 5m
etc : 1m
kubectl get configmap prometheus-server -n kube-monitor -o yaml
따라서 수집 대상인 kubernetes-nodes의 scrape_interval을 30s로 해야하지만
원활한 실습을 위해서 configmap의 ~.global.scrape_interval을 30s로 변경합니다.
kubectl edit configmap prometheus-server -n kube-monitor -o yaml
기존
prometheus.yml: | global: evaluation_interval: 1m scrape_interval: 1m scrape_timeout: 10s
변경
prometheus.yml: | global: evaluation_interval: 1m scrape_interval: 30s scrape_timeout: 10s
작업 이후 configmap/prometheus-server 변경 상태를 확인해야 합니다.
kubectl get configmap prometheus-server -n kube-monitor -o yaml | grep -E "(global|job_name|scrape_interval)"
global:
scrape_interval: 30s
- job_name: prometheus
job_name: kubernetes-apiservers
job_name: kubernetes-nodes
job_name: kubernetes-nodes-cadvisor
job_name: kubernetes-service-endpoints
job_name: kubernetes-service-endpoints-slow
scrape_interval: 5m
job_name: prometheus-pushgateway
job_name: kubernetes-services
job_name: kubernetes-pods
job_name: kubernetes-pods-slow
scrape_interval: 5m
이후, vCPU 1 ~ 8 구간에서 30초 테스트, 30초 휴식을 다시 진행했습니다.
for i in {1..8}; do stress --cpu $i --timeout 30s; sleep 30; done
테스트 결과 예상/실제 사용율이 유사하게 일치하는 것을 확인했습니다.
C.2. [테스트] 시간대별 MEM 사용율
💡
C.1. [테스트]의 경우 Range를 사용하기 때문에 쿼리를 수정해야 했습니다. 하지만 C.2. [테스트]의 경우 Instant이므로 기존 쿼리를 사용했습니다.
단일 워커에서 MEM 1 ~ 5 구간에서 30초 테스트, 30초 휴식을 진행했습니다.
for i in {1..5}; do stress --vm 1 --vm-bytes ${i}G --timeout 30s; sleep 30; done
노드(cp-k8s)은 총 7.65GB, 기본 3.38 GB (44.2%)을 사용하며
예상 사용율(녹색)은 57.25, 70.32, 83.39, 96.47 이후 OOM이 발생해야 하지만
실제 사용율은 47.5, 63.3, 65.2, 81.2 이후 OOM이 발생하는 이슈가 있었습니다.
(예상과 실제 사용율 간에 약 ±10% 오차범위가 발생하는 것을 발견하였습니다.)
실제 노드(cp-k8s) 내부에서는
free 명령어를 통해서 각 항목 별로 확인이 가능합니다.
free -m
total used free shared buff/cache available
Mem: 7829 2586 3590 489 1653 4492
Swap: 0 0 0
위 명령어는 원하는 정보를 비율로 보기에 불편하기 때문에,
watch, awk 등을 활용한 다음 ShellScript를 활용할 수 있습니다.
watch -n 1 'free -m | awk "{if (NR==2) printf \"Memory: %.2f GiB / %.2f GiB (%.2f%%)\n\", \$3/1024, \$2/1024, \$3/\$2*100}"'
실행한 결과는 33.21%로 기본인 44.2%와는 큰 오차 범위(±10%)를 보입니다.
그 이유는 B.7 시간대별 MEM 사용율에서 배웠는데 혹시 기억할 수 있을까요?
Memory: 2.54 GiB / 7.65 GiB (33.21%)
위에서 사용한 ShellScript는 아래와 같은 형태를 가지고 있습니다.
[watch -n 1] 매 1초 마다 괄호('free -m ...') 안의 명령어를 실행한다
[free -m] : 메모리 사용 정보를 MB 단위로 출력한다.
[| (파이프라인)] : 앞 명령어의 출력을 뒤로 전달한다. (free -m -> awk)
awk "{...}" : awk 프로그램으로 복잡한 문자열 파싱 및 연산에 사용
[if (NR==2)] - 명령어의 두번째 줄(Mem: ....)인 경우에만 아래 실행
[printf \"Memory; ... \"] - 출력문의 형태와 변수의 숫자를 지정
[콤마(,)] - 콤마 뒷 부분에는 변수에 넣을 값과 연산식을 지정
printf \"Memory: %.2f GiB / %.2f GiB (%.2f%%)\n\"
[첫 번쨰 %.2f] - 첫 번쨰 입력값을 소수점 둘쨰 자리까지 표기
[두 번째 %.2f] - 두 번째 입력값을 소수점 둘째 자리까지 표기
[세 번째 (%.2f%%)] - 세 번쨰 입력값을 백분율로 표기 (== %.2f% %)
\$3/1024, \$2/1024, \$3/\$2*100%
[\$3/1024] - $3은 Used 아래 위치를 의미
[\$2/1024] - $2은 Total 아래 위치를 의미
[\$3/\$2*100%] - $3/$2*100은 Used/Total*100을 의미
정답은 사용율에 Unreclaimable Buff/Cache를 포함하지 않았기 때문입니다.
free -m에서 보여주는 기준으로 설명을 하면 다음과 같습니다.
[Used / Total] : cp-k8s: wrong과 동일하며, 회수불가능 Buff/Cache 미포함
[Total - Available / Total] : cp-k8s와 동일하며, 이를 포함
변경된 ShellScript에서는 $3/$2*100이 아닌, ($2-$7)/$2*100을 사용합니다.
직관적으로는 Used/Total*100에서 (Total-Available)/Total*100과 동일합니다.
기존
watch -n 1 'free -m | awk "{if (NR==2) printf \"Memory: %.2f GiB / %.2f GiB (%.2f%%)\n\", \$3/1024, \$2/1024, \$3/\$2*100}"'
변경
watch -n 1 'free -m | awk "{if (NR==2) printf \"Memory: %.2f GiB / %.2f GiB (%.2f%%)\n\", (\$2-\$7)/1024, \$2/1024, (\$2-\$7)/\$2*100}"'
실행한 결과는 43.24%로 기본인 44.2%와는 미세한 오차(0.96%)를 보입니다.
두 결과가 서로 다른 시점의 결과인 것을 고려하면 동일하다고 간주해도 좋아 보입니다.
Memory: 3.31 GiB / 7.65 GiB (43.24%)
따라서 Grafana와 위 쿼리를 동시에 키고
단일 워커에서 MEM 1 ~ 5 구간에서 30초 테스트, 30초 휴식을 진행했습니다.
for i in {1..5}; do stress --vm 1 --vm-bytes ${i}G --timeout 30s; sleep 30; done
노드(cp-k8s)은 총 7.65GB, 기본 3.38 GB (44.2%)을 사용하며
예상, 실제(grafana), 노드(free) 사용율을 아래 표와 같이 비교하였습니다.
구분 | 부하량 (단위 : GB) | |||||
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | |
예상 | - | 57.25 | 70.32 | 83.39 | 96.47 | OOM |
실제(grafana) | 44.2 | 47.5 | 63.3 | 65.6 | 81.2 | OOM |
노드(free -m) | 43.44 | 55.2 | 69.00 | 83.3 | 96.45 | OOM |
이 과정에서 흥미로운 부분은 노드(free -m)에서
메모리가 순간적으로 고점으로 치솟고 이후 빠르게 가라앉는 부분이 보이는 점입니다.
4구간을 예시로 들면 순식간에 96.45%가 되었다가 이후 43.44%로 내려갑니다.
이는 stress라는 도구가 malloc으로 메모리를 할당하고 즉시 회수하기 때문이며
원활한 테스트를 위해서는 --vm-hang 혹은 --vm-keep 을 사용해야 합니다.
--vm-hang : 메모리 할당 후 시간(30s)
--vm-keep : 메모리 할당 후 회수 안함
원활한 테스트를 위해서 --vm-keep을 이용한 아래 구문으로 다시 테스트를 진행합니다.
for i in {1..5}; do stress --vm 1 --vm-bytes ${i}G --vm-keep --timeout 30s; sleep 30; done
노드(cp-k8s)은 총 7.65GB, 기본 3.38 GB (44.2%)을 사용하며
예상, 실제(grafana), 노드(free) 사용율을 아래 표와 같이 비교하였습니다.
구분 | 부하량 (단위 : GB) | |||||
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | |
예상 | - | 57.25 | 70.32 | 83.39 | 96.47 | OOM |
실제(grafana) | 44.2 | 52.2 | 65.3 | 78.5 | 91.5 | OOM |
노드(free -m) | 43.44 | 52.33 | 68.0 | 84.2 | 91.75 | OOM |
이후 노드(free -m)에서
메모리가 순간적으로 고점으로 치솟고 가라앉는 현상이 사라졌습니다.
C.3. [테스트] 시간대별 디스크 사용율
10, 20, 30, 40, 50 GB 파일 생성 및 제거를 각 30초 휴식을 포함해서 진행했습니다.
for i in {1..5}; do dd if=/dev/zero of=~/prometheus/$i.file bs=1G count=$((10*$i)); sleep 30; done
for i in {1..5}; do rm ~/prometheus/$i.file; sleep 30; done
노드(cp-k8s)은 총 233 GB, 기본 26 GB (11.2%)이므로
예상 사용율은 15.45, 24.03, 36.90, 54.07, 75.53으로 증가해야 했으며
실제 사용율도 15.40, 23.09, 36.70, 54.00, 75.40으로 증가했습니다.
테스트 구간에서 조회(쓰기) 시간에서 지연시간(latency)이 증가하는 것을 볼 수 있습니다.
일반적으로 디스크 제품 별[C.3.1]로 조회 및 쓰기 속도는 달라질 수 밖에 없습니다.
따라서 아래 2가지를 고려해서 threshold를 지정하는 것이 좋아보입니다.
타입별 표준 읽기/쓰기 속도
타입별 표준 병목 지점
혹은 서비스 수준 목표(SLO)로 지정한 지연시간(latency)를 기준으로 삼는 것도 좋아 보입니다.
C.4. [테스트] 시간대별 네트워크 사용율
노드 외부로 나가는 요청과 노드 내부로 들어가는 요청을 테스트했습니다.
for i in {1..5}; do hey -n 1000 -c 100 https://google.com/ ; done
kubectl run test-nginx --image=nginx
sleep 30;
for i in {1..5}; do hey -n 1000 -c 100 http://$(kubectl get pod/test-nginx -o=custom-columns=IP:.status.podIP | sed -n 2p); done
sleep 5
kubectl delete pod/test-nginx
테스트 구간에서 네트워크 송신, 수신량이 정상적으로 관측됨을 알 수 있었습니다.
네트워크 인터페이스 또한 대역폭(bandwidth)이 어느 정도 존재합니다.
따라서 아래의 2가지를 지정해서 threshold를 지정하는 것이 좋아보입니다.
네트워크 타입별 표준 읽기/쓰기 속도
아래 구문들을 사용해서 대역폭을 측정할 수 있었습니다.
각각 external 23.4 mb/sec, internal(lo) 39.1 gb/sec 이었지만
대역폭 측정의 오차범위가 크고 네트워크 라우팅 경로에 따라 대역폭이 다르게 나오는 등의 문제가 있기 때문에 100% 신뢰할 수 없어 보입니다.
iperf -c www.google.com -p 443
------------------------------------------------------------
Client connecting to www.google.com, TCP port 443
TCP window size: 85.0 KByte (default)
------------------------------------------------------------
[ 1] local 172.30.1.48 port 42860 connected with 142.250.76.132 port 443
tcp write failed: Broken pipe
shutdown failed: Transport endpoint is not connected
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-0.0374 sec 107 KBytes 23.4 Mbits/sec
kubectl run test-nginx --image=nginx
sleep 30;
iperf -c $(kubectl get pod/test-nginx -o=custom-columns=IP:.status.podIP | sed -n 2p) -p 80
sleep 5;
kubectl delete pod/test-nginx
------------------------------------------------------------
Client connecting to 10.244.0.47, TCP port 80
TCP window size: 85.0 KByte (default)
------------------------------------------------------------
[ 1] local 10.244.0.1 port 40508 connected with 10.244.0.47 port 80
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-10.0002 sec 39.1 GBytes 33.6 Gbits/sec
D. 정리
📖
모든 테스트 결과는 폐쇄된 환경에서 이루어 졌습니다. 하지만 CDPU, MEM, Disk 사용율에 대한 테스트는 유의미해 보였습니다.
모니터링의 핵심은 대상의 상태를 확인하고 위험을 판단하는 것이라 느꼈습니다.
대상의 유형 별로 위험의 징표는 다르게 표현될 수 있기 때문에,
대상의 특징을 이해하고 그에 맞는 위험을 정의하는 것이 중요하다고 생각했습니다.
폐쇄된 실험실 환경에서 테스트를 진행하면서 크게 얻은 지식은 다음과 같습니다.
CPU 점유율이 높아지면 시스템의 처리 속도가 느려진다.
MEM 점유율이 높아지면 시스템의 일부 혹은 전체가 다운된다.
Disk, Network는 요쳥량에 따라서 지연시간이 증가하게 되며,
어느 정도의 지연 시간을 위험으로 판단할 지는 기준에 따라 달라질 수 있다.
정답이 있는 문제는 아니라고 생각했으며, 그래서 더 어렵다고 생각했습니다.