글 작성자: cjwoov
반응형

0. 요약

 ISR과 DPC는 OS가 하드웨어 인터럽트를 효율적으로 처리하기 위한 2단계 메커니즘입니다.

랜카드에 패킷이 도착하면 먼저 최우선 순위인 ISR이 실행되어 하드웨어 상태만 빠르게 응답하고, TCP/IP 스택 분석 같은 무거운 작업은 DPC 큐에 지연시켜 처리합니다.

 

 여기서 서버 개발자가 주의해야 할 점은 우선순위(IRQL)입니다. DPC는 일반 유저 스레드(IOCP Worker)보다 높은 IRQL에서 실행되므로, 대규모 트래픽 발생 시 특정 코어에 DPC 처리가 집중되면 해당 코어의 유저 스레드는 CPU를 할당받지 못해 심각한 지연(Latency)이 발생합니다.

 

 이를 해결하기 위해 실무에서는 랜카드의 RSS(Receive Side Scaling) 기능을 활성화하여 하드웨어 인터럽트와 DPC 처리를 여러 CPU 코어로 분산시킴으로써, 커널 레벨의 네트워크 병목을 해소하고 서버의 처리량(Throughput)을 극대화해야 합니다.

 

1. 왜 알아야 할까?

 "원인 모를 서버 렉(Lag)의 진짜 범인을 찾기 위해서"입니다.

대규모 공성전이 열렸습니다. 유저 수천 명이 한 화면에서 스킬을 난사합니다.

유저들은 렉이 걸린다고 아우성인데, 모니터링 툴(작업 관리자)을 봅니다.

  • 전체 CPU 사용률: 고작 15%
  • Worker Thread 상태: 널널함, 병목 없음.
  • DB 쿼리 속도: 정상.

"어? 서버는 널널한데 왜 렉이 걸리지? 클라이언트 문제 아냐?"라고 책임을 넘기기 십상입니다.

하지만 이때 CPU 코어별 사용량을 자세히 까보면 충격적인 진실을 마주하게 됩니다.

  • Core 1 ~ Core 23: CPU 사용률 5%
  • Core 0: CPU 사용률 100% (빨간불 삐뽀삐뽀)

왜 0번 코어 혼자 죽어나가고 있을까요?

바로 수만 개의 패킷이 도착했다는 하드웨어 인터럽트(ISR)와 네트워크 처리(DPC)가 0번 코어에 몰빵되었기 때문입니다.

이 현상을 이해하고 세팅을 고치지 못하면, 아무리 C++ 코드를 최적화해도 서버 렉을 잡을 수 없습니다.

 

2. 배경: 패킷이 도착했을 때 OS의 딜레마

 랜카드(NIC)에 인터넷 선을 타고 데이터(패킷)가 도착했습니다.

랜카드는 CPU에게 "패킷 왔어! 빨리 처리해 줘!"라며 전기 신호(인터럽트)를 찌릅니다.

  • 초창기 무식한 방법: CPU가 하던 일(게임 로직 처리)을 모두 멈추고, 패킷을 메모리에 복사하고, TCP/IP 헤더를 분석하고, 어플리케이션(IOCP)에 전달할 때까지 계속 그 일만 합니다.
  • 문제점: 패킷이 폭포수처럼 쏟아지면, CPU는 영원히 패킷 처리만 하다가 게임 로직(유저 이동, 데미지 계산)은 단 한 줄도 실행하지 못하고 서버가 멈춰버립니다.

[OS 설계자들의 해결책: 2단계 분리]

"야, 하드웨어 알림은 최소한으로 짧게 끝내고(ISR), 진짜 무거운 패킷 분석 작업은 나중에 시간 날 때 하자(DPC)!"

이것이 바로 ISR과 DPC가 탄생한 배경입니다.

 

3. 핵심 개념 (택배 배달 비유)

 이해를 돕기 위해 택배 기사님(랜카드)과 집주인(CPU)으로 비유해 보겠습니다.

① ISR (Interrupt Service Routine) - "현관문 열고 도장 찍기"

  • 상황: 택배 기사님이 벨을 냅다 누릅니다. (하드웨어 인터럽트 발생)
  • 동작: 집주인은 화장실에 있든 밥을 먹든 모든 행동을 즉시 멈추고 뛰어나가야 합니다. (가장 높은 우선순위)
  • 처리: 문을 열고, 송장에 사인만 하고, 택배 상자를 거실 바닥에 휙 던져놓고(DPC 큐에 등록) 바로 하던 일(화장실)로 돌아갑니다.
  • 특징: 미친 듯이 빠릅니다. 하드웨어에게 "알았어, 받았어"라는 대답만 해주고 진짜 무거운 작업은 뒤로 미룹니다.

② DPC (Deferred Procedure Call) - "상자 까서 물건 정리하기"

  • 상황: 화장실을 다녀온 집주인이 거실을 보니 택배 상자(DPC 큐)가 쌓여있습니다.
  • 동작: 이제 상자를 칼로 뜯고(TCP/IP 헤더 분석), 뽁뽁이를 벗기고, 내용물을 서랍(유저 버퍼)에 정리합니다.
  • 특징: ISR보다는 덜 급하지만, 여전히 일반 게임 로직(User Thread)보다는 우선순위가 높습니다. 즉, 거실에 택배가 쌓여있으면 집주인은 게임 개발(유저 로직)을 못 하고 하루 종일 택배 상자만 까야 합니다.

★ OS의 우선순위 (IRQL: Interrupt Request Level)

  • IRQL 2 (DISPATCH_LEVEL): DPC가 도는 레벨.
  • IRQL 0 (PASSIVE_LEVEL): 우리가 짠 일반적인 C++ 스레드(IOCP Worker Thread 등)가 도는 레벨.
  • 절대 법칙: 높은 레벨(DPC)이 일하고 있으면, 낮은 레벨(우리 스레드)은 절대 CPU를 차지할 수 없습니다. (이래서 코어 0번이 DPC로 꽉 차면, 코어 0번에 할당된 우리 로직 스레드는 영원히 굶어 죽습니다.)

 

4. 실전 활용

 다시 1번의 렉 걸린 상황으로 돌아가 봅시다.

서버의 0번 코어가 100%를 찍고 있는 이유는, 수만 개의 패킷 택배를 0번 코어(집주인 1명) 혼자서 다 까고 있었기 때문입니다.

우리가 아무리 코드를 잘 짜서 IOCP Worker 스레드를 24개 만들어 놨어도, 패킷이 IOCP 큐에 도달하기도 전에 DPC 단(OS 커널)에서 병목이 걸린 겁니다.

 

[해결책: RSS (Receive Side Scaling)]

서버 엔지니어는 이럴 때 소스 코드를 고치는 게 아니라, 서버 장비의 랜카드(NIC) 설정을 엽니다.

그리고 RSS(Receive Side Scaling)라는 기능을 켭니다.

  • RSS의 마법: 랜카드가 패킷을 받을 때, 해시 알고리즘을 써서 "이번 패킷 인터럽트(ISR/DPC)는 0번 코어 말고 1번 코어에 찔러! 다음 건 2번 코어에 찔러!" 하고 분산시켜 줍니다.
  • 결과: 0번 코어 혼자 하던 택배 상자 까기(DPC)를 24개의 코어가 나눠서 하게 됩니다. CPU 0번의 점유율이 뚝 떨어지며, 꽉 막혔던 패킷들이 순식간에 IOCP로 넘어가고 렉이 마법처럼 사라집니다.
반응형