- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 지연의 원천 분해 — 패킷은 어디서 시간을 잃는가
- 커널 바이패스 스펙트럼 — 어디까지 갈 것인가
- CPU 격리 레시피 — 코어를 전세 내기
- NUMA 정렬 — 메모리와 NIC의 로컬리티
- 인터럽트 vs 폴링 — 깨워줄 때까지 기다리지 않는다
- 휴지 상태 제거 — 잠든 코어는 느리다
- 메모리 — 페이지 폴트와 TLB 미스를 박멸
- 네트워크 스택 튜닝 파라미터
- 시간 동기화 — PTP와 하드웨어 타임스탬프
- 측정 방법론 — 평균은 거짓말을 한다
- 애플리케이션 측 고려 — 커널만 튜닝하면 끝이 아니다
- 일반 서비스에는 왜 과한가 — 트레이드오프 정산
- 체크리스트
- 함정과 안티패턴
- 마치며
- 참고 자료
들어가며
대부분의 서비스에서 100마이크로초의 지연은 측정조차 하지 않는 오차 범위입니다. 그러나 고빈도 트레이딩(HFT)이나 마켓메이킹의 세계에서 100마이크로초는 "주문이 체결되느냐, 남의 체결을 구경하느냐"를 가르는 시간입니다. 거래소의 매칭 엔진에 먼저 도착한 주문이 유리한 가격을 가져가고, 늦게 도착한 주문은 불리해진 호가를 받습니다. 이 도메인에서 지연(latency)은 추상적인 품질 지표가 아니라 손익계산서에 직접 기록되는 숫자입니다.
이 글은 저지연 트레이딩 시스템의 관점에서 리눅스 커널 튜닝을 정리한 것입니다. 다만 미리 말해 두면, 여기 나오는 기법 대부분은 일반 서비스에는 과합니다. 글 후반부에 "왜 여러분의 웹 서버에는 이걸 적용하면 안 되는가"도 함께 다룹니다. 그리고 이 글은 시스템 엔지니어링 관점의 기술 글이며, 투자 자문이 아닙니다.
단위 감각부터 — ns, us, ms
저지연 튜닝을 논하려면 먼저 시간 단위의 감각이 필요합니다.
| 단위 | 크기 | 비유적 감각 | 해당 작업 예시 |
|---|---|---|---|
| 1 ns | 10억분의 1초 | CPU 한 사이클(약 0.3ns)의 몇 배 | L1 캐시 접근(약 1ns) |
| 10 ns | --- | L2 캐시 접근 | 분기 예측 실패 페널티 |
| 100 ns | --- | 메인 메모리 접근(약 60~100ns) | NUMA 원격 노드 접근 |
| 1 us | 100만분의 1초 | 잘 튜닝된 유저스페이스 네트워킹 왕복의 절반 | 커널 바이패스 NIC 송신 |
| 10 us | --- | 컨텍스트 스위치 1회 + 캐시 오염 | 표준 커널 스택 UDP 수신 처리 |
| 100 us | --- | 잘못 설정된 시스템의 인터럽트 지연 스파이크 | C-state 깊은 잠에서 깨어나기 |
| 1 ms | 1000분의 1초 | 일반 웹 서비스의 "빠른" 응답 | 디스크가 아닌 SSD 접근, GC 마이너 수집 |
| 10 ms | --- | 서울-부산 광섬유 왕복보다 김 | HDD 시크, 타임슬라이스 만료 |
트레이딩 시스템의 tick-to-trade(시세 수신부터 주문 송신까지)는 잘 만든 소프트웨어 기반 시스템이 수 마이크로초, FPGA 기반은 수백 나노초 영역에서 경쟁합니다. 즉, 우리가 싸우는 단위는 us이고, 적은 "어쩌다 한 번 발생하는 ms급 스파이크"입니다.
지연의 원천 분해 — 패킷은 어디서 시간을 잃는가
시세 패킷이 NIC에 도착해 애플리케이션이 주문을 내보내기까지의 경로를 분해해 봅니다.
[거래소 시세]
|
v
(1) NIC 수신: 와이어 --> NIC 버퍼 물리 계층, 수십 ns
|
(2) DMA + 인터럽트/폴링 수백 ns ~ 수 us
| - 인터럽트 경로: IRQ -> softirq -> 프로토콜 스택
| - 폴링/바이패스 경로: 유저스페이스가 직접 링버퍼 읽기
v
(3) 프로토콜 처리 (IP/UDP 디코드) 표준 스택: 1~5 us / 바이패스: 수백 ns
|
(4) 소켓/큐 전달 --> 애플리케이션 웨이크업 여기서 큰 분산 발생:
| - 스레드가 이미 코어에서 busy-wait 중이면 ~0
| - 스케줄러 웨이크업이 필요하면 1~10 us + 스파이크
v
(5) 전략 로직 (호가 계산, 리스크 체크) 수백 ns ~ 수 us (앱 설계 영역)
|
(6) 주문 인코딩 + 송신 (2)~(3)의 역방향
v
[거래소 주문 게이트웨이]
핵심 통찰은 두 가지입니다. 첫째, 평균은 (3)·(5)·(6)이 지배하지만 꼬리(tail)는 (2)와 (4)가 지배합니다. 인터럽트 병합, 스케줄러 개입, C-state 기상, TLB 미스, SMI(시스템 관리 인터럽트) 같은 이벤트가 99.99분위를 망칩니다. 둘째, 튜닝의 본질은 평균을 줄이는 것이 아니라 분산을 죽이는 것입니다. 트레이딩에서 예측 불가능한 시스템은 느린 시스템보다 나쁩니다.
커널 바이패스 스펙트럼 — 어디까지 갈 것인가
커널 네트워크 스택을 얼마나 우회할지는 비용과 효과의 스펙트럼입니다.
| 접근 | 대표 기술 | 수신 지연(대략) | 개발 난이도 | 운영 난이도 | 적합 대상 |
|---|---|---|---|---|---|
| 표준 스택 튜닝 | sysctl, busy_poll, IRQ 어피니티 | 5~20 us | 낮음 | 낮음 | 일반 저지연, 백오피스 피드 |
| XDP/AF_XDP | eBPF 기반 커널 내 처리 | 2~5 us | 중간 | 중간 | 필터링, 시세 팬아웃, DDoS 방어 |
| 커널 바이패스 소켓 호환 | Onload류 (소켓 API 유지) | 1~3 us | 낮음(재컴파일 불필요) | 중간 | 기존 소켓 앱의 가속 |
| 풀 유저스페이스 스택 | DPDK + 자체 UDP/TCP 처리 | 1 us 미만 | 높음 | 높음 | tick-to-trade 코어 경로 |
| 하드웨어 오프로드 | FPGA/SmartNIC | 수백 ns | 매우 높음 | 매우 높음 | 최상위 HFT |
판단 기준은 단순합니다. 경쟁이 us 단위인가, ms 단위인가. 시장 데이터 분석이나 리스크 배치라면 표준 스택 튜닝으로 충분하고, 매칭 엔진과의 속도 경쟁이라면 DPDK 또는 소켓 호환 바이패스(Onload, VMA류)가 출발점입니다. AF_XDP는 그 중간에서 "커널과 협력하는 바이패스"라는 독특한 위치를 차지하며, 하나의 NIC에서 일부 트래픽만 저지연 경로로 빼내는 구성에 유용합니다.
CPU 격리 레시피 — 코어를 전세 내기
저지연의 제1원칙은 "핫패스 스레드가 도는 코어에서 다른 모든 것을 쫓아내는 것"입니다. 커널 부트 파라미터 3종 세트가 기본입니다.
# /etc/default/grub
# 24코어 머신에서 코어 4~23을 트레이딩 전용으로 격리하는 예
GRUB_CMDLINE_LINUX="isolcpus=nohz,domain,managed_irq,4-23 \
nohz_full=4-23 \
rcu_nocbs=4-23 \
rcu_nocb_poll \
irqaffinity=0-3 \
intel_idle.max_cstate=0 processor.max_cstate=1 \
intel_pstate=disable idle=poll \
mitigations=off \
transparent_hugepage=never \
audit=0 nosoftlockup"
# 적용
sudo update-grub && sudo reboot
각 항목의 의미는 다음과 같습니다.
- isolcpus: 지정 코어를 스케줄러의 일반 로드밸런싱에서 제외합니다. 명시적으로 어피니티를 지정한 스레드만 그 코어에 올라갑니다.
- nohz_full: 해당 코어에서 실행 중인 태스크가 1개뿐이면 주기적 스케줄러 틱(기본 초당 100~1000회)을 꺼서, 틱 인터럽트로 인한 마이크로 지터를 제거합니다.
- rcu_nocbs: RCU 콜백 처리를 격리 코어에서 하우스키핑 코어(여기서는 0~3)로 옮깁니다.
- idle=poll / max_cstate: 코어가 잠들지 않게 합니다(뒤에서 상세히).
- mitigations=off: 사이드채널 완화 기능을 끕니다. 시스템콜·컨텍스트 스위치 비용이 유의미하게 줄지만, 보안 트레이드오프를 조직이 공식 승인한 폐쇄 전용망 환경에서만 고려할 수 있는 옵션입니다.
부팅 후 스레드와 인터럽트를 배치합니다.
#!/usr/bin/env bash
set -euo pipefail
# 1) irqbalance는 격리 환경의 적 — 비활성화
systemctl stop irqbalance || true
systemctl disable irqbalance || true
# 2) 모든 IRQ의 기본 어피니티를 하우스키핑 코어(0-3)로
for irq in /proc/irq/*/smp_affinity_list; do
echo "0-3" > "$irq" 2>/dev/null || true
done
# 3) 트레이딩 NIC의 수신 큐 IRQ만 전용 코어(4,5)에 핀
NIC="ens1f0"
i=0
for irq in $(grep "$NIC" /proc/interrupts | awk -F: '{print $1}'); do
core=$((4 + i % 2))
echo "$core" > "/proc/irq/$irq/smp_affinity_list"
i=$((i + 1))
done
# 4) 워커 스레드를 격리 코어에 핀 (앱 내부에서 pthread_setaffinity_np로
# 하는 것이 정석이지만, 외부에서라면 taskset)
taskset -c 6 ./market_data_handler &
taskset -c 7 ./order_gateway &
# 5) 커널 워크큐도 하우스키핑 코어로
echo 0f > /sys/devices/virtual/workqueue/cpumask
검증은 perf와 /proc 인터페이스로 합니다.
# 격리 코어에 틱이 정말 꺼졌는지 확인
watch -n1 'grep -E "LOC|RES" /proc/interrupts | awk "{print \$1, \$8, \$9, \$10}"'
# 특정 코어의 컨텍스트 스위치 모니터링 (0이어야 정상)
perf stat -C 6 -e context-switches,cpu-migrations -- sleep 10
NUMA 정렬 — 메모리와 NIC의 로컬리티
멀티소켓 서버에서 NUMA를 무시하면 메모리 접근마다 수십 ns의 세금을 냅니다. 더 중요한 것은 NIC가 어느 NUMA 노드에 물려 있는가입니다. PCIe 슬롯은 특정 소켓에 직결되므로, NIC와 같은 노드의 코어·메모리를 쓰는 것이 원칙입니다.
# NIC의 NUMA 노드 확인
cat /sys/class/net/ens1f0/device/numa_node
# 출력: 1 --> 이 NIC는 노드 1에 연결
# 노드 1의 코어 목록 확인
lscpu | grep "NUMA node1"
# 핫패스 프로세스를 노드 1의 코어와 메모리에 묶어 기동
numactl --cpunodebind=1 --membind=1 ./trading_engine
# 교차 노드 트래픽이 없는지 확인
numastat -p $(pgrep trading_engine)
애플리케이션 설계 차원에서는 시세 수신 스레드 → 전략 스레드 → 주문 송신 스레드의 체인을 모두 같은 노드에 두고, 스레드 간 통신은 같은 L3 캐시를 공유하는 코어 쌍의 락프리 링버퍼로 처리하는 것이 정석입니다.
인터럽트 vs 폴링 — 깨워줄 때까지 기다리지 않는다
인터럽트는 "이벤트가 오면 깨워주는" 효율적인 모델이지만, 깨우는 비용(IRQ 처리 + 스케줄러 웨이크업 + 캐시 워밍)이 us 단위입니다. 저지연 경로에서는 코어를 하나 태워서라도 폴링합니다.
표준 스택을 유지하면서 절충하는 장치가 busy polling입니다.
# 소켓 계층의 busy poll (마이크로초 단위)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50
# epoll 기반 앱이라면 그룹 단위 busy poll 설정도 가능 (커널 5.11 이상)
# SO_BUSY_POLL_BUDGET, EPIOCSPARAMS ioctl 등 앱 레벨 설정과 병행
# NAPI 폴링과 인터럽트 병합의 균형 — 저지연 방향은 병합 끄기
ethtool -C ens1f0 adaptive-rx off adaptive-tx off rx-usecs 0 tx-usecs 0
인터럽트 병합(coalescing)을 끄면 PPS가 높을 때 CPU 사용량이 치솟는다는 점에 주의합니다. 시세처럼 "패킷 하나하나가 즉시 중요한" 트래픽에서만 끄고, 벌크 트래픽 NIC는 병합을 유지합니다. DPDK나 Onload를 쓰는 경우 이 절충 자체가 사라지고 유저스페이스 폴 모드가 기본이 됩니다.
휴지 상태 제거 — 잠든 코어는 느리다
현대 CPU는 idle 시 C-state로 잠들고, 부하에 따라 P-state(주파수)를 바꿉니다. 둘 다 저지연의 적입니다. 깊은 C-state(C6)에서 깨어나는 데 수십~수백 us가 걸릴 수 있고, 주파수 전환도 수십 us의 히커피를 만듭니다.
# C-state: 부트 파라미터로 고정했다면 확인만
cat /sys/devices/system/cpu/cpu6/cpuidle/state*/disable
# 런타임에서 제어하려면 /dev/cpu_dma_latency에 0을 write한 채 유지
# (파일을 연 프로세스가 살아있는 동안 적용 — tuned의 latency-performance가 이 방식)
tuned-adm profile latency-performance
# P-state: governor를 performance로 고정
for g in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance > "$g"
done
# 터보 부스트: 결정론을 원하면 끄는 쪽이 일관적
# (켜면 평균은 빨라지지만 열 상황에 따라 주파수가 출렁임)
echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo
놓치기 쉬운 두 가지가 있습니다. 첫째, **SMI(System Management Interrupt)**는 OS가 인지하지 못하는 펌웨어 레벨 정지로, 수백 us짜리 스파이크의 단골 범인입니다. BIOS에서 USB 레거시 지원, 전력 관리 관련 SMI 소스를 끄고, turbostat의 SMI 카운터로 감시합니다. 둘째, 하이퍼스레딩은 핫패스 코어에서는 끄거나, 형제 스레드를 비워 둡니다. 형제가 캐시와 실행 포트를 오염시키기 때문입니다.
메모리 — 페이지 폴트와 TLB 미스를 박멸
핫패스에서 페이지 폴트 한 번은 us 단위의 재앙입니다. 기동 시점에 모든 메모리를 만지고 잠급니다.
# 휴지페이지(2MB) 예약 — TLB 미스 감소
echo 4096 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 1GB 휴지페이지는 부트 파라미터로
# default_hugepagesz=1G hugepagesz=1G hugepages=16
/* 애플리케이션 기동 시퀀스의 정석 */
#include <sys/mman.h>
int main(void) {
/* 1) 현재와 미래의 모든 페이지를 RAM에 고정 */
mlockall(MCL_CURRENT | MCL_FUTURE);
/* 2) 힙/풀을 미리 확보하고 전 페이지를 터치 (prefault) */
/* 3) 핫패스 진입 후에는 malloc/free, 시스템콜, 페이지 폴트 금지 */
return run_engine();
}
THP(Transparent Huge Pages)는 논쟁거리입니다. TLB 관점에서는 이득이지만, khugepaged가 백그라운드에서 페이지를 합치는 순간 ms급 정지가 올 수 있습니다. 저지연 진영의 통설은 THP는 never로 끄고, 명시적 휴지페이지를 쓴다입니다. 결정론이 목적이라면 "커널이 알아서 해주는" 기능은 대부분 끄는 방향이 맞습니다.
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
sysctl -w vm.swappiness=0
sysctl -w vm.stat_interval=120 # vmstat 갱신 주기도 줄여 지터 감소
네트워크 스택 튜닝 파라미터
표준 스택을 쓰는 경로(주문 게이트웨이의 TCP 등)의 기본 세트입니다.
# 버퍼: 저지연 관점에서는 "크게"가 아니라 "적당히" — 과대 버퍼는 지연 은폐
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
# TCP: Nagle은 앱에서 TCP_NODELAY로 끄는 것이 정석
sysctl -w net.ipv4.tcp_low_latency=1 # 구버전 커널에서만 의미
sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_slow_start_after_idle=0
# 큐 규율: 송신 지연 최소화
sysctl -w net.core.default_qdisc=fq
tc qdisc replace dev ens1f0 root mq
# UDP 시세 수신: 드롭 감시가 생명
watch -n1 'cat /proc/net/udp | awk "{print \$13}" | sort | uniq -c'
ethtool -S ens1f0 | grep -iE "drop|miss|fifo"
# RSS/RPS: 시세 멀티캐스트 그룹별로 수신 큐를 분리해 코어에 1:1 매핑
ethtool -X ens1f0 weight 1 1 0 0
시간 동기화 — PTP와 하드웨어 타임스탬프
지연을 줄이는 것만큼 중요한 것이 지연을 정확히 재는 것이고, 그 전제는 시계입니다. 규제 측면에서도 유럽 MiFID II의 RTS 25는 HFT 사업자에게 UTC 대비 100마이크로초 이내의 시계 정확도와 정밀한 타임스탬프 기록을 요구합니다. NTP(ms급)로는 부족하고 PTP(IEEE 1588, us 미만)가 표준입니다.
# NIC의 하드웨어 타임스탬프 지원 확인
ethtool -T ens1f0
# "hardware-transmit / hardware-receive / PTP Hardware Clock: 0" 확인
# linuxptp: NIC의 PHC를 그랜드마스터에 동기화
ptp4l -i ens1f0 -f /etc/ptp4l.conf --summary_interval 6 &
# PHC --> 시스템 클럭 동기화
phc2sys -s ens1f0 -O 0 -u 64 &
# 오프셋 모니터링 (rms 수십 ns 수준이 목표)
pmc -u -b 0 'GET CURRENT_DATA_SET'
애플리케이션 측에서는 SO_TIMESTAMPING 소켓 옵션으로 NIC 하드웨어 수신 타임스탬프를 받아, "와이어 도착 시각" 기준의 구간별 지연을 계측합니다. 소프트웨어 타임스탬프는 측정 대상(커널 경로)이 측정 도구에 포함되는 모순이 있으므로, 진지한 측정은 하드웨어 타임스탬프 또는 광 탭 + 캡처 장비로 합니다.
측정 방법론 — 평균은 거짓말을 한다
저지연 시스템의 성적표는 평균이 아니라 분위수, 특히 99.9, 99.99분위입니다.
어떤 시스템의 tick-to-trade 분포 (가상의 예)
p50 : 3.2 us <-- 마케팅 자료에 실리는 숫자
p99 : 5.8 us
p99.9 : 14.0 us <-- 인터럽트/타이머 개입
p99.99 : 180.0 us <-- C-state 기상, SMI, 페이지 폴트
max : 2.1 ms <-- 이 한 번이 최악의 시점에 오면 사고
히스토그램 없이 평균(3.5us)만 보면 이 시스템은 "훌륭"합니다.
하지만 1만 번에 1번, 시장이 가장 격렬할 때(=부하가 가장 높을 때)
180us가 발생한다면, 바로 그 순간이 돈을 잃는 순간입니다.
도구 체인은 다음과 같습니다.
# 1) 플랫폼 자체의 지터 측정 — cyclictest (rt-tests 패키지)
# 격리 코어에서 20분, 히스토그램 출력
cyclictest -m -p95 -t1 -a 6 -i 100 -h 400 -D 20m -q
# 2) 커널 경로 추적 — bpftrace로 softirq 처리 시간 분포
bpftrace -e 'tracepoint:irq:softirq_entry { @ts[cpu] = nsecs; }
tracepoint:irq:softirq_exit /@ts[cpu]/ {
@dist = hist(nsecs - @ts[cpu]); delete(@ts[cpu]); }'
# 3) 스케줄러 개입 감시 — 격리 코어에서 발생하면 안 되는 이벤트
perf record -C 6 -e sched:sched_switch,sched:sched_wakeup -- sleep 60
perf script | head
# 4) 하드웨어 카운터로 캐시/TLB 미스 상관 분석
perf stat -C 6 -e cycles,instructions,cache-misses,dTLB-load-misses -- sleep 10
애플리케이션 레벨에서는 HdrHistogram류의 무손실 히스토그램으로 전 구간을 상시 기록하고, "조정된(coordinated) 누락" 함정 — 측정 루프 자체가 멈춰 있던 동안의 샘플이 빠져 분포가 좋아 보이는 현상 — 을 보정하는 것이 중요합니다. 측정은 반드시 실부하와 같은 메시지 레이트에서, 시장 개장 직후 같은 버스트 조건을 재현해 수행합니다.
애플리케이션 측 고려 — 커널만 튜닝하면 끝이 아니다
커널이 제공하는 것은 "방해 없는 코어"까지입니다. 그 위의 코드도 같은 규율을 지켜야 합니다.
- Java 계열: 핫패스에서 할당 제로(zero-allocation) 설계로 GC 자체를 회피합니다. 객체 풀, 프리미티브 기반 자료구조, 오프힙 링버퍼(Aeron, Chronicle류 패턴)가 표준이고, GC는 장 마감 후에만 허용하는 운용도 흔합니다. ZGC 같은 저정지 수집기도 ms 미만 정지를 보장할 뿐 0이 아닙니다.
- C++ 계열: 핫패스에서 동적 할당, 락, 시스템콜, 예외를 금지합니다. 스핀으로 대기하고, 분기를 줄이고, 핫 데이터를 캐시라인 단위로 정렬하며, false sharing을 패딩으로 제거합니다.
- 공통: 로그는 비동기 + 바이너리 + 별도 코어로. 핫패스의 printf 한 줄이 모든 커널 튜닝을 무력화할 수 있습니다.
일반 서비스에는 왜 과한가 — 트레이드오프 정산
이 글의 레시피를 일반 서비스에 적용하면 안 되는 이유를 정산해 봅니다.
| 기법 | 트레이딩에서의 이득 | 일반 서비스에서의 비용 |
|---|---|---|
| 코어 격리 + 폴링 | 꼬리지연 us 단위 단축 | 코어 상시 100% 점유 — 전력·비용 낭비, 집적도 하락 |
| C-state 비활성 | 기상 지연 제거 | 유휴 전력 수배 증가, 발열, 터보 여력 감소 |
| mitigations=off | 시스템콜 수백 ns 단축 | 사이드채널 취약점 노출 — 멀티테넌트에서 금기 |
| 인터럽트 병합 off | 패킷당 즉시 처리 | 높은 PPS에서 CPU 폭증, 처리량 급락 |
| THP off + 수동 휴지페이지 | ms급 정지 제거 | 운영 복잡도 증가, 일반 워크로드선 THP가 이득인 경우 많음 |
| 커널 바이패스 | us 미만 수신 | 커널 보안·관측 도구 무력화, 전담 인력 필요 |
요약하면 저지연 튜닝은 처리량·전력 효율·보안·운영성을 지불하고 결정론을 사는 거래입니다. 99분위 50ms면 충분한 API 서버라면, 이 거래는 명백한 손해입니다. 반대로 us가 손익인 시스템이라면, 위 비용은 모두 정당화됩니다. 자신의 시스템이 어느 쪽인지 먼저 정직하게 판단하는 것이 가장 중요한 튜닝입니다.
체크리스트
하드웨어/펌웨어
- BIOS에서 C-state 제한, 하이퍼스레딩 정책, SMI 소스 점검을 완료했는가
- NIC가 PTP 하드웨어 타임스탬프를 지원하고 PCIe 슬롯이 올바른 NUMA 노드에 있는가
- turbostat으로 SMI 카운터가 0에 가깝게 유지되는지 확인했는가
커널 부트/격리
- isolcpus, nohz_full, rcu_nocbs가 동일한 코어 집합으로 설정되어 있는가
- irqbalance가 비활성화되고 IRQ 어피니티가 명시적으로 배치되었는가
- 격리 코어에서 perf로 컨텍스트 스위치 0을 확인했는가
메모리/전력
- mlockall과 프리폴트가 기동 시퀀스에 포함되어 있는가
- THP가 never이고 명시적 휴지페이지가 예약되어 있는가
- governor performance, C-state 고정, 터보 정책이 의도대로인가
네트워크/시간
- 시세 NIC의 인터럽트 병합·드롭 카운터·RSS 매핑을 점검했는가
- ptp4l/phc2sys 오프셋이 목표(수십 ns) 안에서 유지되는가
- 하드웨어 타임스탬프 기반 구간별 계측이 동작하는가
측정/운영
- p99.99와 max를 상시 히스토그램으로 기록하는가
- cyclictest 기반 플랫폼 지터 기준선이 문서화되어 있는가
- 튜닝 변경마다 전후 분포를 비교하는 절차가 있는가 (한 번에 한 가지만 변경)
함정과 안티패턴
- 한 번에 열 가지를 바꾸기. 어떤 변경이 효과였는지 알 수 없게 됩니다. 기준선 측정 → 단일 변경 → 재측정의 루프를 지킵니다.
- 평균만 보고 배포. 꼬리는 부하가 높을 때, 즉 시장이 격렬할 때 나타납니다. 한가한 시간의 벤치마크는 거의 무의미합니다.
- 격리했는데 커널 스레드가 남아 있음. kworker, ksoftirqd, 타이머가 격리 코어에 남는 경우가 있습니다. 워크큐 cpumask와 nohz_full 동작을 실측으로 확인해야 합니다.
- NUMA를 잊은 업그레이드. 서버 교체나 NIC 슬롯 변경 후 numa_node가 바뀌어 조용히 느려지는 사고가 흔합니다. 기동 시 어서션으로 박아 두는 것을 권합니다.
- mitigations=off의 무신경한 복사. 인터넷에 노출되거나 멀티테넌트인 시스템에서 이 옵션을 복사해 쓰는 것은 보안 사고로 가는 지름길입니다.
- 시계 없는 측정. 동기화되지 않은 두 호스트의 타임스탬프 차이로 "지연"을 계산하면 음수 지연 같은 코미디가 나옵니다. PTP가 먼저입니다.
마치며
저지연 커널 튜닝의 기술적 내용은 결국 한 문장으로 요약됩니다. "핫패스에서 예측 불가능한 모든 것을 제거하라." 스케줄러의 선의도, 전력 관리의 절약도, 커널의 편의 기능도, 핫패스 위에서는 전부 지터의 원천입니다. 그래서 이 작업은 기능을 더하는 튜닝이 아니라 기능을 빼는 튜닝이며, 뺀 자리를 측정으로 채우는 작업입니다.
마이크로초와의 전쟁에서 이기는 팀의 공통점은 화려한 비법이 아니라, 분위수 히스토그램을 매일 보는 습관과 변경 하나하나를 계측으로 검증하는 규율이었습니다. 측정 없는 튜닝은 미신이고, 측정 있는 튜닝은 공학입니다.
참고 자료
- Linux Kernel Documentation — NO_HZ: Reducing Scheduling-Clock Ticks
- Linux Kernel Documentation — CPU Isolation
- Linux Kernel Documentation — The kernel command-line parameters
- Linux Kernel Documentation — Timestamping
- Linux Kernel Documentation — Busy Polling
- DPDK 공식 문서
- AF_XDP — Linux Kernel Documentation
- ebpf.io — eBPF 소개와 생태계
- linuxptp 프로젝트
- rt-tests (cyclictest) — Linux Foundation Wiki
- perf — Linux profiling with performance counters
- ESMA — MiFID II 규제 기술 표준(시계 동기화 RTS 25 포함)
- Red Hat — Low Latency Performance Tuning 가이드