NUMA 구조에서 발생하는 성능 문제
NUMA(Non-Unifrom Memory Access) 아키텍처는 현대 멀티 코어 시스템에서 성능을 극대화하기 위해 도입되었지만
그 특성을 제대로 이해하지 못하고 애플리케이션이나 시스템을 구성하면 심각한 성능 저하를 겪을 수 ㅣㅇㅆ다.
NUMA #
과거의 다중프로세서 시스템은 cpu가 하나의 메모리 버스를 공유하는 UMA(Uniform Memory Access) 구조였다.
코어 수가 적을때는 문제가 없었지만 코어가 늘어나면서 메모리 버스에 병목현상이 발생했다.
이를 해결하기 위해 등장한것이 NUMA
- NUMA Node: CPU와 해당 CPU에서 직접 연결된 물리적 메모리 RAM의 묶음이다
- Local Memory Access: CPU가 자신이 속한 노드의 메모리에 접근하는 것이다. 버스를 독점하므로 매우 빠르다.
- Remote Memory Access: CPU가 다른 노드에 연결된 메모리에 접근하는것이다. 노드간 연결 통로 Intel의(QPI/UPI, AMD의 Infinity Fabric등)를 거쳐야 하므로 상대적으로 느리고 지연이 발생한다.
- First-Touch 정책: 리눅스등 대부분의 os는 메모리 할당시에 해당 메모리를 처음사용하는 first touch는 스레드가 실행중인 cpu의 노드에 메모리를 할당한다.
성능 문제 #
원격 메모리 접근 지연이 발생할 수 있는데 다른 노드의 메모리를 빈번하게 참조하면, 메모리 접근 속도가 크게 떨어져 cpu가 데이터를 기다리며 노는 시간 stall이 길어진다.
노드간 인터커넥트 병목도 있는데 노드간 통신 대역폭은 한계가 있다. 대량의 데이터가 노드간 통로를 오가면 대역폭 포화가 발생해 시스템 성능이 저하된다.
메모리 불균형 NUMA Imbalance 및 스와핑도 있는데 노드 0의 메모리는 100%를 사용중이고 1은 메모리가 텅텅 비어있다고 가정해보겠다. 이때 노드 0에 추가 메모리 할당 요청이 오면 os는 노드1의 여유 메모리를 쓰기보다 노드 0의 데이터를 디스크로 swap out 해버리는 경우가 발생한다.
실제 문제 예시 #
데이터베이스 mysql, postgresql에서 swap insanity 현상이 있는데. 스왑 발작이라고하고. 시스템 전체 여유 메모리는 30gb가 넘는데, db 프로세스가 특정 노드의 메모리만 다 써버려서 디스크 스와이 발생하고, 쿼리 처리 속도가 갑자기 수십 배 느려진다.
In-memory 캐시 (redis, memcached) 응답속도에도 영향이 갈 수 있다. 일정하지 않고 튀는 현상이 있는데 redis 스레드가 노드 0에서 실행되다가 os 스케쥴러에 의해 노드1로 context migration 되면 자신이 캐싱해둔 데이터 노드0의 존재를 계속 원격으로 읽어야해 급감할 수 있다.
java app에서도 jvm gc 스레드가 여러 노드에 흩어져 있어 gc가 메모리를 스캔하는 과정에서 대량의 remote access가 발생해 stw 시간이 비정상적으로 길어질 수 있다.
현상 확인 방법 #
시스템 NUMA 토폴로지 확인부터 보자. numactl --hardware 를 통해 시스템의 노드 구성과 노드 간의 거리를 확인한다 거리가 멀수록 접근 속도가 느리다.
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 32768 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 32768 MB
node distances:
node 0 1
0: 10 21
1: 21 10
노드 0에서 노드0으로 접근 거리는 10(local) 노드 0에서 노드 1로의 원격 접근은 21(remote)로 약 2배 이상 느릴 수 있다.
local memeory access는 ns단위로 차이나는데 70
100ns가 local memory access 이상적인 시간이고 remote memory 1hop정도면 130150n 약 1.52배 느려지며 2hop이상이면 200300ns로 약 3배이상 느낌이다 4소켓 이상의 대형 서버에서 발생할 수 있으며 통로를 여러번 건너뛰어야하므로 지연시간이 급격하게 증가한다.
내 서버에서 실제 레이턴시 측정법은 사용중인 서버의 정확한 나노초 단위를 시간을 눈으로 확인해보려면 MLC라는 Intel Memory Latency Checker를 사용할 수 있다.
# Intel MLC를 다운로드 후 실행하면 아래와 같은 매트릭스를 볼 수 있습니다.
$ ./mlc --idle_latency
Numa node
Numa node 0 1
0 78.5 135.2 <-- 노드 0에서 노드 1로 갈 때 135.2ns 소요
1 136.1 79.2
NUMA 메모리 할당 및 적중 실패율 확인도 numastat으로 가능하다
$ numastat
node0 node1
numa_hit 85032910 12049582
numa_miss 5902104 8920194 <-- 주의!
numa_foreign 8920194 5902104
interleave_hit 32014 31988
local_node 84901020 11902030
other_node 6033994 9067746 <-- 주의!
numa_miss, other_node의 수치가 높고 지속적으로 증가한다면 스레드가 원격 메모리에 빈번하게 접근하여 성능 손해를 보고있다는 강력한 증거다
노드별 메모리 불균형 확인을 하는 방법은 numstat -m 또는 cat /proc/zoneinfo로 특정 노드만 메모리가 고갈되어 스왑이 발생할 위험이 있는지 확인한다.
$ numastat -m
Node 0 Node 1 Total
------- ------- -------
MemTotal 32768.00 32768.00 65536.00
MemFree 250.00 28000.00 28250.00 <-- 불균형!
MemUsed 32518.00 4768.00 37286.00
전체 메모리는 여유가 있지만 node 0의 메모리는 거의 꽉 찼다.
이 상태에서 node 0의 cpu 메모리가 추가 요구하면 스왑이 발생할 수 있다.
해결 방법 및 최적화 전략 #
성능 문제를 해결하는 방법은 애플리케이션 특성에 따라 크게 numa 친화적 구성과 걍 끄는걸로 두가지로 나뉜다.
메모리 인터리빙 - db등에 적합 #
mysql 같은 단일 대형 프로세스가 메모리를 고르게 써야할 때 사용한다.
first-touch 정책을 무시하고 메모리 할당을 모든 노드에 라운드로빈 방식으로 분산시킨다.
원격 접근이 발생하긴 하지만, 한노드에 메모리만 고갈되어 발생하는 스왑현상을 막을 수 있다.
$ numactl --interleave=all /usr/local/mysql/bin/mysqld &
또는 커널 파라미터로 vm.zone_reclaim_mode = 0가 디폴트긴한데 이걸로 설정해 적극적인 원격 노드 메모리 사용을 허용한다.
CPU/Mem 고정 pinning/binding - redis cache, hpc에 적합 #
특정 프로세스가 절대 원격 메모리에 접근하지 않도록 특정 numa 노드의 cpu와 메모리만 사용도록 강제한다.
numactl을 사용해서 노드를 지정한다.
# Redis 서버를 NUMA 노드 0의 CPU와 메모리만 사용하도록 제한
$ numactl --cpunodebind=0 --membind=0 redis-server /etc/redis/redis.conf
os 레벨에서 자동화 도구 사용 numad #
프로세스의 메모리 접근 패턴을 모니터링하여 동적으로 스레드와 메모리 페이지 같은 노드로 이주시켜주는 리눅스 데몬이다.
수동 설정이 어려울 때 유용하다.
BIOS에서 Node Interleaving 설정 (하드웨어 레벨) #
정말 NUMA 튜닝이 까다롭고 모든 프로세스가 균등하게 돌아야한다면, 서버 BIOS 설정에서 Node Interleaving을 Enable로 변경하여 하드웨어 차원에서 UMA처럼 동작할 수 있게 만들 수 있다.
다만 전체적인 메모리 레이턴시가 소폭 상승하므로 최후의 수단으로 사용한다.
결론 #
이렇게 numa에서 발생하는 성능 문제와 해결법을 알아보았다.
이게 ns단위라서 딱히 문제없지 않을까 신경쓰지 않아도 싶을 수 잇는데 반은 맞고 반은 틀리다.
rps보다는 시스템의 메모리 사용량과 물리적 하드웨어 구성에 훨씬 더 즉각적으로 반응하는 현상이다
즉, 트래픽이 엄청나게 많아도 numa가 잘 구성되어잇으면 끄떡없지만 트래픽이 평범한 수준이어도 메모리 설정이 잘못되어있으면 뻗을수있는것
왜냐면 각각의 request에 모두 지연을 주는 현상이 아니라 swap같은게 발생하면 예를들어 1000~2000qps 정도의 트래픽환경에서 어느순간 노드 0의 64gb가 메모리가 꽉찼는데
os는 노드 1에 남은 64gb를 놔두고 노드 0의 데이터를 디스크로 스왑해버린다면 이때 디스크 io가 폭주하면서 평소 10ms 이하이던 쿼리 응답속도가 수 초 단위로 멈춰버린다.
혹은 인메모리 캐시에서 네트워크 카드 nic이 물리적으로 연결된 numa노드와 redis프로세스가 실행중인 numa노드가 다르다면 nic가 받은 데이터를 다른 노드에 있는 redis로 끌고가야하기 때문에 인터럽트는 노드 0에서 처리되지만 노드1에서 redis가 돌고잇는 상황이 되어
redis는 워낙 빠르기 때문에 초당 1 2만건정도는 노드간 지연이있어도 티가 잘 안난다. 근데 5~10만건 시스템에서는 노드간 연결통로 qpi upi의 대역폭이 꽉차서 1ms미만 응답시간이던 redis가 수십ms로 튈수도 있다. 이건 고성능 환경에서 치명적인현상.
즉, 작은 서버나 일반적인 클라우드 환경에서는 절대 발생하진 않지만 데이터 관리 잘못하면 가끔 있을수도 있음 정도다.
근데 사실 그것도 메모리 64~218 이상인 엔터프라이즈급 베어메탈 물리서버에서나 발생할듯. 근데 numa 튜닝 가치는 1순위긴하다.
단일 프로세스가 메모리를 엄청 낭비하는 시스템이면 튜닝 옵션을 켜놓기만해도 20~30% 퍼포먼스 (응답시간)을 올리는 느낌이라. rdbms를 쓴다면 좋다. db하나 딱 띄워두고 버퍼풀을 수십gb쓰게된다면 ㅇㅇ
대규모 인메모리시스템은 또 트래픽에 따라 더 봐야겠지만, 일단 신경아예 안써도될 애매한 포지션이라 해야할까. 서버 한대에 여러개의 무거운 vm을 쪼개서 직접 서비스하는 인프라 관리자라면 신경써야할지도.