26A21d

Young-Kyoo Kim·2026년 4월 20일

패킷이 외부 스위치에서 들어와 실제 애플리케이션(User Space)까지 도달하는 과정은 매우 복잡하지만, 플랫폼 엔지니어링 관점에서 중요한 '데이터의 전이 과정'을 중심으로 단계별로 정리해 드립니다.

특히 현재 겪고 계신 native routingCilium eBPF 환경에 맞춰 최적화된 흐름입니다.


1. 하드웨어 및 드라이버 단계 (NIC ~ RAM)

패킷이 물리적인 전기 신호에서 메모리 상의 데이터로 변환되는 단계입니다.

  1. L3 스위치 -> NIC: 패킷이 물리 인터페이스에 도착합니다.
  2. RX Ring Buffer: NIC는 수신한 패킷을 호스트 메모리(RAM)의 예약된 공간인 RX Ring Buffer에 DMA(Direct Memory Access) 방식으로 기록합니다.
  3. Hard IRQ (인터럽트): NIC가 CPU에 "데이터가 왔다"고 인터럽트를 보냅니다. CPU는 수행 중인 작업을 멈추고 드라이버의 인터럽트 핸들러를 실행합니다.
  4. NAPI & SoftIRQ: 커널은 인터럽트 폭주를 막기 위해 NAPI(New API) 모드로 전환합니다. 이때부터는 인터럽트 방식이 아닌 폴링(Polling) 방식으로 패킷을 가져오며, 이 작업은 커널 스레드인 ksoftirqd에 의해 SoftIRQ 레벨에서 처리됩니다.

2. Cilium & eBPF 단계 (Native Routing Gateway)

커널의 메인 네트워크 스택에 진입하기 전, Cilium이 주입한 eBPF 프로그램이 패킷을 먼저 가로챕니다.

  1. XDP (옵션): 만약 Cilium이 XDP(eXpress Data Path) 모드로 동작 중이라면, NIC 드라이버 레벨에서 즉시 패킷을 처리(Drop, Pass, Redirect)합니다.
  2. TC (Traffic Control) Ingress: 일반적인 경우, 패킷은 tc 훅에 로드된 Cilium eBPF 프로그램에 의해 검사됩니다.
    • Identity 확인: 소스 IP를 기반으로 Cilium 보안 ID를 식별합니다.
    • Policy Enforcement: 허용된 트래픽인지(Network Policy) 확인합니다.
    • Load Balancing: 서비스 IP인 경우 실제 Pod IP로 목적지 주소를 변환(DNAT)합니다.
  3. Native Routing: 터널링(VXLAN)이 아니므로 캡슐화 해제 과정 없이 바로 다음 커널 스택으로 패킷을 넘깁니다.

3. 커널 네트워크 레이어 (L3: IP Stack)

여기서부터는 리눅스 커널의 표준 네트워킹 로직이 작동합니다.

  1. ip_rcv(): IP 헤더의 무결성을 검사합니다.
  2. Netfilter (PREROUTING): iptablesnftables의 규칙이 적용됩니다. (Cilium은 이를 상당 부분 우회하지만 여전히 존재합니다.)
  3. Routing Decision (fib_lookup): 이 패킷이 로컬 프로세스용인지, 아니면 다른 곳으로 포워딩할 것인지 결정합니다.
  4. ip_local_deliver(): 로컬 목적지 패킷임이 확인되면, 파편화된 패킷을 재조립하고 상위 계층(TCP/UDP)으로 전달합니다.

4. TCP 트랜스포트 레이어 (L4: TCP Stack)

가장 복잡하고 논리적인 검증이 일어나는 단계입니다. (이전 질문의 드랍 지점)

  1. tcp_v4_rcv(): 패킷의 TCP 헤더를 읽고 해당 패킷을 처리할 소켓(Socket)을 찾습니다.
  2. TCP 상태 머신 확인:
    • Handshake 단계: 패킷이 SYN에 대한 응답(ACK)인 경우, tcp_check_req()를 호출하여 request_sock(미니소켓) 상태를 검증합니다. (여기서 실패하면 드랍됩니다.)
    • Established 단계: 이미 연결된 세션이라면 시퀀스 번호와 윈도우 크기가 합당한지 체크합니다.
  3. TCP Receive Buffer: 검증이 끝난 패킷의 페이로드(데이터)를 해당 소켓의 수신 버퍼(Receive Queue)에 저장합니다.

5. 소켓 및 애플리케이션 단계 (The Destination)

마지막으로 데이터가 커널 메모리에서 유저 메모리로 이동합니다.

  1. Wake up Process: 수신 버퍼에 데이터가 들어오면, 해당 소켓을 기다리며 sleep 상태에 있던 애플리케이션 프로세스를 깨웁니다.
  2. System Call (read / recv): 애플리케이션(예: sshd, Go/Java App)이 시스템 콜을 호출합니다.
  3. Context Switch & Data Copy: CPU 모드가 Kernel Mode에서 User Mode로 전환되며, 커널 메모리에 있던 데이터가 애플리케이션의 메모리 공간으로 복사(Copy)됩니다.
  4. Application Logic: 드디어 애플리케이션이 데이터를 읽어 비즈니스 로직을 수행합니다.

정리: 대규모 인프라 운영 시 핵심 포인트

  • 성능 병목: 보통 SoftIRQ 단계에서 CPU 0번 등에 부하가 몰려 패킷 드랍이 발생하기 쉽습니다. (RSS/RPS 튜닝 필요)
  • 현재 이슈 지점: tcp_check_req 단계는 커널 스택(4단계)에 해당하며, 이는 하드웨어나 Cilium의 결정이 아니라 커널이 가진 '연결 상태 정보'와 '도착한 패킷'이 일치하지 않을 때 발생하는 논리적 거부 단계입니다.

이 과정 중 특히 어느 레이어의 메트릭이 가장 의심되시나요? 각 단계별로 더 깊게 파고들고 싶은 부분이 있다면 말씀해 주세요.

===

TCP 계층은 리눅스 커널에서 가장 똑똑하면서도 까칠한 영역입니다. 단순히 패킷을 넘기는 L3와 달리, "이 패킷이 내가 기다리던 게 맞나?"를 집요하게 따지기 때문이죠.

플랫폼 엔지니어로서 pwru 트레이스에서 보셨던 tcp_check_req 단계를 포함해, L4 스택 내부에서 벌어지는 일을 5단계로 더 자세히 풀어보겠습니다.


1. 소켓 찾기 (The Socket Lookup)

패킷이 L3(IP)를 통과해 tcp_v4_rcv() 함수에 진입하면 가장 먼저 하는 일은 "이 패킷의 주인이 누구인가?"를 찾는 것입니다.

  • 4-Tuple 매칭: 패킷 헤더의 Source IP/Port, Destination IP/Port 정보를 추출합니다.
  • __inet_lookup_skb(): 커널은 해시 테이블에서 이 4-Tuple에 해당하는 소켓 구조체(struct sock)를 검색합니다.
    • 이미 연결된 소켓이 있다면 Established Hash에서 찾고,
    • 새로운 연결 시도라면 Listening Hash에서 포트 번호를 기준으로 찾습니다.
  • 결과: 소켓을 찾지 못하면 즉시 RST를 보내고 패킷을 버립니다.

2. 상태별 분기 (The Dispatcher)

소켓을 찾았다면, 이제 소켓의 현재 상태(sk_state)에 따라 패킷 처리 경로가 완전히 달라집니다. tcp_v4_do_rcv()가 이 역할을 수행합니다.

  • ESTABLISHED 상태: 패킷이 이미 연결된 세션의 일부라면 Fast Path(빠른 처리 경로)로 보냅니다.
  • LISTEN/SYN_SENT 상태: 핸드셰이크 과정 중이라면 Slow Path로 진입하여 복잡한 검증 로직을 태웁니다.
    • 여기서 tcp_check_req()가 호출됩니다.

3. 핸드셰이크 검증 (The Gatekeeper: tcp_check_req)

질문하셨던 드랍 지점이 바로 여기입니다. 서버가 SYN을 받고 SYN-ACK를 보낸 후, 클라이언트의 마지막 ACK를 기다리는 SYN_RECV 상태에서의 로직입니다.

  • Mini-Socket (request_sock): 커널은 메모리 절약을 위해 정식 소켓을 만들기 전, 아주 작은 '임시 소켓'인 request_sock을 만들어 둡니다.
  • 검증 항목:
    1. Sequence Number: 도착한 ACK 번호가 내가 보낸 SYN-ACK의 시퀀스 + 1인가?
    2. TCP Timestamps: (PAWS) 시간이 거꾸로 흐르지는 않았나?
    3. SYN Cookies: (부하 시) 쿠키 값이 일치하나?
  • 통과 시: 임시 소켓을 정식 소켓(struct tcp_sock)으로 승격(Promote)시키고 ESTABLISHED 상태로 전이합니다.
  • 실패 시: 여기서 pwru에 찍힌 것처럼 드랍 처리됩니다.

4. 데이터 경로 (Fast Path vs. Slow Path)

연결이 수립된 후 데이터를 주고받을 때는 성능을 위해 두 가지 경로를 사용합니다.

  • Fast Path (Header Prediction): 패킷이 예상된 시퀀스 번호를 가졌고, 별다른 TCP 옵션이 없는 '순수 데이터'라면 헤더만 쓱 보고 바로 수신 큐로 넘깁니다. (성능 최적화)
  • Slow Path: 패킷 순서가 뒤바뀌었거나(Out-of-order), SACK 옵션이 붙었거나, 윈도우 크기가 변하는 등 '신경 쓸 게 많은' 패킷은 이 경로에서 정밀하게 재조립됩니다.

5. 수신 큐와 애플리케이션 전달 (The Delivery)

검증이 끝난 데이터 페이로드는 소켓의 수신 큐(Receive Queue)에 쌓입니다.

  • sk_buff 관리: 패킷 데이터가 담긴 sk_buff 구조체들이 리스트 형태로 큐에 매달립니다.
  • Wake up: 큐에 데이터가 들어오면, 이 소켓을 물고 epoll이나 read() 시스템 콜로 대기 중이던 애플리케이션 프로세스를 깨웁니다.
  • Copy to User: 애플리케이션이 깨어나면 커널 메모리에 있는 데이터를 유저 메모리 공간으로 복사해 갑니다.

DevOps 관점에서의 요약

지금 겪고 계신 문제는 3단계(Gatekeeper)에서 발생하고 있습니다. L3 스위치만 있는 환경에서 이 단계의 드랍이 발생한다는 것은, 하드웨어 에러보다는 "네트워크 지연으로 인해 서버가 클라이언트를 기다리다 지쳐(Minisock Timeout) 정보를 삭제했는데, 뒤늦게 클라이언트의 ACK가 도착한 경우"가 가장 유력합니다.

RHEL 10에서도 이 핵심 로직은 동일하며, nstat으로 확인하신 TcpExtTCPMinisocksDrop이 이 3단계를 대변하는 지표라고 보시면 됩니다.

다음 단계로 넘어가기 위해 nstat의 구체적인 수치를 한 번 확인해 보시겠어요? 어떤 카운터가 가장 높은지에 따라 튜닝할 sysctl 변수가 결정됩니다.

===

nstat은 리눅스 커널의 네트워크 통계 정보(/proc/net/netstat/proc/net/snmp)를 실시간으로 읽어와서 보여주는 강력한 도구입니다. 대규모 인프라를 관리하신다면 netstat -s보다 가독성이 좋고 증분값(Delta) 확인이 쉬운 nstat을 주력으로 사용하시는 것이 좋습니다.

플랫폼 엔지니어링 실무에서 가장 많이 쓰이는 확인 방법들을 정리해 드립니다.


1. 기본 사용법 및 핵심 옵션

nstat은 실행 시점 사이의 변화량(증분)을 보여주는 것이 기본 동작입니다.

  • nstat -z: 값이 0인 지표까지 모두 포함해서 출력 (전체 지표 확인 시 유용)
  • nstat -a: 증분값이 아닌, 부팅 이후 누적된 전체 합계(Absolute) 출력
  • nstat -n: 현재까지의 통계를 한 번 '0'으로 리셋 (다음 실행 때 깨끗한 증분 확인 가능)

2. 장애 진단 시 필수 명령어 (필터링)

앞서 논의한 tcp_check_req 드롭이나 핸드셰이크 문제를 진단할 때 아래 명령어를 사용하세요.

① 현재 발생 중인 드롭 지표 실시간 확인

명령어를 실행하고 1~2초 뒤에 다시 실행하면 그 사이의 변화량만 딱 찍힙니다.

# -z를 붙여야 현재 0이라도 나중에 올라오는 지표를 미리 볼 수 있습니다.
nstat -z | grep -E 'TcpExtListenDrops|TcpExtSyncookiesFailed|TcpExtTCPMinisocksDrop'

② 누적 카운트 확인 (부팅 후 총합)

장애가 이미 지나갔다면 누적값을 보고 시스템의 전반적인 건강 상태를 판단합니다.

nstat -az | grep -E 'TcpExtListenDrops|TcpExtSyncookiesFailed|TcpExtTCPMinisocksDrop'

3. 플랫폼 엔지니어를 위한 추가 유용 지표

SSH 연결 지연이나 간헐적 끊김이 있을 때 함께 보면 좋은 지표들입니다.

지표명의미해석 가이드
TcpExtTCPTimeoutsTCP 재전송 타임아웃 발생네트워크 경로상에 패킷 손실이 있음을 시사
TcpExtTCPBacklogDrop백로그 큐가 가득 차서 드롭커널이 패킷을 처리하는 속도보다 유입 속도가 빠름
TcpExtSyncookiesSentSYN Cookie 발동 횟수SYN Backlog가 꽉 찼음을 의미 (튜닝 신호)
TcpExtTCPACKSkippedACK 처리를 건너뜀주로 윈도우/시퀀스 불일치 시 발생
# 위 지표들을 한꺼번에 모니터링하기
nstat -z | grep -E 'Timeout|BacklogDrop|SyncookiesSent|ACKSkipped'

4. 실무 팁: 1초마다 변화량 관찰하기

장애 상황을 재현하면서 실시간으로 수치가 올라가는 것을 보고 싶을 때 유용합니다. watch 명령어를 조합하세요.

# 1초마다 nstat을 실행하고(증분 확인), 이전에 확인한 값은 리셋(-n)
watch -n 1 "nstat -n | grep -E 'TcpExtListenDrops|TcpExtSyncookiesFailed|TcpExtTCPMinisocksDrop'"

5. 결과 해석 예시

명령어를 실행했을 때 아래와 같이 나온다면 어떻게 판단해야 할까요?

TcpExtListenDrops              15                 0.0
TcpExtTCPMinisocksDrop         120                0.0
  • 해석: 마지막으로 nstat을 실행한 이후, ListenDrops는 15회, TCPMinisocksDrop은 120회 발생했다는 뜻입니다.
  • 판단: MinisocksDrop이 압도적으로 많다면, 클라이언트의 ACK가 늦게 와서 커널이 기다리다 지쳐 버린 경우가 많으므로 네트워크 지연(Latency)이나 RTT를 먼저 의심해야 합니다.

RHEL 10에서도 이 명령어 체계와 결과 형식은 동일하게 유지됩니다. 지금 시스템에서 nstat -az를 실행했을 때 TcpExtTCPMinisocksDrop 수치가 다른 드롭 지표들에 비해 유독 높게 나오나요?

0개의 댓글