9.5.2.1 캐시 미스(Cache Miss) 방지를 위한 스레드 고정(Pinning) 및 코어 이식성 제어
Zenoh가 초당 수백만 건의 메시지 스루풋(Throughput)을 감당할 수 있도록 소프트웨어 레벨의 비동기 I/O와 무파싱(No-parsing) 직렬화를 연마했다 할지라도, 최종적으로 이 코어가 달리는 물리적 트랙은 하드웨어 CPU다. 멀티 코어(Multi-core) 시스템에서 운영체제의 기본 스케줄러는 공평성(Fairness)을 우선순위에 두며, 수신 스레드와 워커 스레드를 시시각각 비어있는 다른 코어로 마이그레이션(Migration)시킨다.
그러나 극초단 저지연(Ultra-low Latency) L2/L3 통신망에서 프로세스의 이동은 엄청난 대가, 즉 L1/L2 캐시 미스(Cache Miss)의 연쇄 파동을 낳는다. 본 절에서는 OS의 제어 권한을 탈취하여 특정 네트워크 스레드를 물리 코어에 강제로 접착시키는 스레드 고정(Thread Pinning) 기법과 메모리 노드 이식성 체계를 해부한다.
1. 운영체제 스케줄러의 이동성 배신과 캐시 플러시(Flush) 패널티
Zenoh 통신 모듈 내부의 z_router 스레드가 0번 코어에서 열심히 데이터를 수신하며 0번 코어의 L1/L2 독립 캐시(d-cache, i-cache) 안에 최근 처리용 배열과 라우팅 테이블(DHT) 주소를 가득 채워두었다. 그런데 1밀리초 후, 리눅스의 CFS(Completely Fair Scheduler)는 이 스레드를 잠시 멈추우고 한가한 7번 코어로 옮겨버린다(Context Migration).
이 순간, 7번 코어의 캐시 메모리 안에는 Zenoh가 아까까지 만들어둔 데이터 렌즈가 전혀 존재하지 않는다. 7번 코어는 메인 메모리(RAM)까지 수만 클럭 사이클을 낭비하며 다시금 데이터를 퍼 올려야(Load) 하는 치명적 캐시 미스(Cache Miss) 에 직면한다. 1초에 이 코어 점프가 수천 번 일어난다면, 서버는 RAM에서 데이터를 읽어오느라 영원한 정지(Stall) 상태에 빠진 것과 같다.
2. Pthreads 기반 코어 친화도(Affinity) 및 스레드 피닝(Pinning)의 구현
이 재난을 타파하기 위한 무기는 바로 CPU 코어 친화도(Affinity) 설정이다.
Zenoh 네트워크 엔진의 코어 파이프라인, 즉 (1) 수신 스레드와 (2) 디코딩 워커 스레드를 결코 자리에서 벗어나지 못하도록 족쇄(Pin)를 채워야 한다.
zenoh-c 혹은 C++ 기반 제어 시스템에서는 리눅스 커널에서 제공하는 pthread_setaffinity_np 함수를 통해 스레드의 운명을 하드코어 레벨로 고립시킬 수 있다.
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>
// Zenoh 수신 스레드가 시작될 때 진입하는 콜백
void* zenoh_pinned_rx_thread(void* arg) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
// 이 스레드를 오직 2번 코어에서만 돌도록 명시적으로 고정 (Pinning)
CPU_SET(2, &cpuset);
pthread_t current_thread = pthread_self();
if (pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset) != 0) {
printf("스레드 피닝 실패. 성능 하락을 각오하십시오.\n");
}
// L1/L2 캐시가 2번 코어 내에 영원히 잔존함을 보증하며 루프 진입
while (running) {
// ... Zenoh 버퍼 폴링 엔진 구동 ...
}
return NULL;
}
이 족쇄를 걸어둔 스레드는 더 이상 엉뚱한 코어로 도망가지 않으며, 2번 코어의 L1 캐시 적중률(Hit Ratio)은 99.9%에 달하여 메인 메모리로 램프 다운하는 일이 아예 소멸해버린다.
3. NUMA 아키텍처 토폴로지에서의 메모리 국소성 제어
서버가 64코어를 넘어가는 거대한 다중 프로세서 다이(Die) 환경, 즉 NUMA (Non-Uniform Memory Access) 구조에서는 스레드 피닝만으로 끝방어선이 구축되지 않는다.
NUMA 환경에서는 0번 CPU 다이와 1번 CPU 다이가 각각 자신만의 물리 메모리 뱅크(Local Node)를 갖고 있다. 만약 앞서 2번 코어(Node 0 소속)에 스레드를 피닝해놨는데, 이 스레드가 연산할 거대 데이터 배열(Queue Array)이 Node 1의 메모리 뱅크에 malloc() 되어 있다면 어찌 될까.
2번 코어는 버스를 타고 원격(Remote) 노드인 1번 메모리 뱅크까지 먼 여정을 떠나는 치명적인 지연(Remote Memory Access Penalty)에 걸식당한다.
궁극의 퍼포먼스를 위해서는 고정(Pinning)된 스레드가 메모리를 할당할 때, 반드시 자신의 코어와 결속된 로컬 NUMA 노드 영역 내에서만 할당되도록 커널 시스템 통제(numactl 또는 numa_alloc_local()) 를 강제해야 한다. 연산 코어(Compute Unit)와 물리적 RAM 조각(Storage Unit)을 완벽한 국소성(Locality)으로 결합하는 설계, 이것이 바로 마이크로초 단위의 Zenoh 스루풋이 하드웨어와 가장 깊게 융합하는 코어 이식성 제어의 예술이다.