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
Initializing 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 커넥티드 소켓 최적화
UDP는 비연결형 프로토콜이지만, connect() 시스템콜을 호출하여
특정 원격 주소에 "연결"할 수 있습니다. 이는 실제 네트워크 연결을 생성하지 않지만,
커널 내부에서 상당한 최적화를 가능하게 합니다.
커넥티드 UDP 소켓은 DNS 리졸버, QUIC, WireGuard, 게임 클라이언트 등
단일 피어와 통신하는 워크로드에서 핵심적인 성능 개선을 제공합니다.
connect()의 커널 내부 동작
/* net/ipv4/udp.c — UDP connect() 내부 동작
*
* connect()는 실제 네트워크 연결을 생성하지 않습니다.
* 커널 내부에서 다음 최적화를 수행합니다:
*/
int __ip4_datagram_connect(struct sock *sk,
struct sockaddr *uaddr, int addr_len)
{
struct inet_sock *inet = inet_sk(sk);
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct rtable *rt;
struct flowi4 fl4;
/* 1. 라우팅 lookup 수행 (이후 캐시됨) */
rt = ip_route_connect(&fl4, usin->sin_addr.s_addr,
inet->inet_saddr, ...);
/* 2. 소스 주소 확정 (바인드 안 된 경우) */
if (!inet->inet_saddr)
inet->inet_saddr = fl4.saddr;
inet->inet_rcv_saddr = inet->inet_saddr;
/* 3. 목적지 정보 저장 */
inet->inet_daddr = fl4.daddr;
inet->inet_dport = usin->sin_port;
/* 4. 소켓 상태를 SS_CONNECTED로 변경 */
sk->sk_state = TCP_ESTABLISHED;
/* 5. 라우팅 캐시 저장 (핵심 최적화)
* 이후 send()에서 라우팅 lookup을 완전히 건너뜀 */
sk_dst_set(sk, &rt->dst);
/* 6. 해시 테이블에서 hash → hash2 이동
* 4-tuple 기반 fast lookup이 가능해짐 */
sk_set_txhash(sk);
inet_rehash(sk);
return 0;
}
/* connect() 후 udp_sendmsg()의 fast path */
static int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
/* msg->msg_name == NULL → connected 소켓 */
if (msg->msg_name) {
/* 비커넥티드: 매번 주소 검증 + 라우팅 */
...
} else {
/* 커넥티드: 저장된 dst 사용
* sk_dst_get() → 라우팅 테이블 접근 불필요
* inet->inet_daddr/inet_dport 직접 사용 */
daddr = inet->inet_daddr;
dport = inet->inet_dport;
}
/* connected 소켓은 flowi4 구성도 단순화 */
rt = (struct rtable *)sk_dst_check(sk, 0);
if (!rt) {
/* dst 캐시 만료 시에만 재lookup */
rt = ip_route_output_flow(net, &fl4, sk);
sk_dst_set(sk, dst_clone(&rt->dst));
}
}
커넥티드 UDP의 수신 경로 최적화
/* 커넥티드 UDP 소켓은 수신 시에도 최적화됩니다:
*
* 1. hash2 테이블의 4-tuple exact match가 우선 시도됨
* 2. 매칭 소켓을 빠르게 찾아 compute_score() 비용 감소
* 3. ICMP 에러를 해당 소켓으로 직접 전달 가능
*/
/* __udp4_lib_lookup()에서 커넥티드 소켓 우선 매칭 */
static struct sock *__udp4_lib_lookup(...)
{
/* 1단계: hash2 (4-tuple) exact match 시도 */
result = udp4_lib_lookup2(net, saddr, sport,
daddr, dport, ...);
if (result)
return result; /* 커넥티드 소켓 즉시 반환 */
/* 2단계: hash (포트만) wildcard match */
result = udp4_lib_lookup1(...);
return result;
}
/* 커넥티드 소켓에서만 ICMP 에러 수신 가능 */
/* 비커넥티드 소켓은 ICMP destination unreachable을
* 수신할 방법이 없음 (어떤 sendto() 호출에 대한 것인지 알 수 없음) */
void udp_err(struct sk_buff *skb, u32 info)
{
/* ICMP 오류 → 커넥티드 소켓의 sk->sk_err에 설정 */
/* send()/recv() 시 errno로 반환됨 */
sk = __udp4_lib_lookup(...);
if (sk) {
if (!sock_owned_by_user(sk))
sk->sk_err = err;
sk->sk_error_report(sk);
}
}
| 특성 | 비커넥티드 (sendto) | 커넥티드 (connect + send) |
|---|---|---|
| 라우팅 lookup | 매 패킷마다 수행 | connect() 시 1회, 이후 캐시 |
| 소스 주소 선택 | 매번 결정 | connect() 시 확정 |
| 해시 테이블 위치 | hash (포트 기반) | hash2 (4-tuple 기반) |
| 수신 lookup 속도 | wildcard match (느림) | exact match (빠름) |
| ICMP 에러 수신 | 불가능 | 가능 (errno로 전달) |
| 다중 목적지 전송 | 가능 | 불가 (1개 목적지 고정) |
| 시스템콜 인터페이스 | sendto() / sendmsg() | send() / write() |
| MSG_MORE/UDP_CORK | 사용 가능하나 주의 필요 | 동일하게 사용 가능 |
| 성능 개선폭 (PPS) | 기준 | ~10-20% 향상 |
커넥티드 UDP 프로그래밍 패턴
/* 패턴 1: 단일 피어 통신 (DNS 리졸버, 게임 클라이언트) */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in peer = {
.sin_family = AF_INET,
.sin_port = htons(53),
.sin_addr.s_addr = inet_addr("8.8.8.8")
};
connect(fd, (struct sockaddr *)&peer, sizeof(peer));
/* 이제 send()/recv() 사용 가능 — sendto()보다 빠름 */
send(fd, query, query_len, 0);
recv(fd, response, sizeof(response), 0);
/* ICMP 에러도 수신 가능 */
if (send(fd, data, len, 0) < 0 && errno == ECONNREFUSED) {
/* 피어가 ICMP Port Unreachable을 보냄 */
}
/* 패턴 2: connect() 해제 (다른 피어로 전환) */
struct sockaddr_in unspec = { .sin_family = AF_UNSPEC };
connect(fd, (struct sockaddr *)&unspec, sizeof(unspec));
/* 이제 다시 sendto()로 임의 목적지 전송 가능 */
/* 패턴 3: 다른 피어로 재연결 */
struct sockaddr_in new_peer = { ... };
connect(fd, (struct sockaddr *)&new_peer, sizeof(new_peer));
/* 라우팅 캐시가 새 목적지로 갱신됨 */
sendto()로
다른 목적지에 전송하면 EISCONN 에러가 발생합니다 (Linux에서는 msg_name이 NULL이 아니면 에러).
목적지를 변경하려면 먼저 AF_UNSPEC으로 disconnect 해야 합니다.
QUIC와 UDP 커널 지원
QUIC(RFC 9000)은 Google이 설계하고 IETF가 표준화한 차세대 전송 프로토콜로, UDP 위에 구현됩니다. HTTP/3(RFC 9114)의 기반이며, TLS 1.3을 전송 계층에 통합하여 0-RTT 연결 설정, 멀티플렉싱, 연결 마이그레이션을 지원합니다. 리눅스 커널은 QUIC 자체를 구현하지 않지만, UDP GSO/GRO, zerocopy, io_uring 등의 커널 기능이 QUIC 성능의 핵심 가속기 역할을 합니다.
QUIC에서 UDP GSO 활용
QUIC의 가장 큰 성능 병목은 시스템콜 빈도입니다. QUIC 패킷은 보통 1200~1450 바이트이며,
각 패킷을 개별 sendmsg()로 전송하면 초당 수만 회의 시스템콜이 발생합니다.
UDP GSO(UDP_SEGMENT)를 사용하면 여러 QUIC 패킷을 하나의 대형 버퍼로 구성하여
단일 시스템콜로 전송할 수 있습니다.
/* QUIC 서버의 UDP GSO 활용 패턴
*
* 핵심: QUIC 패킷은 개별 암호화되지만,
* 동일 크기 세그먼트로 구성하여 GSO 배치 전송 가능
*/
/* 1. GSO 세그먼트 크기 설정 (ancillary data) */
struct msghdr msg = {};
struct iovec iov;
char control[CMSG_SPACE(sizeof(uint16_t))];
/* 여러 QUIC 패킷을 연속 버퍼에 배치
* 각 세그먼트는 개별 암호화된 QUIC 패킷 */
uint8_t buf[64 * 1200]; /* 최대 64개 QUIC 패킷 */
int total_len = 0;
for (int i = 0; i < num_packets; i++) {
/* 각 QUIC 패킷을 개별 암호화 (AEAD) */
quic_encrypt_packet(&buf[i * 1200], packet[i]);
total_len += 1200;
}
iov.iov_base = buf;
iov.iov_len = total_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
/* GSO 세그먼트 크기 설정 */
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type = UDP_SEGMENT;
cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
*((uint16_t *)CMSG_DATA(cm)) = 1200; /* 세그먼트 크기 */
/* 단일 시스템콜로 64개 패킷 전송! */
sendmsg(fd, &msg, 0);
/* 커널 내부:
* udp_sendmsg() → ip_make_skb() → 64KB skb 생성
* → validate_xmit_skb() → __udp_gso_segment()
* → 64개 개별 UDP 패킷으로 분할 → NIC 전송
*
* 결과: 시스템콜 64x → 1x 감소
* Cloudflare 측정: GSO로 QUIC throughput 5-7x 향상
*/
QUIC에서 UDP GRO 활용
/* QUIC 서버의 UDP GRO 수신 패턴
*
* GRO 활성화 시 커널이 동일 흐름의 UDP 패킷을 병합하여
* 단일 recvmsg()로 여러 QUIC 패킷을 수신 가능
*/
/* GRO 활성화 */
int val = 1;
setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val));
/* GRO된 패킷 수신 */
char buf[65536];
char control[CMSG_SPACE(sizeof(uint16_t))];
struct msghdr msg = {};
struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
ssize_t n = recvmsg(fd, &msg, 0);
/* GRO 세그먼트 크기 추출 */
uint16_t gro_size = 0;
for (struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm; cm = CMSG_NXTHDR(&msg, cm)) {
if (cm->cmsg_level == SOL_UDP && cm->cmsg_type == UDP_GRO) {
gro_size = *((uint16_t *)CMSG_DATA(cm));
break;
}
}
/* GRO 병합된 버퍼를 개별 QUIC 패킷으로 분리 */
if (gro_size > 0) {
int offset = 0;
while (offset < n) {
int pkt_len = (n - offset > gro_size) ? gro_size : n - offset;
quic_process_packet(buf + offset, pkt_len);
offset += pkt_len;
}
}
/* 커널 내부:
* udp_gro_receive() → 동일 4-tuple 패킷 병합
* → 최대 64KB까지 하나의 skb로 합침
* → recvmsg()에서 한 번에 전달
*
* 결과: recvmsg() 호출 횟수 대폭 감소
* Google 측정: GRO로 QUIC 수신 CPU 40% 절감
*/
io_uring과 UDP (커널 5.6+)
/* io_uring으로 UDP 비동기 송수신
*
* 커널 5.6+: IORING_OP_SENDMSG / IORING_OP_RECVMSG 지원
* 커널 6.0+: multishot recvmsg 지원
* 커널 6.7+: IORING_OP_SEND_ZC (zero-copy send)
*
* QUIC 서버에서 io_uring 사용 시:
* - 시스템콜 오버헤드 완전 제거 (SQ polling)
* - 비동기 I/O로 이벤트 루프 단순화
* - zero-copy + GSO 결합으로 최대 성능
*/
/* io_uring 초기화 */
struct io_uring ring;
struct io_uring_params params = {
.flags = IORING_SETUP_SQPOLL, /* 커널 SQ 폴링 스레드 */
.sq_thread_idle = 1000, /* 1ms 유휴 시 절전 */
};
io_uring_queue_init_params(4096, &ring, ¶ms);
/* multishot recvmsg 등록 (커널 6.0+)
* 한 번 등록하면 패킷이 올 때마다 자동으로 CQE 생성 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recvmsg_multishot(sqe, fd, &msg, 0);
sqe->buf_group = 0; /* provided buffer group */
io_uring_submit(&ring);
/* CQE 수집 루프 */
struct io_uring_cqe *cqe;
while (1) {
io_uring_wait_cqe(&ring, &cqe);
if (cqe->res > 0) {
/* UDP 패킷 수신 완료 */
process_udp_packet(cqe);
}
io_uring_cqe_seen(&ring, cqe);
}
/* GSO + zero-copy 송신 (커널 6.7+) */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_send_zc(sqe, fd, buf, total_len, 0, 0);
sqe->msg_flags = MSG_ZEROCOPY;
/* cmsg로 UDP_SEGMENT 설정하여 GSO 적용 */
| 커널 기능 | QUIC 이점 | 성능 영향 | 최소 커널 |
|---|---|---|---|
UDP GSO (UDP_SEGMENT) | 다중 QUIC 패킷을 1회 syscall로 전송 | TX 처리량 5-7x 향상 | 4.18 |
| UDP GRO | 다중 QUIC 패킷을 1회 recvmsg로 수신 | RX CPU 40% 절감 | 5.0 |
| MSG_ZEROCOPY | 사용자 공간 → 커널 버퍼 복사 제거 | 대용량 전송 시 CPU 30% 절감 | 4.18 |
| sendmmsg() | 배치 시스템콜 (GSO 미지원 시 대안) | TX syscall 오버헤드 감소 | 3.0 |
| recvmmsg() | 배치 수신 (GRO 미지원 시 대안) | RX syscall 오버헤드 감소 | 2.6.33 |
| io_uring SENDMSG | 비동기 UDP 송신, SQ polling | syscall 진입 제거 | 5.6 |
| io_uring multishot RECVMSG | 지속적 비동기 수신 | CQE 자동 생성 | 6.0 |
| SO_REUSEPORT + eBPF | QUIC Connection ID 기반 소켓 분배 | 연결 마이그레이션 지원 | 4.6 |
| Connected UDP | 라우팅 캐시, fast path | per-packet 오버헤드 감소 | 전 버전 |
| ECN (IP_TOS cmsg) | 혼잡 감지 신호 전달 | 혼잡 제어 정확도 향상 | 전 버전 |
QUIC 최적화 sysctl 설정
# QUIC/HTTP/3 서버 최적화 커널 파라미터
# 1. UDP 수신 버퍼 (QUIC은 자체 흐름 제어이므로 충분히 설정)
$ sysctl -w net.core.rmem_max=67108864 # 64MB max
$ sysctl -w net.core.rmem_default=26214400 # 25MB default
# 2. UDP 송신 버퍼
$ sysctl -w net.core.wmem_max=67108864
$ sysctl -w net.core.wmem_default=26214400
# 3. softnet backlog (높은 PPS 대응)
$ sysctl -w net.core.netdev_max_backlog=250000
# 4. Busy Polling (저지연 QUIC 응답)
$ sysctl -w net.core.busy_poll=50 # 50μs 폴링
$ sysctl -w net.core.busy_read=50
# 5. 소켓별 설정 (QUIC 서버 코드에서)
# setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 25MB)
# setsockopt(fd, IPPROTO_UDP, UDP_GRO, 1)
# setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, 1)
# 6. NIC 설정
$ ethtool -K eth0 tx-udp-segmentation on # HW UDP GSO
$ ethtool -K eth0 rx-udp-gro-forwarding on # HW UDP GRO
$ ethtool -C eth0 adaptive-rx on # 적응형 코얼레싱
net/quic 프로젝트가 진행 중입니다 (kQUIC). 이는 암호화/복호화를
커널에서 처리하여 zero-copy와 splice를 지원하고, 사용자 공간 QUIC 라이브러리의
crypto 오버헤드를 줄이는 것이 목표입니다. 아직 메인라인에 합류하지 않았으므로
현재는 사용자 공간 QUIC 구현이 표준입니다.
UDP 보안과 공격 방어
UDP의 비연결형 특성은 공격자에게도 매력적입니다. 소스 IP 위조가 용이하고, handshake 없이 패킷을 대량 전송할 수 있으며, 증폭(amplification) 공격에 악용될 수 있습니다. 리눅스 커널의 UDP 보안 메커니즘과 DDoS 방어 전략을 분석합니다.
UDP Flood 방어
# 1. XDP 기반 UDP Flood 방어 (가장 빠른 경로)
# 드라이버 레벨에서 패킷 드롭, 커널 스택 진입 전 처리
# XDP 프로그램 예시 (rate limiting)
$ cat udp_ratelimit.bpf.c
/* SEC("xdp") int xdp_udp_ratelimit(struct xdp_md *ctx) {
* // UDP 패킷 파싱
* // per-source IP rate counter (BPF map)
* // 초과 시 XDP_DROP, 아니면 XDP_PASS
* } */
# 2. nftables 기반 UDP rate limiting
$ nft add table inet filter
$ nft add chain inet filter input '{ type filter hook input priority 0; }'
# UDP 패킷 레이트 제한 (초당 10000개, 버스트 5000)
$ nft add rule inet filter input \
udp dport 53 limit rate over 10000/second burst 5000 packets drop
# 소스 IP별 레이트 제한 (meter/set 사용)
$ nft add rule inet filter input \
udp dport 53 meter ratelimit '{ ip saddr limit rate over 100/second }' drop
# 3. conntrack 기반 UDP 연결 추적 제한
# UDP conntrack 타임아웃 단축 (공격 시 빠른 엔트리 해제)
$ sysctl -w net.netfilter.nf_conntrack_udp_timeout=10
$ sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=30
# 4. SYN proxy 스타일의 UDP 방어는 불가능
# → 대신 애플리케이션 레벨 challenge-response 사용
# QUIC: Retry Token, DTLS: HelloVerifyRequest, DNS: Cookies
Amplification 공격과 방어
| 프로토콜 | 증폭 배율 | 포트 | 방어 방법 |
|---|---|---|---|
| memcached | ~51,000x | 11211/udp | UDP 비활성화 (-U 0), 방화벽 |
| NTP (monlist) | ~556x | 123/udp | noquery 설정, restrict |
| DNS (ANY) | ~28-54x | 53/udp | Response Rate Limiting (RRL) |
| SSDP | ~30x | 1900/udp | 외부 접근 차단 |
| SNMP v2 | ~6x | 161/udp | 커뮤니티 문자열 제한, ACL |
| CHARGEN | ~358x | 19/udp | 서비스 비활성화 |
| CLDAP | ~56-70x | 389/udp | 외부 접근 차단 |
| TFTP | ~60x | 69/udp | 외부 접근 차단 |
IP Spoofing 방어 (BCP38/BCP84)
# Reverse Path Filtering (rp_filter) — 소스 IP 검증
# 수신 인터페이스로 해당 소스 IP에 도달 가능한지 검증
# Strict mode (권장): 수신 인터페이스가 최적 경로여야 함
$ sysctl -w net.ipv4.conf.all.rp_filter=1
$ sysctl -w net.ipv4.conf.default.rp_filter=1
# Loose mode: 어떤 인터페이스든 경로가 존재하면 허용
# (비대칭 라우팅 환경에서 사용)
$ sysctl -w net.ipv4.conf.all.rp_filter=2
# ICMP rate limiting (Port Scanning 방어)
# Port Unreachable 응답 빈도 제한
$ sysctl -w net.ipv4.icmp_ratelimit=1000 # ms 단위
$ sysctl -w net.ipv4.icmp_msgs_per_sec=100 # 초당 메시지
# UDP 포트 스캔 방어: 닫힌 포트의 ICMP 응답 억제
$ iptables -A OUTPUT -p icmp --icmp-type destination-unreachable \
-m limit --limit 10/second -j ACCEPT
$ iptables -A OUTPUT -p icmp --icmp-type destination-unreachable -j DROP
커널 레벨 UDP 보안 sysctl
| sysctl 파라미터 | 기본값 | 설명 | 보안 영향 |
|---|---|---|---|
net.ipv4.conf.all.rp_filter | 0 | 역방향 경로 필터링 | 1(strict)로 설정하여 IP spoofing 방지 |
net.ipv4.icmp_ratelimit | 1000 | ICMP 응답 빈도 제한 (ms) | 낮출수록 포트 스캔 정보 노출 감소 |
net.ipv4.icmp_msgs_per_sec | 1000 | 초당 ICMP 메시지 상한 | 과도한 ICMP 응답 방지 |
net.core.rmem_max | 212992 | 수신 버퍼 상한 | 적절한 제한으로 메모리 고갈 방지 |
net.netfilter.nf_conntrack_max | 65536 | conntrack 테이블 크기 | UDP flood 시 conntrack 오버플로우 방지 |
net.netfilter.nf_conntrack_udp_timeout | 30 | UDP conntrack 타임아웃 (초) | 짧게 설정하여 엔트리 빠른 회수 |
net.ipv4.ip_local_port_range | 32768-60999 | 로컬 포트 범위 | 예측 가능한 포트 회피 |
net.core.netdev_budget | 300 | NAPI 처리 예산 | softirq CPU 독점 방지 |
UDP와 eBPF/XDP
eBPF(extended Berkeley Packet Filter)와 XDP(eXpress Data Path)는
UDP 워크로드의 성능과 유연성을 극대화하는 리눅스 커널 기술입니다.
SO_REUSEPORT eBPF 프로그램, XDP에서의 UDP 패킷 처리,
TC hookpoint에서의 필터링, 그리고 sk_lookup 프로그램을 통한
커널 소켓 분배 로직 커스터마이징까지 다룹니다.
SO_REUSEPORT eBPF 프로그램
/* SO_REUSEPORT eBPF: 패킷 내용에 기반한 소켓 분배
*
* 기본 SO_REUSEPORT는 4-tuple 해시 기반으로 소켓을 선택합니다.
* eBPF 프로그램을 부착하면 패킷 페이로드를 검사하여
* 커스텀 로직으로 소켓을 선택할 수 있습니다.
*
* 대표 사용 사례:
* - QUIC: Connection ID로 소켓 분배 (연결 마이그레이션 지원)
* - DNS: 쿼리 이름/유형으로 분배
* - 게임 서버: 세션 ID로 분배
*/
/* BPF 프로그램 (libbpf CO-RE) */
SEC("sk_reuseport")
int quic_reuseport(struct sk_reuseport_md *ctx)
{
/* UDP 페이로드에서 QUIC Connection ID 추출 */
__u8 *data = ctx->data;
__u32 data_len = ctx->len;
if (data_len < 1)
return SK_DROP;
/* QUIC Long Header (첫 바이트 bit 7 = 1) */
if (data[0] & 0x80) {
/* Initial/Handshake: 기본 해시 분배 */
return SK_PASS;
}
/* QUIC Short Header → Connection ID 기반 분배 */
if (data_len < 5)
return SK_PASS;
/* Connection ID 해시로 소켓 인덱스 결정 */
__u32 conn_id;
bpf_skb_load_bytes(ctx, 1, &conn_id, 4);
__u32 index = conn_id % ctx->reuseport_id;
/* BPF map에서 소켓 FD 찾아 선택 */
return bpf_sk_select_reuseport(ctx, &sock_map,
&index, 0);
}
/* 사용자 공간에서 BPF 프로그램 부착 */
/*
* int prog_fd = bpf_program__fd(skel->progs.quic_reuseport);
* setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF,
* &prog_fd, sizeof(prog_fd));
*/
XDP에서 UDP 패킷 처리
/* XDP UDP 처리 예시: DNS 응답 캐시 (커널 바이패스)
*
* XDP에서 직접 DNS 쿼리에 응답하여
* 커널 스택을 완전히 우회합니다.
* 초당 수백만 쿼리 처리 가능.
*/
SEC("xdp")
int xdp_dns_cache(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* Ethernet → IP → UDP 헤더 파싱 */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (struct iphdr *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_UDP)
return XDP_PASS;
struct udphdr *udp = (struct udphdr *)(ip + 1);
if ((void *)(udp + 1) > data_end)
return XDP_PASS;
if (udp->dest != bpf_htons(53))
return XDP_PASS;
/* DNS 쿼리 캐시 히트 확인 */
struct dns_key key = {};
parse_dns_query(udp + 1, data_end, &key);
struct dns_response *resp = bpf_map_lookup_elem(
&dns_cache, &key);
if (!resp)
return XDP_PASS; /* 캐시 미스: 커널 스택으로 */
/* 캐시 히트: XDP에서 직접 응답 생성 */
build_dns_response(ctx, eth, ip, udp, resp);
/* MAC/IP 주소 swap 후 XDP_TX로 즉시 반환 */
swap_mac(eth);
swap_ip(ip);
swap_udp_ports(udp);
return XDP_TX; /* 동일 인터페이스로 전송 */
}
/* 성능:
* - 일반 BIND9: ~500K qps
* - XDP DNS 캐시: ~10M qps (20x 향상)
* - Facebook의 katran 로드밸런서: XDP 기반 UDP LB
*/
sk_lookup 프로그램 (커널 5.9+)
/* sk_lookup: UDP 소켓 lookup 로직을 eBPF로 오버라이드
*
* 기본 커널 lookup은 bind된 주소/포트에만 매칭하지만,
* sk_lookup BPF 프로그램으로 임의의 소켓에 패킷을 전달할 수 있습니다.
*
* 사용 사례:
* - 모든 포트의 UDP 패킷을 단일 소켓으로 집중
* - IP 범위 기반 소켓 분배
* - 동적 포트 매핑
*/
SEC("sk_lookup")
int udp_dispatch(struct bpf_sk_lookup *ctx)
{
/* UDP 프로토콜만 처리 */
if (ctx->protocol != IPPROTO_UDP)
return SK_PASS;
/* 목적지 포트 범위에 따라 소켓 선택 */
__u32 port = ctx->local_port;
__u32 key;
if (port >= 10000 && port <= 20000)
key = 0; /* 게임 서버 소켓 */
else if (port >= 20001 && port <= 30000)
key = 1; /* 미디어 서버 소켓 */
else
return SK_PASS; /* 기본 lookup */
struct bpf_sock *sk = bpf_map_lookup_elem(
&server_sockets, &key);
if (!sk)
return SK_PASS;
/* 선택한 소켓에 패킷 전달 */
long err = bpf_sk_assign(ctx, sk, 0);
bpf_sk_release(sk);
return err ? SK_PASS : SK_PASS;
}
| BPF 프로그램 타입 | hookpoint | UDP 사용 사례 | 최소 커널 |
|---|---|---|---|
BPF_PROG_TYPE_XDP | NIC 드라이버 | UDP 패킷 필터링, 리다이렉트, 응답 생성 | 4.8 |
BPF_PROG_TYPE_SCHED_CLS | TC ingress/egress | UDP 트래픽 셰이핑, 패킷 수정 | 4.1 |
BPF_PROG_TYPE_SK_REUSEPORT | SO_REUSEPORT 선택 | QUIC Connection ID 분배 | 4.6 |
BPF_PROG_TYPE_SK_LOOKUP | 소켓 lookup | 포트 범위 → 소켓 매핑 | 5.9 |
BPF_PROG_TYPE_SOCKET_FILTER | 소켓 수신 | 패킷 필터링 (tcpdump 대안) | 3.18 |
BPF_PROG_TYPE_CGROUP_SKB | cgroup ingress/egress | 컨테이너별 UDP 정책 | 4.10 |
BPF_PROG_TYPE_SK_MSG | 소켓 메시지 | UDP → 다른 소켓 리다이렉트 (sockmap) | 4.17 |
UDP 메모리 관리 심화
UDP 소켓의 메모리 관리는 성능과 안정성의 핵심입니다.
sk_rmem_alloc, sk_wmem_alloc, forward allocation,
memory pressure 메커니즘을 이해하면 수신 버퍼 오버플로우와 메모리 고갈을 방지하고
최적의 처리량을 달성할 수 있습니다.
수신 버퍼 회계: __udp_enqueue_schedule_skb()
/* net/ipv4/udp.c — UDP 수신 버퍼 enqueue
*
* 수신된 UDP 패킷을 소켓 큐에 넣기 전에
* 메모리 제한을 체크합니다. 이 함수가 실패하면
* RcvbufErrors 카운터가 증가합니다.
*/
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, err = -ENOMEM;
int size;
/* 1. skb의 실제 메모리 사용량 계산
* (데이터 + struct sk_buff + 패딩 포함) */
size = skb->truesize;
/* 2. 현재 수신 버퍼 사용량 확인 */
rmem = atomic_read(&sk->sk_rmem_alloc);
/* 3. 수신 버퍼 상한 초과 체크
* sk_rcvbuf에 여유 공간이 있는지 확인 */
if (rmem + size > sk->sk_rcvbuf) {
/* forward_alloc으로 보전할 수 있는지 확인 */
amt = sk->sk_rcvbuf - rmem;
if (amt < size) {
/* 완전히 초과 → 드롭 */
atomic_inc(&sk->sk_drops);
goto drop;
}
}
/* 4. 메모리 과금 (accounting) */
atomic_add(size, &sk->sk_rmem_alloc);
skb_set_owner_r(skb, sk);
/* 5. 큐에 추가 및 wakeup */
__skb_queue_tail(list, skb);
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_data_ready(sk);
return 0;
drop:
/* RcvbufErrors 카운터 증가 */
__UDP_INC_STATS(sock_net(sk),
UDP_MIB_RCVBUFERRORS, 0);
kfree_skb(skb);
return err;
}
Forward Allocation 메커니즘
/* UDP의 forward allocation:
*
* recvmsg() 시 즉시 해제하지 않고 "여유분"으로 남겨둡니다.
* 다음 수신 패킷이 이 여유분을 사용하므로
* sk_rmem_alloc 업데이트의 원자적 연산을 줄입니다.
*
* 이는 고 PPS 환경에서 atomic 경합을 완화하는 핵심 최적화입니다.
*/
/* recvmsg() 시 forward allocation */
static void udp_rmem_release(struct sock *sk, int size,
int partial, bool rx_queue_lock_held)
{
struct udp_sock *up = udp_sk(sk);
int amt;
if (likely(googler_per_socked_optimization)) {
/* forward_alloc에 여유분 추가 */
up->forward_deficit += size;
/* threshold 미만이면 실제 해제 보류 */
if (up->forward_deficit < up->forward_threshold)
return;
/* threshold 도달: 누적된 만큼 한 번에 해제 */
amt = up->forward_deficit;
up->forward_deficit = 0;
} else {
amt = size;
}
/* atomic 연산은 여기서만 1번 수행 */
atomic_sub(amt, &sk->sk_rmem_alloc);
/* writer 대기 중이면 wakeup */
if (sock_has_waiters(sk))
sk->sk_write_space(sk);
}
/* forward_threshold 기본값:
* sk->sk_rcvbuf >> 2 (수신 버퍼의 1/4)
*
* 이 값까지는 atomic_sub를 보류하고
* 도달 시 한 번에 해제하여 캐시라인 경합을 줄임
*/
전역 UDP 메모리 제한
# 전역 UDP 메모리 제한 확인
$ cat /proc/sys/net/ipv4/udp_mem
# 188319 251092 376638
# [min] [pressure] [max] (페이지 단위, 1 페이지 = 4KB)
#
# min (188319): UDP가 자유롭게 사용할 수 있는 메모리 (753MB)
# pressure (251092): 이 값 초과 시 메모리 압박 → 새 소켓 할당 제한
# max (376638): 절대 상한, 초과 시 패킷 드롭 (1.5GB)
# 현재 UDP 메모리 사용량
$ cat /proc/net/sockstat
# UDP: inuse 42 mem 1234
# → 42개 UDP 소켓, 1234 페이지(~5MB) 사용 중
# 메모리 제한 조정 (대규모 UDP 서버)
$ sysctl -w net.ipv4.udp_mem='376638 502184 753276'
# 소켓 단위 모니터링
$ ss -u -n -e -m
# 출력:
# UNCONN 0 0 *:8080 *:* uid:1000 ino:12345
# skmem:(r128000,rb8388608,t0,tb8388608,f0,w0,o0,bl0,d0)
# r: sk_rmem_alloc (현재 수신 버퍼 사용)
# rb: sk_rcvbuf (수신 버퍼 상한)
# t: sk_wmem_alloc (현재 송신 버퍼 사용)
# tb: sk_sndbuf (송신 버퍼 상한)
# f: sk_forward_alloc (forward allocation 여유분)
# d: sk_drops (드롭 카운터)
skb->truesize는 실제 패킷 데이터보다 훨씬 큽니다.
sizeof(struct sk_buff)(~240바이트) + 메모리 할당 패딩이 포함되므로,
100바이트 UDP 패킷의 truesize는 약 768바이트가 될 수 있습니다.
따라서 sk_rcvbuf=8MB라도 실제 수용 가능한 패킷 수는
8MB / 768 ≈ 10,000개 수준입니다. 작은 패킷이 많은 워크로드에서는
수신 버퍼를 넉넉하게 잡아야 합니다.
UDP IPv6 특화 경로
IPv6에서의 UDP 처리는 IPv4와 대부분 동일하지만, 체크섬 필수화,
flow label 활용, 확장 헤더 처리, dual-stack 소켓 동작 등
중요한 차이점이 있습니다. net/ipv6/udp.c의 핵심 경로를 분석합니다.
| 특성 | IPv4 UDP | IPv6 UDP |
|---|---|---|
| 체크섬 | 선택 (0이면 미검증) | 필수 (RFC 8200, 0 금지) |
| 체크섬 예외 | 없음 | 터널에서 0 허용 (RFC 6935/6936) |
| 소스 파일 | net/ipv4/udp.c | net/ipv6/udp.c |
| 수신 함수 | __udp4_lib_rcv() | __udp6_lib_rcv() |
| 송신 함수 | udp_sendmsg() | udpv6_sendmsg() |
| 소켓 lookup | 4-tuple (IP + port) | 4-tuple + flow label |
| proto_ops | inet_dgram_ops | inet6_dgram_ops |
| Jumbogram | 불가 (IP 16bit) | 가능 (확장 헤더, RFC 2675) |
| 스코프 ID | 해당 없음 | link-local 주소에 sin6_scope_id 필요 |
| Dual-stack | IPv4 전용 | IPv4-mapped IPv6로 IPv4 수신 가능 |
IPv6 체크섬 필수화
/* net/ipv6/udp.c — IPv6 UDP 체크섬은 필수
*
* IPv4에서는 UDP 체크섬이 0이면 미검증으로 통과하지만,
* IPv6에서는 체크섬이 0인 패킷을 드롭합니다 (RFC 8200).
*
* 예외: UDP 터널에서 no_check6_rx 비트가 설정된 경우 (RFC 6935)
*/
static int udp6_csum_init(struct sk_buff *skb,
struct udphdr *uh, int proto)
{
if (uh->check == 0) {
/* IPv6: 체크섬 0은 허용되지 않음 */
__UDP6_INC_STATS(net, UDP_MIB_CSUMERRORS, proto);
__UDP6_INC_STATS(net, UDP_MIB_INERRORS, proto);
kfree_skb(skb);
return -1;
}
/* 체크섬 검증 (pseudo header 포함) */
if (skb_checksum_init(skb, IPPROTO_UDP,
ip6_compute_pseudo))
goto csum_error;
return 0;
}
/* 터널용 체크섬 0 허용 설정 (VXLAN, Geneve 등)
*
* setsockopt(fd, IPPROTO_UDP, UDP_NO_CHECK6_TX, &val, sizeof(val));
* setsockopt(fd, IPPROTO_UDP, UDP_NO_CHECK6_RX, &val, sizeof(val));
*
* 이는 udp_sock의 no_check6_tx, no_check6_rx 비트를 설정합니다.
* VXLAN 등 터널 드라이버가 setup_udp_tunnel_sock()에서 자동 설정합니다.
*/
Flow Label 기반 소켓 분배
/* IPv6 flow label을 활용한 UDP 소켓 분배
*
* IPv6 헤더의 20-bit flow label은 동일 흐름의 패킷을
* 식별하는 데 사용됩니다.
*
* SO_REUSEPORT + hash2에서 flow label이 해시 입력에 포함되어
* 동일 소스에서의 다중 흐름을 다른 소켓에 분산시킵니다.
*/
/* udp6_lib_lookup2()의 compute_score에서 */
static int compute_score(struct sock *sk,
const struct net *net,
const struct in6_addr *saddr,
__be16 sport,
const struct in6_addr *daddr,
unsigned short hnum,
int dif, int sdif)
{
/* ... */
score = 0;
/* 네트워크 네임스페이스 검증 */
if (!net_eq(sock_net(sk), net))
return -1;
/* 포트 매칭 */
if (inet->inet_num != hnum)
return -1;
score++;
/* IPv6 주소 매칭 */
if (!ipv6_addr_any(&sk->sk_v6_rcv_saddr)) {
if (!ipv6_addr_equal(&sk->sk_v6_rcv_saddr, daddr))
return -1;
score++;
}
/* ... flow label은 hash2 키에 포함 ... */
return score;
}
/* 자동 flow label 생성 (송신 측)
*
* net.ipv6.auto_flowlabels = 1 (기본값)
* → 커널이 자동으로 flow label 생성
* → 동일 소스/목적지 + 프로토콜의 흐름 식별
*
* ECMP 라우터에서 flow label로 해싱하면
* 동일 흐름의 패킷이 같은 경로로 전달됩니다.
*/
Dual-Stack 소켓 (IPv4-mapped IPv6)
/* Dual-stack UDP 소켓:
*
* AF_INET6 소켓이 IPv4 패킷도 수신할 수 있습니다.
* IPv4 소스 주소는 ::ffff:a.b.c.d 형태로 매핑됩니다.
*
* 기본 동작: IPV6_V6ONLY=0 (dual-stack 활성)
*/
/* IPv4 → IPv6 dual-stack 소켓 수신 */
int fd = socket(AF_INET6, SOCK_DGRAM, 0);
/* dual-stack 활성화 (기본값) */
int v6only = 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
struct sockaddr_in6 addr = {
.sin6_family = AF_INET6,
.sin6_port = htons(8080),
.sin6_addr = in6addr_any, /* :: — IPv4 + IPv6 모두 수신 */
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
/* IPv4 클라이언트(192.168.1.100)에서 수신 시:
* recvfrom()의 소스 주소:
* sin6_addr = ::ffff:192.168.1.100
* sin6_port = 클라이언트 포트
*
* 커널 내부:
* __udp4_lib_rcv() → v4-mapped 소켓 발견
* → udpv6_rcv()로 전달 → IPv6 소켓 큐에 enqueue
*/
/* 커널 코드: net/ipv4/udp.c */
/* __udp4_lib_rcv에서 IPv6 소켓이 매칭된 경우 */
if (sk_is_ipv6(sk)) {
/* IPv4 주소를 IPv4-mapped IPv6로 변환 후 전달 */
ipv6_addr_set_v4mapped(ip_hdr(skb)->saddr,
&ipv6_hdr(skb)->saddr);
}
UDP 커널 버전별 변경 이력
리눅스 커널의 UDP 구현은 지속적으로 발전해왔습니다. 주요 커널 버전별 UDP 관련 변경사항과 성능 개선을 정리합니다.
| 커널 버전 | 변경사항 | 영향 |
|---|---|---|
| 2.6.33 | recvmmsg() 시스템콜 추가 | 배치 수신으로 시스템콜 오버헤드 감소 |
| 3.0 | sendmmsg() 시스템콜 추가 | 배치 송신 지원 |
| 3.9 | SO_REUSEPORT 도입 | 다중 소켓 바인드, 멀티코어 수신 분산 |
| 4.6 | SO_REUSEPORT eBPF 프로그램 지원 | 커스텀 소켓 분배 로직 |
| 4.8 | XDP 프레임워크 도입 | 드라이버 레벨 패킷 처리 |
| 4.11 | UDP 해시 테이블 4-tuple hash2 도입 | 커넥티드 소켓 fast lookup |
| 4.14 | MSG_ZEROCOPY UDP 지원 | 대용량 전송 시 복사 제거 |
| 4.18 | UDP GSO (UDP_SEGMENT) 도입 | 단일 syscall로 다중 패킷 전송, QUIC 가속 |
| 5.0 | UDP GRO 도입 | 수신 패킷 병합, QUIC RX 최적화 |
| 5.0 | UDP early demux 개선 | 커넥티드 소켓의 라우팅 캐시 효율화 |
| 5.6 | io_uring UDP SENDMSG/RECVMSG 지원 | 비동기 UDP I/O |
| 5.9 | BPF_PROG_TYPE_SK_LOOKUP 도입 | 소켓 lookup 로직 커스터마이징 |
| 5.17 | SKB_DROP_REASON 인프라 | 패킷 드롭 원인 추적 (UDP 포함) |
| 5.19 | UDP GRO list 최적화 | GRO 병합 성능 개선 |
| 6.0 | io_uring multishot recvmsg | 지속적 비동기 수신 |
| 6.2 | UDP 소켓 해시 테이블 RCU 개선 | read-side lock-free 성능 향상 |
| 6.5 | UDP GRO forwarding 지원 | GRO된 패킷의 직접 포워딩 |
| 6.7 | io_uring zero-copy send (SEND_ZC) | io_uring + GSO + zerocopy 결합 |
| 6.8 | UDP per-netns 해시 테이블 (옵션) | 컨테이너 환경 lookup 격리 개선 |
| 6.11 | UDP GRO 개선 (timestamp 보존) | GRO 병합 시 타임스탬프 정확도 유지 |
UDP 프로그래밍 흔한 실수
UDP는 단순한 프로토콜이지만, 그 단순함이 오히려 미묘한 버그와 성능 문제를 유발합니다. 커널 개발자와 애플리케이션 개발자가 자주 범하는 실수 패턴과 올바른 해결 방법을 정리합니다.
실수 1: 수신 버퍼 크기 미설정
/* ❌ 잘못된 코드: 기본 버퍼로 고 PPS 처리 시도 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
bind(fd, ...);
/* rmem_default (보통 212992 = ~208KB)로 시작
* → 고 PPS에서 즉시 RcvbufErrors 발생 */
/* ✅ 올바른 코드: 워크로드에 맞게 버퍼 설정 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
/* rmem_max 이하로 설정 (커널이 2배로 적용) */
int rcvbuf = 8388608; /* 8MB (실제 16MB 할당) */
setsockopt(fd, SOL_SOCKET, SO_RCVBUF,
&rcvbuf, sizeof(rcvbuf));
/* 확인: 실제 할당된 크기 */
int actual;
socklen_t len = sizeof(actual);
getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &actual, &len);
/* actual = 16777216 (2배) — 커널이 관리 오버헤드용으로 2배 할당 */
bind(fd, ...);
실수 2: sendto()에서 MTU 초과 데이터 전송
/* ❌ 잘못된 코드: MTU 무시하고 대용량 전송 */
char buf[65507]; /* UDP 최대 페이로드 */
sendto(fd, buf, sizeof(buf), 0, &dest, sizeof(dest));
/* → IP 프래그먼테이션 발생
* → 중간 라우터가 프래그먼트 드롭할 수 있음
* → 하나라도 손실되면 전체 데이터그램 재조립 실패
* → 실질 전달률 급감 */
/* ✅ 올바른 코드: PMTU(Path MTU) 이하로 전송 */
/* 방법 1: IP_PMTUDISC_DO로 PMTU 검색 */
int pmtu = IP_PMTUDISC_DO;
setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER,
&pmtu, sizeof(pmtu));
/* MTU 초과 시 EMSGSIZE 에러 반환 → 분할 후 재전송 */
if (sendto(...) < 0 && errno == EMSGSIZE) {
/* MTU 조회 */
int mtu;
socklen_t len = sizeof(mtu);
getsockopt(fd, IPPROTO_IP, IP_MTU, &mtu, &len);
/* mtu - 20(IP) - 8(UDP) = 최대 페이로드 */
}
/* 방법 2: 안전한 기본 크기 사용 */
/* 인터넷: 1280(IPv6 최소 MTU) - 48(헤더) = 1232바이트 (QUIC 기본) */
/* LAN: 1500 - 28 = 1472바이트 (일반적 Ethernet) */
실수 3: recvfrom() 버퍼 크기 부족
/* ❌ 잘못된 코드: 작은 버퍼로 수신 */
char buf[512];
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, ...);
/* UDP 데이터그램이 512바이트보다 크면?
* → 초과분이 잘려나감 (MSG_TRUNC flag 설정)
* → n = 512 (실제 수신 크기), 나머지 데이터 손실
* → 알림도 없이 조용히 데이터 손실! */
/* ✅ 올바른 코드: 충분한 버퍼 + MSG_TRUNC 체크 */
char buf[65535]; /* 최대 UDP 페이로드 */
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, ...);
/* 또는 MSG_TRUNC로 실제 크기 확인 (Linux 전용) */
ssize_t n = recvfrom(fd, buf, sizeof(buf), MSG_PEEK | MSG_TRUNC, ...);
/* n = 실제 데이터그램 크기 (버퍼보다 클 수 있음) */
실수 4: 멀티캐스트 소켓 바인드 누락
/* ❌ 잘못된 코드: INADDR_ANY에 바인드 후 멀티캐스트 그룹 가입 */
bind(fd, INADDR_ANY, port);
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, ...);
/* → 모든 인터페이스의 유니캐스트 패킷도 수신
* → 보안 문제 + 불필요한 패킷 처리 */
/* ✅ 올바른 코드: 멀티캐스트 주소에 바인드 */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = inet_addr("239.1.2.3") /* 멀티캐스트 그룹 */
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
/* 수신 인터페이스 명시 */
struct ip_mreqn mreq = {
.imr_multiaddr.s_addr = inet_addr("239.1.2.3"),
.imr_ifindex = if_nametoindex("eth0")
};
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
실수 5: 비커넥티드 소켓에서 ICMP 에러 무시
/* ❌ 잘못된 코드: sendto()의 에러가 비동기로 발생 */
sendto(fd, data, len, 0, &dest, sizeof(dest));
/* sendto()는 성공 반환하지만
* 실제로는 피어의 포트가 닫혀 있음
* → ICMP Port Unreachable이 돌아오지만
* → 비커넥티드 소켓에서는 이 에러를 수신할 방법 없음
* → 다음 sendto()에서도 성공 반환 (에러 영원히 감지 못함) */
/* ✅ 올바른 코드: connect()로 ICMP 에러 수신 */
connect(fd, &dest, sizeof(dest));
if (send(fd, data, len, 0) < 0) {
if (errno == ECONNREFUSED) {
/* 피어가 ICMP Port Unreachable을 보냄 */
}
}
/* 또는 IP_RECVERR로 에러 큐 사용 (비커넥티드에서도 가능) */
int val = 1;
setsockopt(fd, IPPROTO_IP, IP_RECVERR, &val, sizeof(val));
/* 에러 메시지 수신 */
struct msghdr msg = {};
char control[256];
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
recvmsg(fd, &msg, MSG_ERRQUEUE);
/* cmsg에서 struct sock_extended_err 추출 */
실수 6: GSO 사용 시 세그먼트 크기 불일치
/* ❌ 잘못된 코드: 마지막 세그먼트 크기가 다름 */
uint16_t gso_size = 1200;
char buf[1200 * 10 + 500]; /* 12500바이트 */
/* → 마지막 세그먼트가 500바이트 (1200 != 500)
* → 일부 NIC에서 HW GSO 실패 → SW fallback
* → 성능 저하 */
/* ✅ 올바른 코드: 마지막 세그먼트만 작을 수 있음 (정상)
* 그러나 HW GSO 지원 확인 필요 */
/* ethtool -k eth0 | grep tx-udp-segmentation
* → on이면 HW GSO 지원 */
/* 안전한 패턴: 동일 크기 세그먼트로 구성 */
int num_full = total_len / gso_size;
int remainder = total_len % gso_size;
/* 전체 세그먼트만 GSO 전송 */
send_gso(buf, num_full * gso_size, gso_size);
/* 나머지는 개별 전송 */
if (remainder > 0)
send(fd, buf + num_full * gso_size, remainder, 0);
| 실수 패턴 | 증상 | 진단 방법 | 해결 |
|---|---|---|---|
| 수신 버퍼 미설정 | RcvbufErrors 급증 | nstat | grep RcvbufErrors | SO_RCVBUF + sysctl rmem_max |
| MTU 초과 전송 | 패킷 손실, 저 처리량 | netstat -s | grep fragment | IP_PMTUDISC_DO |
| recvfrom 버퍼 부족 | 데이터 잘림 | MSG_TRUNC으로 확인 | 버퍼 65535+ 사용 |
| 단일 소켓 수신 | CPU 단일 코어 100% | mpstat -P ALL | SO_REUSEPORT |
| 비커넥티드에서 에러 무시 | 실패 감지 못함 | tcpdump icmp | connect() 또는 IP_RECVERR |
| 블로킹 recvfrom 무한대기 | 애플리케이션 hang | strace | SO_RCVTIMEO 또는 poll() |
| SO_RCVBUF 2배 비인지 | 예상보다 큰 메모리 | getsockopt | 커널이 2배 할당함을 인지 |
| GRO 미활성 QUIC | 높은 CPU, 낮은 처리량 | ss -u -e | setsockopt UDP_GRO |
관련 문서
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