epoll과 Busy Polling은 저지연 트레이딩 시스템에서 어떻게 다른가? | Trading Engineering — Deep Dive #5

epoll과 Busy Polling은 저지연 트레이딩 시스템에서 어떻게 다른가?
Trading Engineering — Deep Dive #5
Linux에서 네트워크 프로그램을 개발하다 보면 epoll()을 자주 만나게 됩니다.
일반적인 서버 프로그램에서는 매우 효율적인 방식입니다.
하지만 저지연 트레이딩 시스템에서는 조금 다른 접근이 필요할 수 있습니다.
특히 시장 데이터나 주문 데이터를 매우 짧은 latency로 처리해야 하는 시스템에서는
CPU가 데이터를 기다리는 방식 자체가 latency에 영향을 줄 수 있습니다.
이번 글에서는 Linux의 epoll과 Busy Polling / Busy Loop의 차이를 살펴보고, 왜 저지연 시스템에서는 CPU를 적극적으로 사용하는 방식이 선택되기도 하는지 알아보겠습니다.
1. 일반적인 네트워크 프로그램의 동작
일반적인 서버 프로그램에서는 데이터가 도착할 때까지 CPU가 계속 작업할 필요가 없습니다.
예를 들어 다음과 같은 구조를 생각할 수 있습니다.
Application
│
↓
epoll()
│
│ Waiting...
│
↓
Network Event
│
↓
read()
│
↓
Process Data
데이터가 없을 때 프로그램은 epoll_wait()에서 기다립니다.
이렇게 하면 CPU를 다른 작업에 사용할 수 있습니다.
즉,
데이터가 없으면 CPU를 사용하지 않는다.
라는 것이 기본적인 설계입니다.
웹 서버, API 서버, 일반적인 TCP 서버에서는 매우 합리적인 방식입니다.
2. epoll이란?
Linux의 epoll은 여러 file descriptor의 이벤트를 효율적으로 감시하기 위한 I/O event notification mechanism입니다.
예를 들어 하나의 프로세스가 여러 socket을 관리한다고 생각해보겠습니다.
Socket 1 ─┐
Socket 2 ─┤
Socket 3 ─┤
Socket 4 ─┤──→ epoll_wait()
Socket 5 ─┤
Socket 6 ─┘
데이터가 들어온 socket이 있으면 epoll_wait()가 해당 이벤트를 반환합니다.
대표적으로 다음과 같은 형태로 사용할 수 있습니다.
struct epoll_event events[64];
int n = epoll_wait(
epfd,
events,
64,
-1
);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
read(fd, buffer, sizeof(buffer));
}
}
일반적인 네트워크 프로그램에서는 매우 유용한 구조입니다.
3. epoll의 핵심은 "기다리는 것"
epoll_wait()의 중요한 특징은 이벤트가 발생할 때까지 기다릴 수 있다는 것입니다.
CPU
│
↓
epoll_wait()
│
│
│ Sleep / Wait
│
↓
Packet Arrival
│
↓
Wakeup
│
↓
Process Packet
데이터가 없는 동안 CPU를 계속 사용할 필요가 없습니다.
따라서 CPU 사용량을 줄이고 시스템의 다른 작업에 CPU 자원을 사용할 수 있습니다.
이것은 일반적인 서버 환경에서는 매우 중요한 장점입니다.
4. 그렇다면 Busy Polling은 무엇인가?
Busy Polling은 완전히 다른 접근입니다.
데이터가 들어왔는지를 계속 확인합니다.
while (running) {
if (data_available()) {
process_data();
}
}
데이터가 없더라도 loop를 계속 실행합니다.
즉,
CPU를 사용하면서 데이터가 도착했는지를 계속 확인한다.
는 방식입니다.
이를 Busy Loop 또는 Polling Loop라고 부르기도 합니다.
5. epoll과 Busy Polling의 차이
두 방식을 단순하게 비교하면 다음과 같습니다.
epollBusy Polling
| 데이터가 없을 때 | 대기 | 계속 확인 |
| CPU 사용량 | 낮음 | 높음 |
| Wake-up 과정 | 필요 | 없음 |
| 구현 | 일반적 | 특수한 환경에서 사용 |
| Latency | 비교적 안정적 | 매우 낮은 latency를 노릴 수 있음 |
| CPU 자원 | 절약 | 많이 사용 |
| 전력 소비 | 상대적으로 낮음 | 높음 |
중요한 점은
Busy Polling이 항상 더 빠르다는 의미는 아니라는 것입니다.
시스템의 workload와 환경에 따라 결과가 달라집니다.
6. 왜 Busy Polling이 빠를 수 있을까?
가장 큰 이유 중 하나는 waiting과 wake-up 과정입니다.
epoll 기반의 일반적인 흐름을 단순화하면:
Packet Arrival
↓
Kernel
↓
Event Notification
↓
Wake-up
↓
Scheduler
↓
Application
↓
read()
↓
Process
반면 Busy Polling은:
Application
↓
Polling
↓
Data Arrived?
↙ ↘
No Yes
│ │
└─ Loop ↓
Process
애플리케이션이 계속 실행 중이기 때문에 별도의 sleep/wakeup 과정에 의존하지 않습니다.
따라서 특정 환경에서는 latency와 특히 tail latency를 줄이는 데 도움이 될 수 있습니다.
7. 하지만 Busy Polling에는 대가가 있다
여기서 중요한 trade-off가 있습니다.
Busy Polling은 CPU를 계속 사용합니다.
예를 들어:
Core 0
└── Network Polling Thread
└── 100% CPU
데이터가 아무것도 들어오지 않는 순간에도 CPU는 계속 실행됩니다.
따라서 다음과 같은 비용이 발생할 수 있습니다.
- 높은 CPU utilization
- 높은 전력 소비
- 다른 작업을 수행할 CPU 부족
- 발열 증가
- CPU resource contention
즉,
Latency를 줄이기 위해 CPU 자원을 사용하는 전략
이라고 볼 수 있습니다.
8. Trading System에서는 이야기가 달라진다
일반적인 서버에서는 CPU를 절약하는 것이 중요합니다.
하지만 특정 저지연 트레이딩 시스템에서는 CPU 비용보다 latency가 더 중요한 경우가 있습니다.
예를 들어:
Market Data
↓
Packet Arrival
↓
Decode
↓
Strategy
↓
Risk Check
↓
Order
↓
FEP
이 전체 과정이 매우 짧은 시간 안에 처리되어야 한다면 네트워크 packet을 기다리는 방식 자체가 중요해집니다.
특히 시장 데이터가 매우 빈번하게 들어오는 환경에서는
"데이터가 없는 시간이 거의 없다."
는 상황도 발생할 수 있습니다.
이런 환경에서는 CPU를 계속 사용하면서 packet을 확인하는 것이 합리적인 선택이 될 수 있습니다.
9. Busy Polling은 "CPU를 낭비하는 것"인가?
반드시 그렇지는 않습니다.
일반적인 프로그램에서는 이렇게 생각할 수 있습니다.
CPU 100%
↓
Bad
하지만 저지연 시스템에서는 조금 다르게 볼 필요가 있습니다.
CPU 100%
↓
Polling
↓
Immediate Processing
↓
Lower Latency
CPU를 계속 사용하는 목적이 명확하다면 이것은 단순한 낭비가 아닙니다.
예를 들어 전용 CPU Core 하나를 네트워크 수신 전용으로 할당할 수 있습니다.
Core 0 → Network Polling
Core 1 → Market Data
Core 2 → Strategy
Core 3 → Order
이 경우 Core 0의 높은 CPU utilization은 설계의 결과입니다.
중요한 것은 CPU 사용량 자체가 아니라 그 CPU 시간이 어떤 작업에 사용되고 있는가입니다.
10. epoll에서도 Busy Polling이 필요할 수 있다
여기서 한 가지 주의할 점이 있습니다.
epoll과 Busy Polling을 완전히 서로 반대되는 기술로만 볼 필요는 없습니다.
Linux 네트워크 stack에는 다양한 polling 관련 기능과 설정이 존재하며, 특정 환경에서는 kernel networking과 NIC의 동작을 조정하여 polling과 event notification을 조합할 수도 있습니다.
또한 매우 낮은 latency가 필요한 시스템에서는 일반적인 socket + epoll 구조에서 더 나아가
- Socket Busy Polling
- NIC polling
- Kernel bypass
- User-space networking
- DPDK
- Onload 계열 기술
등을 검토하기도 합니다.
즉, 실제 시스템에서는 다음과 같은 여러 단계의 선택지가 존재합니다.
Traditional I/O
↓
epoll
↓
Polling Optimization
↓
Busy Polling
↓
Kernel Bypass
↓
User-Space Networking
모든 시스템이 가장 아래 단계까지 갈 필요는 없습니다.
11. Busy Polling과 CPU Affinity
앞선 Deep Dive #3에서 CPU Affinity를 살펴봤습니다.
Busy Polling과 CPU Affinity는 매우 밀접하게 연결됩니다.
예를 들어:
Core 0
└── Network Polling Thread
Core 1
└── Market Data Thread
Core 2
└── Strategy Thread
Core 3
└── Order Thread
각 역할을 특정 Core에 배치하면 thread migration을 줄이고 cache locality를 유지하는 데 도움이 될 수 있습니다.
반면 Busy Polling Thread를 일반적인 작업과 같은 Core에서 실행하면 문제가 발생할 수 있습니다.
Core 0
├── Polling
├── Logging
├── Monitoring
└── Other Tasks
이 경우 polling thread와 다른 작업이 CPU 자원을 경쟁하게 됩니다.
따라서 Busy Polling을 적용할 때는 CPU topology와 thread placement를 함께 설계해야 합니다.
12. NUMA까지 연결된다
Deep Dive #4에서 NUMA를 살펴봤습니다.
이제 세 가지 개념이 연결됩니다.
CPU Affinity
↓
Busy Polling
↓
NUMA Locality
예를 들어 NIC가 NUMA Node 0에 연결되어 있다면:
NUMA Node 0
NIC
↓
Polling Thread
↓
Market Data
↓
Strategy
와 같이 같은 NUMA 영역 안에서 데이터 흐름을 구성하는 것이 유리할 수 있습니다.
반대로:
NIC
↓
NUMA Node 0
↓
Interconnect
↓
NUMA Node 1
↓
Strategy
처럼 packet 처리 경로가 계속 NUMA node를 넘어가면 locality 측면에서 불리할 수 있습니다.
결국 앞선 글에서 살펴본
CPU → Cache → Memory → NUMA → NIC
가 하나의 네트워크 처리 경로로 연결됩니다.
13. TCP와 UDP에서도 차이가 있다
Busy Polling을 이야기할 때 TCP와 UDP를 함께 생각할 필요가 있습니다.
예를 들어 시장 데이터 수신은 UDP multicast를 사용하는 경우가 많습니다.
Exchange
↓
Multicast
↓
NIC
↓
UDP
↓
Market Data
반면 주문 전송이나 일부 세션 기반 통신에서는 TCP가 많이 사용됩니다.
Application
↓
TCP
↓
NIC
↓
Broker / Exchange
TCP와 UDP는 각각 특성이 다르기 때문에 polling 방식 역시 시스템의 전체 요구사항에 맞춰 결정해야 합니다.
특히 UDP에서는 packet loss, sequence number, gap detection, recovery 등의 문제도 함께 고려해야 합니다.
따라서 단순히
"UDP니까 Busy Polling"
이라고 결정하는 것은 적절하지 않습니다.
14. Latency만 보면 안 된다
Busy Polling을 적용했는데 latency가 낮아졌다고 해서 시스템 전체가 개선되었다고 단정할 수는 없습니다.
함께 봐야 할 지표가 있습니다.
Latency
- Average
- P50
- P95
- P99
- P99.9
CPU
- CPU utilization
- CPU cycles
- IPC
- Context switch
- CPU migration
Network
- Packet rate
- Packet loss
- Receive queue
- NIC utilization
Application
- Throughput
- Queue depth
- Processing time
- End-to-end latency
특히 trading system에서는 평균값보다 tail latency가 중요할 수 있습니다.
15. 언제 epoll을 사용하는가?
epoll은 여전히 매우 좋은 선택입니다.
다음과 같은 경우에는 특히 적합합니다.
- 많은 socket을 관리해야 하는 경우
- Connection 수가 많은 경우
- CPU 자원을 효율적으로 사용해야 하는 경우
- latency 요구사항이 극단적으로 낮지 않은 경우
- 일반적인 financial server / API server
- 관리·모니터링 시스템
예를 들어:
Client 1 ─┐
Client 2 ─┤
Client 3 ─┤
Client 4 ─┤
Client 5 ─┤
↓
epoll
↓
Event Loop
이런 구조는 매우 효율적입니다.
16. 언제 Busy Polling을 고려하는가?
반대로 다음과 같은 환경에서는 Busy Polling을 검토할 수 있습니다.
- 매우 낮은 latency가 핵심인 경우
- packet rate가 매우 높은 경우
- 전용 CPU Core를 할당할 수 있는 경우
- CPU 자원보다 latency가 중요한 경우
- NIC와 CPU topology를 세밀하게 제어할 수 있는 경우
- HFT / DMA / Market Data Feed Handler
하지만 중요한 것은 실제 측정 결과입니다.
Busy Polling을 적용했는데 latency 개선은 거의 없고 CPU 사용량만 크게 증가한다면 좋은 최적화가 아닙니다.
17. 가장 중요한 것은 "Trade-off"
결국 epoll과 Busy Polling의 선택은 다음과 같은 trade-off입니다.
CPU Efficiency
↑
│
epoll ●
│
│
│
│
│ ● Busy Polling
│
└────────────────────→
Low Latency
일반적인 서버에서는 CPU 효율성이 중요할 수 있습니다.
반면 특정 HFT 시스템에서는 latency가 훨씬 중요한 목표가 될 수 있습니다.
따라서 하나의 방식이 모든 시스템에 적합한 것은 아닙니다.
18. FontesFintech Engineering Approach
저지연 시스템에서 네트워크 처리를 설계할 때 중요한 것은 특정 API를 선택하는 것이 아닙니다.
중요한 것은 전체 데이터 경로의 latency를 측정하고 병목을 찾는 것입니다.
NIC
↓
Kernel / User Space
↓
Polling / Event Notification
↓
Packet Processing
↓
Market Data
↓
Strategy
↓
Risk
↓
Order
↓
FEP
이 과정에서 어느 지점에서 latency가 발생하는지 확인해야 합니다.
따라서 FontesFintech의 저지연 시스템 설계에서는 다음과 같은 질문이 중요합니다.
데이터는 얼마나 자주 들어오는가?
CPU가 데이터를 기다리는 시간이 얼마나 되는가?
Event Wake-up이 실제 병목인가?
전용 CPU Core를 사용할 수 있는가?
NIC와 CPU의 NUMA topology는 어떻게 구성되어 있는가?
Busy Polling으로 얻는 latency 개선이 CPU 비용을 정당화하는가?
이러한 질문에 대한 답을 측정한 뒤 시스템 구조를 결정해야 합니다.
Conclusion
epoll()과 Busy Polling은 단순히 "느린 방식과 빠른 방식"의 차이가 아닙니다.
둘은 CPU를 사용하는 방식 자체가 다릅니다.
epoll은 데이터가 없을 때 CPU를 쉬게 하면서 효율적으로 이벤트를 기다립니다.
반면 Busy Polling은 CPU를 적극적으로 사용하여 데이터 도착을 지속적으로 확인하고, 특정 환경에서는 wake-up과 scheduling 관련 지연을 줄일 수 있습니다.
따라서 저지연 트레이딩 시스템에서는
CPU를 아끼는 것이 항상 최적화는 아닙니다.
때로는 CPU Core 하나를 전용으로 사용하는 것이 더 합리적일 수 있습니다.
하지만 그 판단 역시 반드시 측정과 Benchmark를 통해 검증해야 합니다.
Next: Trading Engineering — Deep Dive #6
Interrupts, Polling and Network Latency
다음 글에서는 조금 더 깊이 들어가서,
"네트워크 패킷이 NIC에 도착한 순간부터 애플리케이션 코드가 실행될 때까지 실제로 어떤 일이 일어나는가?"
를 살펴봅니다.
NIC Interrupt → IRQ → Kernel → NAPI → Socket → Application
이라는 흐름을 이해하면, 왜 저지연 시스템에서 Interrupt와 Polling의 선택이 중요한지 자연스럽게 연결됩니다.