왜 단일 CPU에서 패킷 처리가 병목이 되는가
고성능 네트워크 서버에서 흔히 겪는 문제는 트래픽이 늘어도 특정 CPU 코어 하나만 100%에 붙어 있고 나머지 코어는 놀고 있는 상황이다. 원인은 대부분 인터럽트 처리에 있다. NIC가 패킷을 받으면 하드웨어 인터럽트(IRQ)를 발생시키고, 커널은 해당 IRQ를 특정 CPU에서 처리한다. 기본 설정에서 모든 수신 IRQ가 CPU0으로 몰리면, softirq(NET_RX) 처리가 한 코어에 집중되어 그 코어가 포화되는 순간 전체 처리량이 상한에 걸린다.
cat /proc/interrupts에서 NIC 큐별 인터럽트 카운트가 특정 CPU 열에만 증가한다면 이 병목을 의심해야 한다. mpstat -P ALL 1로 봤을 때 한 코어의 %soft만 높은 것도 같은 신호다.
RSS: NIC가 하드웨어로 큐를 나눈다
RSS(Receive Side Scaling)는 NIC가 패킷 헤더(보통 4-tuple: src/dst IP, src/dst port)를 해시하여 여러 수신 큐로 분산시키는 하드웨어 기능이다. 각 큐는 독립된 IRQ를 가지므로, 큐를 여러 개로 늘리고 각 IRQ를 서로 다른 CPU에 매핑하면 수신 처리가 코어 전체로 퍼진다. 먼저 큐 개수를 확인하고 조정한다.
# 현재/최대 큐 개수 확인
ethtool -l eth0
# combined 큐를 16개로 설정 (물리 코어 수에 맞춤)
ethtool -L eth0 combined 16
# RSS 해시 필드 확인 (TCP over IPv4)
ethtool -n eth0 rx-flow-hash tcp4
# 4-tuple 해시로 설정 (같은 연결은 같은 큐로 유지됨)
ethtool -N eth0 rx-flow-hash tcp4 sdfn
큐 개수는 물리 코어 수를 넘기지 않는 것이 원칙이다. 하이퍼스레딩 논리 코어까지 큐를 붙이면 캐시 경합으로 오히려 손해를 볼 수 있다.
IRQ affinity: 각 큐의 인터럽트를 코어에 고정
큐를 나눴어도 IRQ가 여전히 몇몇 코어에 몰리면 소용이 없다. /proc/irq/에 CPU 번호를 써서 큐 IRQ를 코어에 1:1로 고정한다.
#!/bin/bash
# eth0의 수신 큐 IRQ를 CPU0부터 순서대로 고정
IFACE=eth0
cpu=0
for irq in $(grep "${IFACE}-rx\|${IFACE}-TxRx" /proc/interrupts | awk -F: '{print $1}'); do
echo $cpu > /proc/irq/${irq}/smp_affinity_list
echo "IRQ ${irq} -> CPU ${cpu}"
cpu=$((cpu + 1))
done
주의할 점은 irqbalance 데몬이 켜져 있으면 이 수동 설정을 주기적으로 덮어쓴다는 것이다. 고정 매핑을 유지하려면 systemctl stop irqbalance로 끄거나, IRQBALANCE_BANNED_CPUS로 특정 코어를 제외해야 한다.
RPS/RFS: NIC가 큐를 못 나눌 때의 소프트웨어 대안
가상화 환경이나 저가 NIC처럼 하드웨어 큐가 부족한 경우, RPS(Receive Packet Steering)로 커널이 소프트웨어적으로 패킷을 여러 코어에 분산할 수 있다. RFS를 함께 쓰면 애플리케이션이 실행 중인 코어로 패킷을 유도해 캐시 지역성을 높인다.
# 큐0의 처리를 CPU 0-7에 분산 (비트마스크: 0xff)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# RFS 플로우 테이블 크기 설정
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
RSS와 RPS 비교
| 항목 | RSS | RPS |
|---|---|---|
| 동작 계층 | NIC 하드웨어 | 커널 소프트웨어 |
| CPU 오버헤드 | 거의 없음 | 추가 softirq 발생 |
| 필요 조건 | 멀티큐 지원 NIC | 제약 없음 |
| 권장 상황 | 물리 서버, 고성능 NIC | 단일 큐 NIC, VM |
검증과 주의점
설정 후에는 반드시 부하를 주면서 분산이 실제로 일어나는지 확인한다. watch -n1 'cat /proc/interrupts | grep eth0'로 큐별 인터럽트가 고르게 증가하는지, mpstat -P ALL 1로 %soft가 코어에 퍼졌는지 본다.
몇 가지 함정이 있다. 첫째, 재부팅하면 ethtool -L과 smp_affinity 설정은 초기화되므로 udev 규칙이나 systemd 유닛으로 영속화해야 한다. 둘째, NUMA 다중 소켓 서버에서는 NIC가 연결된 소켓의 로컬 코어에만 IRQ를 붙여야 하고, 원격 노드 코어에 붙이면 메모리 접근 지연이 오히려 커진다. 셋째, RSS 해시에 포트가 포함되지 않으면(2-tuple) 같은 IP 쌍의 대량 연결이 한 큐로 쏠리므로, 다수 연결 환경에서는 4-tuple 해시를 확인하는 것이 좋다.