UDP 프로토콜
Linux UDP 프로토콜을 커널 내부 관점에서 심층 분석합니다.
udp_sock 구조체와 소켓 해시 테이블, 송신(udp_sendmsg)과 수신(__udp4_lib_rcv) 전체 경로,
체크섬 오프로드 메커니즘, UDP-GRO/GSO 고속 처리, UDP Encapsulation 프레임워크(VXLAN/WireGuard/IPsec),
멀티캐스트/브로드캐스트 수신, SO_REUSEPORT 부하 분산, UDP 터널 오프로드,
소켓 버퍼 튜닝과 Busy Polling, UDP-Lite 부분 체크섬까지
net/ipv4/udp.c의 모든 경로를 다룹니다.
핵심 요약
- 비연결형(Connectionless) — handshake 없이 즉시 전송하며, 연결 상태를 유지하지 않습니다.
- 메시지 경계 보존 — TCP와 달리 datagram 단위로 수신하여 애플리케이션 프로토콜 경계가 그대로 유지됩니다.
- UDP GRO/GSO — QUIC, WireGuard 등 고성능 워크로드의 핵심 최적화로 시스템콜 오버헤드를 최대 7배 이상 줄입니다.
- Encapsulation — VXLAN, WireGuard, IPsec NAT-T, L2TP 등 터널 프로토콜의 캡슐화 계층으로 사용됩니다.
- SO_REUSEPORT — 동일 포트에 다중 소켓을 바인드하여 멀티코어 환경에서 수신 부하를 분산합니다.
단계별 이해
- 구조체 이해
udp_sock과udp_table해시 테이블의 관계를 먼저 파악합니다. - TX/RX 경로 추적
udp_sendmsg()와__udp4_lib_rcv()의 전체 호출 체인을 따라갑니다. - 성능 기능 이해
GRO/GSO, SO_REUSEPORT, Busy Polling을 실제 워크로드에 맞춰 조정합니다. - 운영 안정화
큐 오버플로우/드롭 원인을/proc/net/snmp와ss로 진단하고 sysctl로 보정합니다.
UDP 심화
UDP(User Datagram Protocol)는 IP 위에 최소한의 전송 계층 기능만 추가한 프로토콜입니다. 연결 설정(handshake), 흐름 제어, 혼잡 제어, 재전송 메커니즘을 모두 생략하여 극도로 낮은 오버헤드와 최소 지연을 달성합니다. 이런 단순성 때문에 DNS, DHCP, NTP 같은 짧은 요청-응답 프로토콜, 실시간 미디어 스트리밍(RTP), 그리고 최근에는 QUIC(HTTP/3)과 같은 사용자 공간 전송 프로토콜의 기반으로 활발히 사용됩니다.
TCP vs UDP 핵심 비교
| 특성 | TCP | UDP |
|---|---|---|
| 연결 방식 | 연결 지향 (3-way handshake) | 비연결형 (즉시 전송) |
| 신뢰성 | 재전송, ACK, 순서 보장 | 최선 노력(best-effort), 보장 없음 |
| 흐름/혼잡 제어 | 윈도우 기반 내장 | 없음 (애플리케이션이 직접 구현) |
| 헤더 크기 | 20~60 바이트 | 8 바이트 고정 |
| 메시지 경계 | 바이트 스트림 (경계 없음) | 데이터그램 단위 보존 |
| 멀티캐스트 | 지원 안 함 | 지원 |
| 커널 상태 | tcp_sock (~2KB) | udp_sock (~수백 바이트) |
| 대표 용도 | HTTP, SSH, FTP, SMTP | DNS, DHCP, QUIC, WireGuard, RTP |
UDP의 커널 내 위치
리눅스 커널에서 UDP 구현은 주로 다음 소스 파일에 분포합니다:
| 소스 파일 | 역할 |
|---|---|
net/ipv4/udp.c | UDP IPv4 핵심 구현 (송수신, 해시 테이블, encap) |
net/ipv6/udp.c | UDP IPv6 핵심 구현 |
net/ipv4/udp_offload.c | UDP GSO/GRO 오프로드 처리 |
net/ipv4/udplite.c | UDP-Lite 프로토콜 (IP proto 136) |
net/ipv4/udp_tunnel_core.c | UDP 터널 소켓 설정 유틸리티 |
include/net/udp.h | UDP 내부 구조체/인라인 함수 |
include/uapi/linux/udp.h | 사용자 공간 UDP 헤더/소켓 옵션 |
include/net/udp_tunnel.h | 터널 관련 구조체/헬퍼 |
UDP 패킷 처리 흐름
NIC에서 수신된 UDP 패킷은 IP 계층을 거쳐 udp_rcv()에 도달합니다.
이후 __udp4_lib_rcv()에서 체크섬 검증과 소켓 lookup을 수행하며,
소켓이 발견되면 수신 큐에 전달하고, 없으면 ICMP Port Unreachable을 응답합니다.
udpv6_rcv() → __udp6_lib_rcv()를 거치며,
체크섬이 필수(0은 허용 안 됨)인 점과 소켓 lookup에 flow label을 사용하는 점이 다릅니다.
UDP 헤더와 udp_sock 구조체
UDP 헤더는 8바이트로 고정되어 있으며, IP 헤더의 프로토콜 필드(17)로 식별됩니다.
커널에서 UDP 소켓은 struct udp_sock으로 표현되며, inet_sock을 확장합니다.
/* include/uapi/linux/udp.h */
struct udphdr {
__be16 source; /* 소스 포트 (0~65535) */
__be16 dest; /* 목적지 포트 (0~65535) */
__be16 len; /* UDP 길이 (헤더 8바이트 + 페이로드) */
__sum16 check; /* 체크섬 (IPv4: 선택, IPv6: 필수) */
};
/* 고정 8바이트 헤더 — 연결 상태 없음, 순서 보장 없음 */
/* UDP 최대 페이로드 = 65535 - 8(UDP) - 20(IP) = 65507 바이트 */
/* include/net/udp.h — UDP 소켓 확장 구조체 */
struct udp_sock {
struct inet_sock inet; /* 상위: inet → sock 체인 */
/* 수신 경로 */
int pending; /* cork 보류 데이터 존재 여부 */
unsigned int corkflag; /* UDP_CORK 소켓 옵션 */
__u8 encap_type; /* 캡슐화 유형 (VXLAN, L2TP 등) */
unsigned char no_check6_tx : 1, /* IPv6 TX 체크섬 비활성화 */
no_check6_rx : 1, /* IPv6 RX 체크섬 비활성화 */
encap_enabled : 1, /* encap 콜백 활성화 여부 */
gro_enabled : 1; /* UDP GRO 활성화 여부 */
/* 캡슐화 콜백 */
int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);
int (*encap_err_lookup)(struct sock *sk, struct sk_buff *skb);
void (*encap_destroy)(struct sock *sk);
/* GRO 콜백 */
struct sk_buff *(*gro_receive)(struct sock *sk,
struct list_head *head,
struct sk_buff *skb);
int (*gro_complete)(struct sock *sk, struct sk_buff *skb, int nhoff);
/* 포워딩/리디렉트 */
struct sk_buff_head reader_queue; /* 수신 대기 큐 */
int forward_deficit; /* 포워드 할당 부족량 */
int forward_threshold; /* 포워드 할당 임계값 */
};
| 필드 | 타입 | 설명 |
|---|---|---|
inet | struct inet_sock | 소켓 주소, 포트, TTL 등 공통 정보 (상위 구조체) |
pending | int | UDP_CORK으로 보류 중인 데이터 존재 여부 (AF_INET/AF_INET6) |
encap_type | __u8 | 캡슐화 유형 (UDP_ENCAP_ESPINUDP, UDP_ENCAP_L2TPINUDP 등) |
encap_rcv | 함수 포인터 | 캡슐화 수신 콜백 (VXLAN, WireGuard 등이 등록) |
gro_enabled | 비트 필드 | UDP GRO 활성 여부 (setsockopt UDP_GRO로 설정) |
reader_queue | sk_buff_head | 소켓 수신 대기 큐 (backlog에서 이동) |
udp_sock → inet_sock → sock → sock_common.
C에서 구조체 첫 멤버를 캐스팅하여 상속을 구현하는 리눅스 커널의 전형적 패턴입니다.
container_of() 매크로로 sock에서 udp_sock을 역참조합니다.
UDP 소켓 해시 테이블과 Lookup
수신된 UDP 패킷을 올바른 소켓에 전달하려면 효율적인 소켓 검색이 필수입니다.
리눅스 커널은 struct udp_table에 두 개의 해시 테이블을 유지합니다:
포트 번호 기반의 hash 테이블과, 4-tuple(src IP, src port, dst IP, dst port) 기반의 hash2 테이블입니다.
/* include/net/udp.h — UDP 해시 테이블 */
struct udp_hslot {
struct hlist_head head; /* 해시 충돌 체인 */
int count; /* 슬롯 내 소켓 수 */
spinlock_t lock; /* per-slot 잠금 */
};
struct udp_table {
struct udp_hslot *hash; /* 1차: 포트 번호 해시 */
struct udp_hslot *hash2; /* 2차: 4-tuple 해시 (정밀 매칭) */
unsigned int mask; /* hash 테이블 크기 - 1 */
unsigned int log; /* log2(mask + 1) */
};
/* 전역 UDP 테이블 (IPv4) */
struct udp_table udp_table __read_mostly;
/* 해시 함수: jhash 기반 */
static inline unsigned int udp4_portaddr_hash(
const struct net *net,
__be32 saddr, unsigned int port)
{
return jhash_1word((__force u32)saddr, net_hash_mix(net))
^ port;
}
/* net/ipv4/udp.c — 소켓 lookup 핵심 경로 */
struct sock *__udp4_lib_lookup(
const struct net *net,
__be32 saddr, __be16 sport, /* 소스 주소/포트 */
__be32 daddr, __be16 dport, /* 목적지 주소/포트 */
int dif, int sdif, /* 디바이스 인덱스 */
struct udp_table *udptable,
struct sk_buff *skb)
{
unsigned int hash2, slot2;
struct udp_hslot *hslot2;
struct sock *result, *sk;
int score, badness;
/* 1단계: hash2 (4-tuple) 검색 */
hash2 = udp4_portaddr_hash(net, daddr, htons(dport));
slot2 = hash2 & udptable->mask;
hslot2 = &udptable->hash2[slot2];
result = NULL;
badness = 0;
udp_portaddr_for_each_entry_rcu(sk, &hslot2->head) {
score = compute_score(sk, net, saddr, sport,
daddr, dport, dif, sdif);
if (score > badness) {
badness = score;
result = sk;
}
}
if (result)
return result;
/* 2단계: 와일드카드 폴백 (INADDR_ANY) */
hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), htons(dport));
slot2 = hash2 & udptable->mask;
hslot2 = &udptable->hash2[slot2];
udp_portaddr_for_each_entry_rcu(sk, &hslot2->head) {
score = compute_score(sk, net, saddr, sport,
daddr, dport, dif, sdif);
if (score > badness) {
badness = score;
result = sk;
}
}
return result;
}
SO_REUSEPORT + eBPF 기반 분산을 사용하면 이 문제를 완화할 수 있습니다.
UDP 송신(TX) 경로
사용자 공간의 sendmsg() 시스템콜은 소켓 계층을 거쳐 udp_sendmsg()에 도달합니다.
이 함수는 라우팅 결정, IP 옵션 처리, 페이로드 복사, skb 생성을 수행하고,
최종적으로 udp_send_skb() → ip_send_skb()를 통해 IP 계층으로 전달합니다.
/* net/ipv4/udp.c — udp_sendmsg() 핵심 경로 (간략화) */
int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
struct inet_sock *inet = inet_sk(sk);
struct udp_sock *up = udp_sk(sk);
struct flowi4 *fl4;
struct rtable *rt;
int err, corkreq;
__be32 daddr, saddr;
__be16 dport;
/* 1. 목적지 결정: connected 소켓 또는 msg_name에서 추출 */
if (msg->msg_name) {
struct sockaddr_in *usin = msg->msg_name;
daddr = usin->sin_addr.s_addr;
dport = usin->sin_port;
} else {
/* connected 소켓: 기존 목적지 사용 */
daddr = inet->inet_daddr;
dport = inet->inet_dport;
}
/* 2. cmsg 처리: IP_PKTINFO, IP_TOS, UDP_SEGMENT(GSO) 등 */
if (msg->msg_controllen)
err = udp_cmsg_send(sk, msg, &ipc.gso_size);
/* 3. 라우팅 lookup */
rt = ip_route_output_flow(net, fl4, sk);
/* 4. corking 판별 (UDP_CORK 또는 MSG_MORE) */
corkreq = (up->corkflag || msg->msg_flags & MSG_MORE);
if (corkreq) {
/* cork: 데이터를 누적하고 나중에 한번에 전송 */
err = ip_append_data(sk, fl4, getfrag, msg, len, ...);
if (!corkreq)
err = udp_push_pending_frames(sk); /* 즉시 전송 */
} else {
/* 일반 전송: skb 생성 + 전송 */
skb = ip_make_skb(sk, fl4, getfrag, msg, len, ...);
err = udp_send_skb(skb, fl4, &cork);
}
return err;
}
/* net/ipv4/udp.c — udp_send_skb(): UDP 헤더 채움 + 체크섬 */
static int udp_send_skb(struct sk_buff *skb,
struct flowi4 *fl4,
struct udp_cork *cork)
{
struct udphdr *uh;
int len = skb->len;
/* UDP 헤더 공간 확보 */
uh = udp_hdr(skb);
uh->source = fl4->fl4_sport;
uh->dest = fl4->fl4_dport;
uh->len = htons(len);
uh->check = 0;
/* 체크섬 처리 (3가지 모드) */
if (is_udplite)
udplite_csum(skb);
else if (sk->sk_no_check_tx)
skb->ip_summed = CHECKSUM_NONE; /* 체크섬 없음 */
else if (skb->ip_summed == CHECKSUM_PARTIAL) {
/* 하드웨어 오프로드: NIC가 체크섬 계산 */
uh->check = ~udp_v4_check(len, fl4->saddr, fl4->daddr, 0);
skb->csum_start = skb_transport_header(skb) - skb->head;
skb->csum_offset = offsetof(struct udphdr, check);
} else {
/* 소프트웨어 체크섬 */
uh->check = udp_v4_check(len, fl4->saddr, fl4->daddr,
csum_partial(uh, len, 0));
if (!uh->check)
uh->check = CSUM_MANGLED_0; /* 0이면 0xFFFF로 변환 */
}
/* GSO 설정 */
if (cork->gso_size) {
skb_shinfo(skb)->gso_size = cork->gso_size;
skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
}
return ip_send_skb(net, skb);
}
setsockopt(fd, SOL_UDP, UDP_CORK, &1, 4)로 설정하면
여러 send() 호출의 데이터를 하나의 UDP 데이터그램으로 합쳐 전송합니다.
MSG_MORE 플래그도 동일한 효과를 제공합니다. 작은 메시지를 묶어 시스템콜 오버헤드를 줄일 때 유용합니다.
UDP 수신(RX) 경로
IP 계층의 ip_local_deliver()가 프로토콜 번호 17(UDP)을 확인하면
udp_rcv() → __udp4_lib_rcv()를 호출합니다.
이 함수에서 체크섬 검증, 소켓 lookup, encap 처리, 수신 큐 전달이 이루어집니다.
/* net/ipv4/udp.c — __udp4_lib_rcv() 핵심 경로 (간략화) */
int __udp4_lib_rcv(struct sk_buff *skb,
struct udp_table *udptable, int proto)
{
struct udphdr *uh;
struct sock *sk;
unsigned short ulen;
/* 1. 기본 유효성 검사 */
if (!pskb_may_pull(skb, sizeof(struct udphdr)))
goto drop;
uh = udp_hdr(skb);
ulen = ntohs(uh->len);
/* UDP 길이 필드 검증 */
if (ulen > skb->len || ulen < sizeof(*uh))
goto short_packet;
/* 2. 체크섬 초기화 */
if (udp4_csum_init(skb, uh, proto))
goto csum_error;
/* 3. 소켓 Lookup */
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk) {
struct dst_entry *dst = skb_dst(skb);
int ret;
/* 3a. Encapsulation 확인 */
if (udp_sk(sk)->encap_type) {
ret = udp_sk(sk)->encap_rcv(sk, skb);
if (ret <= 0)
return -ret; /* encap이 처리 완료 */
}
/* 3b. 유니캐스트 전달 */
return udp_unicast_rcv_skb(sk, skb, uh);
}
/* 4. 소켓 없음: ICMP 응답 + 드롭 */
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto drop;
if (udp_lib_checksum_complete(skb))
goto csum_error;
__UDP_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
drop:
kfree_skb(skb);
return 0;
}
/* net/ipv4/udp.c — 수신 큐 전달 (오버플로우 처리 포함) */
static int __udp_enqueue_schedule_skb(struct sock *sk,
struct sk_buff *skb)
{
struct sk_buff_head *list = &sk->sk_receive_queue;
int rmem, delta, amt;
int err = -ENOMEM;
/* 수신 버퍼 한계 검사 */
rmem = atomic_read(&sk->sk_rmem_alloc);
if (rmem > sk->sk_rcvbuf)
goto drop; /* UDP_MIB_RCVBUFERRORS++ */
/* 전방 할당(forward allocation) 확인 */
amt = sk->sk_rcvbuf - rmem;
delta = skb->truesize;
if (amt < delta)
goto drop;
/* 큐에 추가하고 대기 프로세스 깨우기 */
spin_lock(&list->lock);
__skb_queue_tail(list, skb);
spin_unlock(&list->lock);
if (!sock_flag(sk, SOCK_DEAD))
sk_data_ready(sk); /* epoll/select 웨이크업 */
return 0;
drop:
atomic_inc(&sk->sk_drops);
__UDP_INC_STATS(sock_net(sk), UDP_MIB_RCVBUFERRORS,
IS_UDPLITE(sk));
kfree_skb(skb);
return err;
}
sk_rcvbuf(소켓 수신 버퍼)보다 많은 데이터가 도착하면
UDP_MIB_RCVBUFERRORS가 증가하고 패킷이 드롭됩니다. 이는 /proc/net/snmp의 InErrors에 반영됩니다.
net.core.rmem_max와 SO_RCVBUF 조정으로 완화할 수 있습니다.
체크섬 오프로드
UDP 체크섬은 IPv4에서는 선택(0 허용), IPv6에서는 필수입니다. 리눅스 커널은 3가지 체크섬 처리 모드를 지원하며, 하드웨어 오프로드를 통해 CPU 부하를 크게 줄입니다.
| 모드 | skb->ip_summed | TX 동작 | RX 동작 |
|---|---|---|---|
| CHECKSUM_NONE | 없음 | 소프트웨어가 전체 체크섬 계산 | 소프트웨어가 전체 체크섬 검증 |
| CHECKSUM_PARTIAL | 부분 | 의사 헤더만 계산, NIC가 나머지 완성 | NIC가 부분 체크섬 제공, 소프트웨어가 최종 검증 |
| CHECKSUM_COMPLETE | 완전 | - | NIC가 전체 payload 체크섬 제공 |
| CHECKSUM_UNNECESSARY | 불필요 | - | NIC/드라이버가 체크섬 검증 완료 보고 |
/* net/ipv4/udp.c — 수신 체크섬 처리 */
static inline int udp4_csum_init(struct sk_buff *skb,
struct udphdr *uh, int proto)
{
const struct iphdr *iph = ip_hdr(skb);
if (uh->check == 0) {
/* IPv4: 체크섬 0 = 체크섬 비활성화 (RFC 768 허용) */
skb->ip_summed = CHECKSUM_UNNECESSARY;
return 0;
}
/* NIC가 CHECKSUM_COMPLETE를 보고한 경우 */
if (skb->ip_summed == CHECKSUM_COMPLETE) {
/* 의사 헤더 체크섬을 추가하여 최종 검증 */
if (!csum_tcpudp_magic(iph->saddr, iph->daddr,
skb->len, proto, skb->csum))
return 0; /* 검증 성공 */
}
/* 소프트웨어 폴백 */
skb->csum = csum_tcpudp_nofold(iph->saddr, iph->daddr,
skb->len, proto, 0);
return 0;
}
# NIC 체크섬 오프로드 상태 확인
$ ethtool -k eth0 | grep checksum
rx-checksumming: on
tx-checksumming: on
tx-checksum-ipv4: on
tx-checksum-ipv6: on
# UDP 체크섬 오프로드 비활성화 (디버깅 목적)
$ ethtool -K eth0 tx-checksum-ipv4 off
$ ethtool -K eth0 rx-checksumming off
udp_sock.no_check6_tx/no_check6_rx 비트로 제어합니다.
UDP GRO/GSO (커널 4.18+)
UDP GSO/GRO는 QUIC, WireGuard, DNS-over-HTTPS 등 고성능 UDP 애플리케이션을 위해 도입되었습니다. TCP의 TSO/GRO와 유사한 원리를 UDP에 적용하여 시스템콜 오버헤드를 줄입니다.
/* === UDP GSO (전송 방향) ===
*
* 기존 UDP 전송:
* sendmsg() x N회 -> N개 skb -> N번 스택 통과 -> N개 패킷
* -> 시스템콜 오버헤드 + per-packet 처리 비용
*
* UDP GSO:
* sendmsg() 1회 (대형 버퍼) -> 1개 대형 skb
* -> 1번 스택 통과 (routing, Netfilter, qdisc)
* -> validate_xmit_skb()에서 __udp_gso_segment()로 분할
* -> N개 UDP 데이터그램으로 전송
*
* 핵심 차이 (TCP GSO와):
* TCP: 시퀀스 번호 연속 -> 하드웨어도 분할 가능 (TSO)
* UDP: 각 데이터그램 독립 -> IP ID 증가, UDP length 조정만 필요
* -> SKB_GSO_UDP_L4 타입 사용
*/
/* 사용자 공간: cmsg로 UDP_SEGMENT 세그먼트 크기 지정 */
struct msghdr msg = {};
struct iovec iov = { .iov_base = buf, .iov_len = 64000 };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
char cmsgbuf[CMSG_SPACE(sizeof(uint16_t))];
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type = UDP_SEGMENT; /* GSO 세그먼트 크기 지정 */
cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
*(uint16_t *)CMSG_DATA(cm) = 1472; /* MTU(1500) - IP(20) - UDP(8) = 1472 */
/* 64KB 데이터를 한 번에 전송
* -> 커널이 1472바이트씩 ~43개 UDP 데이터그램으로 분할
* -> 마지막 세그먼트는 나머지 크기 (64000 % 1472 바이트)
*/
sendmsg(fd, &msg, 0);
/* net/ipv4/udp_offload.c — GSO 세그먼트 분할 핵심 */
struct sk_buff *__udp_gso_segment(
struct sk_buff *gso_skb,
netdev_features_t features,
bool is_ipv6)
{
struct sk_buff *segs;
unsigned int mss = skb_shinfo(gso_skb)->gso_size;
/* IP 레벨에서 세그먼트 분할 수행 */
segs = skb_segment(gso_skb, features & ~NETIF_F_GSO_MASK);
/* 각 세그먼트에 UDP 헤더 업데이트 */
for (seg = segs; seg; seg = seg->next) {
struct udphdr *uh = udp_hdr(seg);
/* UDP length = 세그먼트 크기 + 8 (UDP 헤더) */
uh->len = htons(seg->len - seg->transport_header
+ seg->head);
/* 체크섬 재계산 (부분 체크섬 또는 전체) */
if (seg->ip_summed == CHECKSUM_PARTIAL)
gso_reset_checksum(seg, seg->transport_header);
else
uh->check = gso_make_checksum(seg, ~uh->check);
}
return segs;
}
/* === UDP GRO (수신 방향) ===
*
* TCP GRO와 달리, UDP GRO는 소켓 옵션으로 명시적 활성화 필요:
* - TCP: 시퀀스 번호로 연속성 판단 -> 자동 병합
* - UDP: 시퀀스 번호 없음 -> 소켓이 GRO 허용 의사를 밝혀야 함
*
* 병합 기준:
* - 동일 src/dst IP + port (5-tuple 일치)
* - 동일 데이터그램 크기 (마지막 제외)
* - UDP 체크섬 일관성
* - 동일 NAPI ID (같은 NIC 큐에서 수신)
*/
int val = 1;
setsockopt(fd, SOL_UDP, UDP_GRO, &val, sizeof(val));
/* 수신 시: recvmsg()로 병합된 대형 버퍼 수신 */
struct msghdr rmsg = {};
char rcmsgbuf[CMSG_SPACE(sizeof(uint16_t))];
rmsg.msg_control = rcmsgbuf;
rmsg.msg_controllen = sizeof(rcmsgbuf);
recvmsg(fd, &rmsg, 0);
/* GRO_UDP_SEGMENT cmsg로 원래 세그먼트 크기 확인 */
struct cmsghdr *rcm;
for (rcm = CMSG_FIRSTHDR(&rmsg); rcm; rcm = CMSG_NXTHDR(&rmsg, rcm)) {
if (rcm->cmsg_level == SOL_UDP && rcm->cmsg_type == UDP_GRO) {
uint16_t gso_size = *(uint16_t *)CMSG_DATA(rcm);
/* gso_size = 각 UDP 데이터그램의 페이로드 크기
* 총 수신 바이트 / gso_size = 병합된 데이터그램 수
* 마지막 데이터그램은 gso_size보다 작을 수 있음 */
}
}
# NIC 레벨에서 UDP GSO HW 오프로드 확인
$ ethtool -k eth0 | grep udp-segmentation
tx-udp-segmentation: on # USO (HW UDP 세그먼트)
tx-udp_tnl-segmentation: on # 터널 내부 UDP GSO
# GRO 상태 확인
$ ethtool -k eth0 | grep generic-receive-offload
generic-receive-offload: on
| GSO/GRO 조건 | GSO (TX) | GRO (RX) |
|---|---|---|
| 활성화 방법 | UDP_SEGMENT cmsg | setsockopt(UDP_GRO) |
| 커널 버전 | 4.18+ | 5.0+ |
| HW 오프로드 | USO (tx-udp-segmentation) | NIC GRO + UDP GRO |
| gso_type | SKB_GSO_UDP_L4 | - |
| 최대 크기 | 64KB (UDP 길이 필드 제한) | NIC 의존 (보통 64KB) |
| 분할/병합 단위 | gso_size (보통 MTU - IP - UDP) | 동일 크기 데이터그램 |
UDP Encapsulation 프레임워크
UDP는 터널 프로토콜의 캡슐화 계층으로 광범위하게 사용됩니다.
커널은 struct udp_tunnel_sock_cfg를 통해 터널 드라이버가 특정 UDP 포트에
수신 콜백을 등록할 수 있는 프레임워크를 제공합니다.
| 프로토콜 | UDP 포트 | 용도 | 커널 모듈 | 캡슐화 계층 |
|---|---|---|---|---|
| VXLAN | 4789 | L2-over-UDP 가상 네트워크 (클라우드 오버레이) | drivers/net/vxlan/ |
Ethernet + VXLAN Header (8B) |
| Geneve | 6081 | Generic Network Virtualization Encapsulation | drivers/net/geneve.c |
Geneve Header (가변 TLV) |
| WireGuard | 51820 | VPN 터널 (Noise Protocol + ChaCha20) | drivers/net/wireguard/ |
WG 메시지 헤더 (4B type) |
| IPSec NAT-T | 4500 | ESP-in-UDP 캡슐화 (NAT 통과) | net/ipv4/udp.c (encap) |
Non-ESP Marker (4B) + ESP |
| L2TP | 1701 | L2 터널링 (PPP over UDP) | net/l2tp/ |
L2TP Header (가변) |
| GTP-U | 2152 | 모바일 네트워크 사용자 평면 | drivers/net/gtp.c |
GTP-U Header (8B) |
| MPLS-in-UDP | 6635 | MPLS 레이블을 UDP로 캡슐화 | net/mpls/ |
MPLS Label Stack |
/* 커널 UDP encap 등록 패턴 (VXLAN, WireGuard 등이 사용) */
struct udp_tunnel_sock_cfg cfg = {
.encap_type = UDP_ENCAP_VXLAN,
.encap_rcv = vxlan_rcv, /* 수신 콜백 */
.encap_err_rcv = vxlan_err_rcv, /* 에러 콜백 */
.encap_destroy = vxlan_del_work, /* 소멸 콜백 */
.gro_receive = vxlan_gro_receive, /* GRO 콜백 */
.gro_complete = vxlan_gro_complete,
};
setup_udp_tunnel_sock(net, sock, &cfg);
/* -> UDP 소켓이 특정 포트에서 터널 패킷을 수신하면
* 일반 UDP 처리 대신 encap_rcv 콜백을 호출
* -> 내부 패킷을 역캡슐화하여 다시 네트워크 스택에 주입 */
/* net/ipv4/udp_tunnel_core.c — 터널 소켓 설정 핵심 */
void setup_udp_tunnel_sock(struct net *net,
struct socket *sock,
struct udp_tunnel_sock_cfg *cfg)
{
struct sock *sk = sock->sk;
struct udp_sock *up = udp_sk(sk);
/* 캡슐화 콜백 등록 */
up->encap_type = cfg->encap_type;
up->encap_rcv = cfg->encap_rcv;
up->encap_err_rcv = cfg->encap_err_rcv;
up->encap_destroy = cfg->encap_destroy;
/* GRO 콜백 등록 (터널 GRO 지원) */
up->gro_receive = cfg->gro_receive;
up->gro_complete = cfg->gro_complete;
up->encap_enabled = 1;
/* 소켓을 encap 모드로 전환 */
udp_sk(sk)->encap_enabled = 1;
udp_tunnel_encap_enable(sock);
}
ethtool -K eth0 rx-udp_tunnel-port-offload on으로 NIC 레벨 터널 오프로드를 활성화하세요.
멀티캐스트와 브로드캐스트
UDP는 멀티캐스트(하나의 패킷을 여러 수신자에게)와 브로드캐스트(서브넷 전체)를 지원하는 유일한 표준 전송 프로토콜입니다.
커널에서 멀티캐스트 수신은 __udp4_lib_mcast_deliver()를 통해 처리됩니다.
/* net/ipv4/udp.c — 멀티캐스트 전달 */
static int __udp4_lib_mcast_deliver(
struct net *net,
struct sk_buff *skb,
struct udphdr *uh,
__be32 saddr, __be32 daddr,
struct udp_table *udptable,
int proto)
{
struct sock *sk, *first = NULL;
unsigned short hnum = ntohs(uh->dest);
struct udp_hslot *hslot;
unsigned int hash2, slot2;
int dif, sdif;
/* 목적지 포트로 해시 슬롯 검색 */
hash2 = udp4_portaddr_hash(net, daddr, hnum);
slot2 = hash2 & udptable->mask;
hslot = &udptable->hash2[slot2];
/* 매칭되는 모든 소켓에 skb 복사본 전달 */
sk_for_each_entry_offset_rcu(sk, &hslot->head, ...) {
if (compute_score(sk, net, saddr, uh->source,
daddr, hnum, dif, sdif) > 0) {
if (first) {
/* 두 번째 이후 소켓: skb 복제 */
struct sk_buff *skb1 = skb_clone(skb, GFP_ATOMIC);
if (skb1)
udp_unicast_rcv_skb(sk, skb1, uh);
} else {
first = sk;
}
}
}
if (first)
udp_unicast_rcv_skb(first, skb, uh); /* 첫 소켓: 원본 전달 */
else
kfree_skb(skb); /* 수신자 없음 */
return 0;
}
/* 사용자 공간: 멀티캐스트 그룹 참여 */
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("239.1.1.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
&mreq, sizeof(mreq));
/* 멀티캐스트 TTL 설정 (기본 1 = 로컬 서브넷) */
int ttl = 4;
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL,
&ttl, sizeof(ttl));
/* 멀티캐스트 루프백 비활성화 */
int loop = 0;
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP,
&loop, sizeof(loop));
/* Source-Specific Multicast (SSM): 특정 소스만 허용 */
struct ip_mreq_source mreqs;
mreqs.imr_multiaddr.s_addr = inet_addr("232.1.1.1");
mreqs.imr_sourceaddr.s_addr = inet_addr("10.0.0.100");
mreqs.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(fd, IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP,
&mreqs, sizeof(mreqs));
| 소켓 옵션 | 레벨 | 설명 |
|---|---|---|
IP_ADD_MEMBERSHIP | IPPROTO_IP | 멀티캐스트 그룹 참여 (IGMP Join) |
IP_DROP_MEMBERSHIP | IPPROTO_IP | 멀티캐스트 그룹 탈퇴 (IGMP Leave) |
IP_MULTICAST_TTL | IPPROTO_IP | 멀티캐스트 TTL (기본 1, 최대 255) |
IP_MULTICAST_LOOP | IPPROTO_IP | 루프백 여부 (기본 1=활성) |
IP_MULTICAST_IF | IPPROTO_IP | 송신 인터페이스 지정 |
IP_ADD_SOURCE_MEMBERSHIP | IPPROTO_IP | SSM: 특정 소스 허용 |
MCAST_JOIN_GROUP | IPPROTO_IP | 프로토콜 독립 멀티캐스트 참여 (IPv4/IPv6 공용) |
SO_BROADCAST | SOL_SOCKET | 브로드캐스트 송신 허용 (기본 비활성) |
SO_REUSEPORT 부하 분산
SO_REUSEPORT 소켓 옵션(커널 3.9+)은 동일한 IP:포트에 여러 소켓을 바인드할 수 있게 하며,
수신 패킷을 이들 소켓 간에 분산합니다. UDP 서버에서 멀티코어 확장성을 달성하는 핵심 메커니즘입니다.
/* SO_REUSEPORT UDP 서버 기본 패턴 */
int create_reuseport_socket(int port)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
/* SO_REUSEPORT 활성화 */
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
return fd;
}
/* 다중 워커 패턴 */
for (int i = 0; i < num_cpus; i++) {
int fd = create_reuseport_socket(8080);
/* 각 워커 스레드가 독립 소켓으로 recvmsg() */
pthread_create(&threads[i], NULL, worker_fn, (void *)(long)fd);
}
/* eBPF를 이용한 커스텀 REUSEPORT 분산 로직 */
/* BPF_PROG_TYPE_SK_REUSEPORT 프로그램 */
SEC("sk_reuseport")
int select_socket(struct sk_reuseport_md *ctx)
{
/* 4-tuple 해시 대신 커스텀 로직으로 소켓 선택 */
__u32 key = ctx->hash; /* 커널이 계산한 패킷 해시 */
/* QUIC Connection ID 기반 분산 (세션 고정) */
__u8 *data = ctx->data;
if (ctx->len >= 9) {
/* QUIC 첫 바이트 이후 Connection ID 추출 */
key = *(__u32 *)(data + 1);
}
/* 소켓 맵에서 선택 */
return bpf_sk_select_reuseport(ctx, &socket_map,
&key, 0);
}
| 분산 방식 | 설명 | 사용 사례 |
|---|---|---|
| 기본 해시 | 커널 4-tuple 해시 기반 균등 분산 | 일반 UDP 서버 |
| eBPF | BPF_PROG_TYPE_SK_REUSEPORT로 커스텀 로직 | QUIC Connection ID 기반 고정 |
| CBPF | 클래식 BPF (레거시) | 단순 해시 기반 분산 |
SO_REUSEPORT를 활용하여
다중 워커 프로세스가 UDP 53번 포트를 공유합니다.
이를 통해 단일 포트에서도 수백만 QPS를 처리할 수 있습니다.
UDP 터널 오프로드
최신 NIC는 UDP 터널(VXLAN, Geneve, GTP 등)의 내부 패킷까지 이해하여
체크섬, TSO/GSO, RSS 해시를 오프로드할 수 있습니다.
커널은 udp_tunnel_nic 프레임워크를 통해 NIC에 터널 포트 정보를 전달합니다.
/* include/net/udp_tunnel.h — 터널 포트 등록 */
struct udp_tunnel_info {
unsigned short type; /* UDP_TUNNEL_TYPE_VXLAN 등 */
sa_family_t sa_family; /* AF_INET 또는 AF_INET6 */
__be16 port; /* 터널 UDP 포트 */
u8 hw_priv; /* 하드웨어 프라이빗 데이터 */
};
/* VXLAN 디바이스 생성 시 NIC에 터널 포트 통보 */
void udp_tunnel_push_rx_port(struct net_device *dev,
struct socket *sock,
unsigned short type)
{
struct udp_tunnel_info ti;
ti.type = type;
ti.sa_family = sock->sk->sk_family;
ti.port = inet_sk(sock->sk)->inet_sport;
/* NIC 드라이버의 ndo_udp_tunnel_add() 호출 */
udp_tunnel_nic_add_port(dev, &ti);
}
# 터널 오프로드 상태 확인
$ ethtool -k eth0 | grep -E "tunnel|udp_tnl"
tx-udp_tnl-segmentation: on
tx-udp_tnl-csum-segmentation: on
rx-udp_tunnel-port-offload: on
# NIC에 등록된 터널 포트 확인 (커널 5.10+)
$ ethtool --show-tunnels eth0
Tunnel Information for eth0:
UDP port table 0:
Size: 4
Types: vxlan
Entries (1):
port 4789, vxlan
UDP port table 1:
Size: 4
Types: geneve, vxlan-gpe
No entries
소켓 버퍼 튜닝
UDP는 커널 내 흐름 제어가 없으므로 소켓 버퍼 크기와 시스템 수준 설정이 성능에 직접적인 영향을 미칩니다. 버퍼가 부족하면 패킷이 드롭되고, 과도하면 메모리를 낭비합니다.
| sysctl 파라미터 | 기본값 | 권장값 (고처리량) | 설명 |
|---|---|---|---|
net.core.rmem_default |
212992 (208KB) | 26214400 (25MB) | 소켓 수신 버퍼 기본 크기 |
net.core.rmem_max |
212992 (208KB) | 67108864 (64MB) | 소켓 수신 버퍼 최대 크기 (SO_RCVBUF 상한) |
net.core.wmem_default |
212992 (208KB) | 26214400 (25MB) | 소켓 송신 버퍼 기본 크기 |
net.core.wmem_max |
212992 (208KB) | 67108864 (64MB) | 소켓 송신 버퍼 최대 크기 (SO_SNDBUF 상한) |
net.core.netdev_max_backlog |
1000 | 10000~50000 | CPU별 수신 큐 최대 길이 |
net.core.optmem_max |
20480 | 40960 | 소켓 옵션 메모리 (cmsg 포함) 최대 크기 |
net.ipv4.udp_mem |
시스템 메모리 기반 | - | UDP 전체 메모리 사용량 제한 (low/pressure/high, 페이지 단위) |
net.ipv4.udp_rmem_min |
4096 | 8192 | UDP 소켓 최소 수신 버퍼 |
net.ipv4.udp_wmem_min |
4096 | 8192 | UDP 소켓 최소 송신 버퍼 |
# UDP 고처리량 서버 기본 튜닝
# 수신 버퍼
$ sysctl -w net.core.rmem_default=26214400
$ sysctl -w net.core.rmem_max=67108864
# 송신 버퍼
$ sysctl -w net.core.wmem_default=26214400
$ sysctl -w net.core.wmem_max=67108864
# CPU별 수신 큐 길이
$ sysctl -w net.core.netdev_max_backlog=50000
# 애플리케이션 레벨 버퍼 설정 (SO_RCVBUF 사용)
# 주의: SO_RCVBUF는 커널이 2배로 적용 (메타데이터 포함)
# SO_RCVBUFFORCE는 rmem_max 제한을 무시 (CAP_NET_ADMIN 필요)
/* 애플리케이션에서 소켓 버퍼 크기 설정 */
int rcvbuf = 33554432; /* 32MB (커널이 2x 적용 -> 실제 64MB) */
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
/* rmem_max 제한 무시 (root/CAP_NET_ADMIN 필요) */
setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf));
/* 설정된 버퍼 크기 확인 */
int actual;
socklen_t optlen = sizeof(actual);
getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &actual, &optlen);
/* actual = rcvbuf * 2 (커널이 메타데이터 공간 포함하여 2배 적용) */
SO_RCVBUF로 설정한 값을 내부적으로 2배로 적용합니다.
이는 sk_buff 메타데이터 오버헤드를 감안한 것입니다.
getsockopt(SO_RCVBUF)로 확인하면 설정한 값의 2배가 반환됩니다.
Busy Polling (저지연 수신)
Busy polling은 소켓이 패킷을 대기할 때 인터럽트를 기다리는 대신 NIC를 직접 폴링하여 수신 지연을 최소화하는 기법입니다. 커널 3.11+에서 지원되며, UDP에서 특히 효과적입니다.
# 시스템 전역 Busy Polling 설정
$ sysctl -w net.core.busy_poll=50 # epoll/poll busy 폴링 시간 (마이크로초)
$ sysctl -w net.core.busy_read=50 # 소켓 read busy 폴링 시간 (마이크로초)
# NIC Adaptive IRQ Coalescing 비활성화 (busy poll과 함께 사용)
$ ethtool -C eth0 adaptive-rx off rx-usecs 0 rx-frames 0
/* 소켓별 Busy Polling 설정 */
/* 방법 1: SO_BUSY_POLL (소켓 단위) */
int busy_poll_usecs = 50; /* 마이크로초 */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL,
&busy_poll_usecs, sizeof(busy_poll_usecs));
/* 방법 2: SO_PREFER_BUSY_POLL (커널 5.11+) */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
&prefer, sizeof(prefer));
/* NAPI ID 기반 소켓 바인딩 (커널 4.12+) */
int napi_id;
socklen_t len = sizeof(napi_id);
getsockopt(fd, SOL_SOCKET, SO_INCOMING_NAPI_ID,
&napi_id, &len);
/* napi_id로 소켓이 어떤 NIC 큐에서 패킷을 받는지 확인 */
| 파라미터 | 설명 | 적용 범위 |
|---|---|---|
net.core.busy_poll | epoll/poll 대기 시 busy 폴링 시간 (us) | 시스템 전역 |
net.core.busy_read | 소켓 read 시 busy 폴링 시간 (us) | 시스템 전역 |
SO_BUSY_POLL | 소켓별 busy 폴링 시간 (us) | 소켓 단위 |
SO_PREFER_BUSY_POLL | NAPI 스레드 대신 busy poll 선호 | 소켓 단위 (5.11+) |
SO_INCOMING_NAPI_ID | 소켓에 바인딩된 NAPI 인스턴스 ID | 읽기 전용 |
isolcpus로 격리하고, IRQ affinity를 설정한 뒤
busy polling 소켓을 해당 코어에 바인딩하는 것이 일반적인 패턴입니다.
UDP-Lite (RFC 3828)
UDP-Lite는 IP 프로토콜 번호 136을 사용하는 UDP 변형으로, 체크섬 커버리지를 부분적으로 적용할 수 있습니다. 오디오/비디오 스트리밍에서 일부 비트 오류를 허용하되 패킷 자체는 전달하고 싶을 때 유용합니다.
/* UDP-Lite: 부분 체크섬 지원 UDP 변형 (IP 프로토콜 136) */
/* 오디오/비디오 스트리밍에서 일부 비트 오류를 허용하되 전달 보장 */
/* 소켓 생성 */
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDPLITE);
/* 체크섬 커버리지 설정 (기본: 전체 패킷) */
int coverage = 8; /* 헤더 8바이트만 체크섬 보호, 페이로드는 무검증 */
setsockopt(fd, IPPROTO_UDPLITE, UDPLITE_SEND_CSCOV,
&coverage, sizeof(coverage));
setsockopt(fd, IPPROTO_UDPLITE, UDPLITE_RECV_CSCOV,
&coverage, sizeof(coverage));
/* 커널 내부: net/ipv4/udplite.c
* udplite4_lib_rcv() -> 체크섬 커버리지 범위만 검증
* 커버리지 미달 패킷은 수신 거부
*
* 체크섬 커버리지 값:
* 0 = 전체 패킷 (UDP와 동일)
* 8 = 헤더만 보호
* N = 처음 N 바이트만 보호
*/
| 속성 | UDP | UDP-Lite |
|---|---|---|
| IP 프로토콜 번호 | 17 | 136 |
| 체크섬 범위 | 전체 패킷 (또는 0=비활성) | 가변 (최소 8 바이트) |
| 헤더 구조 | src_port, dst_port, length, checksum | src_port, dst_port, coverage, checksum |
| 비트 오류 시 | 패킷 드롭 | 보호 영역만 검증, 비보호 영역은 전달 |
| 주요 사용처 | 일반 UDP 통신 | VoIP(Opus), 비디오(RTP), 센서 네트워크 |
| 커널 소스 | net/ipv4/udp.c | net/ipv4/udplite.c |
트러블슈팅
UDP 드롭 진단 체크리스트
# 1. UDP 프로토콜 통계 확인
$ cat /proc/net/snmp | grep Udp:
Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors InCsumErrors IgnoredMulti MemErrors
Udp: 1234567 890 456 987654 123 0 0 0 0
# 핵심 카운터:
# InErrors = 수신 오류 (체크섬 포함)
# RcvbufErrors = 소켓 버퍼 오버플로우로 드롭된 패킷
# InCsumErrors = 체크섬 오류
# MemErrors = 메모리 부족으로 드롭
# 2. 소켓별 드롭 확인
$ ss -u -n -e
UNCONN 0 0 *:8080 *:*
sk:ffff... skmem:(r212992,rb212992,t0,tb212992,f0,w0,o0,bl0,d123)
# d=drops
# 3. 상세 소켓 정보
$ ss -u -n -e -p
# -p: 프로세스 정보 포함
# -e: 확장 정보 (소켓 메모리 사용량)
# 4. NIC 레벨 드롭 통계
$ ethtool -S eth0 | grep -i drop
rx_dropped: 0
tx_dropped: 0
rx_queue_0_drops: 0
# 5. 전역 네트워크 통계
$ netstat -su
Udp:
1234567 packets received
890 packets to unknown port received
456 packet receive errors
987654 packets sent
123 receive buffer errors
0 send buffer errors
# 6. 실시간 모니터링 (1초 간격)
$ watch -n 1 'cat /proc/net/snmp | grep Udp:'
# 7. nstat으로 증분값 확인
$ nstat -a | grep -i udp
UdpInDatagrams 12345
UdpNoPorts 89
UdpInErrors 4
UdpOutDatagrams 9876
UdpRcvbufErrors 1
UdpInCsumErrors 0
UDP 드롭 원인별 대응
| 증상 | 카운터 | 원인 | 해결 방법 |
|---|---|---|---|
| RcvbufErrors 증가 | UDP_MIB_RCVBUFERRORS |
소켓 수신 버퍼 부족 | rmem_max 증가 + SO_RCVBUF 조정 |
| InErrors 증가 (Csum 아님) | UDP_MIB_INERRORS |
메모리 할당 실패, 소켓 큐 풀 | 메모리 확보, netdev_budget 증가 |
| InCsumErrors 증가 | UDP_MIB_CSUMERRORS |
네트워크 손상, NIC 오류 | 케이블/NIC 교체, 체크섬 오프로드 확인 |
| NoPorts 증가 | UDP_MIB_NOPORTS |
수신 소켓 없음 | 애플리케이션 바인딩 확인, 방화벽 점검 |
| NIC rx_dropped 증가 | ethtool -S | NIC ring 버퍼 부족 | ethtool -G eth0 rx 4096 |
| softnet_stat backlog | /proc/net/softnet_stat |
CPU backlog 큐 오버플로우 | netdev_max_backlog 증가 |
| MemErrors 증가 | UDP_MIB_MEMERRORS |
udp_mem 전역 제한 초과 |
net.ipv4.udp_mem 조정 |
# 종합 UDP 성능 진단 스크립트
# NIC ring 버퍼 최대화
$ ethtool -G eth0 rx 4096 tx 4096
# CPU별 softnet backlog 오버플로우 확인
$ cat /proc/net/softnet_stat
# 3번째 열이 0이 아니면 backlog 오버플로우 발생
# 열 순서: processed, dropped, time_squeeze, ...
# IRQ affinity 확인 (멀티큐 NIC)
$ cat /proc/interrupts | grep eth0
$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus
# UDP 메모리 사용량 확인
$ cat /proc/net/sockstat
UDP: inuse 42 mem 256
# mem = 현재 UDP 메모리 사용량 (페이지 단위)
# 커널 tracepoint로 드롭 추적
$ perf trace -e 'skb:kfree_skb' --filter 'reason == SKB_DROP_REASON_UDP_RCVBUF'
NoPorts 카운터가 급증하면 UDP flood 공격일 수 있습니다.
iptables -A INPUT -p udp --dport 53 -m limit --limit 1000/s -j ACCEPT
등의 rate limiting과 nf_conntrack 기반 상태 추적을 함께 사용하세요.
UDP 소켓 옵션 레퍼런스
UDP 소켓에서 사용 가능한 주요 소켓 옵션을 정리합니다.
SOL_UDP 레벨 옵션은 UDP 전용이고, SOL_SOCKET과 IPPROTO_IP 레벨 옵션도 함께 사용됩니다.
| 옵션 | 레벨 | 타입 | 설명 |
|---|---|---|---|
UDP_CORK | SOL_UDP | int (bool) | 데이터 합치기 (send 지연, uncork 시 전송) |
UDP_ENCAP | SOL_UDP | int | 캡슐화 유형 설정 (ESP, L2TP 등) |
UDP_SEGMENT | SOL_UDP (cmsg) | uint16_t | GSO 세그먼트 크기 (TX 방향) |
UDP_GRO | SOL_UDP | int (bool) | GRO 활성화 (RX 방향) |
UDP_NO_CHECK6_TX | SOL_UDP | int (bool) | IPv6 TX 체크섬 비활성화 |
UDP_NO_CHECK6_RX | SOL_UDP | int (bool) | IPv6 RX 체크섬 검증 비활성화 |
SO_RCVBUF | SOL_SOCKET | int | 수신 버퍼 크기 (커널 2x 적용) |
SO_SNDBUF | SOL_SOCKET | int | 송신 버퍼 크기 |
SO_REUSEPORT | SOL_SOCKET | int (bool) | 포트 재사용 + 부하 분산 |
SO_BUSY_POLL | SOL_SOCKET | int (us) | Busy polling 시간 |
SO_TIMESTAMP | SOL_SOCKET | int (bool) | 수신 타임스탬프 (SW) |
SO_TIMESTAMPNS | SOL_SOCKET | int (bool) | 수신 타임스탬프 (나노초) |
SO_TIMESTAMPING | SOL_SOCKET | int (flags) | HW/SW TX/RX 타임스탬프 |
IP_PKTINFO | IPPROTO_IP | int (bool) | 수신 인터페이스/주소 정보 (cmsg) |
IP_RECVORIGDSTADDR | IPPROTO_IP | int (bool) | 원래 목적지 주소 (TPROXY용) |
/* HW 타임스탬프를 활용한 UDP 지연 측정 */
int flags = SOF_TIMESTAMPING_RX_HARDWARE
| SOF_TIMESTAMPING_TX_HARDWARE
| SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
/* recvmsg()에서 cmsg로 타임스탬프 수신 */
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
if (cm->cmsg_type == SO_TIMESTAMPING) {
struct timespec *ts = (struct timespec *)CMSG_DATA(cm);
/* ts[0] = 소프트웨어 타임스탬프
* ts[1] = 하드웨어 변환 타임스탬프 (deprecated)
* ts[2] = 하드웨어 원시 타임스탬프 */
printf("HW timestamp: %ld.%09ld\n",
ts[2].tv_sec, ts[2].tv_nsec);
}
}
SO_TIMESTAMPING은 이 메커니즘의 사용자 공간 인터페이스입니다.
UDP 소켓 룩업 심화
__udp4_lib_lookup()은 UDP 수신 경로에서 가장 빈번하게 호출되는 함수 중 하나입니다.
이 함수의 내부 구현을 상세히 분석하면 성능 병목 지점을 정확히 이해할 수 있습니다.
커널 6.x에서는 hash2 테이블 우선 검색으로 대부분의 패킷을 O(1)에 가까운 시간에 매칭합니다.
compute_score() 스코어링 시스템
소켓 lookup의 핵심은 compute_score() 함수입니다.
각 후보 소켓에 대해 얼마나 정확하게 수신 패킷과 매칭되는지를 점수로 계산하여,
최고 점수 소켓을 선택합니다. 이 스코어링 시스템이 와일드카드 바인드와 구체적 바인드의 우선순위를 결정합니다.
/* net/ipv4/udp.c — compute_score() 상세 구현 */
static int compute_score(struct sock *sk,
const struct net *net,
__be32 saddr, __be16 sport,
__be32 daddr, unsigned short hnum,
int dif, int sdif)
{
struct inet_sock *inet;
int score;
bool dev_match;
/* 네트워크 네임스페이스 불일치 -> 즉시 제외 */
if (!net_eq(sock_net(sk), net) ||
udp_sk(sk)->udp_port_hash != hnum ||
ipv6_only_sock(sk))
return -1;
/* 기본 스코어 시작 */
score = 0;
inet = inet_sk(sk);
/* 목적지 주소 매칭 (+2) */
if (inet->inet_rcv_saddr) {
if (inet->inet_rcv_saddr != daddr)
return -1; /* 불일치 -> 제외 */
score += 4; /* 구체적 주소 바인드 */
}
/* 소스 주소 매칭 (+2) — connected 소켓 */
if (inet->inet_daddr) {
if (inet->inet_daddr != saddr)
return -1;
score += 4;
}
/* 소스 포트 매칭 (+1) — connected 소켓 */
if (inet->inet_dport) {
if (inet->inet_dport != sport)
return -1;
score += 4;
}
/* 디바이스 바인딩 (SO_BINDTODEVICE) (+2) */
dev_match = udp_sk_bound_dev_eq(net, sk->sk_bound_dev_if,
dif, sdif);
if (!dev_match)
return -1;
if (sk->sk_bound_dev_if)
score += 4;
/* 소켓에 REUSEPORT 커널 필터 적용 여부 */
if (READ_ONCE(sk->sk_reuseport_cb))
score++;
return score;
}
| 매칭 조건 | 스코어 증가 | 의미 | 해당 소켓 유형 |
|---|---|---|---|
inet_rcv_saddr == daddr | +4 | 구체적 주소 바인드 (bind(10.0.0.1)) | 특정 IP 바인드 |
inet_daddr == saddr | +4 | connected 소켓 (connect()된 UDP) | connect()된 UDP |
inet_dport == sport | +4 | connected 소켓 원격 포트 일치 | connect()된 UDP |
sk_bound_dev_if == dif | +4 | 디바이스 바인딩 (SO_BINDTODEVICE) | 인터페이스 고정 |
sk_reuseport_cb 존재 | +1 | REUSEPORT 그룹 소속 | SO_REUSEPORT |
| 와일드카드 바인드 | +0 | bind(0.0.0.0:port) | 일반 서버 |
connect()를 호출한 UDP 소켓은 스코어가 최대 +12까지 올라가
lookup에서 최우선으로 매칭됩니다. 또한 라우팅 캐시가 소켓에 고정되어
sendmsg()마다 라우팅 lookup을 반복하지 않아 성능이 약 10~15% 향상됩니다.
QUIC, DNS 리졸버 등에서 적극 활용됩니다.
RCU 보호와 해시 테이블 동시성
/* 소켓 lookup은 RCU read-side critical section에서 수행 */
struct sock *__udp4_lib_lookup_skb(
struct sk_buff *skb,
__be16 sport, __be16 dport,
struct udp_table *udptable)
{
const struct iphdr *iph = ip_hdr(skb);
/* RCU 보호 하에 lock-free lookup 수행
* - softirq 컨텍스트: 이미 rcu_read_lock_bh() 상태
* - per-slot spinlock은 소켓 추가/제거 시에만 획득
* - lookup 자체는 RCU로 lock-free */
return __udp4_lib_lookup(
dev_net(skb_dst(skb)->dev),
iph->saddr, sport,
iph->daddr, dport,
inet_iif(skb),
inet_sdif(skb),
udptable, skb);
}
/* 소켓 삽입 시 per-slot spinlock 사용 */
static int udp_lib_lport_inuse(
struct net *net,
__u16 num,
const struct udp_hslot *hslot,
struct sock *sk)
{
struct sock *sk2;
struct hlist_node *node;
/* spinlock 보호 하에 충돌 검사 */
spin_lock_bh(&hslot->lock);
udp_portaddr_for_each_entry(sk2, node, &hslot->head) {
/* 포트 충돌 검사 로직 */
}
spin_unlock_bh(&hslot->lock);
return 0;
}
uhash_entries=N으로 수동 설정이 가능합니다.
대규모 UDP 서버(수만 소켓)에서는 테이블 크기를 늘려 해시 충돌을 줄이는 것이 좋습니다.
TX 경로 상세 심화
udp_sendmsg()의 내부를 더 깊이 분석합니다.
연결 상태 판별, cmsg 처리, cork/uncork 메커니즘, GSO 통합, 그리고 ip_make_skb()에서
ip_send_skb()까지의 전체 skb 생명주기를 다룹니다.
ip_make_skb()와 getfrag 콜백
/* net/ipv4/ip_output.c — ip_make_skb() 핵심 동작
*
* 사용자 공간 데이터를 커널 skb로 변환하는 핵심 함수.
* UDP sendmsg에서 호출되며, 다음을 수행:
* 1. skb 할당 (alloc_skb)
* 2. IP 헤더 공간 예약 (skb_reserve)
* 3. getfrag 콜백으로 사용자 데이터 복사
* 4. frags 배열로 대형 데이터 scatter/gather 처리
*/
struct sk_buff *ip_make_skb(
struct sock *sk,
struct flowi4 *fl4,
int (*getfrag)(void *, char *, int, int, int, struct sk_buff *),
void *from, int length,
int transhdrlen,
struct ipcm_cookie *ipc,
struct rtable **rtp,
struct inet_cork *cork,
unsigned int flags)
{
/* 1. cork 초기화 (라우트, fragsize, TTL 등) */
ip_setup_cork(sk, cork, ipc, rtp);
/* 2. __ip_append_data()로 skb 구성
* - 첫 프레임: alloc_skb + transport 헤더 공간 확보
* - 이후: skb_append_pagefrags 또는 frags[] 사용
* - getfrag: ip_generic_getfrag() -> copy_from_iter() */
err = __ip_append_data(sk, fl4, &queue,
&cork->base, sk->sk_allocation,
getfrag, from, length,
transhdrlen, flags);
/* 3. __ip_make_skb()로 최종 skb 조립
* - IP 헤더 채움 (protocol, TTL, src/dst 등)
* - 큐의 모든 frags를 하나의 skb로 병합 */
return __ip_make_skb(sk, fl4, &queue, &cork->base);
}
UDP_CORK 동작 상세
/* UDP_CORK 모드에서의 데이터 축적과 일괄 전송 */
/* 애플리케이션 패턴 */
int cork = 1;
setsockopt(fd, SOL_UDP, UDP_CORK, &cork, sizeof(cork));
/* 여러 send() 호출 — 데이터가 커널에 누적됨 */
send(fd, header, header_len, 0); /* ip_append_data() */
send(fd, payload, payload_len, 0); /* ip_append_data() */
send(fd, trailer, trailer_len, 0); /* ip_append_data() */
/* uncork — 누적된 모든 데이터를 하나의 UDP 데이터그램으로 전송 */
cork = 0;
setsockopt(fd, SOL_UDP, UDP_CORK, &cork, sizeof(cork));
/* -> udp_push_pending_frames()
* -> udp_send_skb() 호출
* -> 단일 UDP 데이터그램 전송 */
/* 또는 MSG_MORE 플래그로 per-send cork 제어 */
send(fd, data1, len1, MSG_MORE); /* 누적 */
send(fd, data2, len2, MSG_MORE); /* 누적 */
send(fd, data3, len3, 0); /* 최종: 모든 데이터 한번에 전송 */
| TX 모드 | 시스템콜 수 | UDP 패킷 수 | 스택 통과 횟수 | 장점 |
|---|---|---|---|---|
| 일반 전송 | N | N | N | 단순 |
| UDP_CORK | N+1 (uncork) | 1 | 1 | 작은 메시지 결합 |
| MSG_MORE | N | 1 | 1 | per-send 제어 |
| UDP GSO | 1 | N (커널 분할) | 1 | 대량 전송 최적화 |
connect()를 호출한 UDP 소켓은 다음과 같은 TX 경로 최적화를 얻습니다:
(1) 라우팅 캐시가 소켓에 고정되어 FIB lookup 생략,
(2) 목적지 주소를 msg_name에서 복사할 필요 없음,
(3) security_socket_sendmsg() 체크가 간소화.
이로 인해 전체 TX 경로에서 약 10~20%의 CPU 사이클을 절감합니다.
RX 경로 상세 심화
UDP 수신 경로는 udp_rcv()에서 시작하여 __udp4_lib_rcv(),
udp_unicast_rcv_skb(), udp_queue_rcv_skb(),
__udp_enqueue_schedule_skb()를 거쳐 최종적으로 sk_receive_queue에 도달합니다.
각 단계의 세부 동작과 드롭 포인트를 상세히 분석합니다.
udp_queue_rcv_skb() 내부 로직
/* net/ipv4/udp.c — udp_queue_rcv_skb() 상세 */
static int udp_queue_rcv_skb(struct sock *sk,
struct sk_buff *skb)
{
struct udp_sock *up = udp_sk(sk);
int is_udplite = IS_UDPLITE(sk);
/* 1. 소켓 BPF 필터 적용 (SO_ATTACH_FILTER) */
if (sk_filter_trim_cap(sk, skb, sizeof(struct udphdr)))
goto drop;
/* 2. UDP-Lite 체크섬 커버리지 검증 */
if (is_udplite) {
if (udplite_checksum_complete(skb))
goto csum_error;
} else if (skb->ip_summed != CHECKSUM_UNNECESSARY &&
skb->ip_summed != CHECKSUM_COMPLETE) {
/* 지연된 체크섬 검증 (lazy checksum) */
if (udp_lib_checksum_complete(skb))
goto csum_error;
}
/* 3. reader_queue (backlog) 또는 receive_queue로 전달 */
if (sock_owned_by_user(sk)) {
/* 소켓이 사용자 컨텍스트에서 잠김 -> backlog 큐 */
if (!__udp_enqueue_schedule_skb(sk, skb))
goto drop;
} else {
/* 직접 receive_queue에 추가 */
__udp_enqueue_schedule_skb(sk, skb);
}
return 0;
csum_error:
__UDP_INC_STATS(sock_net(sk), UDP_MIB_CSUMERRORS, is_udplite);
drop:
__UDP_INC_STATS(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
atomic_inc(&sk->sk_drops);
kfree_skb(skb);
return -1;
}
reader_queue와 backlog 처리
/* UDP 수신 큐 2단계 구조
*
* 1. sk_receive_queue: 기본 수신 큐
* - recvmsg()에서 직접 읽는 큐
* - spinlock으로 보호
*
* 2. reader_queue: UDP 전용 보조 큐 (커널 4.10+)
* - softirq와 recvmsg() 사이의 경합을 줄이기 위한 큐
* - recvmsg()가 reader_queue에서 먼저 읽고,
* 비어있으면 sk_receive_queue로 전환
*
* 이 2단계 구조로 recvmsg()와 softirq의 락 경합이 감소:
* softirq: sk_receive_queue에 추가 (spinlock)
* recvmsg: reader_queue에서 읽기 (lock-free)
* -> 비어있으면 splice로 한번에 이동
*/
/* net/ipv4/udp.c — udp_recvmsg() 수신 큐 접근 */
int udp_recvmsg(struct sock *sk, struct msghdr *msg,
size_t len, int flags, int *addr_len)
{
struct sk_buff_head *queue;
struct sk_buff *skb;
/* reader_queue에서 먼저 시도 */
queue = &udp_sk(sk)->reader_queue;
skb = __skb_try_recv_from_queue(sk, queue, flags, ...);
if (!skb) {
/* reader_queue 비어있음 -> receive_queue에서 splice */
spin_lock_bh(&sk->sk_receive_queue.lock);
skb_queue_splice_tail_init(
&sk->sk_receive_queue, queue);
spin_unlock_bh(&sk->sk_receive_queue.lock);
skb = __skb_try_recv_from_queue(sk, queue, flags, ...);
}
/* skb에서 데이터를 사용자 버퍼로 복사 */
if (skb)
err = skb_copy_datagram_msg(skb, sizeof(struct udphdr),
msg, len);
/* cmsg 전달: 타임스탬프, GRO 크기, pktinfo 등 */
ip_cmsg_recv(msg, skb);
return err;
}
recvmsg()에 MSG_PEEK 플래그를 사용하면
데이터를 큐에서 제거하지 않고 복사만 합니다.
이는 UDP에서 패킷 헤더를 먼저 확인하고 적절한 버퍼 크기로 다시 수신할 때 유용하지만,
매번 큐를 순회하므로 고빈도 호출 시 성능에 영향을 줄 수 있습니다.
recvmsg(MSG_TRUNC)로 실제 데이터그램 크기를 확인하는 것이 더 효율적입니다.
체크섬 오프로드 심화
UDP 체크섬 처리는 TX와 RX 양쪽에서 하드웨어 오프로드를 활용할 수 있습니다.
커널 내부의 ip_summed 필드 상태 전이와 NIC 오프로드 동작의 정확한 관계를 이해하면
체크섬 관련 문제를 효과적으로 디버깅할 수 있습니다.
/* NIC 드라이버의 체크섬 보고 예시 (mlx5 등) */
static void handle_rx_csum(struct sk_buff *skb,
u32 cqe_status)
{
if (cqe_status & CQE_RX_IP_CSUM_OK &&
cqe_status & CQE_RX_L4_CSUM_OK) {
/* NIC가 IP + L4 체크섬 모두 검증 완료 */
skb->ip_summed = CHECKSUM_UNNECESSARY;
} else if (cqe_status & CQE_RX_L4_CSUM_MASK) {
/* NIC가 L4 체크섬 계산 결과 제공 */
skb->ip_summed = CHECKSUM_COMPLETE;
skb->csum = csum_unfold(cqe->l4_checksum);
} else {
/* NIC가 체크섬 지원 안 함 -> 소프트웨어 처리 */
skb->ip_summed = CHECKSUM_NONE;
}
}
# 체크섬 오프로드 기능별 상태 확인
$ ethtool -k eth0 | grep -E "checksumming|checksum"
rx-checksumming: on # RX 체크섬 오프로드
tx-checksumming: on # TX 체크섬 오프로드
tx-checksum-ipv4: on
tx-checksum-ip-generic: on
tx-checksum-ipv6: on
tx-checksum-fcoe-crc: off [not requested]
tx-checksum-sctp: off [not requested]
# TX 체크섬 관련 tcpdump 문제 해결
# 방법 1: tcpdump에서 체크섬 검증 비활성화
$ tcpdump -K -i eth0 udp port 8080
# 방법 2: TX 오프로드 일시 비활성화 (디버깅 전용)
$ ethtool -K eth0 tx-checksum-ipv4 off
# 디버깅 후 반드시 재활성화
$ ethtool -K eth0 tx-checksum-ipv4 on
tx-checksumming: off로 설정하면
모든 체크섬이 소프트웨어로 계산되어 CPU 사용률이 5~15% 증가합니다.
디버깅 목적으로만 일시적으로 비활성화하고, 완료 후 반드시 재활성화하세요.
UDP-GRO/GSO 상세 심화
UDP GSO/GRO의 커널 내부 구현을 더 깊이 분석합니다. GSO 세그먼트 분할의 정확한 메커니즘, GRO 병합 조건, 그리고 터널 프로토콜에서의 중첩 GRO/GSO 처리를 다룹니다.
GSO 세그먼트 분할 상세
/* net/ipv4/udp_offload.c — udp4_ufo_fragment()
*
* UDP GSO 분할은 2가지 경로로 수행:
*
* 1. SKB_GSO_UDP_L4 (커널 4.18+, 권장)
* - L4 레벨 분할: 각 세그먼트가 독립 UDP 데이터그램
* - IP ID가 각 세그먼트마다 증가
* - __udp_gso_segment()에서 처리
*
* 2. SKB_GSO_UDP (레거시, deprecated)
* - IP 프래그먼테이션 기반 분할
* - 첫 프래그먼트만 UDP 헤더 포함
* - 중간 라우터에서 재조립 문제 발생 가능
* - 커널 6.x에서 제거 진행 중
*/
/* GSO 분할 시 각 세그먼트의 필드 업데이트 */
static struct sk_buff *__udp_gso_segment_list_csum(
struct sk_buff *segs)
{
struct sk_buff *seg;
unsigned int offset = 0;
skb_walk_frags(segs, seg) {
struct udphdr *uh = udp_hdr(seg);
/* 각 세그먼트에 고유한 UDP 헤더 */
uh->source = udp_hdr(segs)->source;
uh->dest = udp_hdr(segs)->dest;
uh->len = htons(seg->len - skb_transport_offset(seg));
/* 체크섬 재계산 (부분 또는 전체) */
if (seg->ip_summed == CHECKSUM_PARTIAL) {
/* 의사 헤더 체크섬만 설정, NIC가 완성 */
uh->check = ~csum_tcpudp_magic(
ip_hdr(seg)->saddr,
ip_hdr(seg)->daddr,
ntohs(uh->len), IPPROTO_UDP, 0);
}
}
return segs;
}
GRO 병합 조건 상세
| GRO 병합 조건 | 필수 여부 | 설명 |
|---|---|---|
| 5-tuple 일치 | 필수 | src/dst IP + src/dst port + protocol 동일 |
| UDP_GRO 소켓 옵션 | 필수 | 수신 소켓에 setsockopt(UDP_GRO) 설정 필요 |
| 동일 데이터그램 크기 | 필수 (마지막 제외) | 마지막 데이터그램만 작을 수 있음 |
| NAPI ID 일치 | 권장 | 같은 NIC 큐에서 수신된 패킷만 병합 |
| 체크섬 일관성 | 필수 | 모든 데이터그램의 체크섬이 유효해야 함 |
| 연속 수신 | 권장 | 같은 NAPI poll에서 수신된 패킷 우선 병합 |
| gro_cells 미초과 | 필수 | GRO 리스트 크기 제한 (기본 8) |
/* net/ipv4/udp_offload.c — udp_gro_receive() 핵심 로직 */
struct sk_buff *udp_gro_receive(
struct list_head *head,
struct sk_buff *skb,
struct udphdr *uh,
struct sock *sk)
{
struct sk_buff *pp = NULL;
struct sk_buff *p;
unsigned int ulen = ntohs(uh->len);
/* GRO 리스트에서 병합 후보 검색 */
list_for_each_entry(p, head, list) {
if (!NAPI_GRO_CB(p)->same_flow)
continue;
/* 병합 조건 검증 */
if (udp_hdr(p)->source != uh->source ||
udp_hdr(p)->dest != uh->dest)
continue;
/* 데이터그램 크기 일관성 확인 */
if (NAPI_GRO_CB(p)->count > 0 &&
skb_gro_len(p) % NAPI_GRO_CB(p)->count
!= ulen - sizeof(*uh))
continue;
/* 병합 수행: skb를 p의 frag_list에 추가 */
skb_gro_receive(p, skb);
NAPI_GRO_CB(p)->count++;
/* flush 한계 도달 시 상위 계층으로 전달 */
if (NAPI_GRO_CB(p)->count >= gro_max_size)
pp = p;
break;
}
return pp;
}
GRO_UDP_SEGMENT cmsg로
원래 세그먼트 크기를 확인하고, 각 데이터그램을 개별적으로 처리해야 합니다.
GRO는 동일 NAPI poll 내의 패킷만 병합하므로 전역적 순서 재배치는 발생하지 않습니다.
UDP 캡슐화 프레임워크 심화
UDP 터널의 수신 경로에서 encap_rcv 콜백이 호출되는 정확한 메커니즘과,
주요 터널 프로토콜(VXLAN, Geneve, WireGuard)의 캡슐화/역캡슐화 과정을 상세히 분석합니다.
VXLAN 캡슐화 상세
/* drivers/net/vxlan/vxlan_core.c — VXLAN 패킷 구조
*
* Outer Ethernet | Outer IP | Outer UDP(4789) |
* VXLAN Header(8B) | Inner Ethernet | Inner IP | Inner Payload
*
* VXLAN 헤더 구조:
* +---+---+---+---+---+---+---+---+
* | Flags (8) | Reserved (24) |
* +---+---+---+---+---+---+---+---+
* | VNI (24) | Reserved (8) |
* +---+---+---+---+---+---+---+---+
*/
struct vxlanhdr {
__be32 vx_flags; /* I 플래그 (bit 4) 설정 시 VNI 유효 */
__be32 vx_vni; /* 상위 24비트: VNI (Virtual Network ID) */
};
/* VXLAN encap_rcv 콜백 */
static int vxlan_rcv(struct sock *sk,
struct sk_buff *skb)
{
struct vxlanhdr *vxh;
__be32 vni;
/* 1. VXLAN 헤더 파싱 */
vxh = (struct vxlanhdr *)(udp_hdr(skb) + 1);
vni = vxlan_vni(vxh->vx_vni);
/* 2. VNI로 VXLAN 디바이스 검색 */
vxlan = vxlan_vs_find_vni(vs, skb->dev->ifindex, vni);
/* 3. 외부 헤더 제거, 내부 Ethernet 프레임 노출 */
__skb_pull(skb, sizeof(struct vxlanhdr));
skb_reset_mac_header(skb);
/* 4. 내부 패킷을 VXLAN 가상 디바이스로 주입 */
skb->dev = vxlan->dev;
netif_rx(skb); /* 네트워크 스택 재진입 */
return 0;
}
터널 TX 경로: 캡슐화
/* VXLAN TX: 내부 패킷을 UDP 캡슐화하여 전송 */
static netdev_tx_t vxlan_xmit(
struct sk_buff *skb,
struct net_device *dev)
{
/* 1. FDB(Forwarding Database)에서 목적지 VTEP 검색 */
struct vxlan_fdb *f = vxlan_find_mac(vxlan, eth->h_dest, vni);
/* 2. 외부 UDP/IP 헤더 추가 */
udp_tunnel_xmit_skb(rt, sk, skb,
local_ip, dst_ip, /* 외부 IP */
tos, ttl, /* IP 옵션 */
df, src_port, dst_port, /* 외부 UDP */
xnet, !udp_sum);
/* 결과: [Outer Eth][Outer IP][Outer UDP:4789][VXLAN][Inner Eth][...] */
}
| 터널 프로토콜 | 오버헤드 (바이트) | MTU 감소 | NIC 오프로드 | GRO 지원 |
|---|---|---|---|---|
| VXLAN | 50 (Eth 14 + IP 20 + UDP 8 + VXLAN 8) | 1450 (MTU 1500 기준) | 대부분 지원 | vxlan_gro_receive |
| Geneve | 50~306 (TLV 가변) | 1450~1194 | 부분 지원 | geneve_gro_receive |
| WireGuard | 60 (IP 20 + UDP 8 + WG 32) | 1420 | UDP GSO | 소켓 GRO |
| IPsec NAT-T | 36~72 (ESP 가변) | 1428~1392 | 제한적 | 제한적 |
ip link set vxlan0 mtu 1450
SO_REUSEPORT 상세 심화
SO_REUSEPORT의 커널 내부 구현, 그룹 관리, eBPF 프로그램 연동,
그리고 소켓 마이그레이션(커널 5.14+) 메커니즘을 상세히 분석합니다.
/* net/core/sock_reuseport.c — reuseport_select_sock() 구현 */
struct sock *reuseport_select_sock(
struct sock *sk,
u32 hash,
struct sk_buff *skb,
int hdr_len)
{
struct sock_reuseport *reuse;
struct bpf_prog *prog;
struct sock *sk2 = NULL;
u16 socks;
rcu_read_lock();
reuse = rcu_dereference(sk->sk_reuseport_cb);
if (!reuse)
goto out;
socks = READ_ONCE(reuse->num_socks);
prog = rcu_dereference(reuse->prog);
if (prog) {
/* eBPF 프로그램이 등록된 경우 */
sk2 = bpf_run_sk_reuseport(reuse, sk, prog, skb, NULL, hash);
} else {
/* 기본: 4-tuple 해시 기반 분산 */
u32 index = reciprocal_scale(hash, socks);
sk2 = reuse->socks[index];
}
out:
rcu_read_unlock();
return sk2;
}
QUIC Connection ID 기반 eBPF 분산
/* QUIC 서버에서 Connection ID 기반 REUSEPORT 분산
*
* 일반 4-tuple 해시: NAT 리바인딩, 네트워크 전환 시 다른 소켓으로 분산
* Connection ID 해시: 동일 QUIC 연결은 항상 동일 소켓에 고정 (세션 유지)
*/
SEC("sk_reuseport")
int quic_select_sock(struct sk_reuseport_md *ctx)
{
__u8 *data = ctx->data;
__u8 *data_end = ctx->data_end;
__u32 key;
/* QUIC Short Header: 첫 바이트 상위 비트가 1이면 Short Header */
if (data + 5 > data_end)
return SK_DROP;
if (data[0] & 0x80) {
/* Long Header (Initial/Handshake): 기본 해시 사용 */
key = ctx->hash;
} else {
/* Short Header: DCID(Destination Connection ID) 추출
* DCID 오프셋은 서버 구현에 따라 다름 (여기서는 바이트 1~4) */
key = *(__u32 *)(data + 1);
}
/* REUSEPORT 소켓 맵에서 선택 */
return bpf_sk_select_reuseport(ctx, &sock_map, &key, 0);
}
/* 소켓 맵 정의 */
struct {
__uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
__uint(max_entries, 256); /* 최대 워커 수 */
__type(key, __u32);
__type(value, __u64); /* 소켓 fd */
} sock_map SEC(".maps");
SO_REUSEPORT 그룹에서 소켓이 닫힐 때,
해당 소켓의 수신 큐에 남아있는 패킷을 그룹 내 다른 소켓으로 마이그레이션할 수 있습니다.
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT_ATTACH_REUSEPORT_CBPF, ...)로 제어합니다.
이를 통해 graceful restart 시 패킷 손실을 방지합니다.
UDP 소켓 버퍼 튜닝 심화
UDP 소켓 버퍼의 내부 메모리 관리와 forward allocation 메커니즘,
udp_mem 전역 제한, 그리고 실전 워크로드별 최적 튜닝 전략을 상세히 다룹니다.
소켓 메모리 회계 (Memory Accounting)
/* UDP 소켓 메모리 관리의 핵심 변수들 */
struct sock {
/* 수신 측 */
atomic_t sk_rmem_alloc; /* 현재 수신 메모리 사용량 */
int sk_rcvbuf; /* 수신 버퍼 한계 (SO_RCVBUF * 2) */
/* 송신 측 */
atomic_t sk_wmem_alloc; /* 현재 송신 메모리 사용량 */
int sk_sndbuf; /* 송신 버퍼 한계 */
/* 드롭 카운터 */
atomic_t sk_drops; /* 드롭된 패킷 수 */
/* 전방 할당 (forward allocation) */
int sk_forward_alloc; /* 미리 예약한 여유 메모리 */
};
/* UDP의 forward allocation 메커니즘
*
* sk_forward_alloc은 소켓에 "미리 예약"된 메모리입니다.
* 새 skb를 큐에 추가할 때:
* 1. forward_alloc >= skb->truesize -> 즉시 추가 (전역 잠금 불필요)
* 2. forward_alloc < skb->truesize -> udp_rmem_schedule()으로 추가 할당
* 3. 할당 실패 -> 드롭
*
* 이 메커니즘으로 per-packet 전역 메모리 회계 오버헤드를 줄입니다.
*/
워크로드별 튜닝 가이드
| 워크로드 | rmem_default | rmem_max | wmem_default | wmem_max | 비고 |
|---|---|---|---|---|---|
| DNS 서버 (고QPS) | 4MB | 16MB | 1MB | 4MB | 작은 패킷, 높은 pps |
| QUIC/HTTP3 | 8MB | 32MB | 8MB | 32MB | GSO/GRO 병용 |
| 비디오 스트리밍 | 16MB | 64MB | 4MB | 16MB | 대용량 수신 버스트 |
| 게임 서버 | 2MB | 8MB | 2MB | 8MB | 저지연 우선 |
| 로그 수집 (syslog) | 32MB | 128MB | 1MB | 4MB | 수신 폭주 대비 |
| VXLAN 터널 | 8MB | 32MB | 8MB | 32MB | 터널 오프로드 병용 |
# 실시간 소켓 버퍼 모니터링
# ss로 소켓 메모리 상태 확인
$ ss -u -n -e -m
UNCONN 0 0 *:8080 *:*
skmem:(r0,rb26214400,t0,tb26214400,f4096,w0,o0,bl0,d0)
# r: sk_rmem_alloc (현재 수신 메모리 사용량)
# rb: sk_rcvbuf (수신 버퍼 한계)
# t: sk_wmem_alloc (현재 송신 메모리 사용량)
# tb: sk_sndbuf (송신 버퍼 한계)
# f: sk_forward_alloc (전방 할당 여유)
# w: sk_wmem_queued (송신 큐 메모리)
# o: sk_omem_alloc (옵션 메모리)
# bl: sk_backlog.len (backlog 큐 길이)
# d: sk_drops (드롭 카운터)
# 드롭이 발생하면: r이 rb에 근접, d가 0이 아님
# 대응: rmem_max 증가 + SO_RCVBUF 증가
# 전역 UDP 메모리 압력 확인
$ cat /proc/net/sockstat
UDP: inuse 42 mem 256
# mem이 net.ipv4.udp_mem의 pressure 값에 근접하면 경고
# net.ipv4.udp_mem 확인 (low, pressure, high 페이지 단위)
$ sysctl net.ipv4.udp_mem
net.ipv4.udp_mem = 188451 251269 376902
SO_RCVBUF를 설정하면 이론적으로 640GB의 메모리 예약이 필요합니다.
net.ipv4.udp_mem의 high 값이 전역 제한으로 작동하지만,
OOM killer가 먼저 동작할 수 있습니다.
프로덕션에서는 SO_RCVBUF를 보수적으로 설정하고 드롭 카운터를 모니터링하세요.
Busy Polling 심화
Busy polling의 커널 내부 구현, NAPI 연동 메커니즘,
그리고 SO_PREFER_BUSY_POLL(커널 5.11+)과 epoll 기반 busy polling의
구체적 동작을 분석합니다.
/* net/core/sock.c — napi_busy_loop() 핵심 구현
*
* busy polling의 핵심: 프로세스가 직접 NAPI poll을 실행
* 인터럽트 -> softirq -> wakeup 경로를 우회하여 지연 감소
*/
void napi_busy_loop(unsigned int napi_id,
bool (*loop_end)(void *, unsigned long),
void *loop_end_arg,
bool prefer_busy_poll, u16 budget)
{
unsigned long start_time = busy_loop_current_time();
struct napi_struct *napi;
/* NAPI ID로 NAPI 인스턴스 검색 */
napi = napi_by_id(napi_id);
if (!napi)
return;
/* busy loop: 시간 제한까지 반복 폴링 */
for (;;) {
/* 직접 NAPI poll 실행 */
napi_poll(napi, budget);
/* 종료 조건 확인 */
if (loop_end(loop_end_arg, start_time))
break; /* 데이터 수신 완료 */
if (busy_loop_timeout(start_time))
break; /* 시간 초과 */
/* CPU를 양보하지 않고 계속 폴링
* prefer_busy_poll=true면 softirq보다 우선 */
if (need_resched())
break; /* 스케줄러 요청 시 양보 */
cpu_relax(); /* 전력 절약 힌트 (pause 명령) */
}
}
epoll + Busy Polling 패턴
/* 저지연 UDP 수신: epoll + busy polling 결합 */
int setup_low_latency_udp(int port)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
int epfd = epoll_create1(0);
/* 1. SO_BUSY_POLL: 소켓별 busy poll 시간 (us) */
int busy_usec = 100;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL,
&busy_usec, sizeof(busy_usec));
/* 2. SO_PREFER_BUSY_POLL: NAPI 스레드보다 busy poll 우선 */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
&prefer, sizeof(prefer));
/* 3. SO_BUSY_POLL_BUDGET: 한 번에 처리할 패킷 수 (5.11+) */
int budget = 64;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET,
&budget, sizeof(budget));
/* 바인드 + epoll 등록 */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
struct epoll_event ev = { .events = EPOLLIN, .data.fd = fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
/* 이벤트 루프: epoll_wait 내부에서 busy polling 수행 */
struct epoll_event events[16];
for (;;) {
/* epoll_wait가 내부적으로 napi_busy_loop() 호출
* -> 인터럽트 없이 직접 NIC에서 패킷 수신 */
int n = epoll_wait(epfd, events, 16, -1);
for (int i = 0; i < n; i++) {
recvmsg(events[i].data.fd, &msg, 0);
/* 패킷 처리 */
}
}
}
| 수신 방식 | 지연 (일반적) | CPU 사용 | 적합한 워크로드 |
|---|---|---|---|
| 인터럽트 기반 | 20~50us | 낮음 | 일반 서버 |
| Busy polling (sysctl) | 5~15us | 중간 | 중간 지연 요구 |
| Busy polling (소켓 + prefer) | 2~8us | 높음 | 저지연 필수 |
| AF_XDP | 1~3us | 매우 높음 (전용 코어) | 최저 지연 (HFT) |
isolcpus=4-7,
nohz_full=4-7을 설정하고, taskset -c 4 ./app으로 프로세스를 바인딩하세요.
IRQ affinity도 동일 코어로 설정하여 캐시 효율을 극대화합니다.
UDP 멀티캐스트 구현 심화
리눅스 커널의 UDP 멀티캐스트 구현을 더 깊이 분석합니다. IGMP 그룹 관리, 소스 필터링(SSM/ASM), 커널 내 멀티캐스트 라우팅, 그리고 고성능 멀티캐스트 수신 패턴을 다룹니다.
SSM(Source-Specific Multicast) vs ASM(Any-Source Multicast)
/* ASM (Any-Source Multicast) — 전통적 멀티캐스트 */
/* 그룹 주소: 224.0.0.0 ~ 239.255.255.255 */
struct ip_mreq mreq = {
.imr_multiaddr.s_addr = inet_addr("239.1.1.1"),
.imr_interface.s_addr = htonl(INADDR_ANY),
};
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
&mreq, sizeof(mreq));
/* 모든 소스에서 오는 239.1.1.1 패킷 수신 */
/* SSM (Source-Specific Multicast) — 특정 소스만 허용 */
/* 그룹 주소: 232.0.0.0/8 (SSM 전용 범위) */
struct ip_mreq_source mreqs = {
.imr_multiaddr.s_addr = inet_addr("232.1.1.1"),
.imr_sourceaddr.s_addr = inet_addr("10.0.0.100"), /* 허용 소스 */
.imr_interface.s_addr = htonl(INADDR_ANY),
};
setsockopt(fd, IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP,
&mreqs, sizeof(mreqs));
/* 10.0.0.100에서만 오는 232.1.1.1 패킷 수신 */
/* 프로토콜 독립 API (IPv4/IPv6 공용) */
struct group_source_req gsr = {
.gsr_interface = if_nametoindex("eth0"),
};
/* gsr_group과 gsr_source에 sockaddr_storage 사용 */
setsockopt(fd, IPPROTO_IP, MCAST_JOIN_SOURCE_GROUP,
&gsr, sizeof(gsr));
고성능 멀티캐스트 수신 패턴
/* 금융 시장 데이터 수신 패턴 (멀티캐스트 + busy polling) */
int setup_market_data_receiver(const char *mcast_addr,
int port,
const char *iface)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
int opt;
/* 1. SO_REUSEPORT: 멀티코어 수신 */
opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
/* 2. 수신 버퍼 최대화 */
opt = 67108864; /* 64MB */
setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &opt, sizeof(opt));
/* 3. 타임스탬프 활성화 (HW 타임스탬프 우선) */
opt = SOF_TIMESTAMPING_RX_HARDWARE
| SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &opt, sizeof(opt));
/* 4. Busy polling */
opt = 100; /* 100us */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &opt, sizeof(opt));
opt = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &opt, sizeof(opt));
/* 5. 바인드 + 멀티캐스트 참여 */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
struct ip_mreqn mreq = {
.imr_multiaddr.s_addr = inet_addr(mcast_addr),
.imr_ifindex = if_nametoindex(iface),
};
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
&mreq, sizeof(mreq));
return fd;
}
/* recvmmsg()로 배치 수신: 시스템콜 오버헤드 감소 */
#define VLEN 64
struct mmsghdr msgs[VLEN];
struct iovec iovecs[VLEN];
char bufs[VLEN][2048];
for (int i = 0; i < VLEN; i++) {
iovecs[i].iov_base = bufs[i];
iovecs[i].iov_len = 2048;
msgs[i].msg_hdr.msg_iov = &iovecs[i];
msgs[i].msg_hdr.msg_iovlen = 1;
}
/* 한 번의 시스템콜로 최대 64개 패킷 수신 */
int n = recvmmsg(fd, msgs, VLEN, MSG_WAITFORONE, NULL);
for (int i = 0; i < n; i++) {
/* msgs[i].msg_len: 수신된 바이트 수 */
process_packet(bufs[i], msgs[i].msg_len);
}
| 멀티캐스트 주소 범위 | 용도 | TTL/스코프 |
|---|---|---|
224.0.0.0/24 | 로컬 네트워크 제어 (IGMP, OSPF, VRRP) | TTL=1 (라우터 미전달) |
224.0.1.0/24 | 인터네트워크 제어 (NTP, SLP) | 라우터 전달 가능 |
232.0.0.0/8 | SSM(Source-Specific Multicast) 전용 | 소스 필터링 필수 |
233.0.0.0/8 | GLOP (AS 번호 기반 할당) | 전역 라우팅 |
239.0.0.0/8 | 관리적 스코프 (사설 멀티캐스트) | 조직 내부 |
bridge mdb show로 스위치의 멀티캐스트 그룹 테이블을 확인하세요.
UDP 성능 벤치마크
다양한 UDP 최적화 기법의 실제 성능 효과를 정량적으로 비교합니다. 벤치마크 결과는 하드웨어와 커널 버전에 따라 달라지므로 상대적 비교에 참고하세요.
| 구성 | 처리량 (pps) | 지연 (avg) | CPU 사용 | 비고 |
|---|---|---|---|---|
| 기본 (recvmsg, 인터럽트) | ~300K | 30~50us | 낮음 | 기준선 |
| + SO_REUSEPORT (4코어) | ~1.1M | 30~50us | 4배 분산 | 선형 확장 |
| + recvmmsg (64 batch) | ~500K | 30~50us | 중간 | 시스템콜 감소 |
| + UDP GRO/GSO | ~2M | 20~40us | 중간 | 대형 패킷 최적화 |
| + Busy polling | ~600K | 3~8us | 높음 (전용) | 저지연 특화 |
| + 모든 최적화 결합 | ~3M+ | 3~8us | 높음 | 최대 성능 |
| AF_XDP (비교) | ~10M+ | 1~3us | 매우 높음 | 커널 우회 |
# UDP 벤치마크 도구: iperf3
# 서버
$ iperf3 -s -p 5001
# 클라이언트: UDP 1Gbps 대역폭 테스트
$ iperf3 -c 10.0.0.1 -u -b 10G -l 1472 -p 5001 -t 30
# neper: 고성능 네트워크 벤치마크 (Google)
# UDP 처리량 측정
$ udp_rr --nolog -c -H 10.0.0.1 -l 30 --num-threads 4
# sockperf: 저지연 측정
# 서버
$ sockperf sr --udp -p 12345
# 클라이언트: 왕복 지연 측정
$ sockperf pp --udp -i 10.0.0.1 -p 12345 -t 30 --mps max
# 결과 예시:
# sockperf: Summary: Latency is 4.532 usec
# sockperf: Total 6,608,215 observations
performance 거버너),
(3) 동일 NUMA 노드 사용,
(4) turbo boost 비활성화 등의 조건을 통일하세요.
tuned-adm profile network-latency가 편리합니다.
UDP-Lite 커널 구현 심화
UDP-Lite(RFC 3828)의 커널 내부 구현을 상세히 분석합니다.
체크섬 커버리지 메커니즘, udplite4_lib_rcv() 수신 경로,
그리고 일반 UDP와의 코드 공유 구조를 다룹니다.
커널 내 UDP-Lite 등록
/* net/ipv4/udplite.c — UDP-Lite 프로토콜 등록
*
* UDP-Lite는 IP 프로토콜 번호 136을 사용합니다.
* 커널에서는 UDP 코드를 최대한 재사용하며,
* 체크섬 처리 부분만 차별화합니다.
*/
static const struct net_protocol udplite4_protocol = {
.handler = udplite_rcv,
.err_handler = udplite_err,
.no_policy = 1,
};
/* UDP-Lite proto_ops: UDP와 동일한 함수 포인터를 대부분 공유 */
struct proto udplite_prot = {
.name = "UDP-Lite",
.owner = THIS_MODULE,
.close = udp_lib_close, /* UDP 공유 */
.sendmsg = udp_sendmsg, /* UDP 공유 */
.recvmsg = udp_recvmsg, /* UDP 공유 */
.hash = udp_lib_hash, /* UDP 공유 */
.unhash = udp_lib_unhash, /* UDP 공유 */
.setsockopt = udplite_setsockopt, /* UDP-Lite 전용 */
.getsockopt = udplite_getsockopt, /* UDP-Lite 전용 */
.obj_size = sizeof(struct udp_sock),
};
/* UDP-Lite 전용 해시 테이블 */
struct udp_table udplite_table __read_mostly;
/* 모듈 초기화 */
static int __init udplite4_register(void)
{
/* IP 프로토콜 136 등록 */
if (inet_add_protocol(&udplite4_protocol, IPPROTO_UDPLITE) < 0)
return -EAGAIN;
/* 소켓 타입 등록 */
inet_register_protosw(&udplite4_protosw);
return 0;
}
체크섬 커버리지 검증 로직
/* net/ipv4/udp.c — UDP-Lite 체크섬 커버리지 처리
*
* UDP-Lite 헤더의 "Checksum Coverage" 필드:
* 0 = 전체 패킷 체크섬 (UDP와 동일 동작)
* N = 처음 N 바이트만 체크섬 보호 (최소 8 = 헤더만)
*
* 수신 측: 송신 커버리지가 수신 최소 요구보다 크거나 같아야 함
*/
static int udplite_checksum_init(struct sk_buff *skb,
struct udphdr *uh)
{
u16 cscov;
/* UDP-Lite에서 len 필드는 체크섬 커버리지로 재해석 */
cscov = ntohs(uh->len);
if (cscov == 0) {
/* 전체 패킷 체크섬 */
cscov = skb->len;
} else if (cscov < 8) {
/* 최소 커버리지: 8바이트 (UDP-Lite 헤더) */
goto drop;
} else if (cscov > skb->len) {
/* 커버리지가 패킷 크기 초과 -> 오류 */
goto drop;
}
/* 체크섬 계산 범위 설정 */
UDP_SKB_CB(skb)->cscov = cscov;
/* 의사 헤더 + 커버리지 범위만큼 체크섬 계산 */
if (skb->ip_summed == CHECKSUM_COMPLETE) {
/* NIC가 전체 체크섬 제공 -> 커버리지에 맞게 조정 */
if (cscov < skb->len) {
/* 커버리지 외 영역을 체크섬에서 제거 */
skb->csum = csum_sub(skb->csum,
csum_partial(skb->data + cscov,
skb->len - cscov, 0));
}
}
return 0;
drop:
__UDP_INC_STATS(net, UDP_MIB_INERRORS, 1);
return -1;
}
UDP-Lite 소켓 옵션
| 소켓 옵션 | 레벨 | 타입 | 기본값 | 설명 |
|---|---|---|---|---|
UDPLITE_SEND_CSCOV | IPPROTO_UDPLITE | int | 0 (전체) | 송신 체크섬 커버리지 (바이트) |
UDPLITE_RECV_CSCOV | IPPROTO_UDPLITE | int | 0 (전체) | 최소 수신 체크섬 커버리지 |
/* UDP-Lite 실제 활용: VoIP 코덱별 커버리지 설정 */
/* Opus 코덱: 헤더(12B RTP + 8B UDPLite) + TOC(1~2B)만 보호
* 오디오 페이로드는 비트 오류가 있어도 디코더가 보정 가능 */
int coverage = 22; /* 8(UDPLite) + 12(RTP) + 2(Opus TOC) */
setsockopt(fd, IPPROTO_UDPLITE, UDPLITE_SEND_CSCOV,
&coverage, sizeof(coverage));
/* 수신 측: 최소 커버리지 요구 (이보다 작은 커버리지 패킷은 거부) */
int min_cov = 8; /* 최소 헤더만이라도 보호 */
setsockopt(fd, IPPROTO_UDPLITE, UDPLITE_RECV_CSCOV,
&min_cov, sizeof(min_cov));
/* 통계 확인 */
/* /proc/net/snmp의 UdpLite 섹션에서 확인 가능 */
UDP 성능 프로파일링
UDP 워크로드의 성능 병목을 진단하는 도구와 기법을 상세히 다룹니다.
perf, bpftrace, dropwatch를 활용한 체계적인 프로파일링 방법론을 제시합니다.
perf를 이용한 UDP 핫스팟 분석
# 1. UDP 수신 경로 CPU 프로파일링
$ perf record -g -p $(pgrep udp_server) -- sleep 30
$ perf report --sort=dso,symbol
# 핵심 관찰 포인트:
# __udp4_lib_rcv -> 소켓 lookup 비용
# udp_queue_rcv_skb -> BPF 필터 + 체크섬 검증 비용
# copy_to_iter -> 사용자 공간 복사 비용
# __softirqentry_text -> softirq 전체 처리 시간
# 2. 패킷 드롭 원인 추적 (커널 5.17+)
$ perf trace -e 'skb:kfree_skb' -a --duration 10
# 출력: kfree_skb 호출 위치 + SKB_DROP_REASON
# SKB_DROP_REASON_UDP_RCVBUF -> 수신 버퍼 부족
# SKB_DROP_REASON_UDP_CSUM -> 체크섬 오류
# SKB_DROP_REASON_SOCKET_FILTER -> BPF 필터 거부
# 3. 특정 함수 호출 빈도 측정
$ perf stat -e 'udp:udp_fail_queue_rcv_skb' -a -- sleep 10
# 4. UDP 관련 tracepoint 목록 확인
$ perf list 'udp:*' 'skb:*' 'net:*'
udp:udp_fail_queue_rcv_skb
skb:kfree_skb
skb:consume_skb
skb:skb_copy_datagram_iovec
net:net_dev_queue
net:net_dev_xmit
net:netif_receive_skb
bpftrace를 이용한 실시간 UDP 분석
# 1. UDP 수신 함수 호출 빈도 (1초 단위)
$ bpftrace -e '
kprobe:__udp4_lib_rcv {
@recv_count = count();
}
interval:s:1 {
printf("UDP recv/s: %d\n", @recv_count);
clear(@recv_count);
}'
# 2. UDP 소켓별 수신 바이트 분포
$ bpftrace -e '
kprobe:udp_recvmsg {
@recv_bytes = hist(arg2);
}
END { print(@recv_bytes); }'
# 3. UDP 드롭 원인별 카운트 (커널 5.17+)
$ bpftrace -e '
tracepoint:skb:kfree_skb {
if (args->reason >= 200 && args->reason <= 210) {
@drops[args->reason] = count();
@stacks[args->reason] = kstack;
}
}
interval:s:5 { print(@drops); }'
# 4. UDP sendmsg 지연 분포
$ bpftrace -e '
kprobe:udp_sendmsg {
@start[tid] = nsecs;
}
kretprobe:udp_sendmsg /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 5. UDP 소켓 버퍼 사용률 모니터링
$ bpftrace -e '
kprobe:__udp_enqueue_schedule_skb {
$sk = (struct sock *)arg0;
$rmem = $sk->sk_rmem_alloc.counter;
$rcvbuf = $sk->sk_rcvbuf;
$usage_pct = ($rmem * 100) / $rcvbuf;
if ($usage_pct > 80) {
printf("HIGH USAGE: rmem=%d rcvbuf=%d (%d%%)\n",
$rmem, $rcvbuf, $usage_pct);
}
}'
dropwatch를 이용한 패킷 드롭 위치 추적
# dropwatch: 커널 패킷 드롭 위치를 실시간으로 추적하는 도구
# 설치
$ apt install dropwatch # Debian/Ubuntu
$ dnf install dropwatch # Fedora/RHEL
# 실행: 드롭 위치와 빈도 표시
$ dropwatch -l kas
Initalizing kallsyms db
dropwatch> start
1 drops at __udp4_lib_rcv+0x3a8 (0xffffffff81a2c3a8)
3 drops at udp_queue_rcv_skb+0x1d0 (0xffffffff81a2b1d0)
7 drops at __udp_enqueue_schedule_skb+0x120 (0xffffffff81a2a520)
# 해석:
# __udp4_lib_rcv: 소켓 없음 (NoPorts) 또는 체크섬 오류
# udp_queue_rcv_skb: BPF 필터 거부
# __udp_enqueue_schedule_skb: 수신 버퍼 오버플로우
# 커널 5.17+ perf 기반 드롭 추적 (dropwatch 대안)
$ perf trace --no-syscalls -e 'skb:kfree_skb' -a --duration 30 2>&1 | \
awk '/reason:/ {print $NF}' | sort | uniq -c | sort -rn
47 reason:UDP_RCVBUF
12 reason:NO_SOCKET
3 reason:UDP_CSUM
| 도구 | 분석 대상 | 오버헤드 | 최적 사용 시점 |
|---|---|---|---|
ss -u -n -e | 소켓별 드롭/메모리 | 매우 낮음 | 초기 진단 |
/proc/net/snmp | 프로토콜 통계 | 없음 | 상시 모니터링 |
nstat | 증분 통계 | 없음 | 실시간 변화 추적 |
perf stat | tracepoint 카운트 | 낮음 | 특정 이벤트 빈도 |
perf trace | kfree_skb 추적 | 중간 | 드롭 원인 분석 |
bpftrace | 커스텀 분석 | 중간 | 심층 프로파일링 |
dropwatch | 드롭 위치 | 중간 | 드롭 위치 특정 |
perf record | CPU 핫스팟 | 중~높음 | CPU 병목 분석 |
ftrace | 함수 호출 흐름 | 높음 | 경로 추적 |
ethtool -S로 NIC 드롭 확인,
(2) softnet_stat으로 CPU backlog 오버플로우 확인,
(3) /proc/net/snmp로 프로토콜 레벨 오류 확인,
(4) ss -e로 소켓별 드롭/메모리 상태 확인,
(5) perf/bpftrace로 CPU 핫스팟 분석.
sendmmsg/recvmmsg 배치 시스템콜
UDP 워크로드에서 시스템콜 오버헤드를 줄이는 핵심 기법인
sendmmsg()/recvmmsg() 배치 시스템콜의 커널 내부 구현과 최적 활용 패턴을 분석합니다.
/* net/socket.c — sendmmsg 커널 구현 */
int __sys_sendmmsg(int fd, struct mmsghdr __user *mmsg,
unsigned int vlen, unsigned int flags,
bool forbid_cmsg_compat)
{
struct socket *sock;
int datagrams = 0;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
/* 핵심: 단일 시스템콜 내에서 vlen번 반복 */
while (datagrams < vlen) {
/* 각 메시지를 개별 sendmsg로 전송 */
err = ___sys_sendmsg(sock, &mmsg[datagrams].msg_hdr,
flags, NULL, 0);
if (err < 0)
break;
/* 전송 바이트 수 기록 */
mmsg[datagrams].msg_len = err;
datagrams++;
/* 시그널 확인 (인터럽트 허용) */
if (signal_pending(current))
break;
cond_resched(); /* 스케줄러에 양보 기회 */
}
return datagrams;
}
/* 고성능 UDP 전송: sendmmsg + GSO 결합 패턴 */
#define BATCH_SIZE 64
#define GSO_SIZE 1472
struct mmsghdr msgs[BATCH_SIZE];
struct iovec iovecs[BATCH_SIZE];
char bufs[BATCH_SIZE][65536]; /* 각 64KB (GSO가 분할) */
/* cmsg: UDP_SEGMENT로 GSO 활성화 */
char cmsgbuf[BATCH_SIZE][CMSG_SPACE(sizeof(uint16_t))];
for (int i = 0; i < BATCH_SIZE; i++) {
iovecs[i].iov_base = bufs[i];
iovecs[i].iov_len = 65000; /* GSO가 1472B씩 분할 */
msgs[i].msg_hdr.msg_iov = &iovecs[i];
msgs[i].msg_hdr.msg_iovlen = 1;
/* GSO cmsg 설정 */
msgs[i].msg_hdr.msg_control = cmsgbuf[i];
msgs[i].msg_hdr.msg_controllen = sizeof(cmsgbuf[i]);
struct cmsghdr *cm = CMSG_FIRSTHDR(&msgs[i].msg_hdr);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type = UDP_SEGMENT;
cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
*(uint16_t *)CMSG_DATA(cm) = GSO_SIZE;
}
/* 1회 시스템콜로 64 x 44 = ~2,816개 UDP 데이터그램 전송 */
int sent = sendmmsg(fd, msgs, BATCH_SIZE, 0);
/* sent: 성공적으로 전송된 메시지 수
* msgs[i].msg_len: 각 메시지의 전송 바이트 수 */
/* 고성능 UDP 수신: recvmmsg + GRO 결합 패턴 */
#define VLEN 128
struct mmsghdr msgs[VLEN];
struct iovec iovecs[VLEN];
char bufs[VLEN][65536]; /* GRO 병합 패킷 수신 대비 */
char ctlbufs[VLEN][CMSG_SPACE(sizeof(uint16_t))];
for (int i = 0; i < VLEN; i++) {
iovecs[i].iov_base = bufs[i];
iovecs[i].iov_len = 65536;
msgs[i].msg_hdr.msg_iov = &iovecs[i];
msgs[i].msg_hdr.msg_iovlen = 1;
msgs[i].msg_hdr.msg_control = ctlbufs[i];
msgs[i].msg_hdr.msg_controllen = sizeof(ctlbufs[i]);
}
/* 타임아웃 설정: 최소 1개 수신 후 1ms 대기 */
struct timespec timeout = { .tv_sec = 0, .tv_nsec = 1000000 };
/* 한 번의 시스템콜로 최대 128개 메시지 수신 */
int n = recvmmsg(fd, msgs, VLEN, MSG_WAITFORONE, &timeout);
for (int i = 0; i < n; i++) {
int len = msgs[i].msg_len;
uint16_t gro_size = 0;
/* GRO cmsg에서 원래 세그먼트 크기 확인 */
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msgs[i].msg_hdr); cm;
cm = CMSG_NXTHDR(&msgs[i].msg_hdr, cm)) {
if (cm->cmsg_type == UDP_GRO)
gro_size = *(uint16_t *)CMSG_DATA(cm);
}
if (gro_size > 0) {
/* GRO 병합 패킷: gro_size 단위로 분리 처리 */
int offset = 0;
while (offset < len) {
int seg_len = (len - offset > gro_size)
? gro_size : (len - offset);
process_datagram(bufs[i] + offset, seg_len);
offset += seg_len;
}
} else {
process_datagram(bufs[i], len);
}
}
| 전송 방식 | 시스템콜 수 | UDP 데이터그램 수 | 예상 pps | 적합한 경우 |
|---|---|---|---|---|
sendmsg() x N | N | N | ~300K | 단순 구현 |
sendmmsg() vlen=64 | 1 | 64 | ~500K | 다수 소형 패킷 |
sendmsg() + GSO | 1 | ~44 | ~1.5M | 대형 전송 |
sendmmsg() + GSO | 1 | ~2,800 | ~3M+ | 최대 처리량 |
recvmmsg()에 MSG_WAITFORONE을 설정하면
첫 번째 메시지는 블로킹으로 대기하고, 이후 메시지는 논블로킹으로 즉시 수신 가능한 것만 가져옵니다.
이는 지연과 배치 효율의 균형을 맞추는 핵심 플래그입니다.
타임아웃과 함께 사용하면 수신 루프의 지연 특성을 세밀하게 제어할 수 있습니다.
UDP와 네트워크 네임스페이스
리눅스 네트워크 네임스페이스 환경에서 UDP 소켓 해시 테이블의 격리 메커니즘과 컨테이너/가상화 환경에서의 UDP 성능 고려사항을 분석합니다.
네임스페이스별 UDP 해시 테이블 격리
/* UDP 소켓 lookup에서 네트워크 네임스페이스 검증
*
* 모든 UDP 소켓은 전역 udp_table을 공유하지만,
* lookup 시 네트워크 네임스페이스가 일치하는 소켓만 매칭됩니다.
*
* compute_score()에서 net_eq() 검사가 첫 번째로 수행됩니다.
*/
static int compute_score(struct sock *sk,
const struct net *net, ...)
{
/* 네트워크 네임스페이스 불일치 -> 즉시 제외 (-1) */
if (!net_eq(sock_net(sk), net))
return -1;
/* 이하 포트/주소 매칭 로직 ... */
}
/* 결과:
* - 컨테이너 A의 UDP 소켓 (netns A, port 8080)
* - 컨테이너 B의 UDP 소켓 (netns B, port 8080)
* - 동일 해시 슬롯에 존재하지만, 서로의 패킷에 매칭되지 않음
* - 각 네임스페이스는 완전히 독립적인 UDP 포트 공간 보유
*/
컨테이너 환경 UDP 성능 고려사항
| 구성 | UDP 오버헤드 | 설명 | 최적화 방안 |
|---|---|---|---|
| host network (--net=host) | 없음 | 호스트 네임스페이스 공유 | 최고 성능, 격리 없음 |
| veth pair | 중간 | 가상 이더넷 페어, netfilter 통과 | XDP redirect, TC offload |
| bridge + veth | 높음 | L2 브리지 + veth, conntrack 추가 | conntrack 비활성화 |
| macvlan | 낮음 | MAC 기반 분리, 브리지 불필요 | NIC SR-IOV 결합 |
| ipvlan L3 | 매우 낮음 | L3 라우팅 기반, ARP 없음 | UDP 서버 권장 |
| SR-IOV VF passthrough | 없음 (HW) | NIC VF 직접 할당 | 최고 성능, HW 지원 필요 |
# 컨테이너 환경 UDP 성능 최적화 체크리스트
# 1. conntrack 비활성화 (불필요한 경우)
$ iptables -t raw -A PREROUTING -p udp --dport 8080 -j NOTRACK
$ iptables -t raw -A OUTPUT -p udp --sport 8080 -j NOTRACK
# 2. 네임스페이스별 sysctl 설정
$ ip netns exec container1 sysctl -w net.core.rmem_max=67108864
$ ip netns exec container1 sysctl -w net.core.rmem_default=26214400
# 3. veth 인터페이스 최적화
$ ethtool -K veth0 tx-checksum-ip-generic on
$ ethtool -K veth0 generic-receive-offload on
$ ethtool -K veth0 tx-udp-segmentation on
# 4. XDP redirect로 veth 오버헤드 우회 (커널 5.10+)
# BPF 프로그램으로 패킷을 직접 컨테이너 네임스페이스로 전달
# 5. Kubernetes Pod 네트워크 성능 확인
$ kubectl exec -it pod -- ss -u -n -e
$ kubectl exec -it pod -- cat /proc/net/snmp | grep Udp:
-p 8080:8080/udp로 포트 매핑 시
커널은 DNAT(conntrack)을 사용하여 패킷을 컨테이너로 전달합니다.
conntrack 테이블 크기(nf_conntrack_max)와 해시 테이블 크기를 충분히 설정하세요.
UDP는 연결 상태가 없으므로 conntrack 타임아웃(nf_conntrack_udp_timeout, 기본 30초)이
짧아 엔트리가 빠르게 소멸되지만, 높은 PPS에서는 conntrack 자체가 병목이 될 수 있습니다.
UDP 커널 디버그 포인트
커널 개발자 관점에서 UDP 코드를 디버깅하기 위한 핵심 함수, tracepoint, 그리고
ftrace/kprobe를 활용한 런타임 분석 방법을 정리합니다.
UDP 핵심 함수 호출 체인
| 경로 | 함수 체인 | 소스 파일 |
|---|---|---|
| TX 메인 | sendmsg -> inet_sendmsg -> udp_sendmsg -> udp_send_skb -> ip_send_skb |
net/ipv4/udp.c |
| TX cork | udp_sendmsg -> ip_append_data -> udp_push_pending_frames |
net/ipv4/udp.c |
| RX 메인 | udp_rcv -> __udp4_lib_rcv -> udp_unicast_rcv_skb -> udp_queue_rcv_skb |
net/ipv4/udp.c |
| RX encap | __udp4_lib_rcv -> encap_rcv() (VXLAN/WG) |
net/ipv4/udp.c |
| RX mcast | __udp4_lib_rcv -> __udp4_lib_mcast_deliver |
net/ipv4/udp.c |
| Lookup | __udp4_lib_lookup_skb -> __udp4_lib_lookup -> compute_score |
net/ipv4/udp.c |
| GSO 분할 | validate_xmit_skb -> __udp_gso_segment -> skb_segment |
net/ipv4/udp_offload.c |
| GRO 병합 | napi_gro_receive -> udp_gro_receive -> skb_gro_receive |
net/ipv4/udp_offload.c |
ftrace를 이용한 UDP 함수 호출 추적
# ftrace로 UDP 수신 경로 전체 추적
# 1. function_graph tracer 설정
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
$ echo udp_rcv > /sys/kernel/debug/tracing/set_graph_function
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
# 2. 트레이스 확인
$ cat /sys/kernel/debug/tracing/trace
# 출력 예시:
# 0) | udp_rcv() {
# 0) | __udp4_lib_rcv() {
# 0) 0.123 us | udp4_csum_init();
# 0) | __udp4_lib_lookup_skb() {
# 0) | __udp4_lib_lookup() {
# 0) 0.456 us | compute_score();
# 0) 1.234 us | }
# 0) 1.567 us | }
# 0) | udp_unicast_rcv_skb() {
# 0) | udp_queue_rcv_skb() {
# 0) 0.789 us | sk_filter_trim_cap();
# 0) | __udp_enqueue_schedule_skb() {
# 0) 0.345 us | sk_data_ready();
# 0) 0.890 us | }
# 0) 2.345 us | }
# 0) 3.456 us | }
# 0) 7.890 us | }
# 0) 8.123 us | }
# 3. kprobe로 특정 함수 인자 확인
$ echo 'p:udp_drop __udp_enqueue_schedule_skb sk=%di sk_rcvbuf=+0x128(%di):s32 sk_rmem=+0x120(%di):s32' \
> /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/udp_drop/enable
# 4. 트레이스 중지 및 정리
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ echo nop > /sys/kernel/debug/tracing/current_tracer
UDP MIB 카운터 전체 레퍼런스
| MIB 카운터 | SNMP 이름 | 의미 | 증가 위치 |
|---|---|---|---|
UDP_MIB_INDATAGRAMS | InDatagrams | 정상 수신된 데이터그램 수 | udp_unicast_rcv_skb 성공 |
UDP_MIB_NOPORTS | NoPorts | 수신 소켓 없는 패킷 수 | __udp4_lib_rcv (sk==NULL) |
UDP_MIB_INERRORS | InErrors | 수신 오류 (버퍼, 체크섬 등) | 여러 드롭 포인트 |
UDP_MIB_OUTDATAGRAMS | OutDatagrams | 송신된 데이터그램 수 | udp_send_skb 성공 |
UDP_MIB_RCVBUFERRORS | RcvbufErrors | 수신 버퍼 오버플로우 | __udp_enqueue_schedule_skb |
UDP_MIB_SNDBUFERRORS | SndbufErrors | 송신 버퍼 오류 | udp_sendmsg 실패 |
UDP_MIB_CSUMERRORS | InCsumErrors | 체크섬 오류 | udp4_csum_init / udp_queue_rcv_skb |
UDP_MIB_IGNOREDMULTI | IgnoredMulti | 무시된 멀티캐스트 | 수신자 없는 멀티캐스트 |
UDP_MIB_MEMERRORS | MemErrors | 메모리 할당 실패 | skb_alloc 실패 등 |
# 실시간 UDP MIB 카운터 변화 추적
$ watch -d -n 1 'nstat -a 2>/dev/null | grep -i udp'
# -d: 변화된 값 강조 표시
# 결과 예시:
# UdpInDatagrams 123456 0.0
# UdpNoPorts 89 0.0
# UdpInErrors 4 0.0
# UdpOutDatagrams 98765 0.0
# UdpRcvbufErrors 1 0.0
# UdpInCsumErrors 0 0.0
# 특정 네임스페이스의 UDP 통계
$ ip netns exec myns nstat -a | grep -i udp
function_graph tracer는 모든 함수 호출에
계측 코드를 삽입하므로 프로덕션 환경에서 심각한 성능 저하를 유발할 수 있습니다.
프로덕션에서는 bpftrace의 kprobe나 perf trace의 tracepoint를 사용하세요.
ftrace는 개발/테스트 환경에서의 상세 분석에만 사용하는 것이 안전합니다.
관련 문서
UDP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
참고자료
- RFC 768 - User Datagram Protocol
- RFC 8085 - UDP Usage Guidelines
- RFC 3828 - The Lightweight User Datagram Protocol (UDP-Lite)
- RFC 6935 - IPv6 and UDP Checksums for Tunneled Packets
- RFC 6936 - Applicability Statement for Zero UDP Checksums in IPv6
- Linux Kernel Source: net/ipv4/udp.c
- Linux Kernel Source: net/ipv4/udp_offload.c
- Linux Kernel Source: include/net/udp.h
- Linux Kernel Documentation: UDP
- LWN: UDP GSO and GRO