GSO/GRO와 네트워크 오프로드
Linux 커널 네트워크 오프로드 메커니즘: 체크섬(Checksum) 오프로드 플래그, GSO(Generic Segmentation Offload)/TSO/UFO, GRO(Generic Receive Offload) 병합 알고리즘, VXLAN/GRE 터널(Tunnel) GSO, HW-GRO, 성능 튜닝 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅(Debugging) 절차까지 실무 관점으로 다룹니다.
핵심 요약
- 체크섬 오프로드 — CHECKSUM_NONE/UNNECESSARY/PARTIAL/COMPLETE 플래그로 NIC에 체크섬 계산을 위임.
- GSO — 커널이 큰 세그먼트를 유지하다 NIC 직전에 분할. TSO 지원 NIC면 NIC가 분할.
- GRO — napi_gro_receive()에서 연속 패킷(Packet)을 병합해 프로토콜 스택 호출 횟수 감소.
- 터널 처리 — VXLAN/GRE 등 터널에서 내부 헤더까지 고려한 GSO 분할 필요.
- HW-GRO — NIC가 하드웨어에서 GRO 수행, 드라이버가 gro_list로 전달.
단계별 이해
- 체크섬 오프로드 이해
sk_buff의 ip_summed 필드와 4가지 체크섬 플래그 의미를 먼저 파악합니다. - GSO 전송 흐름
dev_queue_xmit() → validate_xmit_skb() → gso_segment() 경로를 추적합니다. - GRO 수신 흐름
napi_gro_receive() → napi_skb_finish() → skb merge 경로를 확인합니다. - 성능 측정
ethtool -k로 NIC 오프로드 기능 확인, ethtool -K로 켜고 끄며 효과 측정합니다.
체크섬 오프로드 (Checksum Offload)
ip_summed 필드는 sk_buff의 체크섬 처리 상태를 나타냅니다. NIC 하드웨어 체크섬 오프로드를 제어하는 핵심 필드입니다:
| 모드 | 수신 (RX) | 전송 (TX) |
|---|---|---|
CHECKSUM_NONE |
HW 체크섬 미지원. 소프트웨어가 검증해야 함 | 소프트웨어가 체크섬을 이미 계산 완료 |
CHECKSUM_UNNECESSARY |
HW가 체크섬 검증 완료, 유효함 | 체크섬 불필요 (loopback 등) |
CHECKSUM_COMPLETE |
HW가 전체 패킷 체크섬을 skb->csum에 제공 |
사용하지 않음 |
CHECKSUM_PARTIAL |
사용하지 않음 | HW에게 체크섬 계산 위임. csum_start/csum_offset 설정 필요 |
CHECKSUM_COMPLETE 상세 동작
CHECKSUM_COMPLETE는 NIC가 L2 페이로드(Payload) 전체의 raw 1's complement 합산값을 skb->csum에 제공하는 모드입니다. 프로토콜 스택은 이 값에 pseudo-header만 추가하면 됩니다:
/* CHECKSUM_COMPLETE 처리 흐름 상세 */
/* 1. NIC 드라이버: raw 체크섬을 skb->csum에 저장 */
static void driver_set_csum_complete(struct sk_buff *skb,
__wsum hw_csum)
{
skb->ip_summed = CHECKSUM_COMPLETE;
skb->csum = hw_csum; /* NIC가 계산한 L2 페이로드 전체 합산값
* = sum(IP header + TCP/UDP header + payload)
* pseudo-header는 포함하지 않음 */
}
/* 2. IP 계층: __skb_checksum_validate_complete()
* NIC의 raw csum에 pseudo-header 체크섬을 추가하여 검증 */
static inline __sum16 __skb_checksum_validate_complete(
struct sk_buff *skb, bool complete, __wsum psum)
{
if (skb->ip_summed == CHECKSUM_COMPLETE) {
/* skb->csum (NIC raw) + psum (pseudo-header) == 0이면 유효 */
if (!csum_fold(csum_add(psum, skb->csum))) {
/* 검증 성공 → UNNECESSARY로 승격 (이후 재검증 생략) */
skb->ip_summed = CHECKSUM_UNNECESSARY;
skb->csum_valid = 1;
return 0;
}
}
/* 실패 → 소프트웨어 전체 재검증 */
return __skb_checksum_complete(skb);
}
/* 3. TCP 수신 경로에서의 사용 예 */
/* tcp_v4_rcv() → tcp_checksum_complete() */
static inline bool tcp_checksum_complete(struct sk_buff *skb)
{
/* CHECKSUM_UNNECESSARY → 즉시 통과 (0 반환)
* CHECKSUM_COMPLETE → pseudo-header 추가 후 검증
* CHECKSUM_NONE → 전체 SW 재계산 */
return !skb_csum_unnecessary(skb) &&
__skb_checksum_complete(skb);
}
/* CHECKSUM_COMPLETE의 장점:
* - NIC가 프로토콜을 이해할 필요 없음 (raw 합산만)
* - L4 프로토콜에 무관하게 동작 (TCP, UDP, SCTP, DCCP 등)
* - CHECKSUM_UNNECESSARY보다 범용적
* - 단점: pseudo-header 추가 연산이 필요 (미미한 오버헤드) */
CHECKSUM_PARTIAL TX 내부 동작
/* CHECKSUM_PARTIAL: 프로토콜 스택이 pseudo-header를 미리 삽입하고
* NIC가 나머지 실제 체크섬을 완성하는 협력 모델 */
/* TCP 전송 시 체크섬 설정 흐름 */
static void tcp_v4_send_check(struct sock *sk, struct sk_buff *skb)
{
struct tcphdr *th = tcp_hdr(skb);
struct inet_sock *inet = inet_sk(sk);
if (skb->ip_summed == CHECKSUM_PARTIAL) {
/* pseudo-header 체크섬만 TCP 체크섬 필드에 삽입
* NIC가 이 위에 실제 데이터 합산값을 더해 최종 체크섬 완성 */
th->check = ~tcp_v4_check(skb_tail_pointer(skb) - (u8 *)th,
inet->inet_saddr, inet->inet_daddr, 0);
/* csum_start/csum_offset 설정 — NIC에게 알려주는 좌표 */
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct tcphdr, check);
/*
* csum_start: 체크섬 계산 시작점 (TCP 헤더 시작)
* csum_offset: 체크섬을 기록할 위치 (TCP 헤더 내 check 필드)
*
* NIC의 동작:
* 1. skb->data + csum_start 부터 끝까지 1's complement 합산
* 2. 결과를 csum_start + csum_offset 위치에 기록
* 3. 이미 삽입된 pseudo-header csum과 자동 합산됨
*/
}
}
/* NIC feature별 체크섬 오프로드 능력 비교 */
/*
* NETIF_F_IP_CSUM:
* - IPv4 TCP/UDP만 지원
* - NIC가 IPv4 pseudo-header 구조를 알고 있음
* - 구형 NIC에서 주로 사용
*
* NETIF_F_IPV6_CSUM:
* - IPv6 TCP/UDP 지원
* - NETIF_F_IP_CSUM과 보통 함께 지원
*
* NETIF_F_HW_CSUM:
* - 프로토콜 무관 범용 체크섬
* - csum_start/csum_offset으로 임의 위치 체크섬 계산
* - 터널, SCTP, 새로운 프로토콜 등 모두 지원
* - 최신 NIC(Intel E810, Mellanox CX-5+)에서 지원
* - NETIF_F_IP_CSUM/IPV6_CSUM을 완전히 대체
*
* 확인:
* # ethtool -k eth0 | grep csum
* tx-checksum-ipv4: on
* tx-checksum-ipv6: on
* tx-checksum-ip-generic: on [NETIF_F_HW_CSUM]
*/
Scatter-Gather와 체크섬 계산
대부분의 고성능 NIC는 Scatter-Gather DMA를 사용합니다. 비선형(nonlinear) skb의 체크섬 계산은 frags[] 배열을 순회해야 합니다:
/* 비선형 skb의 체크섬 계산 — skb_checksum() 내부 */
__wsum skb_checksum(const struct sk_buff *skb,
int offset, int len, __wsum csum)
{
/* 1단계: 선형(linear) 영역의 체크섬 */
int copy = min(len, skb_headlen(skb) - offset);
if (copy > 0) {
csum = csum_partial(skb->data + offset, copy, csum);
len -= copy;
offset += copy;
}
/* 2단계: frags[] 배열의 각 페이지 체크섬 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
struct skb_frag_struct *frag = &skb_shinfo(skb)->frags[i];
int frag_size = skb_frag_size(frag);
if (copy < frag_size) {
u8 *vaddr = kmap_local_page(skb_frag_page(frag));
csum = csum_partial(vaddr + skb_frag_off(frag) + offset,
min(len, frag_size - offset), csum);
kunmap_local(vaddr);
}
}
/* 3단계: frag_list 체인 (있으면) */
skb_walk_frags(skb, frag_iter) {
csum = skb_checksum(frag_iter, offset, len, csum); /* 재귀 */
}
return csum;
}
/* 성능 영향:
* - CHECKSUM_NONE + 비선형 skb = 최악의 조합
* → 모든 frag 페이지를 kmap하면서 체크섬 계산
* → 10Gbps 환경에서 CPU 50%+ 소비 가능
* - CHECKSUM_UNNECESSARY → 이 과정 전체 생략
* - GSO skb의 경우: 체크섬은 분할 시 한꺼번에 계산
* → skb_gso_segment() 내부에서 각 세그먼트별 계산
*/
터널/캡슐화(Encapsulation) 체크섬 처리
VXLAN, GRE 등 터널 환경에서는 외부(outer)와 내부(inner) 두 겹의 체크섬을 처리해야 합니다:
/* 터널 체크섬 이중 처리 구조 */
/*
* [Outer Eth][Outer IP][Outer UDP csum][VXLAN][Inner Eth][Inner IP][Inner TCP csum][Payload]
* ↑ 외부 체크섬 ↑ 내부 체크섬
*
* NETIF_F_HW_CSUM 지원 NIC:
* → 내부 체크섬: CHECKSUM_PARTIAL로 NIC에 위임
* → 외부 체크섬: 비활성화(0) 또는 NIC가 별도 처리
*
* 터널 체크섬 관련 NIC feature:
* NETIF_F_GSO_UDP_TUNNEL_CSUM: 외부 UDP csum + 내부 GSO
* → NIC가 내부 세그먼트 분할 후 각각의 외부 UDP csum도 계산
*/
/* VXLAN TX에서의 체크섬 설정 */
static void vxlan_xmit_skb(struct sk_buff *skb, ...)
{
/* 내부 패킷 체크섬은 이미 CHECKSUM_PARTIAL로 설정됨 */
/* 외부 UDP 체크섬 설정 */
if (!udp_sum) {
/* 외부 UDP csum 비활성화 (0으로 설정) — 성능 최적화
* RFC 7348: VXLAN에서 외부 UDP csum은 선택사항 */
uh->check = 0;
} else {
/* 외부 UDP csum 활성화 — 무결성 강화 */
udp_set_csum(skb->ip_summed != CHECKSUM_PARTIAL,
skb, saddr, daddr, skb->len);
}
/* 터널 encap 후 체크섬 정보 갱신 */
skb_set_inner_protocol(skb, htons(ETH_P_TEB));
skb_set_inner_network_header(skb, inner_nhoff);
skb_set_inner_transport_header(skb, inner_thoff);
/* 이 inner offset 정보가 NIC의 터널 체크섬 오프로드에 사용됨 */
}
/* 수신 측: 터널 디캡슐화 시 체크섬 검증 */
/*
* 1. 외부 UDP csum 검증 (0이면 생략)
* 2. VXLAN 헤더 파싱
* 3. 내부 패킷의 ip_summed 복원:
* - 외부가 CHECKSUM_UNNECESSARY → 내부도 UNNECESSARY 가능
* - skb_checksum_simple_validate()로 내부 csum 검증 최적화
* 4. remcsum 오프로드: VXLAN GPE 등에서 원격 체크섬 보조
* → TUNNEL_REMCSUM 플래그로 NIC가 내부 csum을 계산
*/
/* 수신 경로: 드라이버에서 체크섬 상태 설정 */
static void my_rx_handler(struct sk_buff *skb, bool hw_csum_ok)
{
if (hw_csum_ok) {
/* NIC가 체크섬 검증 완료 → 소프트웨어 재검증 생략 */
skb->ip_summed = CHECKSUM_UNNECESSARY;
} else {
/* HW 미지원 → 프로토콜 스택이 직접 검증 */
skb->ip_summed = CHECKSUM_NONE;
}
}
/* 전송 경로: 소프트웨어 체크섬 fallback */
if (skb->ip_summed == CHECKSUM_PARTIAL) {
/* NIC가 NETIF_F_HW_CSUM을 지원하지 않으면 */
if (skb_checksum_help(skb)) /* SW로 체크섬 계산 */
goto drop;
}
/* 체크섬 관련 유틸리티 */
skb_checksum(skb, offset, len, 0); /* skb 데이터의 체크섬 계산 */
csum_tcpudp_magic(saddr, daddr, len, proto, csum); /* pseudo-header 포함 */
skb_postpull_rcsum(skb, hdr, hdr_len); /* pull 후 csum 보정 */
GSO/GRO와 sk_buff
GSO (Generic Segmentation Offload)와 GRO (Generic Receive Offload)는 대량 데이터 전송/수신 시 성능을 극대화하는 핵심 메커니즘입니다. 기본 원리는 단순합니다: 네트워크 스택(Network Stack)을 통과하는 패킷 수를 줄여 per-packet 오버헤드(Overhead)(헤더 파싱, 룩업, lock 경합(Contention), cache miss)를 최소화합니다.
핵심 개념: GSO는 전송(TX) 방향에서 대형 skb를 마지막 순간에 분할하고, GRO는 수신(RX) 방향에서 작은 패킷들을 하나의 대형 skb로 병합합니다. 둘 다 네트워크 스택 통과를 한 번으로 줄여 성능을 극대화합니다. MTU=1500 기준 64KB 데이터 처리 시, ~43개 패킷을 개별 처리하는 대신 1개의 대형 skb로 스택을 한 번만 통과합니다.
napi_gro_receive() 호출 경로, GRO flush 타이머(Timer), busy polling과의 연동 등 NAPI 관점의 GRO 통합 아키텍처는 NAPI — GRO 통합 문서를 참고하세요.GSO (전송 오프로드)
GSO(Generic Segmentation Offload)는 TSO(TCP Segmentation Offload)의 소프트웨어 일반화입니다. 핵심 아이디어: 가능한 한 늦게(late) 세그먼트를 분할하여 네트워크 스택의 중간 계층(Netfilter, TC, qdisc)에서 처리하는 패킷 수를 최소화합니다.
TSO에서 GSO로의 발전
- === 일반 전송 (세그먼트 오프로드 없음) ===
- tcp_sendmsg()가 MSS 단위로 skb를 생성:
- write(fd, buf, 64000)
- → 44개 skb 생성 (각 1460 바이트)
- → 44번 TCP 헤더 생성 + 체크섬 계산
- → 44번 IP 계층 통과 + Netfilter/TC 처리
- → 44번 qdisc enqueue/dequeue
- → 44번 NIC DMA → 매우 높은 CPU 부하
- === TSO (하드웨어 오프로드) ===
- 커널이 최대 64KB의 대형 skb를 생성하여 NIC에 직접 전달:
- → 1개 skb (64KB) → NIC 하드웨어가 MSS 단위 분할
- → NIC가 각 세그먼트에 TCP/IP 헤더 복사 + 시퀀스 번호 증가 + 체크섬 계산
- → 장점: CPU 부하 최소, 10Gbps+ 환경에서 매우 중요
- → 단점: NIC가 TSO를 지원해야 함, 터널/암호화(Encryption) 등에선 미지원 가능
- 확인: ethtool -k eth0 | grep tcp-segmentation
- === GSO (소프트웨어 일반화, 커널 2.6.18+) ===
- TSO와 동일한 "대형 skb" 전략을 소프트웨어로 구현:
- → 커널이 대형 skb를 유지하며 네트워크 스택을 통과
- → validate_xmit_skb()에서 NIC feature 확인:
- NIC가 TSO 지원 → 그대로 NIC에 전달 (HW offload)
- NIC가 TSO 미지원 → skb_gso_segment()로 소프트웨어 분할
- → 중간 계층(Netfilter, TC, qdisc)은 1개 skb만 처리 → 효율 극대화
- GSO의 핵심 이점:
- TSO 미지원 NIC에서도 성능 향상 (SW 분할이라도 늦은 분할이 유리)
- 터널, 가상화(Virtualization) 등 복잡한 경로에서도 동작
- UDP, SCTP 등 TCP 외 프로토콜도 지원
GSO 타입 전체 목록
skb_shared_info→gso_type 필드에 설정되는 비트마스크입니다. 여러 타입이 OR로 조합될 수 있습니다:
| GSO 타입 플래그 | 값 | 대상 프로토콜 | 설명 |
|---|---|---|---|
SKB_GSO_TCPV4 | 1 << 0 | IPv4 TCP | 가장 일반적인 TSO/GSO. MSS 단위로 TCP 세그먼트 분할 |
SKB_GSO_TCPV6 | 1 << 5 | IPv6 TCP | IPv6 환경의 TSO/GSO |
SKB_GSO_UDP | 1 << 1 | UDP (IP frag) | IP 단편화(fragmentation) 기반 UDP GSO. UFO(UDP Fragmentation Offload) |
SKB_GSO_UDP_L4 | 1 << 11 | UDP (L4 분할) | UDP 세그먼트 단위 분할 (커널 4.18+). QUIC, WireGuard 등에 사용 |
SKB_GSO_DODGY | 1 << 2 | 모두 | 신뢰할 수 없는 GSO skb (VM에서 전달 등). 재검증 필요 |
SKB_GSO_TCP_ECN | 1 << 3 | TCP + ECN | ECN(Explicit Congestion Notification) 플래그 있는 TCP GSO |
SKB_GSO_TCP_FIXEDID | 1 << 9 | TCP | 모든 세그먼트가 동일 IP ID 사용 (드문 경우) |
SKB_GSO_GRE | 1 << 6 | GRE 터널 | GRE 캡슐화 안의 내부 패킷 GSO |
SKB_GSO_GRE_CSUM | 1 << 7 | GRE + 체크섬 | GRE 체크섬이 활성화된 터널 GSO |
SKB_GSO_UDP_TUNNEL | 1 << 8 | VXLAN/Geneve | UDP 기반 터널 내부 패킷 GSO |
SKB_GSO_UDP_TUNNEL_CSUM | 1 << 10 | VXLAN + csum | 외부 UDP 체크섬이 활성화된 터널 GSO |
SKB_GSO_PARTIAL | 1 << 13 | 터널/복합 | 부분 GSO — 외부 헤더만 NIC가 처리, 내부는 소프트웨어 분할 |
SKB_GSO_TUNNEL_REMCSUM | 1 << 12 | 터널 | 터널 원격 체크섬 오프로드 |
SKB_GSO_SCTP | 1 << 14 | SCTP | SCTP 청크 단위 GSO |
SKB_GSO_ESP | 1 << 15 | IPsec ESP | ESP(Encapsulating Security Payload) GSO |
SKB_GSO_FRAGLIST | 1 << 17 | UDP/IP | frag_list 기반 GSO (GRO에서 병합된 skb 재전송(Retransmission) 시) |
GSO 관련 skb_shared_info 필드
/* include/linux/skbuff.h — skb_shared_info의 GSO 관련 필드 */
struct skb_shared_info {
...
unsigned short gso_size; /* 분할 단위 크기 (MSS 또는 세그먼트 크기)
* TCP: MSS (예: 1460)
* UDP GSO: 각 데이터그램 크기 (예: 1472)
* 0이면 GSO 미사용 */
unsigned short gso_segs; /* 예상 분할 세그먼트 수 (힌트 값)
* DIV_ROUND_UP(skb->len - hdr_len, gso_size)
* NIC의 BQL(Byte Queue Limit) 계산에 활용 */
unsigned int gso_type; /* SKB_GSO_* 비트마스크 (위 표 참조)
* 여러 플래그 OR 조합 가능
* 예: SKB_GSO_TCPV4 | SKB_GSO_TCP_ECN */
...
};
/* GSO skb인지 확인하는 헬퍼 함수들 */
static inline bool skb_is_gso(const struct sk_buff *skb)
{
return skb_shinfo(skb)->gso_size; /* gso_size != 0이면 GSO skb */
}
static inline bool skb_is_gso_v6(const struct sk_buff *skb)
{
return skb_shinfo(skb)->gso_type & SKB_GSO_TCPV6;
}
/* GSO 세그먼트의 실제 와이어(wire) 크기 계산 */
static inline unsigned int skb_gso_network_seglen(const struct sk_buff *skb)
{
unsigned int hdr_len = skb_transport_header(skb) - skb_network_header(skb);
return hdr_len + skb_shinfo(skb)->gso_size;
}
코드 설명
- skb_shared_info
include/linux/skbuff.h에 정의된 구조체로, 모든 skb 뒤에 위치하는 메타데이터 영역입니다.gso_size는 분할 단위(TCP의 경우 MSS),gso_segs는 예상 세그먼트 수,gso_type은SKB_GSO_TCPV4등 프로토콜별 비트마스크를 저장합니다. - skb_is_gso()
gso_size가 0이 아니면 GSO skb로 판별합니다. 전송 경로의validate_xmit_skb()에서 이 함수로 분할 여부를 결정합니다. - skb_gso_network_seglen()GSO 분할 후 각 세그먼트의 네트워크 계층 크기를 계산합니다.
ip_finish_output()에서 MTU 초과 여부를 검사할 때 사용하며, 전송 헤더 오프셋과gso_size를 합산합니다.
GSO 전송 경로 상세
TCP 전송을 예로 들어 GSO skb가 생성되고 분할되는 전체 경로를 추적합니다:
/* 1단계: tcp_sendmsg() — 사용자 데이터를 skb에 적재 */
/*
* tcp_sendmsg_locked()에서 sk_stream_alloc_skb()로 skb 할당
* → tcp_send_mss()로 현재 MSS 결정 (경로 MTU - 헤더)
* → size_goal: GSO 활성 시 MSS * max_segs (최대 64KB)
* → 하나의 skb에 size_goal만큼 데이터를 적재
*/
int mss_now = tcp_send_mss(sk, &size_goal, flags);
/* size_goal 예시:
* MSS=1460, sk->sk_gso_max_segs=44 → size_goal = 1460 * 44 = 64240
* → 하나의 skb에 최대 64KB 데이터 적재
*/
/* 2단계: tcp_write_xmit() — skb에 TCP 헤더 부착 및 GSO 설정 */
/*
* tcp_init_tso_segs()에서 GSO 관련 필드 설정:
*/
static void tcp_set_skb_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
struct skb_shared_info *shinfo = skb_shinfo(skb);
if (skb->len <= mss_now) {
/* MSS 이하 → 분할 불필요, GSO 미사용 */
shinfo->gso_segs = 1;
shinfo->gso_size = 0;
shinfo->gso_type = 0;
} else {
/* MSS 초과 → GSO skb로 설정 */
shinfo->gso_segs = DIV_ROUND_UP(skb->len, mss_now);
shinfo->gso_size = mss_now;
shinfo->gso_type = sk->sk_gso_type; /* SKB_GSO_TCPV4 등 */
}
}
/* 3단계: ip_queue_xmit() → __dev_queue_xmit() → qdisc */
/*
* IP 계층에서 라우팅 조회, TTL 설정 등을 한 번만 수행
* qdisc에서도 1개의 대형 skb만 큐잉
* → 여기까지 N개 패킷이 아닌 1개의 대형 skb로 처리
*/
/* 4단계: validate_xmit_skb() — GSO 분할 결정의 핵심 */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
struct net_device *dev, bool *again)
{
netdev_features_t features;
/* NIC가 지원하는 feature 확인 */
features = netif_skb_features(skb);
if (skb_is_gso(skb)) {
/* GSO skb이고 NIC가 해당 오프로드를 지원하면 → HW 처리 (분할 안 함) */
/* NIC가 미지원이면 → __skb_gso_segment()로 소프트웨어 분할 */
struct sk_buff *segs = __skb_gso_segment(skb, features, true);
if (IS_ERR_OR_NULL(segs)) {
if (!segs)
return skb; /* NIC가 HW 처리 가능 → 원본 skb 그대로 */
kfree_skb(skb);
return NULL;
}
/* SW 분할 완료: segs는 분할된 skb 리스트 */
consume_skb(skb);
return segs;
}
...
}
코드 설명
- tcp_send_mss()
net/ipv4/tcp_output.c에서 현재 MSS와size_goal을 결정합니다. GSO 활성 시size_goal = MSS × max_segs로 하나의 skb에 최대 64KB 데이터를 적재하여 시스템 콜 오버헤드를 줄입니다. - tcp_set_skb_tso_segs()skb 크기가 MSS를 초과하면
gso_size,gso_segs,gso_type을 설정하여 GSO skb로 표시합니다. MSS 이하이면 GSO 필드를 0으로 설정하여 일반 skb로 처리합니다. - validate_xmit_skb()
net/core/dev.c의 핵심 결정 지점입니다.netif_skb_features()로 NIC의 오프로드 능력을 확인하고, NIC가 해당 GSO 타입을 지원하면 대형 skb를 그대로 하드웨어에 전달합니다. 미지원 시__skb_gso_segment()로 소프트웨어 분할을 수행합니다. - HW vs SW 분기
__skb_gso_segment()가NULL을 반환하면 NIC가 하드웨어 처리 가능하다는 의미이며, 유효한 skb 리스트를 반환하면 소프트웨어 분할이 완료된 것입니다. 이 투명한 폴백 구조 덕분에 상위 계층은 NIC 능력을 알 필요가 없습니다.
skb_gso_segment() 내부 동작
/* net/core/skbuff.c — 소프트웨어 GSO 분할의 핵심 함수 */
struct sk_buff *skb_gso_segment(struct sk_buff *skb, netdev_features_t features)
{
/* 프로토콜별 GSO 콜백 호출 */
/* TCP: tcp4_gso_segment() 또는 tcp6_gso_segment()
* UDP: udp4_ufo_fragment() 또는 __udp_gso_segment()
* 터널: skb_udp_tunnel_segment() 등 */
return skb_mac_gso_segment(skb, features);
}
/* net/ipv4/tcp_offload.c — TCP GSO 분할 */
struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
/* tcp_gso_segment()가 실제 분할 수행:
*
* 1. 원본 skb의 데이터를 gso_size(MSS) 단위로 분할
* 2. 각 세그먼트에 새 TCP 헤더 복사:
* - seq 번호 증가 (prev_seq += gso_size)
* - PSH 플래그는 마지막 세그먼트에만 설정
* - FIN 플래그는 마지막 세그먼트에만 설정
* - CWR 플래그는 첫 세그먼트에만 설정
* 3. 각 세그먼트의 IP 헤더 업데이트:
* - total_length 재계산
* - IP ID 증가 (SKB_GSO_TCP_FIXEDID 아닌 경우)
* 4. 체크섬 재계산 (CHECKSUM_PARTIAL이면 pseudo-header만)
* 5. 분할된 skb들을 linked list로 반환 (skb->next)
*/
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
return ERR_PTR(-EINVAL);
return tcp_gso_segment(skb, features);
}
/* 분할 결과 사용 예시 */
struct sk_buff *segs = skb_gso_segment(skb, features);
struct sk_buff *seg, *tmp;
skb_list_walk_safe(segs, seg, tmp) {
skb_mark_not_on_list(seg);
/* 각 세그먼트는 독립적인 skb:
* - 자체 TCP/IP 헤더 보유
* - 정확한 시퀀스 번호
* - 올바른 체크섬
* - skb->len == gso_size + hdr_len (마지막은 작을 수 있음)
*/
dev_queue_xmit(seg);
}
코드 설명
- skb_gso_segment()
net/core/skbuff.c에 정의된 소프트웨어 GSO 분할의 진입점입니다.skb_mac_gso_segment()를 통해 프로토콜별 GSO 콜백을 호출하며, TCP는tcp4_gso_segment(), UDP는__udp_gso_segment()가 실제 분할을 수행합니다. - tcp4_gso_segment()
net/ipv4/tcp_offload.c에서 TCP GSO 분할을 담당합니다. 원본 skb를gso_size(MSS) 단위로 나누고, 각 세그먼트에 올바른 시퀀스 번호, PSH/FIN/CWR 플래그, IP ID를 설정합니다. - skb_list_walk_safe()분할 결과는
skb->next로 연결된 링크드 리스트입니다. 각 세그먼트는 독립적인 TCP/IP 헤더와 올바른 체크섬을 가지므로dev_queue_xmit()로 개별 전송할 수 있습니다.
GSO_PARTIAL — 터널 환경의 부분 오프로드
SKB_GSO_PARTIAL은 NIC가 외부(outer) 헤더는 처리할 수 있지만 내부(inner) 패킷의 GSO는 지원하지 않는 경우를 위한 메커니즘입니다:
/* VXLAN 터널에서의 GSO_PARTIAL 동작:
*
* [Outer Eth][Outer IP][Outer UDP][VXLAN Hdr][Inner Eth][Inner IP][Inner TCP][Payload]
*
* NIC가 NETIF_F_GSO_UDP_TUNNEL은 지원하지만
* NETIF_F_GSO_UDP_TUNNEL_CSUM은 미지원하는 경우:
*
* 1. 커널이 내부(inner) TCP 세그먼트를 소프트웨어로 분할
* 2. 각 분할된 세그먼트에 외부(outer) 헤더를 붙임
* 3. 외부 헤더의 처리(체크섬 등)는 NIC에 위임
*
* 관련 NIC feature 확인:
*/
/* # ethtool -k eth0 | grep tunnel
* tx-udp_tnl-segmentation: on (외부 UDP 터널 GSO)
* tx-udp_tnl-csum-segmentation: off (외부 csum 미지원 → PARTIAL 필요)
*/
/* net_device features 비트 매핑 */
NETIF_F_TSO /* TCP Segmentation Offload (IPv4) */
NETIF_F_TSO6 /* TCP Segmentation Offload (IPv6) */
NETIF_F_GSO_GRE /* GRE 터널 내부 GSO */
NETIF_F_GSO_GRE_CSUM /* GRE 체크섬 + 내부 GSO */
NETIF_F_GSO_UDP_TUNNEL /* UDP 터널 (VXLAN/Geneve) 내부 GSO */
NETIF_F_GSO_UDP_TUNNEL_CSUM /* UDP 터널 + 외부 UDP 체크섬 GSO */
NETIF_F_GSO_PARTIAL /* 부분 GSO: 외부는 HW, 내부는 SW */
NETIF_F_GSO_UDP_L4 /* UDP L4 세그먼트 오프로드 (4.18+) */
NETIF_F_GSO_ESP /* IPsec ESP GSO */
NETIF_F_GSO_SCTP /* SCTP GSO */
GSO 최대 크기 제어
/* net_device의 GSO 크기 제한 */
struct net_device {
unsigned int gso_max_size; /* GSO skb 최대 크기 (기본 65536)
* ip link set dev eth0 gso_max_size 32768
* 으로 조절 가능 */
u16 gso_max_segs; /* 최대 세그먼트 수 (기본 65535)
* NIC 하드웨어 제한에 따라 설정 */
unsigned int gso_ipv4_max_size; /* IPv4 전용 GSO 최대 크기 (6.3+) */
};
/* 소켓 레벨에서도 GSO 크기 제어 가능 */
struct sock {
int sk_gso_max_size; /* 소켓별 GSO 최대 크기 */
u16 sk_gso_max_segs; /* 소켓별 최대 세그먼트 수 */
int sk_gso_type; /* 지원하는 GSO 타입 비트마스크 */
};
/* BIG TCP (커널 6.3+): IPv6에서 64KB 넘는 GSO 허용 */
/* ip link set dev eth0 gso_max_size 185000
* → IPv6 jumbogram 활용, 단일 skb에 ~185KB 적재 가능
* → GRO에서도 gro_max_size로 대응
*
* 주의: IPv4는 IP 헤더의 total_length가 16비트이므로 64KB가 한계
* IPv6 jumbogram 확장 헤더로 이 제한을 우회
*/
GRO (수신 오프로드(Receive Offload))
GRO(Generic Receive Offload)는 수신된 작은 패킷들을 하나의 큰 skb로 합치는 메커니즘입니다. LRO(Large Receive Offload)의 소프트웨어 일반화로, LRO와 달리 원본 헤더 정보를 보존하여 라우팅(Routing)/포워딩 환경에서도 안전하게 동작합니다.
LRO vs GRO 비교
| 특성 | LRO (Large Receive Offload) | GRO (Generic Receive Offload) |
|---|---|---|
| 구현 위치 | NIC 드라이버 또는 하드웨어 | 커널 네트워크 스택 (NAPI 레벨) |
| 헤더 보존 | 병합 시 원본 헤더 정보 손실 가능 | 원본 헤더 정보 완전 보존 |
| 라우팅/포워딩 | 패킷 재분할 시 헤더 복구 불가 → 부적합 | 안전하게 재분할 가능 → 적합 |
| 프로토콜 지원 | TCP만 | TCP, UDP, GRE, VXLAN 등 확장 가능 |
| 병합 기준 | 느슨함 (IP/TCP 4-tuple만) | 엄격함 (헤더 완전 일치 검증) |
| 현재 상태 | deprecated (대부분 GRO로 대체) | 표준 수신 오프로드 |
GRO 수신 파이프라인(Pipeline)
/* === GRO 수신 경로 전체 흐름 ===
*
* NIC IRQ
* └→ napi_schedule()
* └→ NAPI poll 함수 (드라이버)
* └→ napi_gro_receive(napi, skb) ← 진입점
* └→ dev_gro_receive(napi, skb)
* ├→ gro_list에서 동일 flow 검색
* └→ 프로토콜별 GRO 콜백 체인:
* └→ inet_gro_receive() (L3: IP)
* └→ tcp4_gro_receive() (L4: TCP)
* └→ GRO 결과 반환
* ├→ GRO_MERGED: 기존 skb에 병합 완료
* ├→ GRO_MERGED_FREE: 병합 + 현재 skb 해제
* ├→ GRO_HELD: gro_list에 보관 (다음 패킷 대기)
* ├→ GRO_NORMAL: 병합 불가 → 일반 경로
* └→ GRO_CONSUMED: 콜백이 직접 처리 완료
*/
/* net/core/gro.c — GRO 핵심 진입점 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
struct sk_buff *skb)
{
/* skb 전처리: VLAN 태그 처리, 해시 설정 등 */
skb_gro_reset_offset(skb);
return napi_skb_finish(napi, skb, dev_gro_receive(napi, skb));
}
static enum gro_result dev_gro_receive(struct napi_struct *napi,
struct sk_buff *skb)
{
struct list_head *gro_head = &napi->gro_hash[bucket].list;
int count = napi->gro_hash[bucket].count;
/* gro_list에서 병합 가능한 기존 skb 검색 */
list_for_each_entry(pp, gro_head, list) {
/* same_flow: 동일 flow인지 빠른 비교 (rxhash 기반) */
NAPI_GRO_CB(pp)->same_flow = 1;
}
/* 프로토콜별 GRO 콜백 호출 (inet_gro_receive 등) */
pp = call_gro_receive(ptype->callbacks.gro_receive, gro_head, skb);
if (pp == skb) return GRO_NORMAL; /* 병합 불가 */
if (pp) return GRO_MERGED; /* 병합 성공 → flush 대상 */
if (NAPI_GRO_CB(skb)->flush)
return GRO_NORMAL; /* 병합 불가 플래그 */
/* 새 flow → gro_list에 추가 (다음 패킷 대기) */
if (count < MAX_GRO_SKBS) /* MAX_GRO_SKBS = 8 */
return GRO_HELD;
else
return GRO_NORMAL; /* 버킷 가득 참 → 일반 경로 */
}
코드 설명
- napi_gro_receive()
net/core/gro.c에 정의된 GRO의 주요 진입점입니다. NIC 드라이버의 NAPI poll 함수에서 호출하며,skb_gro_reset_offset()로 GRO 오프셋을 초기화한 뒤dev_gro_receive()에 처리를 위임합니다. - dev_gro_receive()GRO 매칭 엔진의 핵심입니다.
gro_hash[]해시 테이블에서 동일 flow를 검색하고,call_gro_receive()로 프로토콜별 콜백 체인(L2→L3→L4)을 호출하여 병합 가능 여부를 판단합니다. - GRO 결과 분기콜백이 skb 자체를 반환하면
GRO_NORMAL(병합 불가), 다른 skb를 반환하면GRO_MERGED(flush 대상),flush플래그가 설정되면 일반 경로로 전달합니다. 새로운 flow이고 버킷에 여유가 있으면(MAX_GRO_SKBS=8)GRO_HELD로 다음 패킷을 대기합니다.
GRO 병합 기준
패킷이 기존 flow에 병합되려면 다음 조건을 모두 만족해야 합니다:
/* === TCP GRO 병합 조건 (tcp4_gro_receive) ===
*
* 1. L2 레벨: 동일 수신 해시 (rxhash) — 빠른 사전 필터링
*
* 2. L3 레벨 (inet_gro_receive):
* - 동일 IP 프로토콜 (IPPROTO_TCP)
* - 동일 src/dst IP 주소
* - 동일 TOS (Type of Service)
* - 동일 TTL (합치면 안 되는 경우 방지)
* - IP 옵션 없음 (옵션이 있으면 병합 거부)
* - IP ID가 순차적으로 증가 (또는 DF 비트 설정 시 무관)
*
* 3. L4 레벨 (tcp_gro_receive):
* - 동일 src/dst 포트
* - TCP 시퀀스 번호가 연속 (이전 패킷의 끝 + 1)
* - TCP 윈도우 크기가 동일
* - ACK 플래그만 설정 (SYN, FIN, RST, URG, ECE, CWR → 병합 거부)
* - TCP 타임스탬프 옵션이 있으면 값이 동일하거나 증가
* - 이전에 병합된 패킷과 옵션 길이가 동일
*
* 4. 크기 제한:
* - 병합된 skb 크기가 gro_max_size를 초과하지 않아야 함
* - 기본 gro_max_size = 65536 (BIG TCP 시 더 큼)
*/
/* net/ipv4/tcp_offload.c — TCP GRO 병합 핵심 검증 */
struct sk_buff *tcp_gro_receive(struct list_head *head,
struct sk_buff *skb)
{
struct tcphdr *th = tcp_hdr(skb);
list_for_each_entry(p, head, list) {
if (!NAPI_GRO_CB(p)->same_flow)
continue;
th2 = tcp_hdr(p);
/* 포트 비교 */
if (*((u32 *)&th->source) ^ *((u32 *)&th2->source)) {
NAPI_GRO_CB(p)->same_flow = 0;
continue;
}
/* 시퀀스 번호 연속 확인 */
if (ntohl(th2->seq) + skb_gro_len(p) != ntohl(th->seq)) {
NAPI_GRO_CB(p)->flush = 1; /* 비연속 → flush */
continue;
}
/* ACK 외 플래그 확인: SYN/FIN/RST → flush */
if (th->fin || th->syn || th->rst || th->urg)
flush = 1;
/* 윈도우 크기 일치 확인 */
if (th->window ^ th2->window)
flush = 1;
/* 타임스탬프 옵션 검증 */
if (pcount > 1 || tcp_flag_word(th2) & TCP_FLAG_CWR)
flush = 1;
/* 병합 수행: skb 데이터를 기존 p에 append */
if (!flush)
skb_gro_receive(p, skb); /* → 아래 설명 */
}
...
}
코드 설명
- tcp_gro_receive()
net/ipv4/tcp_offload.c에서 TCP GRO 병합 조건을 검증합니다.same_flow가 1인 기존 skb에 대해 소스/대상 포트 비교(4바이트 XOR 1회), 시퀀스 번호 연속성, TCP 플래그, 윈도우 크기를 순차적으로 확인합니다. - 포트 비교
th->source와th->dest를u32로 캐스팅하여 한 번의 XOR 연산으로 소스·대상 포트를 동시에 비교합니다. 불일치하면same_flow = 0으로 표시하고 다음 후보로 넘어갑니다. - 시퀀스 번호 확인기존 skb의 끝 시퀀스(
ntohl(th2->seq) + skb_gro_len(p))와 새 패킷의 시작 시퀀스가 일치해야 합니다. 불일치하면flush = 1로 설정하여 기존 skb를 상위 스택으로 전달합니다. - 플래그 검증SYN, FIN, RST, URG 플래그가 설정된 패킷은 연결 상태 변경을 의미하므로 병합하지 않습니다. ACK만 있는 일반 데이터 패킷만 병합 대상입니다.
GRO 데이터 병합 방식
GRO는 두 가지 방식으로 수신 데이터를 병합합니다:
| 병합 방식 | 조건 | 데이터 구조 | 장단점 |
|---|---|---|---|
| frag 기반 | skb가 선형(linear) 데이터일 때 | skb_shared_info→frags[] 배열에 페이지(Page) 추가 |
메모리 효율적, scatter-gather DMA에 적합. MAX_SKB_FRAGS(17) 제한 |
| frag_list 기반 | frag 공간 부족 또는 skb 자체가 이미 비선형일 때 | skb_shared_info→frag_list에 skb 체인 연결 |
구조 단순, 제한 없음. 하지만 GSO 재분할 시 오버헤드 |
/* net/core/gro.c — skb_gro_receive() 핵심 로직 */
int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
{
unsigned int new_len = p->len + skb->len;
/* gro_max_size 초과 검사 */
if (new_len > gro_max_size(p))
return -E2BIG;
if (skb_headlen(skb) <= offset) {
/* === frag 기반 병합 ===
* 새 skb의 페이지를 기존 skb의 frags[]에 추가 */
struct skb_shared_info *pinfo = skb_shinfo(p);
if (pinfo->nr_frags + skb_shinfo(skb)->nr_frags > MAX_SKB_FRAGS)
goto merge_frag_list;
/* frags[] 배열에 새 페이지들을 복사 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
pinfo->frags[pinfo->nr_frags++] = skb_shinfo(skb)->frags[i];
}
} else {
merge_frag_list:
/* === frag_list 기반 병합 ===
* 새 skb를 기존 skb의 frag_list 체인에 연결 */
if (!skb_shinfo(p)->frag_list)
skb_shinfo(p)->frag_list = skb;
else
NAPI_GRO_CB(p)->last->next = skb;
NAPI_GRO_CB(p)->last = skb;
}
/* 병합된 skb의 총 길이 업데이트 */
p->len += skb->len;
p->data_len += skb->len;
p->truesize += skb->truesize;
NAPI_GRO_CB(p)->count++; /* 병합된 패킷 수 */
return 0;
}
코드 설명
- skb_gro_receive()
net/core/gro.c에 정의된 실제 데이터 병합 함수입니다. 기존 skbp에 새 skb의 데이터를 추가하며, 먼저gro_max_size초과 여부를 검사하여 과도한 병합을 방지합니다. - frag 기반 병합새 skb의 헤더 길이가 오프셋 이하이면,
skb_shared_info->frags[]배열에 페이지 참조를 복사합니다.MAX_SKB_FRAGS(17)개까지 가능하며, scatter-gather DMA에 최적화된 방식입니다. - frag_list 기반 병합frag 공간이 부족하거나 skb가 이미 비선형이면,
frag_list체인에 skb 전체를 연결합니다. 구조는 단순하지만 나중에 GSO로 재분할할 때 오버헤드가 발생할 수 있습니다. - 메타데이터 갱신병합 후
p->len,p->data_len,p->truesize를 누적하고NAPI_GRO_CB(p)->count를 증가시킵니다. 이 count 값은 나중에gso_segs로 변환되어 상위 스택에서 실제 패킷 수를 파악하는 데 활용됩니다.
GRO Flush 메커니즘
gro_list에 보관 중인 병합된 skb는 다음 조건에서 상위 스택으로 전달(flush)됩니다:
/* GRO flush 발생 조건과 동작 */
/* 1. napi_complete_done() 호출 시 — 가장 일반적
* NAPI poll에서 budget 미만 처리 → 인터럽트 재활성화 전 flush */
bool napi_complete_done(struct napi_struct *napi, int work_done)
{
/* gro_list의 모든 보류 skb를 flush */
gro_normal_list(napi); /* → netif_receive_skb_list_internal() */
...
}
/* 2. MAX_GRO_SKBS(8) 초과 시 — 버킷당 보관 제한
* 동일 해시 버킷에 8개 이상 skb가 쌓이면 가장 오래된 것 flush */
/* 3. 비연속 패킷 수신 시 (flush 플래그)
* 시퀀스 번호 불연속, 다른 플래그 등 → 현재 보관 중인 skb flush */
/* 4. gro_flush_timeout 타이머 만료 (busy polling 관련) */
/* sysctl: net.core.gro_flush_timeout
* 기본값: 0 (즉시 flush, 타이머 미사용)
* 설정 시: napi_complete에서 바로 flush하지 않고 타이머 대기
* → 더 많은 패킷을 병합할 기회를 제공하지만 지연 증가
*
* 관련 sysctl:
* net.core.napi_defer_hard_irqs = N
* → N번의 빈 poll 후에야 IRQ 재활성화
* → gro_flush_timeout과 함께 사용하면 GRO 효율 극대화
*/
/* 5. 명시적 flush 호출 */
napi_gro_flush(napi, false); /* 모든 보류 skb를 즉시 flush */
/* flush된 skb는 netif_receive_skb_list_internal()로 전달되어
* 일반 수신 경로(IP → TCP → 소켓)를 탐니다.
* 이때 skb->len은 원래 여러 패킷의 합이므로
* TCP 수신 측에서 효율적으로 처리됩니다. */
GRO API 변형
/* 드라이버에서 사용하는 GRO 진입점들 */
/* 1. napi_gro_receive() — 가장 일반적
* 완전한 skb를 GRO 처리 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
struct sk_buff *skb);
/* 2. napi_gro_frags() — 헤더/데이터 분리 수신 시
* NIC가 헤더를 선형 영역에, 페이로드를 페이지에 배치한 경우
* napi->skb에 미리 설정된 skb 사용 */
gro_result_t napi_gro_frags(struct napi_struct *napi);
/*
* 사용 패턴 (고성능 드라이버):
* napi->skb = netdev_alloc_skb(...);
* skb_put(napi->skb, hdr_len); // 헤더를 선형 영역에
* skb_fill_page_desc(napi->skb, ...); // 페이로드를 frag으로
* napi_gro_frags(napi);
* // napi->skb는 내부에서 소비/해제됨
*/
/* 3. napi_gro_complete() — 내부 함수
* GRO 완료 시 호출, 프로토콜별 gro_complete 콜백 실행 후 상위 전달 */
/* GRO 결과 타입 */
enum gro_result {
GRO_MERGED, /* 기존 skb에 병합 성공 — flush 대상으로 표시 */
GRO_MERGED_FREE, /* 병합 성공 + 현재 skb 해제 가능 */
GRO_HELD, /* gro_list에 보관 (새 flow, 다음 패킷 대기) */
GRO_NORMAL, /* 병합 불가 — 일반 수신 경로로 */
GRO_CONSUMED, /* 콜백이 직접 처리 완료 */
};
코드 설명
- napi_gro_receive()완전한 skb를 GRO 처리하는 가장 일반적인 진입점입니다. 대부분의 NIC 드라이버(e1000e, ixgbe, mlx5 등)가 NAPI poll 루프에서 이 함수를 호출합니다.
- napi_gro_frags()고성능 드라이버가 헤더와 페이로드를 분리 수신할 때 사용합니다.
napi->skb에 미리 설정된 skb를 사용하며, 헤더는 선형 영역에, 페이로드는skb_fill_page_desc()로 frag에 배치합니다. 불필요한 메모리 복사를 피하는 제로카피(zero-copy) 최적화입니다. - gro_result enum
include/linux/netdevice.h에 정의된 GRO 결과 타입입니다.GRO_MERGED_FREE는GRO_MERGED와 달리 현재 skb를 해제해도 되는 의미이며,napi_skb_finish()에서 이 값에 따라 skb 메모리를 관리합니다.
하드웨어 GRO (HW-GRO)
커널 5.19+에서 도입된 HW-GRO는 NIC 하드웨어가 GRO를 수행하되, LRO와 달리 헤더 정보를 보존합니다:
/* HW-GRO vs SW-GRO vs LRO 비교
*
* LRO (deprecated):
* - NIC가 패킷 병합, 헤더 정보 손실
* - 포워딩 시 문제 → ip_summed 등 불일치
* - ethtool -K eth0 lro on/off
*
* SW-GRO (기본):
* - 커널 NAPI 레벨에서 병합
* - 헤더 완전 보존, 모든 프로토콜 지원
* - CPU 오버헤드 있음
* - ethtool -K eth0 gro on/off
*
* HW-GRO (5.19+):
* - NIC 하드웨어가 병합하되 개별 헤더를 보존
* - NIC가 "header split" 또는 RSC(Receive Side Coalescing) 활용
* - CPU 오버헤드 최소화 + 헤더 보존의 장점
* - ethtool -K eth0 rx-gro-hw on/off
*
* 확인:
* # ethtool -k eth0 | grep gro
* generic-receive-offload: on
* rx-gro-hw: on [requested on]
*/
/* NIC feature 플래그 */
NETIF_F_GRO /* 소프트웨어 GRO 지원 (기본 on) */
NETIF_F_GRO_HW /* 하드웨어 GRO 지원 (5.19+) */
/* 드라이버에서 HW-GRO 결과를 커널에 전달하는 방법 */
/* NIC가 병합한 패킷을 수신하면, 드라이버는:
* 1. skb->len에 병합된 총 크기 설정
* 2. skb_shinfo(skb)->gso_size에 원래 MSS 설정
* 3. skb_shinfo(skb)->gso_type에 SKB_GSO_TCPV4 등 설정
* 4. napi_gro_receive()가 아닌 일반 경로로 전달 가능
* (이미 병합됨, 커널 GRO 불필요)
*/
프로토콜별 GRO 콜백(Callback)
/* GRO 콜백 등록 구조 */
struct net_offload {
struct offload_callbacks callbacks;
};
struct offload_callbacks {
struct sk_buff *(*gro_receive)(struct list_head *head,
struct sk_buff *skb);
int (*gro_complete)(struct sk_buff *skb, int nhoff);
};
/* 프로토콜별 GRO 콜백 체인 (L2 → L3 → L4):
*
* Ethernet:
* → eth_gro_receive()
* → inet_gro_receive() (IPv4) 또는 ipv6_gro_receive()
* → tcp4_gro_receive() 또는 udp4_gro_receive()
*
* VXLAN 터널:
* → eth_gro_receive()
* → inet_gro_receive()
* → udp4_gro_receive()
* → vxlan_gro_receive() ← 터널 디캡슐화
* → eth_gro_receive() ← 내부 패킷 재귀 처리
* → inet_gro_receive()
* → tcp4_gro_receive()
*/
/* UDP GRO 콜백 (4.18+ UDP GSO와 짝) */
static struct sk_buff *udp4_gro_receive(struct list_head *head,
struct sk_buff *skb)
{
/* UDP는 TCP와 달리 시퀀스 번호가 없으므로
* 병합 기준이 다름:
* - 동일 src/dst IP + port
* - 동일 데이터그램 크기 (마지막 제외)
* - 소켓이 UDP_GRO 옵션을 설정했어야 함
*/
/* 터널 프로토콜 콜백이 등록되어 있으면 터널 GRO */
struct udp_sock *up = udp_sk(sk);
if (up->encap_type && up->gro_receive)
return call_gro_receive(up->gro_receive, head, skb);
/* 일반 UDP GRO */
return udp_gro_receive(head, skb);
}
드라이버에서의 GRO 사용 패턴
/* 전형적인 NAPI poll 함수에서의 GRO 사용 */
static int my_napi_poll(struct napi_struct *napi, int budget)
{
struct my_adapter *adapter = container_of(napi, struct my_adapter, napi);
int work_done = 0;
while (work_done < budget) {
struct my_rx_desc *desc = my_get_rx_desc(adapter);
struct sk_buff *skb;
if (!desc)
break;
skb = my_build_skb(adapter, desc);
if (!skb) {
adapter->stats.alloc_fail++;
break;
}
/* 필수 skb 메타데이터 설정 — GRO 정확도에 영향 */
skb->protocol = eth_type_trans(skb, adapter->netdev);
/* 체크섬 오프로드 상태: GRO 효율에 직접 영향
* CHECKSUM_UNNECESSARY → GRO가 체크섬 재검증 생략 → 빠른 병합 */
if (desc->rx_status & RX_CSUM_OK)
skb->ip_summed = CHECKSUM_UNNECESSARY;
/* RX 해시: GRO가 동일 flow를 빠르게 찾는 데 사용 */
skb_set_hash(skb, desc->rss_hash, PKT_HASH_TYPE_L4);
/* VLAN 태그 (있으면) */
if (desc->rx_status & RX_VLAN_STRIPPED)
__vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), desc->vlan_tci);
/* GRO 진입 — 여기서 패킷 병합 시도 */
napi_gro_receive(napi, skb);
work_done++;
}
if (work_done < budget) {
/* 모든 수신 패킷 처리 완료 → IRQ 재활성화 */
if (napi_complete_done(napi, work_done))
my_enable_irq(adapter);
/* napi_complete_done() 내부에서 gro_list flush 수행 */
}
return work_done;
}
UDP GSO (USO — UDP Segmentation Offload)
커널 4.18에서 도입된 UDP GSO(흔히 USO라 부름)는 TCP GSO와 동일한 "지연(Latency) 분할" 전략을 UDP에 적용합니다. QUIC, WireGuard, DNS-over-HTTPS, 게임 서버 등 대량 UDP 전송에서 획기적인 성능 향상을 제공합니다.
UDP GSO 사용법 (사용자 공간(User Space) API)
/* 사용자 공간에서 UDP GSO 사용 — sendmsg() + UDP_SEGMENT ancillary data */
#include <netinet/udp.h>
int send_udp_gso(int fd, const void *buf, size_t len, uint16_t gso_size)
{
struct msghdr msg = {};
struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
char control[CMSG_SPACE(sizeof(uint16_t))];
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
/* UDP_SEGMENT cmsg로 세그먼트 크기 지정 */
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type = UDP_SEGMENT; /* 커널 4.18+ */
cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
*((uint16_t *)CMSG_DATA(cm)) = gso_size;
/* 64KB 버퍼를 한 번의 sendmsg()로 전송
* → 커널이 내부적으로 gso_size 단위 분할
* → sendmsg() 시스템콜 1회로 44개 UDP 데이터그램 전송 */
return sendmsg(fd, &msg, 0);
}
/* 사용 예시 */
char buf[65536];
send_udp_gso(sockfd, buf, sizeof(buf), 1472);
/* → 커널이 65536 / 1472 = 44+1개 UDP 데이터그램으로 분할 전송
* 마지막 세그먼트: 65536 - 1472*44 = 768 바이트 (짧을 수 있음)
*/
/* 수신 측: UDP GRO로 병합된 패킷을 효율적으로 수신 */
int val = 1;
setsockopt(sockfd, SOL_UDP, UDP_GRO, &val, sizeof(val));
/* → 커널이 GRO로 병합된 대형 UDP 메시지를 한 번에 전달
* → recvmsg()의 GRO_UDP ancillary data로 원본 세그먼트 크기 확인 가능
*/
UDP GSO 커널 내부
/* net/ipv4/udp_offload.c — __udp_gso_segment() 핵심 로직 */
struct sk_buff *__udp_gso_segment(struct sk_buff *gso_skb,
netdev_features_t features,
bool is_ipv6)
{
unsigned int mss = skb_shinfo(gso_skb)->gso_size;
struct sk_buff *segs, *seg;
struct udphdr *uh;
/* skb_segment()로 mss 단위 분할 수행 */
segs = skb_segment(gso_skb, features & ~NETIF_F_SG, false);
if (IS_ERR(segs))
return segs;
/* 각 세그먼트에 독립 UDP 헤더 설정 */
seg = segs;
do {
uh = udp_hdr(seg);
/* UDP length = 세그먼트 데이터 크기 + UDP 헤더(8) */
uh->len = htons(sizeof(*uh) + seg->len - skb_transport_offset(seg)
- sizeof(*uh));
/* 마지막 세그먼트가 아니면 동일 UDP src/dst 포트 유지
* (TCP와 달리 시퀀스 번호 증가 없음 — 각 세그먼트가 독립 데이터그램) */
/* 체크섬 재계산 */
if (seg->ip_summed == CHECKSUM_PARTIAL)
udp_set_csum(!0, seg, ...);
} while ((seg = seg->next));
return segs;
}
/* UDP GSO vs TCP GSO 핵심 차이 */
/*
* TCP GSO:
* - 시퀀스 번호 증가: seq += gso_size (각 세그먼트)
* - PSH/FIN 마지막에만, CWR 첫 세그먼트에만
* - 수신 측 TCP가 자동 재조립 (시퀀스 기반)
*
* UDP GSO:
* - 시퀀스 번호 없음 → 각 세그먼트가 완전 독립 데이터그램
* - IP ID는 증가 (IPv4)
* - 수신 측 재조립은 애플리케이션 책임
* - UDP_GRO 소켓 옵션으로 GRO 병합 시 원본 크기 보존
*
* UDP GSO가 유용한 워크로드:
* - QUIC (자체 시퀀스/신뢰성)
* - WireGuard (고정 크기 패킷 대량 전송)
* - DNS-over-HTTPS 서버 (다수 응답 일괄 전송)
* - 미디어 스트리밍 (RTP/UDP 대량 전송)
* - 게임 서버 (다수 클라이언트에 상태 업데이트)
*/
UDP GSO 성능 영향
| 시나리오 | sendmsg() 호출 수 | 시스템콜 오버헤드 | 스택 통과 횟수 | 처리량 (10Gbps NIC) |
|---|---|---|---|---|
| 개별 UDP 전송 (1472B × 44) | 44회 | 44 × syscall | 44회 | ~3.5 Gbps |
| sendmmsg() 사용 | 1회 (배치) | 1 × syscall | 44회 | ~5.2 Gbps |
| UDP GSO (64KB) | 1회 | 1 × syscall | 1회 | ~9.2 Gbps |
| UDP GSO + HW USO | 1회 | 1 × syscall | 1회 | ~9.8 Gbps (CPU 최소) |
gso_size를 QUIC 패킷 크기(보통 1200~1350B)로 설정하면, 단일 sendmsg()로 수십 개의 QUIC 패킷을 일괄 전송할 수 있습니다.
BIG TCP (대형 세그먼트, 커널 6.3+)
전통적으로 GSO/GRO의 최대 크기는 64KB(IP 헤더의 Total Length 필드가 16비트)로 제한되었습니다. BIG TCP는 IPv6 Jumbogram 확장 헤더를 활용하여 이 제한을 우회, 최대 512KB까지의 super-skb를 허용합니다.
BIG TCP 설정과 구조
/* BIG TCP 활성화 (커널 6.3+, IPv6 전용) */
# GSO/GRO 최대 크기를 64KB 이상으로 확장
# ip link set dev eth0 gso_max_size 185000
# ip link set dev eth0 gro_max_size 185000
#
# 더 큰 값도 가능 (NIC 지원 시):
# ip link set dev eth0 gso_max_size 524280
# ip link set dev eth0 gro_max_size 524280
#
# 확인:
# ip -d link show eth0 | grep -E 'gso_max|gro_max'
# gso_max_size 185000 gso_max_segs 65535
# gro_max_size 185000
/* BIG TCP 조건과 제약사항 */
/*
* 1. IPv6 전용: IPv4는 IP 헤더 total_length가 16비트라 64KB 제한 불변
* → IPv6 Jumbogram 확장 헤더로 32비트 payload length 사용
*
* 2. NIC 지원 필요: TSO/GRO 하드웨어가 64KB 이상을 처리할 수 있어야 함
* → Intel E810, Mellanox CX-5/6/7, Broadcom BCM5750X 등 지원
* → ethtool -k eth0에서 TSO max size 확인
*
* 3. Jumbogram은 로컬 스택에서만 사용:
* → 와이어(wire)에는 여전히 MSS 단위 세그먼트가 전송됨
* → Jumbogram 헤더는 GRO 병합/GSO 분할 내부에서만 존재
* → 네트워크 장비(라우터/스위치)에 영향 없음
*
* 4. 커널 내부 변경점:
* → skb->len이 u32 (4GB까지) → 문제 없음
* → skb_shared_info->gso_size는 u16 (65535) → 문제 없음 (MSS 단위)
* → net_device->gso_max_size가 unsigned int로 확장 (6.3+)
*/
/* 커널 내부: BIG TCP skb 생성 */
static int tcp_sendmsg_locked(struct sock *sk, ...)
{
/* size_goal 계산에서 BIG TCP 반영 */
int mss_now = tcp_send_mss(sk, &size_goal, flags);
/* BIG TCP 활성 시:
* sk->sk_gso_max_size = 185000 (또는 더 큰 값)
* size_goal = min(sk->sk_gso_max_size, 65535 * mss_now / mss_now)
* = 185000 (IPv6)
* → 하나의 skb에 185KB 데이터 적재
* → gso_segs = 185000 / 1460 = 126개
*/
}
/* IPv6 Jumbogram 처리 */
/* tcp_gso_segment()에서 BIG TCP skb 분할 시:
* - payload_length > 65535 감지
* - Hop-by-Hop Jumbo Payload 옵션 헤더 추가
* - 각 세그먼트는 일반 크기 (MSS)이므로 Jumbogram 불필요
* - 수신 GRO 측: 일반 패킷을 병합하면 자동으로 BIG TCP skb 생성
*/
- IPv4 미지원: IPv4는 total_length 16비트 한계로 BIG TCP 불가. IPv6 전용.
- tcpdump: GRO 시 185KB+ 패킷이 캡처될 수 있음. MTU 초과처럼 보이지만 정상.
- iptables/nftables: conntrack이 대형 skb를 처리하므로 패킷 카운터가 크게 다를 수 있음.
- MTU 경로: BIG TCP는 로컬 스택 내부만 영향. 와이어에는 MSS 단위 전송.
GRO 내부 자료구조
GRO의 핵심은 napi_struct 내부의 해시 테이블(Hash Table)과 NAPI_GRO_CB 제어 블록입니다. 이 자료구조가 O(1)에 가까운 flow 매칭을 가능하게 합니다.
GRO 해시 테이블 구조
/* include/linux/netdevice.h — GRO 해시 테이블 */
#define GRO_HASH_BUCKETS 8 /* 해시 버킷 수 */
struct gro_list {
struct list_head list; /* 이 버킷의 skb 리스트 */
int count; /* 리스트의 skb 수 (최대 MAX_GRO_SKBS=8) */
};
struct napi_struct {
...
struct gro_list gro_hash[GRO_HASH_BUCKETS];
unsigned long gro_bitmask; /* 비어있지 않은 버킷의 비트맵
* → 빈 버킷은 건너뛰어 flush 최적화 */
struct list_head rx_list; /* GRO 완료 후 상위 전달 대기 리스트 */
int rx_count; /* rx_list의 skb 수 */
...
};
/* 해시 버킷 선택: skb의 rxhash를 버킷 수로 모듈러 */
static inline unsigned int gro_hash_bucket(struct sk_buff *skb)
{
/* skb->hash: NIC의 RSS(Receive Side Scaling) 또는 SW 해시
* → 동일 flow의 패킷은 동일 해시 → 동일 버킷
* → 해시 품질이 GRO 효율에 직결 */
return skb_get_hash_raw(skb) & (GRO_HASH_BUCKETS - 1);
}
/* NAPI_GRO_CB: skb->cb[] 공간을 GRO 제어 블록으로 활용 */
struct napi_gro_cb {
/* frag0 최적화: 첫 번째 frag의 가상 주소를 캐시
* → 비선형 skb에서 헤더 접근 시 kmap 비용 절감 */
void *frag0;
unsigned int frag0_len;
/* 데이터 오프셋: L4 페이로드 시작 위치
* → 프로토콜 콜백이 이전 계층의 헤더 길이를 전달 */
int data_offset;
/* 병합 제어 플래그 */
u16 flush; /* !0 → 이 skb를 즉시 flush */
u16 flush_id; /* IP ID 불연속 시 flush 카운터 */
u16 count; /* 이 skb에 병합된 패킷 수 */
/* flow 매칭 */
u8 same_flow:1; /* 1: 동일 flow로 판정 (프로토콜 콜백이 설정) */
u8 encap_mark:1; /* 터널 캡슐화 표시 */
u8 csum_valid:1; /* 체크섬 검증 완료 */
u8 csum_cnt; /* 체크섬 불필요 카운트 (터널 중첩) */
u8 is_flist; /* frag_list 기반 GRO */
/* frag_list 병합용 */
struct sk_buff *last; /* frag_list의 마지막 skb */
/* 재귀적 GRO (터널) */
int recursion_counter; /* 터널 중첩 깊이 (최대 3) */
int network_offset; /* 네트워크 헤더 오프셋 */
int inner_network_offset; /* 내부 네트워크 헤더 오프셋 */
};
/* sizeof(napi_gro_cb) ≤ 48 (skb->cb[] = 48 바이트)
* → 별도 메모리 할당 없이 skb 내부 공간 활용
* → 캐시 라인 효율 극대화 */
GRO Flush 상세 경로
/* GRO flush: gro_list의 병합된 skb를 상위 스택으로 전달 */
/* 1. napi_gro_flush() — 전체 버킷 flush */
void napi_gro_flush(struct napi_struct *napi, bool flush_old)
{
unsigned long bitmask = napi->gro_bitmask;
unsigned int i;
/* bitmask로 비어있지 않은 버킷만 순회 — 불필요한 탐색 제거 */
while ((i = __ffs(bitmask)) < GRO_HASH_BUCKETS) {
struct list_head *head = &napi->gro_hash[i].list;
struct sk_buff *skb, *p;
list_for_each_entry_safe(skb, p, head, list) {
if (flush_old && NAPI_GRO_CB(skb)->age == jiffies)
continue; /* 이번 NAPI 사이클에서 추가된 건 유지 */
/* flush: 프로토콜별 gro_complete 콜백 호출 후 상위 전달 */
gro_flush_oldest(napi, head);
}
bitmask &= ~(1UL << i);
}
}
/* 2. gro_normal_list() — rx_list 일괄 전달 */
static void gro_normal_list(struct napi_struct *napi)
{
if (!napi->rx_count)
return;
/* rx_list의 모든 skb를 한꺼번에 상위 스택으로 전달
* → netif_receive_skb_list_internal() — 리스트 기반 최적화
* → 개별 netif_receive_skb() 대비 RCU/lock 오버헤드 감소 */
netif_receive_skb_list_internal(&napi->rx_list);
INIT_LIST_HEAD(&napi->rx_list);
napi->rx_count = 0;
}
/* 3. gro_complete 콜백 체인 */
/*
* flush 시 호출되는 프로토콜별 gro_complete:
*
* tcp4_gro_complete():
* - TCP 헤더의 ACK number 최종 설정
* - TCP 윈도우 업데이트
* - skb_shinfo->gso_type = SKB_GSO_TCPV4 설정
* - skb_shinfo->gso_segs = 병합된 패킷 수
* → GSO로 다시 분할할 때 필요한 정보 보존
*
* inet_gro_complete():
* - IP 헤더의 total_length 업데이트 (병합 후 총 크기)
* - IP ID 범위 기록
*
* 최종적으로 netif_receive_skb()로 전달:
* → ip_rcv() → tcp_v4_rcv() → sk_receive_queue
* → 애플리케이션의 recv() 한 번으로 64KB+ 수신 가능
*/
가상화 환경의 GSO/GRO
가상화, 컨테이너(Container), SDN 환경에서 GSO/GRO는 가상 인터페이스 간 패킷 전달 최적화의 핵심입니다. virtio-net, veth, macvtap, TAP 등 가상 디바이스는 하드웨어 오프로드가 없으므로 소프트웨어 GSO/GRO에 전적으로 의존합니다.
virtio-net GSO/GRO 연동
/* virtio-net의 GSO 오프로드 핵심: vnet_hdr */
/* Guest → Host: GSO skb를 분할하지 않고 그대로 전달 */
struct virtio_net_hdr_v1 {
__u8 flags; /* VIRTIO_NET_HDR_F_NEEDS_CSUM 등 */
__u8 gso_type; /* VIRTIO_NET_HDR_GSO_TCPV4/V6/UDP */
__le16 hdr_len; /* 모든 헤더의 총 길이 */
__le16 gso_size; /* MSS (분할 단위) — skb_shinfo->gso_size에 매핑 */
__le16 csum_start; /* 체크섬 시작 오프셋 */
__le16 csum_offset; /* 체크섬 필드 오프셋 */
__le16 num_buffers; /* MRG_RXBUF: 사용된 버퍼 수 */
};
/* Guest TX: GSO skb → vnet_hdr 변환 */
/*
* 1. Guest의 TCP 스택이 64KB GSO skb 생성
* 2. virtio-net 드라이버가 vnet_hdr에 GSO 정보 기록
* 3. skb 데이터를 분할하지 않고 vring으로 전달
* 4. Host의 vhost-net 또는 QEMU가 수신:
* a. vnet_hdr의 gso_type/gso_size를 읽어
* b. Host skb의 skb_shinfo에 GSO 정보 복원
* c. Host 네트워크 스택은 이 skb를 GSO skb로 인식
* d. Host → 물리 NIC 전달 시: HW TSO 또는 SW GSO로 분할
*
* → Guest-Host 간 패킷이 분할 없이 전달 → 매우 효율적
*/
/* VIRTIO_NET_F_MRG_RXBUF (Mergeable Receive Buffers) */
/*
* Host → Guest 방향에서 GRO 효율을 극대화:
* - 여러 vring 버퍼를 하나의 큰 패킷으로 병합
* - num_buffers 필드로 사용된 버퍼 수 표시
* - Guest가 64KB skb를 받을 때:
* → 여러 4KB vring 버퍼가 하나의 skb로 조립
* → skb_shinfo->frags[]에 각 버퍼 페이지 매핑
* → GRO가 병합한 대형 패킷을 Guest가 효율적으로 수신
*
* MRG_RXBUF 없으면:
* → 각 vring 버퍼가 별도 skb → 64KB 패킷을 위해 44개 skb
* → Guest의 수신 성능 크게 저하
*/
/* VIRTIO_NET_F_GUEST_TSO4/TSO6/UFO */
/*
* Host가 Guest에 GSO skb를 직접 전달할 수 있음:
* - Host의 GRO로 병합된 패킷 → 분할 없이 Guest에 전달
* - Guest의 네트워크 스택이 GSO skb로 인식
* - Guest가 다른 NIC로 포워딩 시: Guest의 GSO가 분할
*
* 이 feature 없으면:
* - Host가 반드시 세그먼트 단위로 분할 후 전달
* - 성능 대폭 저하 (특히 포워딩 시)
*/
veth (Container) GSO/GRO
/* veth: 컨테이너 네트워킹의 핵심 가상 인터페이스 */
/* veth의 GSO 처리가 특별한 이유:
* - veth는 peer 디바이스와 1:1 연결
* - TX 측의 GSO skb를 분할하지 않고 RX peer로 직접 전달
* - peer의 GRO에서 재병합할 필요 없음 (이미 대형 skb)
*
* drivers/net/veth.c */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct veth_priv *priv = netdev_priv(dev);
struct net_device *rcv = rcu_dereference(priv->peer);
/* GSO skb를 분할하지 않고 peer로 직접 전달!
* → 64KB skb가 그대로 peer의 수신 경로에 진입
* → peer 측에서 netif_rx() 또는 napi_gro_receive() */
if (likely(veth_forward_skb(rcv, skb, priv, rq, false) == NET_RX_SUCCESS))
return NETDEV_TX_OK;
...
}
/* veth features: 사실상 모든 오프로드를 "지원"
* → 실제 HW가 아니므로 SW GSO fallback이지만,
* peer 전달 시 분할이 발생하지 않으므로 무관 */
/*
* veth가 광고하는 features:
* NETIF_F_SG → Scatter-Gather (항상)
* NETIF_F_HW_CSUM → 체크섬 (소프트웨어로 처리)
* NETIF_F_TSO → TCP GSO
* NETIF_F_TSO6 → TCP GSO IPv6
* NETIF_F_GSO_UDP_L4 → UDP GSO
* NETIF_F_GSO_UDP_TUNNEL → 터널 GSO
* ...
*
* 이 features 덕분에:
* Container → veth TX: GSO skb 그대로 전달 (분할 안 함)
* veth peer RX → Bridge/OVS: 대형 skb로 전달
* Bridge → Physical NIC TX: NIC의 실제 feature에 맞춰 분할
*
* 성능 영향:
* veth + GSO ON: ~40 Gbps (Container 간)
* veth + GSO OFF: ~8 Gbps (44배 더 많은 skb 처리)
*/
XDP와 GSO/GRO 상호작용
XDP는 GRO 이전에 동작합니다. NIC IRQ → NAPI poll 과정에서 XDP 프로그램이 먼저 실행되고, 그 결과에 따라 GRO 경로 진입 여부가 결정됩니다:
- XDP_PASS — GRO로 진입합니다 (
napi_gro_receive). - XDP_DROP — 즉시 드롭되며, GRO에 도달하지 않습니다.
- XDP_TX — 같은 NIC로 반송합니다 (GRO 우회).
- XDP_REDIRECT — 다른 NIC/AF_XDP로 전달합니다 (GRO 우회).
핵심은 XDP_TX/XDP_REDIRECT가 개별 패킷 단위로 동작한다는 점입니다. GRO 병합 이전이므로 각 패킷이 독립적이며, 대량 트래픽에서 per-packet XDP는 CPU 집약적입니다.
XDP에서 GSO skb를 처리할 수 없는 이유
xdp_frame/xdp_buff는 단일 선형 버퍼만 지원합니다:
frags[]미지원 (XDP multi-buffer는 5.18+에서 부분 지원)skb_shared_info의gso_size/gso_type필드 없음- 따라서 GSO super-skb를 XDP로 처리할 수 없습니다
GRO → XDP 경로를 시도하는 경우, GRO가 병합한 대형 skb는 XDP_PASS 후 상위 스택으로 전달됩니다. XDP generic 모드는 skb 기반이므로 GSO skb 처리가 가능하지만, 성능 이점이 사라집니다 (native XDP 대비 10배 이상 느림). XDP에서 GRO와 유사한 기능이 필요하면 AF_XDP + 사용자 공간 병합(DPDK 스타일) 또는 eBPF TC(GRO 이후 동작, GSO skb 처리 가능)를 사용해야 합니다.
eBPF TC classifier와 GSO/GRO
TC eBPF는 GRO 이후, GSO 이전에 동작합니다:
GRO → IP → [TC ingress eBPF] → routing → [TC egress eBPF] → GSO → NIC
따라서 TC eBPF에서는 다음과 같이 동작합니다:
- 수신 — GRO로 병합된 대형 skb를 처리합니다 (효율적).
- 송신 — GSO 분할 전 대형 skb를 처리합니다 (효율적).
skb->len이 64KB 이상일 수 있으므로 주의해야 합니다.bpf_skb_change_tail()등 크기 변경 시 GSO가 무효화될 수 있습니다.
IPsec ESP GSO
IPsec ESP(Encapsulating Security Payload) 환경에서 GSO는 특별한 도전 과제를 제기합니다. 암호화는 패킷 단위로 수행해야 하지만, GSO는 분할을 최대한 지연시키기 때문입니다.
ESP GSO의 핵심 문제: ESP는 패킷 단위로 암호화/인증을 수행하여 IV, SN, ICV가 패킷별로 고유합니다. 반면 GSO는 분할을 최대한 지연합니다. 대형 skb 상태에서 패킷별 ESP 처리를 어떻게 수행할 것인지가 핵심 과제입니다.
- 해결책 1: SW ESP + GSO 분할 선행 (기존 방식)
- GSO를 먼저 분할하고 44개 패킷에 각각 ESP를 적용합니다. 스택 효율 이점이 상실됩니다.
- 해결책 2: ESP GSO offload (
NETIF_F_GSO_ESP) - 대형 skb에 ESP 메타데이터만 설정하고,
validate_xmit_skb()에서 분할과 ESP를 동시에 처리합니다. 또는 NIC 하드웨어가 암호화+분할을 일괄 처리합니다 (inline crypto).
xfrm offload 설정 확인
# ip xfrm state list ... offload dev eth0 dir out ← NIC 오프로드 활성 # ethtool -k eth0 | grep esp esp-hw-offload: on tx-esp-segmentation: on ← ESP GSO 지원 # 설정: # ip xfrm state add ... offload dev eth0 dir out
ESP GSO 분할 시 각 세그먼트 처리
esp4_gso_segment() / esp6_gso_segment()는 다음 과정을 수행합니다:
- 원본 대형 skb를 MSS 단위로 분할합니다.
- 각 세그먼트에 다음을 적용합니다:
- 고유 ESP SPI 유지 (동일 SA)
- 고유 Sequence Number 할당 (증가)
- 고유 IV(Initialization Vector) 생성
- ESP 트레일러(Padding + Next Header + ICV) 추가
- AES-GCM/CBC 등으로 암호화
- ICV(Integrity Check Value) 계산
- 각 세그먼트가 독립적인 ESP 패킷으로 완성됩니다.
HW offload 시에는 위 과정을 NIC 하드웨어가 수행하며, CPU는 SA 메타데이터만 DMA 디스크립터에 기록합니다. 100Gbps IPsec 환경에서 필수적입니다.
TCP Autocorking과 GSO
TCP autocorking은 GSO 효율을 극대화하는 보조 메커니즘입니다. 작은 write()가 연속될 때 자동으로 데이터를 모아 하나의 큰 GSO skb를 만듭니다.
/* TCP Autocorking (커널 3.14+, 기본 활성) */
/* 문제 상황:
* 애플리케이션이 작은 write()를 반복:
* write(fd, buf1, 100); // 100B
* write(fd, buf2, 200); // 200B
* write(fd, buf3, 150); // 150B
*
* Autocorking 없으면:
* → 각 write()마다 별도 skb → 3개 작은 패킷 전송
* → GSO 효과 없음, Nagle과 다름 (Nagle은 ACK 대기)
*
* Autocorking 있으면:
* → 이미 전송 큐에 미전송 skb가 있고, 아직 ACK 안 왔으면
* → 새 데이터를 기존 skb에 append
* → 최종적으로 하나의 큰 skb로 GSO 전송
*/
/* net/ipv4/tcp.c — tcp_sendmsg_locked()에서의 autocorking 판단 */
static bool tcp_should_autocork(struct sock *sk,
struct sk_buff *skb,
int size_goal)
{
/* autocorking 조건:
* 1. net.ipv4.tcp_autocorking = 1 (sysctl, 기본 on)
* 2. 전송 큐에 이미 미전송 데이터가 있음 (sk->sk_wmem_queued > 0)
* 3. 마지막 전송 후 아직 ACK를 받지 못함
* 4. 현재 skb에 충분한 공간이 남아있음 (< size_goal)
*/
return sysctl_tcp_autocorking &&
skb != tcp_send_head(sk) &&
refcount_read(&sk->sk_wmem_alloc) > SKB_TRUESIZE(1) &&
tcp_packets_in_flight(tcp_sk(sk)) >= tcp_sk(sk)->snd_cwnd;
}
/* Autocorking ↔ GSO 시너지 */
/*
* Autocorking이 데이터를 모으는 동안:
* → 하나의 skb에 여러 write() 데이터가 축적
* → skb->len이 size_goal(= MSS × max_segs)에 도달하면 전송
* → 결과: 최대 64KB GSO skb로 전송
*
* Autocorking이 없으면:
* → 각 write()가 즉시 tcp_push() → 작은 skb 다수
* → GSO 효과 미미 (skb당 데이터가 적음)
*
* sysctl 제어:
* # sysctl net.ipv4.tcp_autocorking
* net.ipv4.tcp_autocorking = 1 (기본)
*
* TCP_CORK 소켓 옵션과의 차이:
* TCP_CORK: 명시적 — setsockopt()로 수동 제어
* Autocorking: 자동 — 전송 상태를 보고 커널이 결정
* → 대부분의 경우 autocorking으로 충분
* → 정밀 제어가 필요하면 TCP_CORK 또는 TCP_NODELAY
*
* 모니터링:
* # nstat -az | grep AutoCorking
* TcpExtTCPAutoCorking 123456 # autocorking 발동 횟수
*/
SCTP GSO
SCTP(Stream Control Transmission Protocol)는 TCP와 달리 청크(chunk) 기반 프로토콜이므로 GSO 분할 방식이 다릅니다:
/* SCTP GSO (커널 4.14+, SKB_GSO_SCTP) */
/* SCTP vs TCP 분할 차이:
*
* TCP GSO:
* [IP][TCP hdr, seq=1000][Payload 64KB]
* → [IP][TCP, seq=1000][1460B] + [IP][TCP, seq=2460][1460B] + ...
* 시퀀스 번호 기반 분할
*
* SCTP GSO:
* [IP][SCTP Common Hdr][DATA chunk 1][DATA chunk 2]...[DATA chunk N]
* → [IP][SCTP Hdr][DATA chunk 1] + [IP][SCTP Hdr][DATA chunk 2] + ...
* 청크 단위 분할 (각 청크가 자체 TSN 보유)
*/
/* net/sctp/offload.c — SCTP GSO 분할 */
static struct sk_buff *sctp_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
/* SCTP 분할 특이점:
* 1. 각 DATA 청크가 이미 독립적 (자체 TSN, Stream ID)
* 2. SCTP 공통 헤더의 CRC32c 체크섬을 각 세그먼트에서 재계산
* 3. IP 헤더의 total_length 업데이트
*
* gso_size = 하나의 DATA 청크 크기
* → skb_segment()로 청크 경계에서 분할
*/
struct sk_buff *segs = skb_segment(skb, features, false);
struct sk_buff *seg;
struct sctphdr *sh;
/* 각 세그먼트의 SCTP CRC32c 재계산 */
skb_list_walk_safe(segs, seg, tmp) {
sh = sctp_hdr(seg);
sh->checksum = sctp_compute_cksum(seg, skb_transport_offset(seg));
}
return segs;
}
/* SCTP GSO의 체크섬 특이점:
* - SCTP는 TCP/UDP와 달리 CRC32c 체크섬 사용
* - pseudo-header 없음 (IP 주소 변경에 무관)
* - CHECKSUM_PARTIAL로 NIC에 위임 불가 (대부분 NIC가 CRC32c 미지원)
* - 따라서 항상 소프트웨어에서 CRC32c 계산
* - NETIF_F_SCTP_CRC: CRC32c HW 오프로드 (일부 NIC, Intel X710+)
*
* # ethtool -k eth0 | grep sctp
* tx-sctp-segmentation: on # SCTP GSO
* tx-checksum-sctp: on # SCTP CRC32c HW offload
*/
GSO ↔ GRO 상호작용
포워딩 환경에서 GRO로 병합된 skb가 다시 GSO로 분할되는 경우가 빈번합니다. 이 "GRO → forward → GSO" 파이프라인이 네트워크 장비(라우터, 브리지(Bridge), 로드밸런서)의 성능을 결정합니다:
/* 포워딩 시 GRO → GSO 동작 */
/*
* 1. 수신 NIC에서 GRO가 43개 패킷을 1개 대형 skb로 병합
* 2. ip_forward()에서 라우팅 결정 (1회)
* 3. Netfilter (conntrack, NAT 등) 통과 (1회)
* 4. 송신 NIC로 전달:
* - 송신 NIC가 TSO 지원 → 대형 skb 그대로 전달
* - 송신 NIC가 TSO 미지원 → GSO가 소프트웨어 분할
*
* 포워딩 효율:
* GRO/GSO OFF: 43 × {routing + conntrack + NAT + filter} = 43 × CPU cycles
* GRO/GSO ON: 1 × {routing + conntrack + NAT + filter} = 1 × CPU cycles
* → 약 43배 효율 향상 (64KB / 1500 MTU 기준)
*/
/* GRO로 병합된 skb가 다시 분할될 때의 GSO 타입 */
/* GRO 병합 시 gso_size와 gso_type이 자동 설정되므로
* 포워딩 경로의 GSO가 원본 패킷과 동일하게 분할 가능 */
if (NAPI_GRO_CB(p)->count > 1) {
skb_shinfo(p)->gso_size = skb_gro_len(skb); /* 원본 세그먼트 크기 */
skb_shinfo(p)->gso_type |= SKB_GSO_TCPV4;
}
성능 튜닝과 모니터링
ethtool 오프로드 제어
현재 오프로드 상태 확인
# ethtool -k eth0 | grep -E 'offload|gso|gro|tso' tcp-segmentation-offload: on ← TSO (HW) generic-segmentation-offload: on ← GSO (SW fallback) generic-receive-offload: on ← GRO (SW) rx-gro-hw: on [requested on] ← HW-GRO (5.19+) udp-segmentation-offload: off ← USO (4.18+) tx-udp_tnl-segmentation: on ← 터널 TSO tx-udp_tnl-csum-segmentation: on ← 터널 TSO + csum large-receive-offload: off [requested off] ← LRO (deprecated)
개별 오프로드 제어
# ethtool -K eth0 gso on|off ← GSO 전환 # ethtool -K eth0 gro on|off ← GRO 전환 # ethtool -K eth0 tso on|off ← TSO 전환 # ethtool -K eth0 rx-gro-hw on|off ← HW-GRO 전환 # ethtool -K eth0 lro on|off ← LRO 전환 (비권장)
GSO/GRO 최대 크기 확인 및 설정
# ip -d link show eth0 | grep gso gso_max_size 65536 gso_max_segs 65535 gro_max_size 65536 # BIG TCP 활성화 (IPv6, 6.3+): # ip link set dev eth0 gso_max_size 185000 # ip link set dev eth0 gro_max_size 185000
GRO 관련 sysctl 튜닝
-
gro_flush_timeout— GRO 패킷 보관 타임아웃(나노초)입니다. 기본값은 0으로,napi_complete시 즉시 flush합니다. 값을 설정하면 타이머 만료까지 더 많은 패킷 병합을 시도하여 처리량이 증가하지만, 지연 시간도 함께 증가합니다.# sysctl -w net.core.gro_flush_timeout=20000 # 20μs
-
napi_defer_hard_irqs— 빈 poll 허용 횟수입니다. 기본값은 0(즉시 IRQ 재활성화)이며, 설정하면 N번 빈 poll 후에야 IRQ를 재활성화합니다.gro_flush_timeout과 함께 사용하면 busy-poll 모드가 됩니다.# sysctl -w net.core.napi_defer_hard_irqs=2 # 2번 빈 poll 허용
-
권장 조합 (10Gbps+ 고처리량 환경):
gro_flush_timeout=20000+napi_defer_hard_irqs=2조합을 사용하면 IRQ 없이 busy-poll로 패킷을 수신하여 GRO 병합을 극대화합니다. 단, CPU 사용률이 약간 증가합니다 (IRQ coalescence와 트레이드오프). -
지연 민감 환경 (금융, 게임):
gro_flush_timeout=0+napi_defer_hard_irqs=0으로 최소 지연을 달성합니다. 하지만 GRO 병합 기회가 감소하며, 극단적인 경우 GRO 자체를 비활성화하는 것을 고려해야 합니다. -
busy_poll/busy_read— 소켓 레벨 busy polling입니다.epoll/poll에서 커널이 먼저 NAPI poll을 시도하여 IRQ 대기 없이 처리합니다. GRO와 결합하면 ultra-low latency와 높은 처리량을 동시에 달성할 수 있습니다.# sysctl -w net.core.busy_poll=50 # 50μs busy poll # sysctl -w net.core.busy_read=50 # 50μs busy read
모니터링과 통계
-
NIC 통계 — GRO 병합 횟수
# ethtool -S eth0 | grep -i gro rx_gro_packets: 1234567 ← GRO로 병합된 패킷 수 rx_gro_bytes: 98765432000 ← GRO로 병합된 바이트 수 rx_gro_dropped: 0 ← GRO 중 드롭
-
/proc/net/dev— 일반 인터페이스 통계# cat /proc/net/dev
GRO 활성 시 RX packets가 줄고 bytes는 동일합니다. 패킷당 평균 크기가 크면 GRO가 잘 동작하고 있는 것입니다.
-
nstat — 프로토콜별 통계
# nstat -az | grep -i gro TcpExtTCPAutoCorking 123456 ← TCP autocorking (GSO 관련)
-
perf로 GSO/GRO 함수 프로파일링
# perf top -g -e cycles -- -K
skb_gso_segment,tcp_gso_segment,dev_gro_receive등의 CPU 비중을 확인합니다. GRO/GSO 오버헤드가 높으면 HW 오프로드를 확인해야 합니다. -
ftrace로 GSO 분할 추적
# echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_xmit/enable # cat /sys/kernel/debug/tracing/trace_pipe
skb->len변화로 GSO 분할 여부를 확인할 수 있습니다. -
GRO 효율 지표 계산
GRO ratio = (NIC rx_packets) / (netif_receive_skb 호출 수)로 계산합니다. 비율이 높을수록 GRO가 효과적으로 동작하며, TCP 워크로드에서 일반적으로 40~60:1입니다 (64KB / 1500 MTU 기준).
GSO/GRO 주의사항
- 패킷 캡처(tcpdump) — GRO가 병합한 대형 패킷이 캡처되어 MTU 초과로 보일 수 있음. 디버깅 시
ethtool -K eth0 gro off로 비활성화하거나tcpdump가 자동 처리 - Netfilter conntrack — GRO skb가 conntrack에서 분할 없이 하나로 추적되므로, 패킷 카운터가 실제보다 적게 표시됨
- TC(Traffic Control) — GSO skb는 분할 전이므로 큐 깊이가 실제보다 적게 보임.
tc -s qdisc출력 해석 시 주의 - MTU 변경 — GSO/GRO 활성 상태에서 MTU 변경 시 기존 skb의 gso_size와 불일치 가능. 트래픽 중단 후 변경 권장
- IPsec — ESP 암호화 후 GSO 분할 필요.
NETIF_F_GSO_ESP미지원 NIC에서 성능 저하. xfrm offload 확인 필요 - Bridge/OVS — 포워딩 경로에서 GRO로 병합된 skb가 재분할 없이 브리지되면, 수신 측에서 예상치 못한 대형 프레임을 받을 수 있음
- 성능 문제 발생 시
ethtool -K eth0 gro off; ethtool -K eth0 tso off로 오프로드를 순차 비활성화하여 원인 분리 ss -ti로 TCP 소켓별 MSS, cwnd를 확인하여 GSO 세그먼트 크기 추정ip -s link show eth0에서 TX/RX 패킷 수 대비 바이트 수 비율로 GSO/GRO 효과 확인- 가상화 환경에서는 virtio-net의
VIRTIO_NET_F_MRG_RXBUF플래그가 GRO에 직접 영향
실전 벤치마크와 성능 분석
GSO/GRO의 효과를 정량적으로 확인하는 벤치마크 방법과 결과입니다. 환경에 따라 수치가 달라지므로 자체 측정이 필수입니다.
벤치마크 측정 방법
-
기준점 설정: 오프로드 비활성화
# ethtool -K eth0 gso off gro off tso off # ethtool -K eth0 rx-gro-hw off 2>/dev/null
-
TCP 처리량 측정 (iperf3)
# 서버: iperf3 -s # 클라이언트: iperf3 -c SERVER_IP -t 30 -P 1 → Bandwidth: 2.8 Gbps (GSO/GRO OFF) # 오프로드 활성화 후 재측정: # ethtool -K eth0 gso on gro on tso on # iperf3 -c SERVER_IP -t 30 -P 1 → Bandwidth: 9.7 Gbps (GSO/GRO ON)
-
CPU 사용률 동시 측정
# mpstat -P ALL 1 30 GSO/GRO OFF: CPU 82% (softirq + ksoftirqd 병목) GSO/GRO ON: CPU 28% (대부분 유휴)
-
포워딩 성능 (netperf)
# Host A → Router → Host B # Router에서: # sysctl net.ipv4.ip_forward=1 # netperf -H HOST_B -t TCP_STREAM -l 30 -- -m 65536 GRO/GSO OFF: 4.5 Gbps (conntrack per-packet 병목) GRO/GSO ON: 9.4 Gbps (conntrack 1/44 실행)
-
UDP GSO 벤치마크
# 서버: iperf3 -s # 클라이언트: iperf3 -c SERVER_IP -u -b 10G -l 64000 --udp-gso UDP 개별: 6.5 Gbps UDP GSO: 9.5 Gbps
-
GRO 효율 확인
# 전송 중 패킷 통계 비교: # watch -n1 'cat /proc/net/dev | grep eth0' GRO OFF: RX packets ~815,000/sec (815K PPS) GRO ON: RX packets ~19,000/sec (19K PPS, 각 ~64KB) → GRO 비율: 815K / 19K ≈ 43:1 (거의 이론치)
-
perf로 핫스팟 분석
# perf record -g -a -- sleep 10 # perf report GSO/GRO OFF 핫스팟: 15.2% tcp_v4_rcv 12.8% ip_rcv 8.5% nf_conntrack_in 7.2% __netif_receive_skb_core GSO/GRO ON 핫스팟: 22.1% copy_to_user (데이터 복사가 주 병목) 8.3% tcp_recvmsg 5.1% skb_gso_segment (GSO 분할)
시나리오별 최적 설정
| 환경 | GSO | GRO | TSO | HW-GRO | gro_flush_timeout | napi_defer_hard_irqs | 비고 |
|---|---|---|---|---|---|---|---|
| 웹 서버 (HTTP/HTTPS) | ON | ON | ON | ON | 0 | 0 | 기본 설정으로 충분 |
| 10Gbps+ 고처리량 | ON | ON | ON | ON | 20000 | 2 | GRO 병합 극대화 |
| 100Gbps IPv6 | ON | ON | ON | ON | 20000 | 2 | BIG TCP 185KB+ |
| 라우터/방화벽(Firewall) | ON | ON | ON | ON | 0 | 0 | 포워딩 효율 핵심 |
| 지연 민감 (금융) | ON | ON | ON | OFF | 0 | 0 | busy_poll=50 추가 |
| 극저지연 (HFT) | OFF | OFF | OFF | OFF | 0 | 0 | 커널 우회(DPDK) 권장 |
| 가상화 (KVM) | ON | ON | ON | ON | 0 | 0 | virtio MRG_RXBUF 필수 |
| 컨테이너 (K8s) | ON | ON | ON | ON | 0 | 0 | veth GSO 자동 활성 |
| IPsec VPN | ON | ON | ON | ON | 0 | 0 | xfrm offload + ESP GSO |
| 패킷 캡처/분석 | ON | OFF | ON | OFF | 0 | 0 | GRO OFF로 원본 패킷 확인 |
장애 사례와 디버깅
실전 트러블슈팅 사례
증상: tcpdump -i eth0 -n에서 TCP, length 65160처럼 MTU(1500)보다 훨씬 큰 패킷이 보입니다.
원인: tcpdump는 GRO 이후의 skb를 캡처하므로, GRO가 병합한 대형 skb가 그대로 pcap에 기록됩니다. 이는 정상 동작이며 문제가 아닙니다.
확인 방법:
# ethtool -K eth0 gro off # tcpdump -i eth0 -n → 이제 MSS 단위 패킷만 보임 (1460B 등) # ethtool -K eth0 gro on ← 측정 후 복원
또는 AF_PACKET 레벨에서 tcpdump --immediate-mode를 사용하면 일부 환경에서 도움이 됩니다.
증상: netstat -s | grep "packets dropped"에서 큰 수의 드롭이 보이며, GRO OFF 시 감소합니다.
원인: GRO가 64KB skb를 생성하여 소켓 수신 버퍼(sk_rcvbuf)를 빠르게 소진합니다. sk_rmem_alloc이 sk_rcvbuf를 초과하면 드롭이 발생합니다.
해결:
# sysctl -w net.core.rmem_max=16777216 # sysctl -w net.core.rmem_default=8388608
또는 애플리케이션에서 setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size))로 설정합니다.
증상: VXLAN 터널 throughput이 물리 NIC의 50% 수준입니다.
원인: NIC가 터널 GSO를 지원하지 않아 모든 패킷이 SW GSO로 분할되고, 외부 UDP 체크섬도 SW로 계산됩니다.
진단:
# ethtool -k eth0 | grep tunnel tx-udp_tnl-segmentation: off [requested on] ← HW 미지원! tx-udp_tnl-csum-segmentation: off
해결:
- 터널 GSO 지원 NIC로 교체합니다 (Intel E810, Mellanox CX-5+ 등).
- 외부 UDP csum을 비활성화합니다:
ip link set vxlan0 type vxlan udpcsum off(RFC 7348에서 허용, 약 10~20% 성능 개선) GSO_PARTIAL을 활용합니다 (NIC가 외부 헤더만 처리, 내부는 SW 분할).
증상: KVM Guest에서 iperf3 결과가 2Gbps 미만입니다.
진단:
# (Guest) ethtool -k eth0 generic-segmentation-offload: off ← GSO 비활성! generic-receive-offload: off
원인: virtio-net feature negotiation 실패 또는 QEMU 옵션에서 오프로드가 비활성화되어 있습니다.
해결:
- QEMU 옵션을 확인합니다:
-device virtio-net-pci,host_tso4=on,guest_tso4=on,host_ufo=on,guest_ufo=on,mrg_rxbuf=on - Guest 내에서 활성화합니다:
ethtool -K eth0 gso on gro on tso on - vhost-net 사용 여부를 확인합니다 (QEMU 프로세스 내 처리보다 빠릅니다).
증상: iptables -m length --length 0:1500 매칭이 동작하지 않습니다 (GSO skb는 64KB이므로 매칭 실패).
원인: GSO skb가 분할 전에 Netfilter를 통과하므로, skb->len이 64KB이고 length 매칭이 MSS 단위가 아닙니다.
해결:
-m connbytes를 사용합니다 (바이트 기반).- NFQUEUE를 사용하여 GSO 분할 후 처리합니다.
- nftables에서는
meta l4proto+ payload 매칭을 권장합니다.
증상: ip link set eth0 mtu 9000 후 기존 TCP 연결이 끊깁니다.
원인: 기존 연결의 MSS가 이전 MTU 기준이며, GSO gso_size가 이전 MSS 기준으로 설정되어 MTU 변경 후 불일치가 발생합니다.
해결:
- MTU 변경 전에 기존 연결을 종료합니다.
TCP_MAXSEG소켓 옵션으로 MSS를 수동 조정합니다.- 점진적 MTU 변경을 서비스 재시작 배포와 함께 수행합니다.
GSO/GRO 디버깅 도구 모음
-
ethtool — 오프로드 상태 및 통계
# ethtool -k eth0 ← 오프로드 플래그 # ethtool -S eth0 | grep -i gro ← GRO 통계 # ethtool -S eth0 | grep -i tso ← TSO 통계 # ethtool --show-features eth0 ← 전체 feature 목록
-
ip 명령 — GSO/GRO 최대 크기
# ip -d link show eth0 | grep -E 'gso|gro' # ip -s link show eth0 ← TX/RX 통계
-
ss — TCP 소켓 상태
# ss -ti state established → mss:1460 cwnd:44 → GSO 가능 세그먼트 수 추정 → delivery_rate 102Mbps → 실제 전송률
-
/proc/net/snmp— 프로토콜 카운터# cat /proc/net/snmp | grep Tcp InSegs: GRO 병합 후 카운트 (적을수록 GRO 효과적) OutSegs: GSO 분할 전 카운트
-
nstat — 세부 통계
# nstat -az | grep -i -E 'gro|cork|seg' TcpExtTCPAutoCorking: autocorking 발동 횟수 TcpExtTCPOrigDataSent: 원본 데이터 세그먼트 수
-
ftrace — GSO/GRO 함수 추적
# echo 'skb_gso_segment' > /sys/kernel/debug/tracing/set_ftrace_filter # echo function > /sys/kernel/debug/tracing/current_tracer # echo 1 > /sys/kernel/debug/tracing/tracing_on # cat /sys/kernel/debug/tracing/trace_pipe → GSO 분할 호출 빈도와 타이밍 확인
-
perf probe — 동적 트레이스포인트
# perf probe --add 'dev_gro_receive skb->len' # perf record -e probe:dev_gro_receive -a -- sleep 5 # perf script → GRO 진입 시 skb 크기 분포 확인
-
bpftrace — eBPF 기반 실시간 분석
# bpftrace -e ' kprobe:napi_gro_receive { @gro_count = count(); @gro_size = hist(((struct sk_buff *)arg1)->len); } interval:s:5 { print(@gro_size); clear(@gro_size); } ' → 5초마다 GRO로 들어오는 skb 크기 히스토그램 -
dropwatch — 패킷 드롭 추적
# dropwatch -l kas → GSO/GRO 관련 드롭 지점 식별 → skb_gso_validate 실패, gro_max_size 초과 등
커널 버전별 GSO/GRO 진화
| 커널 버전 | 기능 | GSO 타입/Feature | 주요 저자 | 핵심 변경 |
|---|---|---|---|---|
| 2.6.18 | GSO 프레임워크 | SKB_GSO_TCPV4 | Herbert Xu | tcp_gso_segment(), validate_xmit_skb() |
| 2.6.18 | UFO | SKB_GSO_UDP | Herbert Xu | UDP Fragmentation Offload (IP 단편화) |
| 2.6.29 | GRO 프레임워크 | NETIF_F_GRO | Herbert Xu | napi_gro_receive(), 프로토콜 콜백 체인 |
| 3.7 | 터널 GSO | SKB_GSO_GRE/UDP_TUNNEL | Pravin Shelar | VXLAN/GRE 내부 패킷 GSO 지원 |
| 3.14 | TCP Autocorking | - | Eric Dumazet | 자동 데이터 취합으로 GSO 효율 향상 |
| 3.18 | GSO_PARTIAL | SKB_GSO_PARTIAL | Alexander Duyck | 터널 부분 오프로드 (외부 HW + 내부 SW) |
| 4.13 | ESP GSO | SKB_GSO_ESP | Steffen Klassert | IPsec xfrm offload, inline crypto |
| 4.14 | SCTP GSO | SKB_GSO_SCTP | Marcelo R. Leitner | SCTP 청크 단위 GSO |
| 4.18 | UDP GSO | SKB_GSO_UDP_L4 | Willem de Bruijn | UDP L4 세그먼트 분할, UDP_SEGMENT cmsg |
| 4.18 | UDP GRO | UDP_GRO sockopt | Eric Dumazet | UDP 수신 병합 (QUIC 지원) |
| 5.1 | GRO 해시 테이블 | gro_hash[8] | Li RongQing | gro_list를 8-bucket 해시(Hash)로 교체 → O(1) 검색 |
| 5.18 | XDP multi-buffer | - | Lorenzo Bianconi | XDP에서 비선형 패킷 부분 지원 |
| 5.19 | HW-GRO | NETIF_F_GRO_HW | Eric Dumazet | NIC 하드웨어 GRO (헤더 보존) |
| 6.3 | BIG TCP | gso/gro_max_size 확장 | Eric Dumazet | IPv6 64KB→512KB, Jumbogram 활용 |
| 6.3 | gso_ipv4_max_size | net_device 필드 | Eric Dumazet | IPv4/IPv6 GSO 크기 독립 제어 |
커널 소스 맵
GSO/GRO 관련 핵심 커널 소스 파일 위치입니다. 커널 소스 탐색 시 참고하세요:
| 소스 파일 | 핵심 함수/구조체(Struct) | 역할 |
|---|---|---|
include/linux/skbuff.h | skb_shared_info, skb_is_gso(), skb_gso_* | GSO/GRO 관련 skb 헤더 정의 |
include/linux/netdevice.h | napi_struct, gro_list, NETIF_F_* | NAPI GRO 구조체, NIC feature 플래그 |
include/net/gro.h | napi_gro_cb, GRO_HASH_BUCKETS | GRO 제어 블록, 해시 상수 |
net/core/gro.c | dev_gro_receive(), napi_gro_receive(), skb_gro_receive() | GRO 핵심 엔진 |
net/core/skbuff.c | skb_segment(), __skb_gso_segment() | GSO 분할 핵심 |
net/core/dev.c | validate_xmit_skb(), netif_skb_features() | GSO 분할 결정 지점 |
net/ipv4/tcp_offload.c | tcp4_gso_segment(), tcp4_gro_receive() | TCP GSO/GRO 콜백 |
net/ipv6/tcpv6_offload.c | tcp6_gso_segment(), tcp6_gro_receive() | IPv6 TCP GSO/GRO |
net/ipv4/udp_offload.c | __udp_gso_segment(), udp4_gro_receive() | UDP GSO/GRO 콜백 |
net/ipv4/af_inet.c | inet_gro_receive(), inet_gro_complete() | IPv4 GRO L3 콜백 |
net/ipv4/ip_output.c | ip_queue_xmit(), ip_finish_output_gso() | IP 출력 경로 GSO 처리 |
net/ipv4/tcp.c | tcp_sendmsg_locked(), tcp_should_autocork() | TCP 전송 + autocorking |
net/ipv4/tcp_output.c | tcp_write_xmit(), tcp_set_skb_tso_segs() | TCP 출력 + GSO 설정 |
net/xfrm/xfrm_device.c | xfrm_dev_state_add() | IPsec xfrm HW offload |
net/ipv4/esp4_offload.c | esp4_gso_segment() | ESP GSO 분할 |
net/sctp/offload.c | sctp_gso_segment() | SCTP GSO 분할 |
drivers/net/veth.c | veth_xmit(), veth_poll() | veth GSO 전달 |
drivers/net/virtio_net.c | virtnet_poll(), free_old_xmit() | virtio-net GSO/GRO |
drivers/net/vxlan/vxlan_core.c | vxlan_xmit_skb(), vxlan_gro_receive() | VXLAN 터널 GSO/GRO |
grep -rn 'gso_segment' net/— 프로토콜별 GSO 분할 함수 찾기grep -rn 'gro_receive' net/— 프로토콜별 GRO 병합 함수 찾기grep -rn 'NETIF_F_GSO' include/linux/netdev_features.h— GSO feature 비트 정의git log --oneline --all -- net/core/gro.c— GRO 변경 이력 추적
GRO Flush 타이밍과 napi_gro_flush 내부
GRO의 성능은 병합 시간(hold time)과 flush 타이밍의 균형에 달려 있습니다. 너무 오래 보관하면 지연이 증가하고, 너무 빨리 flush하면 병합 효율이 떨어집니다. 이 섹션에서는 flush 발생의 모든 경로와 타이밍 제어 메커니즘을 상세히 다룹니다.
/* === napi_gro_flush() 내부 구현 상세 === */
/* net/core/gro.c — napi_gro_flush의 실제 동작 */
void napi_gro_flush(struct napi_struct *napi, bool flush_old)
{
unsigned long bitmask = napi->gro_bitmask;
unsigned int i;
while ((i = __ffs(bitmask)) < GRO_HASH_BUCKETS) {
struct list_head *head = &napi->gro_hash[i].list;
struct sk_buff *skb, *p;
list_for_each_entry_safe(skb, p, head, list) {
if (flush_old && NAPI_GRO_CB(skb)->age == jiffies)
continue;
/* age == jiffies: 이번 jiffy에 추가된 skb
* → flush_old=true일 때 이번 사이클의 fresh skb는 유지
* → 다음 사이클에서 추가 병합 기회 제공 */
/* gro_complete 콜백 체인 호출 */
napi_gro_complete(napi, skb);
/* 내부적으로:
* 1. ptype->callbacks.gro_complete(skb, nhoff) 호출
* → tcp4_gro_complete(): gso_type, gso_segs 설정
* → inet_gro_complete(): IP total_length 업데이트
* 2. skb를 rx_list에 추가 (아직 상위 전달 안 함)
* 3. rx_count++ */
napi->gro_hash[i].count--;
if (!napi->gro_hash[i].count)
__clear_bit(i, &napi->gro_bitmask);
}
bitmask &= ~(1UL << i);
}
}
/* gro_flush_timeout 타이머의 정확한 동작 메커니즘 */
/*
* 1. napi_complete_done()에서 gro_list에 보류 skb가 남아있으면:
* → gro_flush_timeout > 0이면 hrtimer 시작
* → 타이머 만료 시 napi_schedule() 호출
* → 다음 poll에서 napi_gro_flush(napi, true) 실행
*
* 2. napi_defer_hard_irqs와의 조합:
* → napi_defer_hard_irqs=N: N번의 빈 poll을 허용
* → 빈 poll 동안 IRQ를 재활성화하지 않음
* → gro_flush_timeout 내에 새 패킷이 오면 병합 계속
* → 결과: busy-poll 효과 + GRO 병합 극대화
*
* 3. 타이밍 시나리오 (gro_flush_timeout=20000, napi_defer_hard_irqs=2):
* t=0: 패킷 도착, NAPI poll 시작
* t=0~10us: 100개 패킷 수신, GRO 병합 → 3개 super-skb
* t=10us: budget 미소진, napi_complete_done()
* → gro_list에 3개 skb 보류
* → hrtimer 20us 설정
* t=10~15us: 빈 poll 1회 (defer_hard_irqs 카운트)
* t=15~18us: 새 패킷 50개 도착, 기존 skb에 추가 병합
* t=18us: budget 미소진, napi_complete_done()
* → hrtimer 리셋 (20us)
* t=20us: IRQ 재활성화 (defer_hard_irqs 소진)
* t=30us: 새 패킷 없음, 타이머 만료
* → napi_gro_flush(napi, true)
* → 150개 패킷이 3개 super-skb로 전달
* → 스택 처리 3회만 수행 (50:1 비율)
*/
/* per-NAPI vs 전역 gro_flush_timeout (커널 6.6+) */
/*
* 커널 6.6 이전: net.core.gro_flush_timeout (전역 sysctl)
* → 모든 NAPI 인스턴스에 동일 적용
* → 단점: NIC마다 다른 워크로드에 대응 불가
*
* 커널 6.6+: per-NAPI 설정 가능
* → /sys/class/net/eth0/napi/N/gro_flush_timeout
* → 각 RX 큐별 독립 타이밍 제어
* → 실시간 큐와 벌크 큐에 다른 타이밍 적용 가능
*
* 예시:
* # echo 0 > /sys/class/net/eth0/napi/1/gro_flush_timeout # 큐 1: 저지연
* # echo 50000 > /sys/class/net/eth0/napi/2/gro_flush_timeout # 큐 2: 고처리량
*/
GRO_NORMAL vs GRO_MERGED_FREE 경로 상세
GRO 처리 결과는 5가지 gro_result 열거값으로 분기됩니다. 각 결과에 따라 skb의 생명주기와 메모리 관리(Memory Management)가 크게 달라집니다. 특히 GRO_NORMAL과 GRO_MERGED_FREE는 성능 최적화의 핵심 분기점입니다.
/* === GRO 결과별 상세 경로 분석 === */
/* net/core/gro.c — napi_skb_finish()에서 결과 처리 */
static gro_result_t napi_skb_finish(struct napi_struct *napi,
struct sk_buff *skb,
gro_result_t ret)
{
switch (ret) {
case GRO_NORMAL:
/* 병합 불가 — skb를 일반 수신 경로로 즉시 전달
*
* 발생 조건:
* a. 새 flow: gro_list에 매칭되는 기존 skb 없음
* + 버킷이 이미 MAX_GRO_SKBS(8)개 가득 참
* b. flush 플래그: 프로토콜 콜백이 병합 거부
* (SYN/FIN/RST, 비연속 seq, 옵션 불일치 등)
* c. GRO 비활성: ethtool -K eth0 gro off
* d. 프로토콜 미지원: GRO 콜백이 없는 프로토콜
*
* 처리:
* → gro_normal_one(napi, skb)
* → skb를 napi->rx_list에 추가
* → rx_count++
* → rx_count >= READ_ONCE(net_hotdata.gro_normal_batch)
* 이면 gro_normal_list() 호출하여 일괄 전달
*
* 성능 영향:
* → 병합 없이 개별 패킷으로 스택 통과
* → per-packet 오버헤드 전부 부담
* → 이 경로가 주류이면 GRO 효과 없음
*/
gro_normal_one(napi, skb, 1);
break;
case GRO_MERGED_FREE:
/* 병합 성공 + 현재 skb 해제 가능
*
* 가장 효율적인 경로:
* → 현재 skb의 데이터가 기존 skb에 복사/링크됨
* → 현재 skb 자체는 더 이상 필요 없음 → 즉시 해제
* → 기존 skb(head)만 gro_list에 남음
*
* skb 해제 방식:
* → skb가 NAPI alloc인 경우: napi_skb_cache에 반환 (빠름)
* → 일반 alloc인 경우: kfree_skb_partial() (느림)
*
* frag 기반 병합 시 발생:
* → 현재 skb의 페이지가 head skb의 frags[]로 이동
* → 현재 skb 쉘(shell)만 해제
* → page refcount는 유지 (데이터는 head skb가 소유)
*/
if (NAPI_GRO_CB(skb)->free == NAPI_GRO_FREE_STOLEN_HEAD)
napi_skb_free_stolen_head(skb);
else
__kfree_skb_defer(skb);
break;
case GRO_HELD:
/* gro_list에 보관 — 다음 패킷 병합 대기
*
* 발생 조건:
* → 새 flow이고 버킷에 공간 있음 (count < MAX_GRO_SKBS)
* → 또는 기존 flow에 첫 패킷 (아직 병합 대상 없음)
*
* 처리:
* → skb를 해당 버킷의 list 앞에 추가
* → count++
* → bitmask에 해당 버킷 비트 설정
* → age = jiffies 기록 (flush 타이밍 결정에 사용)
*
* 이후:
* → 같은 flow의 다음 패킷이 오면 GRO_MERGED로 병합
* → 타임아웃/napi_complete 시 flush
*/
break;
case GRO_MERGED:
/* 병합 성공 — 현재 skb도 gro_list에 유지
*
* frag_list 기반 병합 시 발생:
* → 현재 skb가 head skb의 frag_list 체인에 연결
* → 현재 skb 자체가 데이터의 일부로 남아야 함
* → 해제하면 안 됨 (데이터 손실)
*
* GRO_MERGED vs GRO_MERGED_FREE 차이:
* MERGED_FREE: 데이터만 복사/이동 → skb 쉘 해제 가능
* MERGED: skb 전체가 체인에 남아야 → 해제 불가
*
* head skb의 상태 업데이트:
* → p->len += skb->len (총 길이)
* → p->data_len += skb->len (비선형 데이터 크기)
* → NAPI_GRO_CB(p)->count++ (병합 패킷 수)
*/
break;
case GRO_CONSUMED:
/* 콜백이 직접 처리 완료 — skb 관리도 콜백 책임
*
* 발생 조건: 드문 경우
* → 특수 프로토콜 콜백이 skb를 직접 소비
* → napi_skb_finish()에서 추가 처리 불필요
*/
break;
}
return ret;
}
/* GRO_NORMAL 경로의 배치 최적화 — gro_normal_batch */
/*
* GRO_NORMAL로 빠진 skb도 개별 전달하지 않고 배치로 모음:
*
* net_hotdata.gro_normal_batch (기본 8):
* → rx_list에 8개 이상 쌓이면 gro_normal_list() 호출
* → netif_receive_skb_list_internal()로 일괄 전달
* → RCU read lock 1회, 프로토콜 핸들러 탐색 1회
* → 개별 netif_receive_skb() 대비 약 15% 성능 향상
*
* sysctl로 조절 가능:
* # sysctl -w net.core.gro_normal_batch=16
* → 더 큰 배치 = 더 높은 처리량, 약간 더 높은 지연
*/
XDP 환경에서의 GRO 비활성화/우회 이슈
XDP(eXpress Data Path)는 네트워크 스택 진입 전에 패킷을 처리하여 극한의 성능을 달성합니다. 그러나 XDP는 GRO보다 먼저 실행되므로, GRO 병합의 이점을 받을 수 없는 구조적 한계가 있습니다. 이 섹션에서는 XDP와 GRO의 상호작용 문제와 해결책을 상세히 다룹니다.
NIC 드라이버 내부의 패킷 처리 순서는 다음과 같습니다:
┌─ NIC IRQ ─────────────────────────────────────────────┐ │ 1. DMA로 패킷 수신 (ring buffer) │ │ 2. XDP 프로그램 실행 ← 이 시점에서는 개별 패킷 │ │ ├─ XDP_DROP: 즉시 드롭, skb 할당 없음 │ │ ├─ XDP_TX: 같은 NIC로 반송 │ │ ├─ XDP_REDIRECT: 다른 NIC/AF_XDP로 전달 │ │ └─ XDP_PASS: GRO 경로로 진입 │ │ 3. skb 할당 (XDP_PASS인 경우) │ │ 4. napi_gro_receive() → GRO 병합 시도 │ └────────────────────────────────────────────────────────┘
핵심 문제: XDP는 skb 할당 전에 xdp_buff/xdp_frame 단위로 동작하므로, GRO 병합된 super-skb를 XDP에서 볼 수 없습니다. XDP_TX/XDP_REDIRECT는 항상 개별 패킷 단위입니다.
XDP generic 모드와 GRO
- XDP generic (SKB 모드) — GRO 이후에 동작
- skb가 이미 할당된 상태에서 XDP가 실행되며, GRO로 병합된 super-skb가 XDP 프로그램에 전달될 수 있습니다. 하지만
xdp_buff는 단일 선형 버퍼를 가정하므로, 비선형 skb(frags[],frag_list)는 linearize가 필요합니다.__skb_linearize()비용(데이터 복사 + 메모리 할당)이 크며, 64KB skb linearize는 심각한 성능 저하를 야기합니다. - XDP native (드라이버 모드) — GRO 이전에 동작
- 개별 패킷 단위로 XDP를 실행하여 빠르게 처리합니다.
XDP_PASS후에만 GRO가 가능하며,XDP_TX/XDP_REDIRECT는 GRO를 우회합니다 (병합 이점 없음).
XDP multi-buffer (커널 5.18+)
XDP_PASS 후 GRO가 병합한 skb를 재분할 없이 활용할 수 있습니다. xdp_buff가 frags를 지원하지만(xdp_buff.mb = 1), GSO super-skb 수준의 대형 패킷은 미지원이며 주로 점보 프레임(9000B MTU)을 위한 것입니다.
# XDP multi-buffer 지원 확인: # bpftool net show dev eth0 → xdp: mode driver, id 42, features [mb]
XDP에서 GRO와 유사한 효과를 얻는 방법
- XDP_PASS → GRO → eBPF TC: XDP로 빠른 필터링(DROP) 후 PASS하면 GRO가 병합하고, TC eBPF에서 병합된 super-skb를 처리합니다. 병합 이점과 eBPF 유연성을 모두 확보할 수 있습니다.
- AF_XDP + 사용자 공간 병합:
XDP_REDIRECT→ AF_XDP 소켓으로 전달한 뒤, 사용자 공간에서 flow별 병합을 구현합니다 (DPDK 스타일). 완전한 제어권을 얻지만 구현이 복잡하고 커널 GRO보다 비효율적입니다. - veth + XDP (컨테이너 환경): 물리 NIC에서 XDP로 필터링 후
XDP_REDIRECT→ veth로 전달하고, veth peer에서 GRO를 활성화하여 병합합니다. 물리 NIC XDP 성능과 veth GRO 효율을 조합하는 방식입니다.
XDP redirect와 GSO 비호환성
xdp_frame은 GSO 메타데이터를 포함하지 않습니다 (gso_size, gso_type 필드 없음). 따라서 XDP_REDIRECT로 전달된 패킷은 항상 MSS 이하이며, GSO super-skb를 XDP로 전달하려면 먼저 분할이 필요합니다. dev_map_generic_redirect()에서 skb_gso_segment()로 개별 패킷으로 분할한 뒤 XDP를 실행하지만, 이 경우 성능 이점이 사라집니다.
설계 원칙: XDP는 와이어에 가까운 곳에서 동작하므로 GSO/GRO 같은 스택 최적화와 본질적으로 상충합니다.
컨테이너/veth 환경 GSO/GRO 주의사항
Kubernetes, Docker 등 컨테이너 환경에서 veth 쌍과 virtio-net을 통한 GSO/GRO 처리는 물리 NIC와 다른 특성을 보입니다. 특히 veth segmentation, virtio offload negotiation, 네트워크 네임스페이스(Namespace) 간 전달에서 예상치 못한 성능 문제가 발생할 수 있습니다.
veth의 NAPI 기반 수신 (drivers/net/veth.c)
- 커널 4.19 이전: veth는
netif_rx()로 수신하여 softirq에서 처리했습니다. GRO가 적용되지 않아 성능이 제한되었습니다. - 커널 4.19+: veth에 NAPI poll 함수가 추가되었습니다.
veth_poll()에서napi_gro_receive()를 호출하여 GRO 병합이 가능해졌고, 수신 성능이 대폭 개선되었습니다. - 커널 5.13+: veth XDP 지원이 강화되었습니다. veth peer에서 XDP 프로그램 실행이 가능하지만, GSO skb와 XDP 충돌 문제가 발생합니다.
veth + virtio-net (KVM Guest) 성능 최적화
KVM Guest에서 컨테이너를 운영하는 경우의 이중 가상화 경로입니다:
Guest App → Guest TCP → virtio-net TX → vhost-net → Host veth TX → Host Bridge → Physical NIC TX
GSO skb 전달 경로는 다음과 같습니다:
- Guest TCP가 64KB GSO skb를 생성합니다.
- virtio-net이
vnet_hdr에 GSO 정보를 기록하며, skb를 분할하지 않습니다. - Host vhost-net이
vnet_hdr에서 GSO 정보를 복원하여 Host skb에 설정합니다. - Host skb는 GSO skb로 Bridge/OVS를 통과합니다 (1개 skb).
- 물리 NIC TX에서 HW TSO로 분할하거나 SW GSO를 수행합니다.
전체 경로에서 분할이 한 번도 일어나지 않을 수 있습니다 (최적 경로). 단, Guest에서 TSO가 OFF이면 Guest TCP가 MSS 단위 skb 44개를 생성하여 각각 별도로 vring 전송되므로, 성능이 5~10배 저하됩니다.
Kubernetes CNI 플러그인별 GSO/GRO 영향
| CNI 플러그인 | 경로 | GSO/GRO 특성 |
|---|---|---|
| Flannel (VXLAN 모드) | Pod → veth → bridge → VXLAN encap → eth0 | VXLAN 터널 GSO 지원 필요 (NETIF_F_GSO_UDP_TUNNEL). NIC 미지원 시 SW 터널 GSO로 성능이 저하됩니다. ethtool -k eth0 | grep udp_tnl로 확인합니다. |
| Calico (BGP 모드) | Pod → veth → routing → eth0 | 터널이 없으므로 일반 GSO/GRO가 적용됩니다. 성능이 가장 좋습니다 (터널 오버헤드 없음). |
| Cilium (eBPF 모드) | Pod → veth → tc-bpf redirect → eth0 | eBPF가 skb를 직접 redirect하여 GSO skb를 그대로 전달합니다 (매우 효율적). 단, bpf_skb_change_head() 호출 시 GSO가 무효화될 수 있습니다. |
| OVN/OVS | Pod → veth → OVS → Geneve/VXLAN → eth0 | OVS 내부에서 GSO skb를 그대로 포워딩합니다. 터널 오프로드 NIC 지원이 핵심입니다. |
veth NAPI 모드 확인 및 활성화
# ethtool -k veth0 | grep gro generic-receive-offload: on ← GRO 활성 # cat /sys/class/net/veth0/gro_flush_timeout 0 ← 기본: 즉시 flush # veth peer에서 NAPI 활성 확인: # ls /sys/class/net/veth0/queues/rx-0/ → napi_id 파일이 있으면 NAPI 활성 # 성능 튜닝 (veth 양쪽 모두 설정해야 함): # ethtool -K veth0 gro on tso on gso on sg on # ethtool -K veth0 tx-checksum-ip-generic on # ip link show veth0 → peer 인터페이스 확인 # ethtool -K veth_peer gro on tso on gso on sg on
GSO Partial과 하드웨어 GSO 오프로드 경로
SKB_GSO_PARTIAL은 NIC가 터널의 외부(outer) 헤더만 처리할 수 있고 내부(inner) 패킷의 세그멘테이션은 소프트웨어가 담당해야 하는 경우를 위한 하이브리드 오프로드 메커니즘입니다. 이 경로의 이해는 터널 환경 성능 최적화에 필수적입니다.
/* === GSO_PARTIAL 상세 동작 메커니즘 === */
/* GSO_PARTIAL이 필요한 상황:
*
* [Outer Eth][Outer IP][Outer UDP][VXLAN][Inner Eth][Inner IP][Inner TCP][Payload]
*
* NIC capabilities 조합에 따른 경로:
*
* Case A: NIC가 NETIF_F_GSO_UDP_TUNNEL + NETIF_F_TSO 지원
* → 전체 HW 오프로드: NIC가 내부 TCP 분할 + 외부 헤더 복사
* → 가장 효율적 (CPU 무관)
*
* Case B: NIC가 NETIF_F_GSO_PARTIAL 지원
* → 하이브리드: 커널이 내부 TCP를 MSS 단위로 분할
* → 각 분할 세그먼트에 외부 헤더(VXLAN 등)를 붙임
* → 외부 헤더의 체크섬/길이는 NIC가 처리
* → 중간 수준 효율
*
* Case C: NIC가 터널 오프로드 미지원
* → 전체 SW GSO: 커널이 내부+외부 모두 분할/재계산
* → 가장 느림 (모든 체크섬 SW 계산)
*/
/* net/core/dev.c — GSO_PARTIAL 경로 결정 로직 */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
struct net_device *dev,
bool *again)
{
netdev_features_t features = netif_skb_features(skb);
if (skb_is_gso(skb)) {
/* gso_features_check(): NIC가 이 GSO 타입을 처리할 수 있는지 확인
*
* GSO_PARTIAL 판정 흐름:
* 1. skb의 gso_type 확인 (예: SKB_GSO_UDP_TUNNEL | SKB_GSO_TCPV4)
* 2. NIC features에 대응하는 NETIF_F_GSO_* 확인
* 3. 내부 GSO 타입(TCPv4)은 지원하지만
* 터널 GSO(UDP_TUNNEL)는 미지원인 경우:
* → NETIF_F_GSO_PARTIAL이 있으면 부분 오프로드
* → NETIF_F_GSO_PARTIAL도 없으면 전체 SW 분할
*/
struct sk_buff *segs;
segs = __skb_gso_segment(skb, features, true);
/* __skb_gso_segment() 내부에서:
* - 부분 오프로드 가능 → 내부만 분할, 외부 유지
* - 전체 분할 필요 → 모든 계층 분할
*/
}
...
}
/* GSO_PARTIAL 분할 과정 상세 */
/*
* 원본: [OuterIP][OuterUDP][VXLAN][InnerIP][InnerTCP][64KB payload]
*
* GSO_PARTIAL 분할 결과 (44개 세그먼트):
*
* Seg 1: [OuterIP'][OuterUDP'][VXLAN][InnerIP'][InnerTCP,seq=0][1460B]
* Seg 2: [OuterIP'][OuterUDP'][VXLAN][InnerIP'][InnerTCP,seq=1460][1460B]
* ...
* Seg 44:[OuterIP'][OuterUDP'][VXLAN][InnerIP'][InnerTCP,seq=62780][420B]
*
* 커널이 하는 일:
* → Inner TCP 분할 (seq 번호 증가, 체크섬)
* → Inner IP 헤더 업데이트 (total_length)
* → Outer VXLAN 헤더 복사
*
* NIC가 하는 일 (PARTIAL):
* → Outer UDP length 업데이트
* → Outer IP total_length 업데이트
* → Outer UDP 체크섬 (설정된 경우)
* → Outer IP 체크섬
*
* 결과: 외부 헤더 계산 부담만 NIC에 위임
*/
/* 하드웨어 GSO 오프로드 NIC별 지원 상황 */
/*
* Intel E810 (ice 드라이버):
* NETIF_F_TSO, NETIF_F_TSO6
* NETIF_F_GSO_UDP_TUNNEL, NETIF_F_GSO_UDP_TUNNEL_CSUM
* NETIF_F_GSO_GRE, NETIF_F_GSO_GRE_CSUM
* NETIF_F_GSO_PARTIAL
* NETIF_F_GSO_ESP (inline crypto)
* → 거의 모든 터널 GSO HW 지원
*
* Mellanox ConnectX-5/6/7 (mlx5 드라이버):
* 모든 위 플래그 + NETIF_F_GSO_UDP_L4
* → UDP GSO까지 HW 지원 (가장 광범위)
*
* Broadcom BCM5750X (bnxt 드라이버):
* NETIF_F_TSO, NETIF_F_TSO6
* NETIF_F_GSO_PARTIAL (터널용)
* → 터널은 부분 오프로드
*
* virtio-net:
* 모든 GSO 타입을 "지원" (광고)
* → 실제로는 Host가 SW 처리
* → Host의 물리 NIC HW가 최종 분할
*
* 확인 방법:
* # ethtool -k eth0 | grep -E 'segmentation|partial'
*/
GRO Cells와 지연 GRO (Delayed GRO)
GRO cells는 터널 디캡슐화 경로나 브리지 포워딩 경로에서 NAPI 컨텍스트 밖에서도 GRO를 적용할 수 있게 하는 메커니즘입니다. 또한 지연 GRO(Delayed GRO)는 GRO 처리를 softirq 컨텍스트로 미뤄 더 많은 패킷을 병합할 기회를 제공합니다.
/* === GRO Cells 구현 상세 === */
/* include/net/gro_cells.h */
struct gro_cell {
struct sk_buff_head napi_skbs; /* skb 큐 */
struct napi_struct napi; /* per-CPU 가상 NAPI */
};
struct gro_cells {
struct gro_cell __percpu *cells;
};
/* gro_cells_receive(): 터널/브리지에서 GRO 적용 */
static inline int gro_cells_receive(struct gro_cells *gcells,
struct sk_buff *skb)
{
struct gro_cell *cell;
struct net_device *dev = skb->dev;
/* preempt 비활성화 + 현재 CPU의 gro_cell 접근 */
cell = this_cpu_ptr(gcells->cells);
if (skb_queue_len(&cell->napi_skbs) > READ_ONCE(
dev->gro_max_size)) {
/* 큐 초과 → 드롭 */
atomic_long_inc(&dev->rx_dropped);
kfree_skb(skb);
return NET_RX_DROP;
}
/* skb를 per-CPU 큐에 추가 */
__skb_queue_tail(&cell->napi_skbs, skb);
/* NAPI poll 스케줄링 (아직 스케줄 안 됐으면) */
if (skb_queue_len(&cell->napi_skbs) == 1)
napi_schedule(&cell->napi);
return NET_RX_SUCCESS;
}
/* gro_cell_poll(): 가상 NAPI poll에서 GRO 수행 */
static int gro_cell_poll(struct napi_struct *napi, int budget)
{
struct gro_cell *cell = container_of(napi, struct gro_cell, napi);
struct sk_buff *skb;
int work_done = 0;
while (work_done < budget) {
skb = __skb_dequeue(&cell->napi_skbs);
if (!skb)
break;
/* 여기서 GRO 적용! */
napi_gro_receive(napi, skb);
work_done++;
}
if (work_done < budget)
napi_complete_done(napi, work_done);
return work_done;
}
/* GRO Cells를 사용하는 주요 코드 */
/*
* VXLAN 디캡슐화:
* vxlan_rcv() → gro_cells_receive(&vxlan->gro_cells, skb)
* → 내부 패킷을 GRO Cells로 전달
* → softirq에서 내부 패킷끼리 GRO 병합
*
* GRE 터널:
* gre_rcv() → gro_cells_receive()
*
* IP-in-IP 터널:
* ipip_rcv() → gro_cells_receive()
*
* macvlan:
* macvlan_broadcast() → gro_cells_receive()
*/
/* Delayed GRO (지연 GRO) 개념 */
/*
* 지연 GRO는 GRO 처리를 의도적으로 미뤄 병합 기회를 늘리는 전략:
*
* 1. gro_flush_timeout + napi_defer_hard_irqs 조합:
* → IRQ를 지연시켜 더 많은 패킷이 ring buffer에 쌓이게 함
* → 다음 poll에서 한꺼번에 GRO 병합
* → 결과: 병합 비율 향상 (30:1 → 50:1 등)
*
* 2. GRO Cells의 자연스러운 지연:
* → gro_cells_receive()는 softirq로 미룸
* → 여러 터널에서 디캡슐화된 패킷이 모인 후 일괄 GRO
* → 터널 환경에서 특히 효과적
*
* 3. TCP coalescing과의 시너지:
* → GRO에서 병합 후 TCP가 추가로 coalesce
* → tcp_try_coalesce(): GRO skb를 기존 소켓 버퍼에 병합
* → 최종적으로 recv() 한 번에 수백KB 수신 가능
*/
GSO/GRO와 Netfilter/conntrack 상호작용
GSO/GRO는 Netfilter/conntrack과 복잡한 상호작용을 합니다. 대형 GSO skb가 iptables/nftables 규칙을 통과하는 방식, conntrack 엔트리의 패킷/바이트 카운터 해석, NAT 변환 시 GSO skb 처리 등을 이해해야 올바른 방화벽 규칙을 작성할 수 있습니다.
nf_conntrack_in()에서 GSO skb 처리
conntrack은 GSO skb를 하나의 "패킷"으로 추적합니다:
- 5-tuple 매칭: src/dst IP, src/dst port, proto로 매칭하며, GSO skb도 동일 5-tuple이므로 정상적으로 매칭됩니다.
- 카운터 왜곡: packets 카운터는 1만 증가하지만 실제 와이어에는 44개 패킷이 전송됩니다. bytes 카운터(
skb->len= 64KB)는 정확합니다. - conntrack helper(ALG): FTP, SIP 등 ALG가 payload를 검사할 때 GSO skb의 전체 64KB payload를 검사합니다. 대부분의 경우 문제가 없지만(프로토콜 명령은 첫 부분에 위치), 드물게 64KB 중간에 제어 메시지가 있으면 놓칠 수 있습니다.
- NAT 변환: conntrack NAT은 skb 헤더만 변경하며, GSO skb의 L3/L4 헤더를 변경합니다. 분할 후 각 세그먼트에 동일 NAT이 적용되고,
CHECKSUM_PARTIAL이면 NIC가 체크섬을 재계산합니다. 정상 동작하며 성능 영향도 최소입니다.
Netfilter flowtable과 GSO
nf_flow_offload(커널 4.16+)는 첫 몇 패킷만 conntrack 전체 경로를 통과시키고, 이후에는 flowtable에서 fastpath 포워딩을 수행합니다. GSO skb도 flowtable fastpath를 사용하여 conntrack/NAT 오버헤드를 거의 제거합니다. 100Gbps 이상 포워딩 환경에서 핵심적인 기능입니다.
# flowtable 설정:
nft add flowtable inet filter f { hook ingress priority 0; devices = { eth0, eth1 }; }
nft add rule inet filter forward ct state established flow add @f
flowtable + GRO/GSO 조합에서는 GRO 병합된 64KB skb가 flowtable에서 1회 포워딩되며, conntrack table 탐색 1회 후 flowtable 캐시에 히트하여 NAT 변환 + TTL 감소 + 출력 NIC으로 직접 전달됩니다. ip_forward() 전체 경로를 우회합니다.
NFQUEUE와 GSO skb
NFQUEUE를 사용하는 IDS/IPS/DPI 시스템에서 주의해야 합니다:
- 자동 분할: NFQUEUE는 GSO skb를 받으면
nfqueue_enqueue()에서skb_gso_segment()를 호출하여 자동 분할합니다. 사용자 공간에는 MSS 단위 패킷이 전달되며, 사용자 공간 프로그램은 GSO를 인지할 필요가 없습니다. - 성능 영향: NFQUEUE에서의 GSO 분할은 GRO 효과를 상쇄합니다. 64KB가 44개 패킷으로 분할된 후 사용자 공간에 전달되므로, NFQUEUE 대역폭이 병목이 됩니다 (커널-사용자 공간 전환 44배).
- 해결책:
--queue-bypass로 연결 설정 후 bypass하거나, NFQUEUE + GRO off로 원본 패킷 단위 처리(정확하지만 느림)를 선택하거나, eBPF TC로 대체(GRO 이후 동작, GSO skb 직접 처리)합니다.
커널 소스 구조
GSO/GRO 관련 커널 코드는 net/core/gro.c, net/core/skbuff.c, net/core/dev.c 세 파일에 핵심 로직이 집중되어 있습니다. 이 섹션에서는 각 파일의 역할과 주요 함수 호출 관계를 상세히 분석합니다.
net/core/gro.c — GRO 엔진 핵심 (커널 6.x 기준)
- 1. GRO 유틸리티 함수
-
skb_gro_receive()— skb 데이터 병합 (frag/frag_list)skb_gro_reset_offset()— GRO 오프셋 초기화skb_gro_header_slow()— 비선형 skb 헤더 접근gro_pull_from_frag0()— frag0 최적화 해제
- 2. GRO 핵심 경로
-
dev_gro_receive()— GRO 매칭 + 프로토콜 콜백napi_gro_receive()— 드라이버 진입점 (skb 기반)napi_gro_frags()— 드라이버 진입점 (frag 기반)napi_gro_complete()— 병합 완료 후 상위 전달
- 3. GRO Flush
-
napi_gro_flush()— 전체 flushgro_normal_list()— rx_list 일괄 전달gro_normal_one()— 개별 skb 큐잉
- 4. GRO 결과 처리
-
napi_skb_finish()—gro_result에 따른 분기napi_frags_finish()— frag 기반 결과 처리
net/core/skbuff.c — GSO 분할 엔진
skb_segment()- GSO 분할의 핵심 함수입니다. 대형 skb를
gso_size단위로 분할하며, 선형/비선형 데이터 모두 처리합니다. 각 세그먼트에 헤더를 복사하고, 분할된 skb linked list(skb->next)를 반환합니다. __skb_gso_segment()skb_segment()의 래퍼 함수입니다. 프로토콜별 GSO 콜백을 먼저 호출하고, NIC feature와 GSO 타입을 매칭합니다.skb_gso_validate_network_len()- GSO 세그먼트가 MTU를 초과하지 않는지 검증합니다.
ip_finish_output()에서 호출됩니다.
net/core/dev.c — GSO/GRO 결정 지점
TX 경로 (GSO):
__dev_queue_xmit()
→ validate_xmit_skb() : GSO 분할 여부 결정
→ netif_needs_gso() : NIC가 이 GSO를 처리할 수 있는지
→ __skb_gso_segment() : 불가하면 SW 분할
→ skb_checksum_help() : 체크섬 SW fallback
→ dev_hard_start_xmit() : NIC 드라이버 xmit 호출
RX 경로 (GRO → 상위 전달):
netif_receive_skb_list_internal()
→ __netif_receive_skb_list_core()
→ __netif_receive_skb_core() : 프로토콜 핸들러 디스패치
→ deliver_skb() : ptype_all, ptype_base 전달
→ ip_rcv() : IPv4 입력
프로토콜별 오프로드 콜백 파일
| 소스 파일 | GSO 함수 | GRO 함수 |
|---|---|---|
net/ipv4/tcp_offload.c | tcp4_gso_segment(), tcp_gso_segment() | tcp4_gro_receive(), tcp4_gro_complete(), tcp_gro_receive() |
net/ipv6/tcpv6_offload.c | tcp6_gso_segment() | tcp6_gro_receive() |
net/ipv4/udp_offload.c | __udp_gso_segment() | udp4_gro_receive(), udp_gro_receive() |
net/ipv4/af_inet.c | - | inet_gro_receive(), inet_gro_complete() |
net/ipv6/ip6_offload.c | ipv6_gso_segment() | ipv6_gro_receive() |
오프로드 콜백 등록 메커니즘
net/ipv4/af_inet.c에서 IPv4 오프로드를 등록합니다. inet_init()에서 inet_add_offload(&tcpv4_offload, IPPROTO_TCP)와 inet_add_offload(&udpv4_offload, IPPROTO_UDP)를 호출하면, inet_offloads[IPPROTO_TCP]에 콜백이 등록됩니다. dev_gro_receive()에서 프로토콜 번호로 콜백을 룩업합니다.
커스텀 프로토콜에 GRO를 추가하려면 inet_add_offload(&my_offload, MY_PROTO_NUM)으로 자체 프로토콜에 GRO/GSO 콜백을 등록할 수 있습니다. 이 방식은 터널 프로토콜에서 주로 사용됩니다.
GRO 해시와 Flow 병합 판단 로직
GRO의 성능은 flow 매칭 속도에 직결됩니다. gro_hash[] 해시 테이블의 구조, RSS 해시를 활용한 버킷 선택, 프로토콜별 same_flow 판정 알고리즘을 상세히 분석합니다.
/* === GRO Flow 매칭 알고리즘 전체 분석 === */
/* 1단계: 해시 버킷 선택 (O(1)) */
static inline unsigned int gro_hash_bucket(struct sk_buff *skb)
{
/* skb->hash 원천:
* a. NIC RSS (Receive Side Scaling) 하드웨어 해시
* → Toeplitz 해시 알고리즘
* → 입력: src/dst IP, src/dst port, protocol
* → 출력: 32비트 해시값
* → NIC가 패킷 수신 시 자동 계산
* → 장점: CPU 오버헤드 0, 동일 flow = 동일 해시
*
* b. 소프트웨어 해시 (NIC 미지원 시)
* → __skb_get_hash() / skb_get_hash()
* → Jenkins 해시 또는 jhash2 사용
* → 성능: NIC RSS 대비 약간 느림
*
* c. skb_set_hash()로 드라이버가 직접 설정
*/
return skb_get_hash_raw(skb) & (GRO_HASH_BUCKETS - 1);
/* GRO_HASH_BUCKETS = 8 → 하위 3비트로 버킷 선택
* → 동일 flow는 항상 같은 버킷에 배치
* → 다른 flow와의 충돌: 선형 검색 (최대 8개)
* → 평균적으로 flow 수 / 8 개의 엔트리를 검색 */
}
/* 2단계: 버킷 내 skb 리스트 순회 (O(n), n ≤ 8) */
static enum gro_result dev_gro_receive(struct napi_struct *napi,
struct sk_buff *skb)
{
unsigned int bucket = gro_hash_bucket(skb);
struct list_head *gro_head = &napi->gro_hash[bucket].list;
/* 초기 same_flow 설정: 모든 기존 skb에 대해 1로 시작 */
list_for_each_entry(pp, gro_head, list) {
NAPI_GRO_CB(pp)->same_flow = 1;
/* 프로토콜 콜백이 0으로 변경할 것들을 걸러냄
* → "guilty until proven innocent" 반대:
* "같다고 가정하고, 다르면 배제" 패턴
* → 대부분의 경우 첫 비교에서 탈락 → 빠름 */
}
/* 3단계: L2 프로토콜 콜백 (eth_gro_receive) */
/* → vlan 태그, 프로토콜 타입 비교
* → 다르면 same_flow = 0 */
/* 4단계: L3 프로토콜 콜백 (inet_gro_receive) */
/* → src/dst IP 주소 비교 (4바이트 또는 16바이트 memcmp)
* → TOS, TTL 비교
* → IP 옵션 유무 확인
* → IP ID 연속성 확인 (DF 비트 없을 때)
* → 다르면 same_flow = 0 */
/* 5단계: L4 프로토콜 콜백 (tcp_gro_receive) */
/* → src/dst 포트 비교 (4바이트 비교 1회)
* → TCP 시퀀스 번호 연속성 확인
* → TCP 윈도우 크기 일치 확인
* → TCP 플래그 확인 (SYN/FIN/RST/URG → flush)
* → TCP 옵션 (타임스탬프) 확인
* → 모두 통과하면 same_flow = 1 유지 → 병합 수행 */
pp = call_gro_receive(ptype->callbacks.gro_receive, gro_head, skb);
...
}
/* GRO 해시 효율과 RSS 해시 품질의 관계 */
/*
* RSS 해시 품질이 GRO 효율에 직접 영향:
*
* 좋은 RSS 해시 (4-tuple 기반):
* → 동일 TCP 연결 = 동일 해시 = 동일 GRO 버킷
* → 다른 연결 = 다른 해시 = 다른 버킷 (대부분)
* → 버킷 내 검색 = 1~2개 (빠름)
* → GRO 병합률 높음
*
* 나쁜 RSS 해시 (src IP만 사용 등):
* → 같은 서버의 여러 연결이 같은 버킷에 몰림
* → 버킷 내 검색 = 6~8개 (느림)
* → same_flow 판정에 CPU 시간 낭비
* → GRO 효율 저하
*
* RSS 해시 타입 확인:
* # ethtool -n eth0 rx-flow-hash tcp4
* TCP over IPV4 flows use these fields for computing Hash flow key:
* IP SA, IP DA, L4 bytes 0 & 1, L4 bytes 2 & 3
* → 4-tuple (이상적)
*
* 해시 타입 변경 (2-tuple → 4-tuple):
* # ethtool -N eth0 rx-flow-hash tcp4 sdfn
* → s: src IP, d: dst IP, f: src port, n: dst port
*/
/* GRO 병합 실패 원인 분석 (디버깅) */
/*
* bpftrace로 GRO 병합 실패 원인 추적:
*
* # bpftrace -e '
* kprobe:tcp_gro_receive {
* @total = count();
* }
* kretprobe:tcp_gro_receive / retval == 0 / {
* @merged = count();
* }
* interval:s:5 {
* printf("merge ratio: %d/%d\n", @merged, @total);
* clear(@merged); clear(@total);
* }
* '
*
* 병합률이 낮은 일반적 원인:
* 1. 다수의 짧은 TCP 연결 (HTTP/1.1 keep-alive 없음)
* → 각 연결당 1~2 패킷 → 병합 기회 없음
* 2. 혼합 트래픽 (TCP + UDP + ICMP)
* → UDP/ICMP는 GRO 대상이 아닌 경우 많음
* 3. NIC RSS 해시 부재
* → skb->hash = 0 → 모든 패킷이 버킷 0에 집중
* 4. MTU 불일치
* → 경로상 다른 MTU → 패킷 크기 불균일 → 병합 실패
* 5. TCP 타임스탬프 옵션 불일치
* → 일부 중간 장비가 타임스탬프 수정 → 병합 거부
*/
코드 설명
- gro_hash_bucket()
skb->hash의 하위 3비트로GRO_HASH_BUCKETS(8)개 버킷 중 하나를 O(1)로 선택합니다. NIC RSS 하드웨어 해시(Toeplitz 알고리즘)가 있으면 CPU 오버헤드 없이 동일 flow를 같은 버킷에 배치합니다. - same_flow 패턴버킷 내 모든 기존 skb의
same_flow를 1로 초기화한 뒤, 프로토콜 콜백 체인(L2→L3→L4)이 불일치 항목을 0으로 변경하는 방식입니다. 대부분의 경우 첫 비교에서 빠르게 탈락하여 효율적입니다. - 프로토콜 콜백 체인
eth_gro_receive()→inet_gro_receive()→tcp_gro_receive()순서로 VLAN 태그, src/dst IP, TOS/TTL, 포트, 시퀀스 번호, TCP 플래그를 점진적으로 검증합니다. 각 단계에서 불일치하면same_flow = 0으로 조기 배제됩니다. - RSS 해시 품질4-tuple(src/dst IP + src/dst port) RSS 해시는 동일 TCP 연결을 동일 버킷에 배치하여 검색 범위를 1~2개로 줄입니다. 2-tuple 해시는 여러 연결이 같은 버킷에 집중되어 GRO 효율이 크게 저하되며,
ethtool -N eth0 rx-flow-hash tcp4 sdfn으로 4-tuple로 변경할 수 있습니다.
ethtool -n eth0 rx-flow-hash tcp4로 4-tuple RSS 해시 확인ethtool -k eth0 | grep gro로 GRO/HW-GRO 활성 확인- NIC의
rx_gro_packets와rx_packets비율로 병합 효율 계산 - 지연 허용 환경이면
gro_flush_timeout=20000설정 - 소켓(Socket)에
SO_INCOMING_NAPI_ID설정하여 NAPI-소켓 친화성 활용 bpftrace로 GRO 병합 비율 실시간(Real-time) 모니터링
관련 문서
GSO/GRO와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.