NUMA(Non-Uniform Memory Access) 아키텍처의 비대칭 메모리 지연과 스레드 피닝(Thread Pinning) 최적화
온프레미스 서버를 2소켓(Dual-CPU) 시스템으로 화려하게 업그레이드한 직후, 기대와는 달리 데이터베이스의 조회 지연 시간이 널뛰기를 반복하는 기괴한 현상과 싸우며 며칠을 허비했습니다. 해답은 OS의 스케줄러가 메모리의 물리적 위치를 완전히 착각하고 있었다는 데 있었습니다.
과거 CPU 코어가 적던 시절에는 모든 코어가 노스브릿지를 통해 하나의 램(RAM) 뱅크 집합을 균등하게 바라보는 SMP(Symmetric Multiprocessing) 구조였습니다. 그러나 코어가 수십 개로 늘어나면서 단일 메모리 버스에 병목이 발생했고, 이에 인텔과 AMD는 메인보드의 CPU 소켓마다 각각 자신만의 로컬 메모리 슬롯을 별도로 직결시키는 NUMA 아키텍처를 도입했습니다. 이제 CPU 0번 소켓은 로컬 메모리 A에 접근할 때는 빛의 속도지만, CPU 1번 소켓이 관리하는 리모트 메모리 B의 데이터를 읽으려면 CPU 간의 QPI/Infinity Fabric 인터커넥트 브릿지를 건너가야 하는 엄청난 지연 페널티를 물게 되었습니다.
저희의 불운은 운영체제 커널의 기본 설정이 이 NUMA 구조를 무시한 것에 있었습니다. 0번 CPU 코어에서 돌고 있던 데이터베이스 커넥션 스레드가 처리하던 메모리를, OS 스케줄러가 컨텍스트 스위칭 과정에서 무심코 빈 1번 CPU 소켓 코어로 튕겨버린 것입니다. 졸지에 코어가 이사 간 1번 CPU는, 데이터가 여전히 0번 소켓 메모리에 남아있기 때문에 매 사이클마다 브릿지를 넘어야 하는 리모트 엑세스 지옥도에 빠져 스루풋이 박살난 것이었습니다.
이 병목을 제거하기 위해 리눅스의 'numactl' 유틸리티와 스레드 피닝(Thread Affinity) 기법을 코드로 강제 주입했습니다. "이 특정 스레드는 무조건 0번 소켓의 코어에서만 돌아야 하며, 메모리 역시 0번 노드에서만 할당받아라"라고 하드코딩된 규칙(Bind)을 명시한 것입니다. 캐시 히트율이 극적으로 치솟고 서버가 스펙 시트의 100% 한계치까지 펀칭을 시작하는 모습을 보며, 클라우드 시대의 논리적 추상화 뒤에 숨겨진 차가운 물리 하드웨어의 지배력을 다시금 우러러보게 되었습니다.
Related Posts
JVM JIT 컴파일러의 극단적 런타임 최적화: 탈출 분석(Escape Analysis)과 스칼라 치환의 마법
정적 컴파일 언어를 압도하는 자바 머신의 동적 스크립트 프로파일링 및 객체 힙 버림 최적화 기법.
리눅스 eBPF와 XDP를 활용한 커널 바이패스(Kernel Bypass) 초저지연 패킷 필터링 아키텍처
운영체제 네트워크 스택의 병목을 우회하여 디바이스 드라이버 레벨에서 직접 샌드박스 코드를 주입하는 eBPF의 혁명.
스플릿 브레인(Split-Brain) 붕괴를 막는 분산 락(Distributed Lock) 시스템과 펜싱(Fencing) 토큰의 도입
Zookeeper, Redis Redlock의 시계 위임 맹점을 찌르는 가비지 컬렉션 시간 정지(Stop-the-World) 현상 롤백 설계.