[CS] 같은 VM인데 왜 Redis CPU 사용률이 다를까?

Hocaron·2025년 10월 11일
0

CS

목록 보기
3/3

상황

Redis 클러스터를 모니터링하다가 이상한 점을 발견했다. 같은 클러스터의 노드들인데, 트래픽은 비슷한데 CPU 사용률이 달랐다.

노드 1: CPU 70%
노드 2: CPU 45%
노드 3: CPU 48%
노드 4: CPU 72%

트래픽이나 key 분포 문제인가 싶어서 확인해봤지만 특별한 차이가 없었다. 그럼 뭐가 문제일까?

원인을 찾다가 물리 서버 스펙을 확인했고, 하나의 단서를 발견했다.

서버 그룹 A: Xeon Silver 4116 (1세대)  → 노드 1, 4 (CPU 높음)
서버 그룹 B: Xeon Silver 4216 (2세대)  → 노드 2, 3 (CPU 낮음)

같은 8코어 VM인데 물리 서버가 다르다는 것만으로 CPU 사용률이 이렇게 차이가 날까?


"같은 코어 수인데 성능이 다르다"는 게 무슨 의미일까?

먼저 기본부터 이해해보자.

CPU 코어는 "일꾼"이라고 생각하면 된다. 8코어 VM은 8명의 일꾼이 있다는 뜻이다.

그런데 "일꾼의 숫자"가 같다고 해서 "일의 처리 속도"가 같은 건 아니다.

왜? 각 일꾼의 "능력"이 다르기 때문이다.

비유하자면

  • 구형 일꾼(4116): 한 번에 상자 1개를 옮김
  • 신형 일꾼(4216): 한 번에 상자 1.2개를 옮김 (더 효율적)

결과적으로 같은 8명이 일해도, 신형 일꾼들이 20% 더 빠르게 일을 끝낸다.

그럼 이제 "왜 신형이 더 효율적인가?"를 구체적으로 알아보자.


CPU 세대 차이 - 왜 성능이 다를까?

두 CPU의 스펙을 비교해보자.

구분Xeon 4116Xeon 4216
CPU 세대Skylake-SPCascade Lake-SP
기본 클럭2.1GHz2.1GHz
L3 캐시16.5MB22MB (+33%)
메모리 속도DDR4-2400DDR4-2933 (+22%)
IPC기준약 10~15% 개선

클럭은 같은데 뭐가 다른 걸까? 핵심은 3가지다.

1) IPC - "한 번에 얼마나 많은 일을 하는가"

IPC가 뭔가요?

IPC (Instructions Per Cycle)는 "CPU가 1초에 똑딱똑딱 하는 그 한 번(클럭)에 몇 개의 일을 처리하는가"를 나타낸다.

클럭 = 시계 초침이 한 번 가는 것
IPC = 그 한 번에 몇 개의 작업을 끝내는가

비유:

  • 구형 일꾼(4116): 1초에 10번 움직임, 한 번에 1개 작업 → 초당 10개 작업
  • 신형 일꾼(4216): 1초에 10번 움직임, 한 번에 1.2개 작업 → 초당 12개 작업

실제 처리량 = 클럭 속도 × IPC

Xeon 4116: 2.1GHz × 1.0 IPC = 2.1 "단위 일"
Xeon 4216: 2.1GHz × 1.15 IPC = 2.4 "단위 일" (+14%)

왜 신형 CPU의 IPC가 높은가?

CPU는 명령을 처리할 때 이런 과정을 거친다:

1. 명령 읽기 (Fetch)
2. 해석하기 (Decode)
3. 실행하기 (Execute)
4. 결과 저장 (Write back)

구형 CPU는 이 과정을 하나씩 순서대로 처리한다. 하지만 신형 CPU는:

1. Branch Predictor 개선 (분기 예측)

Redis 코드를 보면 if-else가 많다:

if (key_exists) {
    return value;
} else {
    return NULL;
}

CPU는 if의 결과가 나오기 전에 미리 "아마 true겠지?" 하고 예측해서 다음 작업을 시작한다. 예측이 맞으면 시간을 벌지만, 틀리면 다시 해야 한다.

신형 CPU는 예측 정확도가 95% → 98%로 향상되어, 다시 하는 경우가 줄었다.

2. Out-of-Order Execution (순서 바꿔 실행)

명령 A가 메모리를 기다리는 동안, 뒤에 있는 명령 B, C를 먼저 실행할 수 있다.

구형: A(메모리 대기 100ns) → B → C
신형: A(메모리 대기 시작) → B, C 먼저 실행 → A 완료

신형 CPU는 이 "재배치 버퍼"가 더 커서, 더 많은 명령을 동시에 처리할 수 있다.

Redis에 미치는 영향

Redis는 단일 스레드다. 즉, 1개 코어의 성능이 전체 성능이다.

IPC가 15% 높으면:

  • 같은 GET 요청을 15% 빠르게 처리
  • 초당 10만 요청 처리 시, 15% 더 빠르게 끝냄
  • 결과: CPU 사용률이 낮아 보이지만 실제로는 더 많은 일을 한다

2) L3 캐시 - "자주 쓰는 걸 가까이 둔다"

캐시가 뭔가요?

CPU가 데이터를 쓸 때마다 RAM(메인 메모리)에 가서 가져오면 너무 느리다.

비유: 창고에서 물건 가져오기

  • RAM = 멀리 있는 창고 (100미터)
  • 캐시 = 책상 서랍 (1미터)

자주 쓰는 물건은 책상 서랍에 두면 훨씬 빠르다.

캐시 계층 구조

CPU에는 3단계 캐시가 있다:

L1 캐시 (64KB)   → 가장 빠름 (1~2 사이클, 약 1ns)
L2 캐시 (1MB)    → 빠름 (10~20 사이클, 약 10ns)
L3 캐시 (16~22MB) → 보통 (40~60 사이클, 약 50ns)
RAM              → 느림 (100~300 사이클, 약 100ns)

"사이클"이란? CPU가 똑딱 한 번 하는 것. 2.1GHz CPU는 1초에 21억 번 똑딱한다.

왜 L3 캐시 크기가 중요한가?

Redis GET 요청이 들어오면:
1. Key의 해시값 계산
2. 해시 테이블에서 위치 찾기 → 메모리 접근 1회
3. 실제 value 읽기 → 메모리 접근 1회

만약 둘 다 L3 캐시에 있으면:

50ns + 50ns = 100ns

만약 하나가 RAM에 있으면:

50ns + 100ns = 150ns (50% 느림)

L3 캐시가 크면 → 더 많은 key를 캐시에 보관 → 캐시 히트율 증가

CPUL3 캐시캐시 히트율 (예상)
Xeon 411616.5MB90%
Xeon 421622MB93%

캐시 히트율 3%p 증가의 효과:

  • 평균 메모리 지연: 100ns × 0.9 + 150ns × 0.1 = 105ns (구형)
  • 평균 메모리 지연: 100ns × 0.93 + 150ns × 0.07 = 103.5ns (신형)

차이가 작아 보이지만, 초당 10만 요청이면:

구형: 105ns × 100,000 = 10.5ms
신형: 103.5ns × 100,000 = 10.35ms

→ 약 1.5% CPU 시간 절약

Redis에 미치는 영향

Redis는 메모리 DB다. 모든 작업이 메모리 접근이다.

캐시가 크면:

  • 핫한 key들이 L3에 더 오래 머문다
  • RAM 접근 횟수가 줄어든다
  • CPU가 "메모리 기다림" 시간이 줄어든다
  • 결과: 같은 workload를 더 빠르게 처리, CPU 사용률 감소

3) 메모리 속도 - "데이터를 얼마나 빨리 가져오는가"

DDR4-2400 vs DDR4-2933이 뭔가요?

"2400"은 "초당 2400메가 전송 (2400 MT/s)"을 의미한다.

비유: 수도관 굵기

  • DDR4-2400 = 가는 수도관 (초당 19.2L)
  • DDR4-2933 = 굵은 수도관 (초당 23.5L, +22%)

왜 중요한가?

Redis는 메모리 접근이 매우 많다:

  • GET 요청: key + value 읽기
  • SET 요청: key + value 쓰기
  • BGSAVE: 전체 메모리 읽기
  • Replication: 데이터 전송

메모리 속도가 22% 빠르면:

메모리 읽기 시간: 100ns → 82ns (-18%)

Redis에 미치는 영향

Redis가 10GB 데이터를 다루고, 초당 10만 QPS를 처리한다고 가정하자.

평균 요청당 메모리 접근: 1KB

초당 메모리 전송량: 100,000 × 1KB = 100MB/s

구형 (DDR4-2400):
- 대역폭: 19.2GB/s
- 사용률: 100MB / 19,200MB = 0.5%
- 메모리 대기: 100ns

신형 (DDR4-2933):
- 대역폭: 23.5GB/s
- 사용률: 100MB / 23,500MB = 0.4%
- 메모리 대기: 82ns (-18%)

메모리 대기 시간이 18% 줄면, CPU는 그만큼 더 빨리 다음 요청을 처리할 수 있다.

결과: CPU 사용률이 낮아진다


종합: 왜 4216이 CPU 사용률이 낮을까?

요소개선 효과의미
IPC 개선+15%한 클럭에 더 많은 일 처리
L3 캐시 증가+3%캐시 히트율 향상, RAM 접근 감소
메모리 속도 증가+18%메모리 대기 시간 감소
총합약 20~30%같은 작업을 더 빠르게 완료

즉, 4216은 같은 workload를 20~30% 빠르게 처리한다.

Redis가 100개 요청을 처리할 때:

  • 4116: 100초 걸림, CPU 사용률 100%
  • 4216: 77초 걸림, CPU 사용률 77%

같은 일을 하는데, 신형은 빨리 끝내고 쉬는 시간이 생긴다.

실제 조치 및 결과

문제가 된 Redis 노드들을 1세대 서버에서 2세대 서버로 이동시켰다.

이동 전: CPU 70%
이동 후: CPU 48%

결과는 명확했다. 이후 운영 정책도 변경되었다:

"CPU 연산 성능의 차이로 서비스에 영향이 있을 수 있어, 1세대 서버는 추가적으로 사용하지 않는다"


하드웨어만으론 설명이 안 되는 부분

여기까지는 하드웨어 차이로 설명이 된다. 그런데 이상한 케이스를 발견했다.

같은 2세대 CPU, 같은 8코어 VM인데:

  • 서버 X: Redis CPU 사용률 55%
  • 서버 Y: Redis CPU 사용률 75%

뭐가 다른 걸까?

OS 설정을 확인해보니, 여러 차이점이 있었다.


OS/커널 설정이 CPU 사용률에 미치는 영향

(1) Transparent Huge Pages (THP) - "큰 상자에 담으면 편하다?"

THP가 뭔가요?

Linux는 메모리를 "페이지"라는 단위로 관리한다. 기본 크기는 4KB다.

비유: 물건을 상자에 담기

  • 일반 페이지: 작은 상자 (4KB)
  • THP: 큰 상자 (2MB, 512배 크기)

큰 상자를 쓰면 관리가 편하다. 1,000개 물건을 담을 때:

  • 작은 상자: 상자 250개 필요, 관리 복잡
  • 큰 상자: 상자 2개면 충분, 관리 간단

이론적으로는 THP가 좋아 보인다. 실제로도 일부 애플리케이션에서는 성능이 향상된다.

그런데 왜 Redis는 THP를 싫어할까?

Redis가 THP를 싫어하는 이유

Redis는 이런 경고를 자주 띄운다:

WARNING you have Transparent Huge Pages (THP) support enabled in your kernel.
This will create latency and memory usage issues with Redis.

이유는 3가지다.

1) fork() 시 COW 문제

Redis는 BGSAVE나 AOF rewrite 시 fork()를 한다. 이게 뭔가?

fork() = 프로세스 복사하기

Redis 부모가 10GB 메모리를 쓰고 있다면, 자식도 10GB를 써야 한다. 하지만 처음부터 10GB를 복사하면 너무 느리다.

그래서 Linux는 "COW (Copy-on-Write)"를 쓴다:

1. 처음엔 복사 안 함 (페이지 테이블만 복사)
2. 부모가 메모리를 수정하려고 할 때
3. 그때 해당 페이지만 복사

작은 상자(4KB)일 때:

부모가 1개 key 수정
→ 그 key가 들어있는 4KB 페이지만 복사
→ 4KB 복사 (아주 빠름)

큰 상자(2MB)일 때:

부모가 1개 key 수정
→ 그 key가 들어있는 2MB 페이지 전체를 복사
→ 2MB 복사 (512배 느림!)

Redis는 작은 key-value를 수없이 다룬다. 평균 100바이트라면:

  • 4KB 페이지: 40개 객체
  • 2MB 페이지: 20,480개 객체

1개 key를 수정해도 20,480개가 들어있는 2MB 전체를 복사해야 한다.

실제 측정:

THP 켜짐: BGSAVE 시 5초 이상 멈춤, CPU 100% spike
THP 꺼짐: BGSAVE 시 0.1초 미만, CPU 정상
2) 메모리 단편화 문제

시간이 지나면서 메모리에 빈 공간이 조각조각 생긴다.

작은 상자(4KB):

빈 공간이 8KB 있으면
→ 4KB 상자 2개를 넣을 수 있음

큰 상자(2MB):

빈 공간이 1.5MB씩 여러 곳에 흩어져 있으면
→ 2MB 상자를 넣을 수 없음
→ 커널이 "메모리 정리(compaction)" 작업을 함
→ 프로세스가 블록됨
→ Redis 응답 지연 발생
3) 메모리 낭비

Redis에 1KB 객체 100만 개를 저장한다면:

작은 상자(4KB):

1KB 객체 4개가 4KB 페이지 1개에 들어감
→ 낭비 없음
→ 필요한 메모리: 약 1GB

큰 상자(2MB):

1KB 객체를 2MB 페이지에 넣으면
→ 2MB 중 1KB만 사용
→ 1.999MB 낭비
→ 실제 사용 메모리: 1.5GB (50% 낭비!)

OS별 THP 기본값

OSTHP 기본값
Ubuntu 18.04/20.04always (항상 켜짐)
CentOS 7/8always (항상 켜짐)
Amazon Linux 2madvise (선택적)

즉, 대부분의 OS는 THP를 켜놓고 있다. Redis에는 독이 되는데.

실제로 여러 Redis에서 THP 경고가 발생했고, 비활성화 작업을 진행했다.


(2) Swappiness - "메모리가 부족하면 어떻게 할까?"

Swappiness가 뭔가요?

Linux는 메모리가 부족하면 디스크로 페이지를 옮긴다 (swap).

비유: 책상과 서랍

  • 메모리 = 책상 (빠름)
  • 스왑 = 서랍 (느림)

책상이 가득 차면 자주 안 쓰는 것을 서랍에 넣는다.

swappiness는 "얼마나 적극적으로 서랍에 넣을 것인가"를 결정한다 (0~100).

대부분 OS 기본값: 60

왜 Redis에 치명적인가?

Redis는 "모든 데이터가 메모리(책상)에 있다"는 전제로 설계되었다.

메모리 접근: 100ns (0.0001ms)
SSD 접근:   100,000ns (0.1ms, 1,000배 느림)
HDD 접근:   10,000,000ns (10ms, 100,000배 느림)

Redis 데이터가 일부라도 swap(서랍)으로 가면:

1. GET 요청이 들어옴
2. 해당 key가 swap에 있음을 발견
3. 커널이 디스크에서 읽어옴 (수십 ms)
4. 이 동안 Redis 메인 스레드가 멈춤
5. 다른 모든 요청도 대기

실제 측정:

메모리 사용률 70% 도달 시:

swappiness=60 (기본값):
→ Redis 메모리 일부 swap out
→ GET 응답: 0.5ms → 20ms (40배!)
→ CPU 사용률: 50% → 70%

swappiness=1:
→ Redis 메모리 swap out 안 됨
→ GET 응답: 일정하게 0.5ms
→ CPU 사용률: 안정적으로 50%

왜 CPU 사용률이 오히려 올라갈까?

Swap이 발생하면 CPU는 이런 일을 한다:

1. Page fault 처리 (CPU busy)
2. 디스크 I/O 대기 (CPU idle)
3. Swap in/out 작업 (CPU busy)
4. Context switch 빈발 (CPU busy)

실제 연산은 적게 하면서, "대기"와 "관리" 작업으로 CPU가 바쁘게 보인다.


(3) TCP/IP 스택 - "연결 요청을 받을 준비가 되어있는가?"

somaxconn이 뭔가요?

Redis는 네트워크로 요청을 받는다. OS는 "연결 대기열"을 관리한다.

비유: 식당 대기줄

  • somaxconn = 대기줄 최대 인원 수
  • 대기줄이 가득 차면 → 새 손님은 거절
sysctl net.core.somaxconn

OS별 기본값

OSsomaxconn
Ubuntu 20.044096
CentOS 7/8128
Amazon Linux 24096

CentOS의 128은 현대 서비스에는 너무 작다.

실제 시나리오

고부하 상황:

초당 10,000개 연결 요청
평균 처리 시간: 1ms
→ 보통은 동시 연결 10개 정도

하지만 순간적으로 spike 발생:
→ 0.1초 동안 1,000개 요청 도착
→ somaxconn=128이면?
→ 128개만 대기열에 들어감
→ 나머지 872개는 거부됨
→ 클라이언트는 connection timeout 발생

Redis 입장에서는 요청을 받지도 못했다. 당연히 CPU를 쓸 일이 없다.

즉, 같은 workload인데 OS 설정에 따라 "실제로 처리되는 요청 수"가 다르고, CPU 사용률도 다르게 나타난다.


(4) CPU Governor - "전력 절약 vs 성능"

CPU Governor가 뭔가요?

CPU는 전력을 아끼기 위해 클럭 속도를 조절할 수 있다.

비유: 자동차 기어

  • 1단 (낮은 클럭): 연비 좋음, 느림
  • 5단 (높은 클럭): 연비 나쁨, 빠름
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

주요 모드:

  • powersave: 항상 최저 클럭 (연비 우선)
  • ondemand: 부하에 따라 조절 (기본값)
  • performance: 항상 최고 클럭 (성능 우선)

왜 CPU 사용률이 달라질까?

powersave 모드 (1.0 GHz):

Redis GET 명령: 1000 사이클 필요
1.0GHz에서 1000 사이클 = 1.0μs

초당 100만 요청 처리:
→ 100만μs = 1초 전부
→ CPU 사용률 100%

performance 모드 (3.0 GHz):

Redis GET 명령: 1000 사이클 필요
3.0GHz에서 1000 사이클 = 0.33μs

초당 100만 요청 처리:
→ 333,000μs = 0.33초
→ CPU 사용률 33%

같은 workload인데, 클럭이 낮으면 CPU가 "더 오래 바쁜 상태"로 보인다.

ondemand의 문제

ondemand는 부하를 감지해서 클럭을 올린다. 하지만:

1. 요청 도착 → CPU 부하 감지
2. 클럭 상승 명령 (수십~수백 μs 소요)
3. 클럭이 올라감
4. 요청 처리
5. 부하가 낮아짐 → 클럭 하락
6. 다음 요청 → 다시 2번부터

Redis처럼 latency-sensitive한 서비스에는 "클럭 올리는 시간" 동안 지연이 발생한다.

실측 결과:

powersave: 평균 0.8ms, 최대 5ms spike
ondemand: 평균 0.5ms, 가끔 2ms spike
performance: 평균 0.2ms, spike 거의 없음

NUMA - 가장 어렵지만 가장 큰 영향

여기까지 분석하고 나니 대부분의 CPU 사용률 차이는 설명이 되었다. 그런데 한 가지 더 이상한 케이스를 발견했다.

같은 2세대 CPU, 같은 OS, 같은 커널 설정인데:

  • 멀티소켓 서버 (2-socket): Redis CPU 70%
  • 단일소켓 서버 (1-socket): Redis CPU 45%

왜?

NUMA가 뭔가요?

NUMA (Non-Uniform Memory Access)는 멀티소켓 서버의 메모리 구조다.

비유로 이해하기

단일소켓 서버 (NUMA 없음):

[CPU] ←→ [메모리]
  ↓
모든 CPU가 같은 거리에서 메모리 접근

멀티소켓 서버 (NUMA 있음):

[CPU 0] ←빠름→ [메모리 A]
   ↓
  느림
   ↓
[CPU 1] ←빠름→ [메모리 B]

각 CPU 소켓마다 "전용 메모리"가 있다.

  • CPU 0이 메모리 A 접근: 빠름 (100ns)
  • CPU 0이 메모리 B 접근: 느림 (250ns, 2.5배)

왜 이런 구조를 쓸까?

단일 메모리 컨트롤러의 문제:

CPU 0 ┐
CPU 1 ├→ [메모리 컨트롤러] → [메모리]
CPU 2 ┘

→ 모든 CPU가 하나의 메모리 컨트롤러를 경쟁
→ 병목 발생

NUMA의 장점:

CPU 0 → [메모리 컨트롤러 0] → [메모리 A]
CPU 1 → [메모리 컨트롤러 1] → [메모리 B]

→ 각 CPU가 전용 메모리 컨트롤러 사용
→ 메모리 대역폭 2배

하지만 단점도 있다:

  • CPU 0이 메모리 B에 접근할 때: CPU 0 → CPU 1 → 메모리 B (느림)

Redis가 NUMA에 취약한 이유

1) 단일 스레드 + 랜덤 메모리 접근

Redis는 이렇게 작동한다:

while (true) {
    request = readFromSocket();
    key = parseKey(request);
    value = hashtable.get(key);  // 어디 있을지 모름
    writeToSocket(value);
}

Key가 어디 있을지 예측할 수 없다. 메모리 A에 있을 수도, 메모리 B에 있을 수도 있다.

만약 40%가 "remote NUMA 노드"에 있다면:

평균 지연 = 100ns × 0.6 + 250ns × 0.4 = 160ns
vs
전부 local = 100ns

→ 60% 느려짐

2) 메모리가 여러 NUMA 노드에 흩어진다

Redis가 오래 실행되면서 메모리를 할당/해제하면:

처음: 모든 메모리가 Node 0에 할당됨
시간 경과 후: Node 0 메모리 부족
→ 새로운 key는 Node 1에 할당됨
→ 메모리가 Node 0, Node 1에 섞임

실제 측정:

numastat -p $(pgrep redis-server)

                 Node 0  Node 1  Total
Numa_Hit        180000  120000  300000
Numa_Miss        40000   80000  120000  # 40% remote!

40%의 접근이 remote면 성능 저하가 크다.

3) 자원 경합 시 문제 심화

단독 실행 시:

Redis 프로세스가 CPU 0에 고정됨
→ 대부분 메모리 A 사용
→ Remote access 최소

여러 VM 동시 실행 시:

Redis 프로세스가 CPU 0 ↔ CPU 1 왔다갔다
→ 메모리 A, B를 번갈아 접근
→ Remote access 급증
→ 메모리 대역폭 경쟁 심화

실제 측정:

# 단독 실행
node-load-misses: 5%

# 자원 경합
node-load-misses: 40%

IPC로 보는 NUMA 영향

NUMA 이슈가 있을 때 IPC를 측정하면:

# NUMA miss 많음 (멀티소켓, 미튜닝)
IPC: 0.6
CPU utilization: 70%

# NUMA miss 없음 (단일소켓 or vCPU pinning)
IPC: 1.8
CPU utilization: 45%

CPU 사용률은 70%로 높지만, 실제로 하는 일의 양은 비슷하다.

IPC가 뭘 의미하는지 다시 보자:

IPC = 0.6 의미:
→ 클럭 1회당 0.6개 명령 실행
→ 클럭 1.7회당 1개 명령 실행
→ 나머지 0.7 클럭은 메모리 대기

IPC = 1.8 의미:
→ 클럭 1회당 1.8개 명령 실행
→ 거의 대기 없이 실행

CPU가 70% 바쁜데 IPC가 0.6이면 → 실제로는 "메모리 기다림"으로 시간을 낭비하고 있다.

vCPU Pinning의 효과

VM의 vCPU를 특정 물리 CPU와 NUMA 노드에 고정하면:

Before (자유 스케줄링):

시간   Redis vCPU → 물리 CPU  메모리 위치   지연
0ms    →  4 (Node 0)         Node 0      100ns
1ms    → 16 (Node 1)         Node 0      250ns (remote!)
2ms    →  2 (Node 0)         Node 0      100ns
3ms    → 18 (Node 1)         Node 0      250ns (remote!)

After (Node 0에 pinning):

시간   Redis vCPU → 물리 CPU  메모리 위치   지연
0ms    →  4 (Node 0)         Node 0      100ns
1ms    →  6 (Node 0)         Node 0      100ns
2ms    →  2 (Node 0)         Node 0      100ns
3ms    →  4 (Node 0)         Node 0      100ns

Remote access가 사라지고 성능이 안정화된다.

실제 검증 결과

가상화 환경 전환 시 성능 검증을 진행했다:

시나리오:

  • 튜닝 미적용 VM: 일반 설정
  • 튜닝 적용 VM: vCPU Pinning, THP 비활성화 등

결과:

단독 실행: 큰 차이 없음
자원 경합 상황: CPU 성능 2배 이상 차이!

왜 자원 경합 시에만 차이가 날까?

단독 실행 시에는 프로세스가 한 곳에 머물 가능성이 높다. 하지만 여러 VM이 동시에 돌면 서로 밀어내면서 NUMA 노드를 넘나든다.

즉, 실제 운영 환경(멀티테넌시)에서 NUMA 설정이 왜 중요한지를 보여준다.


정리

같은 8코어 VM에서 같은 Redis를 실행해도 CPU 사용률이 다른 이유:

1. 하드웨어 세대 차이 (20~30%)

핵심:

  • IPC: 한 클럭에 더 많은 일 처리 (+15%)
  • 캐시: 자주 쓰는 데이터를 가까이 보관 (+3%)
  • 메모리 속도: 데이터를 더 빠르게 가져옴 (+18%)

비유: 신형 일꾼은 같은 시간에 20% 더 많은 일을 한다.

결과: 같은 workload를 빠르게 끝내고, CPU 사용률이 낮게 측정됨.

2. OS/커널 설정 차이 (30~50%)

THP (Transparent Huge Pages):

  • 큰 상자에 작은 물건 담기
  • fork() 시 512배 더 많이 복사
  • 실제: BGSAVE 5초 vs 0.1초

Swappiness:

  • 메모리 → 디스크 이동
  • 1,000배 느린 접근
  • 실제: 응답 시간 40배 증가

TCP 스택:

  • 대기열 크기 차이 (128 vs 4096)
  • CentOS는 872개 요청 거부
  • 실제: 처리되는 요청 수 자체가 다름

CPU Governor:

  • 클럭 속도 조절
  • 1.0GHz vs 3.0GHz = CPU 사용률 3배 차이

3. NUMA 구조 (10~30%)

핵심:

  • Remote memory = Local의 2.5배 느림
  • 메모리가 여러 노드에 흩어짐 (40% remote)
  • CPU는 "메모리 기다림"으로 시간 낭비

비유: 창고가 멀리 있으면 왕복 시간이 오래 걸린다.

실제 측정:

NUMA miss 40%: IPC 0.6, CPU 70%
NUMA miss 0%: IPC 1.8, CPU 45%

핵심 인사이트: CPU 사용률은 높은데 실제 일의 양은 비슷함 (대기 시간이 많음)


핵심 교훈

1. "높은 CPU 사용률" ≠ "많은 일"

CPU 사용률이 높다고 해서 실제로 많은 일을 하고 있는 건 아니다.

Case 1: IPC 1.8, CPU 45%
→ 효율적으로 일하고, 빨리 끝내고 쉼

Case 2: IPC 0.6, CPU 70%
→ 메모리 기다리며 시간 낭비
→ 실제 일의 양은 Case 1과 비슷

IPC를 함께 봐야 정확한 판단이 가능하다.

2. 단독 vs 경합 환경의 극적인 차이

단독 실행: NUMA 설정 차이가 적음
자원 경합: NUMA 설정에 따라 2배 차이

벤치마크(단독 실행)에서는 문제가 없어 보여도, 실제 운영(멀티테넌시)에서 문제가 터질 수 있다.

3. OS 기본값의 함정

대부분의 OS는 "일반적인 워크로드"에 최적화되어 있다. Redis 같은 특수한 워크로드에는 맞지 않을 수 있다.

Ubuntu/CentOS: THP=always → Redis에 독
CentOS: somaxconn=128 → 고부하 처리 불가
CentOS: zone_reclaim_mode=1 → NUMA 문제 심화

같은 하드웨어, 같은 애플리케이션인데 OS만 다르면 성능이 2배 차이날 수 있다.


확인 방법

내 환경에서 어떤 요인이 영향을 주는지 확인:

# 1. 하드웨어 정보
lscpu
cat /proc/cpuinfo | grep "model name"

# 2. THP 상태 (never여야 함)
cat /sys/kernel/mm/transparent_hugepage/enabled

# 3. Swappiness (1 이하가 좋음)
cat /proc/sys/vm/swappiness

# 4. TCP 설정 (최소 4096 이상)
sysctl net.core.somaxconn

# 5. CPU Governor (performance가 좋음)
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

# 6. NUMA 구성 (VM은 1개 노드가 이상적)
numactl --hardware
lscpu | grep NUMA

# 7. Redis NUMA 분포 (Numa_Miss가 적어야 함)
numastat -p $(pgrep redis-server)

# 8. IPC 측정 (1.5 이상이 좋음)
perf stat -p $(pgrep redis-server) sleep 10

# 9. NUMA miss 비율 (10% 이하가 좋음)
perf stat -e 'node-load-misses,node-loads' -p $(pgrep redis-server) sleep 10

참고 자료

profile
기록을 통한 성장을

0개의 댓글