네트워크 트래픽이 몰릴 때 top이나 mpstat으로 코어별 사용률을 보면 CPU 6개 중 딱 하나만 계속 90% 넘게 찍히고 나머지는 한가한 경우가 있다. 부하 분산이 안 되고 있다는 신호인데, 애플리케이션 스레드 배치를 아무리 바꿔도 해결이 안 될 때가 있다. 원인이 애플리케이션이 아니라 인터럽트(IRQ) 처리가 특정 코어에 고정돼 있는 경우가 많기 때문이다. 이 글에서는 /proc/interrupts로 IRQ 쏠림을 진단하고 smp_affinity로 재분배하는 방법을 실제 VM에서 재현한 데이터로 정리한다.
/proc/interrupts 읽는 법
각 줄은 IRQ 번호 하나에 대응하며, 컬럼은 해당 IRQ가 각 CPU에서 몇 번 발생했는지 누적 카운트를 보여준다.
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5
0: 124 0 0 0 0 0 IO-APIC 2-edge timer
19: 2659 0 0 0 0 0 IO-APIC 19-fasteoi ehci_hcd:usb2, enp0s3
주요 필드는 다음과 같다.
| 필드 | 의미 |
|---|---|
| IRQ 번호 | 줄 맨 앞 숫자. /proc/irq/<번호>/로 개별 제어 가능 |
| CPU0~CPUn | 부팅 이후 해당 CPU에서 이 IRQ가 처리된 누적 횟수 |
| IO-APIC / fasteoi / edge | 인터럽트 컨트롤러 종류와 트리거 방식 |
| 마지막 컬럼 | 이 IRQ를 공유하는 디바이스 드라이버 이름(콤마로 여러 개 공유 가능) |
위 예시는 하드웨어 인터럽트(hardirq) 카운트다. 소프트웨어 인터럽트(softirq) 처리 시간까지 코어별로 보려면 mpstat -P ALL의 %irq(hardirq)와 %soft(softirq) 컬럼을 함께 봐야 한다.
IRQ 쏠림 재현하기
6코어 Ubuntu 24.04 VM(VirtualBox, NIC 1개)에서 500MB 파일을 scp로 전송하며 NIC(enp0s3)의 IRQ 카운트와 코어별 부하를 비교했다. VM은 NIC 큐가 1개뿐이라 모든 수신 인터럽트가 한 IRQ 번호(19)로 들어온다.
# 전송 전
$ grep enp0s3 /proc/interrupts
19: 2714 0 0 0 0 0 IO-APIC 19-fasteoi ehci_hcd:usb2, enp0s3
# 500MB scp 전송 후
$ grep enp0s3 /proc/interrupts
19: 7617 0 0 0 0 0 IO-APIC 19-fasteoi ehci_hcd:usb2, enp0s3
4903회 증가한 카운트가 전부 CPU0에만 쌓였다. 전송 중 mpstat -P ALL 1로 찍은 코어별 사용률도 이를 뒷받침한다.
12:48:14 AM CPU %usr %sys %iowait %irq %soft %idle
12:48:14 AM 0 0.00 0.00 0.00 0.00 54.90 45.10
12:48:14 AM 1 4.35 9.78 0.00 0.00 0.00 85.87
12:48:14 AM 2 11.67 33.33 3.33 0.00 1.67 50.00
12:48:14 AM 3 0.00 0.00 0.00 0.00 1.01 98.99
12:48:14 AM 4 0.00 1.35 2.70 0.00 5.41 90.54
12:48:14 AM 5 0.00 4.05 0.00 0.00 0.00 95.95
CPU0의 %soft만 50~78% 구간을 오갔고 나머지 코어는 대체로 한가했다. RPS(Receive Packet Steering)가 켜져 있지 않으면 하드웨어 인터럽트를 받은 코어가 그 패킷의 softirq(네트워크 스택 처리)까지 그대로 떠맡기 때문에, 트래픽이 늘수록 CPU0 하나만 포화되는 구조다.
smp_affinity로 재분배하기
/proc/irq/<번호>/smp_affinity에 CPU 비트마스크(16진수)를 쓰면 해당 IRQ를 처리할 코어를 지정할 수 있다. smp_affinity_list는 같은 정보를 CPU 번호 목록(사람이 읽기 쉬운 형태)으로 보여준다.
$ cat /proc/irq/19/smp_affinity
3f
$ cat /proc/irq/19/smp_affinity_list
0-5
# CPU2로 고정 (비트마스크 04 = 0b000100)
$ echo 04 | sudo tee /proc/irq/19/smp_affinity
$ cat /proc/irq/19/smp_affinity_list
2
기본값 3f(0b111111)는 6개 코어 전체가 후보라는 뜻이지만, 실제로는 커널이 매번 CPU0으로 몰아준 것이 앞서 본 재현 결과다. CPU2로 강제 고정한 뒤 같은 500MB 전송을 다시 실행하자 카운트 증가분이 정확히 CPU2로 옮겨갔다.
# 재설정 직후
$ grep enp0s3 /proc/interrupts
19: 7723 0 36 0 0 0 IO-APIC 19-fasteoi ehci_hcd:usb2, enp0s3
# 500MB 전송 후
$ grep enp0s3 /proc/interrupts
19: 7723 0 5195 0 0 0 IO-APIC 19-fasteoi ehci_hcd:usb2, enp0s3
CPU0 쪽 카운트는 그대로 멈췄고 CPU2가 5159 증가했다. 특정 IRQ를 유휴 코어로 옮기는 것만으로 병목 코어를 바꿀 수 있다는 뜻이다. 다만 이 값은 재부팅하면 초기화되므로 영구 적용하려면 systemd 서비스나 udev 규칙으로 부팅 시 스크립트를 실행해야 한다.
주의사항
| 항목 | 내용 |
|---|---|
| irqbalance 데몬과 충돌 | irqbalance가 실행 중이면 수동으로 설정한 smp_affinity를 주기적으로 되돌릴 수 있다. 수동 튜닝 전 systemctl stop irqbalance 필요 |
| 재부팅 시 초기화 | /proc 값이라 재부팅하면 커널 기본값으로 복귀. 영구 적용은 systemd 서비스로 스크립트화 |
| 공유 IRQ 주의 | 같은 IRQ 번호를 여러 디바이스가 공유하면(위 예시의 usb2 + enp0s3) 하나만 옮기고 싶어도 전부 같이 이동한다. MSI-X를 지원하는 실제 서버 NIC는 큐마다 별도 IRQ를 받아 이 문제가 없음 |
| 단일 큐 NIC의 한계 | VM처럼 NIC 큐가 1개면 하드웨어 IRQ 자체는 항상 코어 1개에만 갈 수밖에 없다. 여러 코어로 수신 처리를 분산하려면 RPS(/sys/class/net/<dev>/queues/rx-0/rps_cpus)를 함께 써야 한다 |
마무리
/proc/interrupts로 IRQ가 어느 코어에 몰리는지 확인하고, mpstat -P ALL의 %soft로 그 코어가 실제로 병목인지 교차 확인한 다음, smp_affinity로 재배치하는 흐름이면 애플리케이션 코드를 건드리지 않고도 코어 편중 문제를 해결할 수 있다. irqbalance 자동화와 수동 설정의 구체적인 비교는 이어지는 글에서 다룬다.