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의 모든 경로를 다룹니다.

전제 조건: 네트워크 스택IP 프로토콜 문서를 먼저 읽으세요. 전송 계층은 재전송, 흐름 제어, 연결 상태 전이가 핵심이므로 패킷 생명주기를 먼저 잡아야 디버깅이 쉬워집니다. sk_buff 구조도 함께 보면 버퍼 관리 동작을 이해하기 수월합니다.
일상 비유: UDP는 엽서 발송과 비슷합니다. TCP가 등기우편(접수 확인, 추적 번호, 재배달)이라면, UDP는 우체통에 엽서를 넣는 것입니다. 빠르지만 도착 보장이 없고, 순서가 바뀔 수 있으며, 분실되어도 알 수 없습니다. 대신 봉투(헤더)가 작아 비용이 낮고, 다수에게 동시에 보내는 것(멀티캐스트)도 가능합니다.

핵심 요약

  • 비연결형(Connectionless) — handshake 없이 즉시 전송하며, 연결 상태를 유지하지 않습니다.
  • 메시지 경계 보존 — TCP와 달리 datagram 단위로 수신하여 애플리케이션 프로토콜 경계가 그대로 유지됩니다.
  • UDP GRO/GSO — QUIC, WireGuard 등 고성능 워크로드의 핵심 최적화로 시스템콜 오버헤드를 최대 7배 이상 줄입니다.
  • Encapsulation — VXLAN, WireGuard, IPsec NAT-T, L2TP 등 터널 프로토콜의 캡슐화 계층으로 사용됩니다.
  • SO_REUSEPORT — 동일 포트에 다중 소켓을 바인드하여 멀티코어 환경에서 수신 부하를 분산합니다.

단계별 이해

  1. 구조체 이해
    udp_sockudp_table 해시 테이블의 관계를 먼저 파악합니다.
  2. TX/RX 경로 추적
    udp_sendmsg()__udp4_lib_rcv()의 전체 호출 체인을 따라갑니다.
  3. 성능 기능 이해
    GRO/GSO, SO_REUSEPORT, Busy Polling을 실제 워크로드에 맞춰 조정합니다.
  4. 운영 안정화
    큐 오버플로우/드롭 원인을 /proc/net/snmpss로 진단하고 sysctl로 보정합니다.
관련 표준: RFC 768 (UDP), RFC 8085 (UDP Usage Guidelines), RFC 3828 (UDP-Lite), RFC 6935/6936 (UDP Checksum for IPv6 Tunnels) — UDP 관련 핵심 표준입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

UDP 심화

UDP(User Datagram Protocol)는 IP 위에 최소한의 전송 계층 기능만 추가한 프로토콜입니다. 연결 설정(handshake), 흐름 제어, 혼잡 제어, 재전송 메커니즘을 모두 생략하여 극도로 낮은 오버헤드와 최소 지연을 달성합니다. 이런 단순성 때문에 DNS, DHCP, NTP 같은 짧은 요청-응답 프로토콜, 실시간 미디어 스트리밍(RTP), 그리고 최근에는 QUIC(HTTP/3)과 같은 사용자 공간 전송 프로토콜의 기반으로 활발히 사용됩니다.

TCP vs UDP 핵심 비교

특성TCPUDP
연결 방식연결 지향 (3-way handshake)비연결형 (즉시 전송)
신뢰성재전송, ACK, 순서 보장최선 노력(best-effort), 보장 없음
흐름/혼잡 제어윈도우 기반 내장없음 (애플리케이션이 직접 구현)
헤더 크기20~60 바이트8 바이트 고정
메시지 경계바이트 스트림 (경계 없음)데이터그램 단위 보존
멀티캐스트지원 안 함지원
커널 상태tcp_sock (~2KB)udp_sock (~수백 바이트)
대표 용도HTTP, SSH, FTP, SMTPDNS, DHCP, QUIC, WireGuard, RTP

UDP의 커널 내 위치

리눅스 커널에서 UDP 구현은 주로 다음 소스 파일에 분포합니다:

소스 파일역할
net/ipv4/udp.cUDP IPv4 핵심 구현 (송수신, 해시 테이블, encap)
net/ipv6/udp.cUDP IPv6 핵심 구현
net/ipv4/udp_offload.cUDP GSO/GRO 오프로드 처리
net/ipv4/udplite.cUDP-Lite 프로토콜 (IP proto 136)
net/ipv4/udp_tunnel_core.cUDP 터널 소켓 설정 유틸리티
include/net/udp.hUDP 내부 구조체/인라인 함수
include/uapi/linux/udp.h사용자 공간 UDP 헤더/소켓 옵션
include/net/udp_tunnel.h터널 관련 구조체/헬퍼
User Space Socket Layer UDP Core IP Layer sendmsg() recvmsg() setsockopt() inet_dgram_ops udp_prot (proto_ops) udp_sendmsg() __udp4_lib_rcv() udp_offload.c udp_tunnel_core.c ip_make_skb() ip_rcv()

UDP 패킷 처리 흐름

NIC에서 수신된 UDP 패킷은 IP 계층을 거쳐 udp_rcv()에 도달합니다. 이후 __udp4_lib_rcv()에서 체크섬 검증과 소켓 lookup을 수행하며, 소켓이 발견되면 수신 큐에 전달하고, 없으면 ICMP Port Unreachable을 응답합니다.

NIC 수신 udp_rcv() __udp4_lib_rcv() udp_unicast_rcv_skb() icmp_send() kfree_skb() 1) checksum 검증 2) 4-tuple 소켓 lookup 소켓 있음 소켓 없음
IPv6 경로: IPv6에서는 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;        /* 포워드 할당 임계값 */
};
필드타입설명
inetstruct inet_sock소켓 주소, 포트, TTL 등 공통 정보 (상위 구조체)
pendingintUDP_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_queuesk_buff_head소켓 수신 대기 큐 (backlog에서 이동)
udp_sock 상속 체인: udp_sockinet_socksocksock_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;
}
udp_table 2단계 해시 Lookup hash (포트 기반) [0] [1] [n] ... hash2 (4-tuple 기반) [0] [1] [m] ... 수신 패킷 src:10.0.0.1:5000 dst:10.0.0.2:8080 1단계: hash2 (4-tuple) 검색 정확한 매칭 시도 2단계: hash (포트) 폴백 와일드카드 매칭 매칭 소켓 udp_sock (sk) 매칭 없음 ICMP Port Unreachable Lookup 스코어 기준 (compute_score) dst IP 일치 +2 | src IP 일치 +2 | dst port 일치 +4 | src port 일치 +1 netns 일치 +1 | SO_BINDTODEVICE 일치 +2 | 최고 점수 소켓 선택
/* 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;
}
해시 충돌 성능: 동일 포트에 많은 소켓이 바인드되면 해시 슬롯 체인이 길어져 lookup이 O(n)으로 퇴화합니다. SO_REUSEPORT + eBPF 기반 분산을 사용하면 이 문제를 완화할 수 있습니다.

UDP 송신(TX) 경로

사용자 공간의 sendmsg() 시스템콜은 소켓 계층을 거쳐 udp_sendmsg()에 도달합니다. 이 함수는 라우팅 결정, IP 옵션 처리, 페이로드 복사, skb 생성을 수행하고, 최종적으로 udp_send_skb()ip_send_skb()를 통해 IP 계층으로 전달합니다.

UDP 송신(TX) 전체 경로 sendmsg() / sendto() inet_sendmsg() udp_sendmsg() 1. 연결/비연결 판별 (connected 소켓?) 2. 라우팅 결정 (ip_route_output_flow) 3. cmsg 처리 (IP_PKTINFO, UDP_SEGMENT 등) ip_append_data() UDP_CORK / MSG_MORE ip_make_skb() 일반 전송 udp_send_skb() UDP 헤더 채움, 체크섬 계산/오프로드 gso_size 설정 (GSO 사용 시) ip_send_skb() → NIC TX
/* 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);
}
UDP_CORK 활용: 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 처리, 수신 큐 전달이 이루어집니다.

UDP 수신(RX) 전체 경로 NIC RX → ip_local_deliver() udp_rcv() __udp4_lib_rcv() 1. 헤더 길이 검증 (최소 8바이트) 2. udp4_csum_init() 체크섬 초기화 __udp4_lib_lookup_skb() encap_rcv() 콜백? VXLAN / WireGuard / IPsec udp_unicast_rcv_skb() 소켓 없음 ICMP + kfree_skb() udp_queue_rcv_skb() __udp_enqueue_schedule_skb() sk_receive_queue에 추가 큐 오버플로우 UDP_MIB_RCVBUFERRORS++
/* 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/snmpInErrors에 반영됩니다. net.core.rmem_maxSO_RCVBUF 조정으로 완화할 수 있습니다.

체크섬 오프로드

UDP 체크섬은 IPv4에서는 선택(0 허용), IPv6에서는 필수입니다. 리눅스 커널은 3가지 체크섬 처리 모드를 지원하며, 하드웨어 오프로드를 통해 CPU 부하를 크게 줄입니다.

모드skb->ip_summedTX 동작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
IPv6 터널 예외: RFC 6935/6936에 따라 IPv6 UDP 터널(VXLAN, Geneve 등)에서는 성능을 위해 외부 UDP 헤더의 체크섬을 0으로 설정하는 것이 허용됩니다. 커널에서는 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 GRO sendmsg() 1회 64KB 데이터 + cmsg 1개 대형 skb gso_size=1472 1번 스택 통과 라우팅/Netfilter/qdisc __udp_gso_segment() N개 UDP 분할 NIC TX (N개 패킷) HW USO 지원 시 NIC에서 분할 NIC RX 동일 5-tuple N개 패킷 udp_gro_receive() 조건 충족 시 병합 1개 대형 skb sk_receive_queue recvmsg() + cmsg GRO_UDP_SEGMENT 애플리케이션 분리 원 세그먼트 크기로 복원 성능 효과 (QUIC 벤치마크 기준) GSO OFF: ~200K pkt/s (1 sendmsg = 1 packet) | GSO ON: ~1.5M pkt/s (1 sendmsg = ~44 packets) ~7.5배 처리량 향상 + 시스템콜 43배 절감
/* === 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
QUIC과 UDP GSO/GRO: QUIC(HTTP/3)은 UDP 위에 구현되므로 UDP GSO/GRO가 성능에 직접적 영향을 줍니다. Google의 QUICHE, cloudflare/quiche 등 주요 QUIC 구현체는 UDP GSO/GRO를 적극 활용합니다. WireGuard VPN도 UDP GSO를 사용하여 암호화된 터널 처리량을 극대화합니다.
GSO/GRO 조건GSO (TX)GRO (RX)
활성화 방법UDP_SEGMENT cmsgsetsockopt(UDP_GRO)
커널 버전4.18+5.0+
HW 오프로드USO (tx-udp-segmentation)NIC GRO + UDP GRO
gso_typeSKB_GSO_UDP_L4-
최대 크기64KB (UDP 길이 필드 제한)NIC 의존 (보통 64KB)
분할/병합 단위gso_size (보통 MTU - IP - UDP)동일 크기 데이터그램

UDP Encapsulation 프레임워크

UDP는 터널 프로토콜의 캡슐화 계층으로 광범위하게 사용됩니다. 커널은 struct udp_tunnel_sock_cfg를 통해 터널 드라이버가 특정 UDP 포트에 수신 콜백을 등록할 수 있는 프레임워크를 제공합니다.

UDP Encapsulation 수신 경로 Outer IP Outer UDP Tunnel Header Inner L2/L3 Inner Payload __udp4_lib_rcv() -> 소켓 lookup encap_type != 0 -> encap_rcv() 호출 VXLAN vxlan_rcv() WireGuard wg_receive() Geneve geneve_rx() IPsec NAT-T xfrm4_udp_encap() L2TP l2tp_udp_rcv() 내부 패킷 역캡슐화 -> netif_rx() -> 네트워크 스택 재진입 inner L2/L3 헤더로 다시 라우팅/포워딩/로컬 전달
프로토콜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);
}
터널 GRO 중요성: VXLAN/Geneve 같은 터널에서 GRO가 비활성화되면 모든 내부 패킷이 개별 처리되어 성능이 급격히 저하됩니다. 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_MEMBERSHIPIPPROTO_IP멀티캐스트 그룹 참여 (IGMP Join)
IP_DROP_MEMBERSHIPIPPROTO_IP멀티캐스트 그룹 탈퇴 (IGMP Leave)
IP_MULTICAST_TTLIPPROTO_IP멀티캐스트 TTL (기본 1, 최대 255)
IP_MULTICAST_LOOPIPPROTO_IP루프백 여부 (기본 1=활성)
IP_MULTICAST_IFIPPROTO_IP송신 인터페이스 지정
IP_ADD_SOURCE_MEMBERSHIPIPPROTO_IPSSM: 특정 소스 허용
MCAST_JOIN_GROUPIPPROTO_IP프로토콜 독립 멀티캐스트 참여 (IPv4/IPv6 공용)
SO_BROADCASTSOL_SOCKET브로드캐스트 송신 허용 (기본 비활성)
IGMP 연동: 멀티캐스트 그룹 참여 시 커널은 자동으로 IGMP(IPv4) 또는 MLD(IPv6) 메시지를 전송합니다. 이를 통해 네트워크 스위치가 멀티캐스트 트래픽을 필요한 포트로만 전달(IGMP Snooping)할 수 있습니다.

SO_REUSEPORT 부하 분산

SO_REUSEPORT 소켓 옵션(커널 3.9+)은 동일한 IP:포트에 여러 소켓을 바인드할 수 있게 하며, 수신 패킷을 이들 소켓 간에 분산합니다. UDP 서버에서 멀티코어 확장성을 달성하는 핵심 메커니즘입니다.

SO_REUSEPORT UDP 부하 분산 수신 UDP 패킷 (port 8080) 커널 분산 (기본: 4-tuple 해시) 또는 eBPF 프로그램 (BPF_PROG_TYPE_SK_REUSEPORT) 소켓 #0 (CPU 0) Worker Thread 0 소켓 #1 (CPU 1) Worker Thread 1 소켓 #2 (CPU 2) Worker Thread 2 소켓 #3 (CPU 3) Worker Thread 3 ... 각 소켓이 독립 수신 큐 보유 -> 락 경합 없음 -> 멀티코어 선형 확장
/* 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 서버
eBPFBPF_PROG_TYPE_SK_REUSEPORT로 커스텀 로직QUIC Connection ID 기반 고정
CBPF클래식 BPF (레거시)단순 해시 기반 분산
DNS 서버 확장: BIND, Unbound, CoreDNS 같은 DNS 서버는 SO_REUSEPORT를 활용하여 다중 워커 프로세스가 UDP 53번 포트를 공유합니다. 이를 통해 단일 포트에서도 수백만 QPS를 처리할 수 있습니다.

UDP 터널 오프로드

최신 NIC는 UDP 터널(VXLAN, Geneve, GTP 등)의 내부 패킷까지 이해하여 체크섬, TSO/GSO, RSS 해시를 오프로드할 수 있습니다. 커널은 udp_tunnel_nic 프레임워크를 통해 NIC에 터널 포트 정보를 전달합니다.

UDP 터널 오프로드 구조 터널 드라이버 VXLAN / Geneve / GTP udp_tunnel_nic 프레임워크 udp_tunnel_push_rx_port() NIC 드라이버 ndo_udp_tunnel_add() NIC 하드웨어 오프로드 기능 Inner TSO/GSO Inner Checksum Inner RSS Hash Tunnel Header Strip ethtool -k eth0 | grep tunnel tx-udp_tnl-segmentation: on | tx-udp_tnl-csum-segmentation: on | rx-udp_tunnel-port-offload: on
/* 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
OVS와 터널 오프로드: Open vSwitch(OVS)는 VXLAN/Geneve 터널과 함께 NIC 터널 오프로드를 활용합니다. OVS-DPDK 또는 TC offload와 결합하면 하드웨어 수준의 터널 처리가 가능합니다.

소켓 버퍼 튜닝

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배 규칙: 커널은 SO_RCVBUF로 설정한 값을 내부적으로 2배로 적용합니다. 이는 sk_buff 메타데이터 오버헤드를 감안한 것입니다. getsockopt(SO_RCVBUF)로 확인하면 설정한 값의 2배가 반환됩니다.

Busy Polling (저지연 수신)

Busy polling은 소켓이 패킷을 대기할 때 인터럽트를 기다리는 대신 NIC를 직접 폴링하여 수신 지연을 최소화하는 기법입니다. 커널 3.11+에서 지원되며, UDP에서 특히 효과적입니다.

일반 수신 (인터럽트 기반) Busy Polling (폴링 기반) recvmsg() 호출 프로세스 sleep (대기 시간 증가) NIC IRQ 발생 NAPI poll 처리 sk_data_ready() 프로세스 wakeup 지연: IRQ + softirq + wakeup recvmsg() 호출 napi_busy_loop() (직접 NIC 폴링) NAPI poll 직접 실행 패킷 즉시 수신 지연: NIC 레지스터 읽기만 Busy Polling: 지연 50~90% 감소 | 단점: CPU 사용률 100% (전용 코어 필요)
# 시스템 전역 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_pollepoll/poll 대기 시 busy 폴링 시간 (us)시스템 전역
net.core.busy_read소켓 read 시 busy 폴링 시간 (us)시스템 전역
SO_BUSY_POLL소켓별 busy 폴링 시간 (us)소켓 단위
SO_PREFER_BUSY_POLLNAPI 스레드 대신 busy poll 선호소켓 단위 (5.11+)
SO_INCOMING_NAPI_ID소켓에 바인딩된 NAPI 인스턴스 ID읽기 전용
고빈도 거래(HFT) 활용: 금융 거래 시스템에서 busy polling은 네트워크 지연을 수십 마이크로초에서 수 마이크로초로 줄이는 핵심 기법입니다. 전용 CPU 코어를 isolcpus로 격리하고, IRQ affinity를 설정한 뒤 busy polling 소켓을 해당 코어에 바인딩하는 것이 일반적인 패턴입니다.

UDP-Lite (RFC 3828)

UDP-Lite는 IP 프로토콜 번호 136을 사용하는 UDP 변형으로, 체크섬 커버리지를 부분적으로 적용할 수 있습니다. 오디오/비디오 스트리밍에서 일부 비트 오류를 허용하되 패킷 자체는 전달하고 싶을 때 유용합니다.

UDP (전체 체크섬) UDP-Lite (부분 체크섬) UDP Header Payload (전체 체크섬 보호) 비트 오류 발생 시 -> 패킷 전체 드롭 전체 패킷 폐기 (체크섬 불일치) UDP-L Header 보호 영역 비보호 영역 비보호 영역 비트 오류 -> 패킷 전달 (손상 허용) 패킷 전달 (페이로드 일부 손상 가능) UDP-Lite 주요 활용: VoIP, 비디오 스트리밍, 센서 데이터 일부 비트 오류보다 패킷 손실이 더 나쁜 시나리오에서 유효
/* 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 바이트만 보호
 */
속성UDPUDP-Lite
IP 프로토콜 번호17136
체크섬 범위전체 패킷 (또는 0=비활성)가변 (최소 8 바이트)
헤더 구조src_port, dst_port, length, checksumsrc_port, dst_port, coverage, checksum
비트 오류 시패킷 드롭보호 영역만 검증, 비보호 영역은 전달
주요 사용처일반 UDP 통신VoIP(Opus), 비디오(RTP), 센서 네트워크
커널 소스net/ipv4/udp.cnet/ipv4/udplite.c

트러블슈팅

패킷 드롭 증가: 애플리케이션 처리 속도보다 커널 큐 유입이 빠른 상황일 수 있습니다. 소켓 버퍼와 NIC ring, IRQ affinity를 함께 점검하세요.

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'
주의 - UDP 폭주 공격: UDP는 연결 상태가 없으므로 DDoS 공격에 취약합니다. 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_SOCKETIPPROTO_IP 레벨 옵션도 함께 사용됩니다.

옵션레벨타입설명
UDP_CORKSOL_UDPint (bool)데이터 합치기 (send 지연, uncork 시 전송)
UDP_ENCAPSOL_UDPint캡슐화 유형 설정 (ESP, L2TP 등)
UDP_SEGMENTSOL_UDP (cmsg)uint16_tGSO 세그먼트 크기 (TX 방향)
UDP_GROSOL_UDPint (bool)GRO 활성화 (RX 방향)
UDP_NO_CHECK6_TXSOL_UDPint (bool)IPv6 TX 체크섬 비활성화
UDP_NO_CHECK6_RXSOL_UDPint (bool)IPv6 RX 체크섬 검증 비활성화
SO_RCVBUFSOL_SOCKETint수신 버퍼 크기 (커널 2x 적용)
SO_SNDBUFSOL_SOCKETint송신 버퍼 크기
SO_REUSEPORTSOL_SOCKETint (bool)포트 재사용 + 부하 분산
SO_BUSY_POLLSOL_SOCKETint (us)Busy polling 시간
SO_TIMESTAMPSOL_SOCKETint (bool)수신 타임스탬프 (SW)
SO_TIMESTAMPNSSOL_SOCKETint (bool)수신 타임스탬프 (나노초)
SO_TIMESTAMPINGSOL_SOCKETint (flags)HW/SW TX/RX 타임스탬프
IP_PKTINFOIPPROTO_IPint (bool)수신 인터페이스/주소 정보 (cmsg)
IP_RECVORIGDSTADDRIPPROTO_IPint (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);
    }
}
PTP와 UDP 타임스탬프: IEEE 1588(PTP) 시간 동기화 프로토콜은 UDP 패킷의 하드웨어 타임스탬프를 활용하여 나노초 수준의 시간 정밀도를 달성합니다. SO_TIMESTAMPING은 이 메커니즘의 사용자 공간 인터페이스입니다.

UDP 소켓 룩업 심화

__udp4_lib_lookup()은 UDP 수신 경로에서 가장 빈번하게 호출되는 함수 중 하나입니다. 이 함수의 내부 구현을 상세히 분석하면 성능 병목 지점을 정확히 이해할 수 있습니다. 커널 6.x에서는 hash2 테이블 우선 검색으로 대부분의 패킷을 O(1)에 가까운 시간에 매칭합니다.

__udp4_lib_lookup() 상세 흐름 수신 패킷 (saddr, sport, daddr, dport, dif) 1단계: hash2[udp4_portaddr_hash(net, daddr, dport)] connected 소켓 (정확한 4-tuple 매칭) compute_score() dport 일치: +4 daddr 일치: +2 | saddr: +2 sport 일치: +1 | dev: +2 result != NULL? (최고 스코어 소켓) return result (빠른 경로) 2단계: hash2[udp4_portaddr_hash(net, INADDR_ANY, dport)] 와일드카드 소켓 (bind(0.0.0.0:port)) result != NULL? return result SO_REUSEPORT 그룹? reuseport_select_sock() eBPF 또는 4-tuple 해시로 그룹 내 소켓 선택 return NULL ICMP Port Unreachable 전송 Yes No Yes No

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+4connected 소켓 (connect()된 UDP)connect()된 UDP
inet_dport == sport+4connected 소켓 원격 포트 일치connect()된 UDP
sk_bound_dev_if == dif+4디바이스 바인딩 (SO_BINDTODEVICE)인터페이스 고정
sk_reuseport_cb 존재+1REUSEPORT 그룹 소속SO_REUSEPORT
와일드카드 바인드+0bind(0.0.0.0:port)일반 서버
connected UDP 소켓의 장점: 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;
}
해시 테이블 크기 조정: UDP 해시 테이블 크기는 부팅 시 메모리 양에 따라 자동 결정됩니다. 커널 부트 파라미터 uhash_entries=N으로 수동 설정이 가능합니다. 대규모 UDP 서버(수만 소켓)에서는 테이블 크기를 늘려 해시 충돌을 줄이는 것이 좋습니다.

TX 경로 상세 심화

udp_sendmsg()의 내부를 더 깊이 분석합니다. 연결 상태 판별, cmsg 처리, cork/uncork 메커니즘, GSO 통합, 그리고 ip_make_skb()에서 ip_send_skb()까지의 전체 skb 생명주기를 다룹니다.

UDP TX: skb 생명주기 상세 sendmsg(fd, msg, flags) 1. 목적지 결정 msg_name 존재 -> sockaddr_in에서 추출 없으면 -> inet_daddr/inet_dport (connected) 2. cmsg 처리 (udp_cmsg_send) IP_PKTINFO: 소스 주소/인터페이스 지정 IP_TOS: 패킷별 ToS 설정 UDP_SEGMENT: GSO 세그먼트 크기 3. ip_route_output_flow() FIB lookup -> rtable 획득 connected: 소켓에 캐시된 rt 재사용 4. PMTU 확인 rt->dst.ops->mtu() 호출 초과 시 EMSGSIZE 반환 UDP_CORK/MSG_MORE 일반 전송 경로 ip_append_data() 페이로드를 cork 큐에 누적 uncork 시 udp_push_pending_frames() ip_make_skb() alloc_skb + 페이로드 복사 IP 헤더 예약 (skb_reserve) udp_send_skb() -> ip_send_skb() dev_queue_xmit() -> NIC TX ring

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 패킷 수스택 통과 횟수장점
일반 전송NNN단순
UDP_CORKN+1 (uncork)11작은 메시지 결합
MSG_MOREN11per-send 제어
UDP GSO1N (커널 분할)1대량 전송 최적화
connected UDP와 성능: 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 RX 경로: 드롭 포인트 분석 __udp4_lib_rcv() 진입 DROP 1: pskb_may_pull() 실패 헤더 8바이트 미만 | UDP len 불일치 udp4_csum_init() 체크섬 초기화 DROP 2: 체크섬 오류 UDP_MIB_CSUMERRORS++ __udp4_lib_lookup_skb() 소켓 검색 DROP 3: 소켓 없음 UDP_MIB_NOPORTS++ | ICMP 전송 xfrm4_policy_check() IPsec 정책 DROP 4: IPsec 정책 거부 xfrm 필터링 udp_queue_rcv_skb() DROP 5: sk_filter() BPF 거부 SO_ATTACH_FILTER/BPF __udp_enqueue_schedule_skb() DROP 6: sk_rcvbuf 초과 UDP_MIB_RCVBUFERRORS++ sk_receive_queue에 추가 완료 sk_data_ready() -> 프로세스 wakeup

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;
}
MSG_PEEK과 성능: recvmsg()MSG_PEEK 플래그를 사용하면 데이터를 큐에서 제거하지 않고 복사만 합니다. 이는 UDP에서 패킷 헤더를 먼저 확인하고 적절한 버퍼 크기로 다시 수신할 때 유용하지만, 매번 큐를 순회하므로 고빈도 호출 시 성능에 영향을 줄 수 있습니다. recvmsg(MSG_TRUNC)로 실제 데이터그램 크기를 확인하는 것이 더 효율적입니다.

체크섬 오프로드 심화

UDP 체크섬 처리는 TX와 RX 양쪽에서 하드웨어 오프로드를 활용할 수 있습니다. 커널 내부의 ip_summed 필드 상태 전이와 NIC 오프로드 동작의 정확한 관계를 이해하면 체크섬 관련 문제를 효과적으로 디버깅할 수 있습니다.

UDP 체크섬 처리: TX vs RX TX 경로 RX 경로 udp_send_skb() sk_no_check_tx? CHECKSUM_NONE CHECKSUM_PARTIAL 의사 헤더만 계산 NIC에서 나머지 완성 CHECKSUM_NONE 소프트웨어 전체 계산 (NIC 미지원 시 폴백) validate_xmit_skb() -> NIC TX NIC RX 수신 NIC 체크섬 보고 ip_summed 설정 CHECKSUM_COMPLETE NIC가 전체 체크섬 계산 skb->csum에 저장 CHECKSUM_UNNECESSARY NIC/드라이버가 검증 완료 커널 검증 불필요 udp4_csum_init() -> 최종 검증/패스 체크섬 검증 tcpdump 주의사항 tcpdump로 캡처 시 CHECKSUM_PARTIAL 상태의 TX 패킷은 체크섬이 미완성 상태 -> "bad udp cksum" 경고가 정상 (NIC가 이후에 완성) 해결: ethtool -K eth0 tx-checksum-ipv4 off (디버깅 시에만) 또는: tcpdump -K (체크섬 검증 비활성화)
/* 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 GRO는 병합된 대형 버퍼를 애플리케이션에 전달합니다. QUIC처럼 패킷 순서에 민감한 프로토콜에서는 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 지원
VXLAN50 (Eth 14 + IP 20 + UDP 8 + VXLAN 8)1450 (MTU 1500 기준)대부분 지원vxlan_gro_receive
Geneve50~306 (TLV 가변)1450~1194부분 지원geneve_gro_receive
WireGuard60 (IP 20 + UDP 8 + WG 32)1420UDP GSO소켓 GRO
IPsec NAT-T36~72 (ESP 가변)1428~1392제한적제한적
터널 MTU 관리: UDP 터널에서 내부 패킷의 MTU는 외부 헤더 오버헤드만큼 줄어듭니다. VXLAN의 경우 내부 MTU가 1450바이트(1500 - 50)가 됩니다. PMTUD(Path MTU Discovery)를 활용하거나, 터널 디바이스에서 직접 MTU를 설정하세요: ip link set vxlan0 mtu 1450

SO_REUSEPORT 상세 심화

SO_REUSEPORT의 커널 내부 구현, 그룹 관리, eBPF 프로그램 연동, 그리고 소켓 마이그레이션(커널 5.14+) 메커니즘을 상세히 분석합니다.

SO_REUSEPORT 커널 내부 구현 sock_reuseport 구조체 max_socks: N | num_socks: 현재 수 prog: eBPF 프로그램 (NULL이면 기본 해시) socks[]: 소켓 배열 BPF_PROG_TYPE_SK_REUSEPORT sk_reuseport_md: 패킷 메타데이터 bpf_sk_select_reuseport(): 소켓 선택 BPF_MAP_TYPE_REUSEPORT_SOCKARRAY 사용 수신 패킷 -> reuseport_select_sock() eBPF prog 존재? BPF_PROG_RUN(prog, ctx) 커스텀 소켓 선택 로직 reciprocal_scale(hash, N) 4-tuple 해시로 socks[] 인덱스 선택된 소켓으로 패킷 전달 udp_unicast_rcv_skb(sk, skb) Yes No
/* 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");
소켓 마이그레이션 (커널 5.14+): 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_defaultrmem_maxwmem_defaultwmem_max비고
DNS 서버 (고QPS)4MB16MB1MB4MB작은 패킷, 높은 pps
QUIC/HTTP38MB32MB8MB32MBGSO/GRO 병용
비디오 스트리밍16MB64MB4MB16MB대용량 수신 버스트
게임 서버2MB8MB2MB8MB저지연 우선
로그 수집 (syslog)32MB128MB1MB4MB수신 폭주 대비
VXLAN 터널8MB32MB8MB32MB터널 오프로드 병용
# 실시간 소켓 버퍼 모니터링

# 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
메모리 과다 할당 경고: 소켓 수가 많은 환경(예: 10,000개 UDP 소켓)에서 각 소켓에 64MB 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_XDP1~3us매우 높음 (전용 코어)최저 지연 (HFT)
Busy polling과 CPU 격리: busy polling은 CPU를 100% 점유하므로 반드시 전용 코어를 할당해야 합니다. 커널 부트 파라미터에 isolcpus=4-7, nohz_full=4-7을 설정하고, taskset -c 4 ./app으로 프로세스를 바인딩하세요. IRQ affinity도 동일 코어로 설정하여 캐시 효율을 극대화합니다.

UDP 멀티캐스트 구현 심화

리눅스 커널의 UDP 멀티캐스트 구현을 더 깊이 분석합니다. IGMP 그룹 관리, 소스 필터링(SSM/ASM), 커널 내 멀티캐스트 라우팅, 그리고 고성능 멀티캐스트 수신 패턴을 다룹니다.

UDP 멀티캐스트 수신 경로 수신 패킷: dst=239.1.1.1 port=5000 ip_rcv -> ip_local_deliver -> udp_rcv ipv4_is_multicast(daddr)? -> YES __udp4_lib_mcast_deliver() 해시 테이블에서 매칭 소켓 모두 검색 소켓 #0 (첫 매칭) 원본 skb 전달 소켓 #1 skb_clone() 복제본 전달 소켓 #2 skb_clone() 복제본 전달 매칭 없음 kfree_skb() IGMP/MLD 자동 관리 IP_ADD_MEMBERSHIP -> igmp_group_added() -> IGMP Join 전송 IP_DROP_MEMBERSHIP -> igmp_group_dropped() -> IGMP Leave 전송

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/8SSM(Source-Specific Multicast) 전용소스 필터링 필수
233.0.0.0/8GLOP (AS 번호 기반 할당)전역 라우팅
239.0.0.0/8관리적 스코프 (사설 멀티캐스트)조직 내부
IGMP Snooping과 성능: 스위치의 IGMP Snooping이 활성화되면 멀티캐스트 트래픽이 가입된 포트로만 전달됩니다. 대규모 멀티캐스트 환경에서 Snooping이 비활성화되면 모든 포트에 트래픽이 범람(flooding)하여 네트워크 대역폭이 낭비됩니다. bridge mdb show로 스위치의 멀티캐스트 그룹 테이블을 확인하세요.

UDP 성능 벤치마크

다양한 UDP 최적화 기법의 실제 성능 효과를 정량적으로 비교합니다. 벤치마크 결과는 하드웨어와 커널 버전에 따라 달라지므로 상대적 비교에 참고하세요.

구성처리량 (pps)지연 (avg)CPU 사용비고
기본 (recvmsg, 인터럽트)~300K30~50us낮음기준선
+ SO_REUSEPORT (4코어)~1.1M30~50us4배 분산선형 확장
+ recvmmsg (64 batch)~500K30~50us중간시스템콜 감소
+ UDP GRO/GSO~2M20~40us중간대형 패킷 최적화
+ Busy polling~600K3~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
벤치마크 조건 통일: 정확한 비교를 위해 (1) NIC 인터럽트 코얼레싱 비활성화, (2) CPU frequency scaling 비활성화 (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_CSCOVIPPROTO_UDPLITEint0 (전체)송신 체크섬 커버리지 (바이트)
UDPLITE_RECV_CSCOVIPPROTO_UDPLITEint0 (전체)최소 수신 체크섬 커버리지
/* 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-Lite의 한계: UDP-Lite는 대부분의 NIC에서 체크섬 하드웨어 오프로드가 지원되지 않습니다. 모든 체크섬이 소프트웨어로 처리되므로 고처리량 환경에서는 CPU 오버헤드가 발생합니다. 또한 NAT 장비와 방화벽이 UDP-Lite(프로토콜 136)를 차단하는 경우가 많아 인터넷 환경에서의 사용은 제한적입니다.

UDP 성능 프로파일링

UDP 워크로드의 성능 병목을 진단하는 도구와 기법을 상세히 다룹니다. perf, bpftrace, dropwatch를 활용한 체계적인 프로파일링 방법론을 제시합니다.

UDP 성능 프로파일링 도구 계층 User Kernel NIC/HW ss -u -n -e -m /proc/net/snmp nstat -a sockperf / iperf3 /proc/net/sockstat perf trace kfree_skb 추적 bpftrace tracepoint/kprobe dropwatch 패킷 드롭 위치 추적 ftrace 함수 호출 추적 perf record/stat CPU 프로파일링 tracepoints net:*, skb:*, udp:* softnet_stat CPU별 수신 통계 mpstat / sar softirq CPU 분석 ethtool -S eth0 NIC 통계 카운터 ethtool -k eth0 오프로드 상태 ethtool -C / -G 코얼레싱/링 설정 /proc/interrupts IRQ 분배 확인 진단 순서: NIC 드롭 -> softnet backlog -> 커널 드롭 -> 소켓 버퍼 -> 애플리케이션 ethtool -S -> softnet_stat -> /proc/net/snmp -> ss -e -> 프로세스 CPU

perf를 이용한 UDP 핫스팟 분석

# 1. UDP 수신 경로 CPU 프로파일링
$ perf record -g -p $(pgrep udp_server) -- sleep 30
$ perf report --sort=dso,symbol

# 핵심 관찰 포인트:
#   __udp4_lib_rcv        -> 소켓 lookup 비용
#   udp_queue_rcv_skb     -> BPF 필터 + 체크섬 검증 비용
#   copy_to_iter          -> 사용자 공간 복사 비용
#   __softirqentry_text   -> softirq 전체 처리 시간

# 2. 패킷 드롭 원인 추적 (커널 5.17+)
$ perf trace -e 'skb:kfree_skb' -a --duration 10
# 출력: kfree_skb 호출 위치 + SKB_DROP_REASON
# SKB_DROP_REASON_UDP_RCVBUF  -> 수신 버퍼 부족
# SKB_DROP_REASON_UDP_CSUM    -> 체크섬 오류
# SKB_DROP_REASON_SOCKET_FILTER -> BPF 필터 거부

# 3. 특정 함수 호출 빈도 측정
$ perf stat -e 'udp:udp_fail_queue_rcv_skb' -a -- sleep 10

# 4. UDP 관련 tracepoint 목록 확인
$ perf list 'udp:*' 'skb:*' 'net:*'
  udp:udp_fail_queue_rcv_skb
  skb:kfree_skb
  skb:consume_skb
  skb:skb_copy_datagram_iovec
  net:net_dev_queue
  net:net_dev_xmit
  net:netif_receive_skb

bpftrace를 이용한 실시간 UDP 분석

# 1. UDP 수신 함수 호출 빈도 (1초 단위)
$ bpftrace -e '
kprobe:__udp4_lib_rcv {
    @recv_count = count();
}
interval:s:1 {
    printf("UDP recv/s: %d\n", @recv_count);
    clear(@recv_count);
}'

# 2. UDP 소켓별 수신 바이트 분포
$ bpftrace -e '
kprobe:udp_recvmsg {
    @recv_bytes = hist(arg2);
}
END { print(@recv_bytes); }'

# 3. UDP 드롭 원인별 카운트 (커널 5.17+)
$ bpftrace -e '
tracepoint:skb:kfree_skb {
    if (args->reason >= 200 && args->reason <= 210) {
        @drops[args->reason] = count();
        @stacks[args->reason] = kstack;
    }
}
interval:s:5 { print(@drops); }'

# 4. UDP sendmsg 지연 분포
$ bpftrace -e '
kprobe:udp_sendmsg {
    @start[tid] = nsecs;
}
kretprobe:udp_sendmsg /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# 5. UDP 소켓 버퍼 사용률 모니터링
$ bpftrace -e '
kprobe:__udp_enqueue_schedule_skb {
    $sk = (struct sock *)arg0;
    $rmem = $sk->sk_rmem_alloc.counter;
    $rcvbuf = $sk->sk_rcvbuf;
    $usage_pct = ($rmem * 100) / $rcvbuf;
    if ($usage_pct > 80) {
        printf("HIGH USAGE: rmem=%d rcvbuf=%d (%d%%)\n",
               $rmem, $rcvbuf, $usage_pct);
    }
}'

dropwatch를 이용한 패킷 드롭 위치 추적

# dropwatch: 커널 패킷 드롭 위치를 실시간으로 추적하는 도구

# 설치
$ apt install dropwatch   # Debian/Ubuntu
$ dnf install dropwatch   # Fedora/RHEL

# 실행: 드롭 위치와 빈도 표시
$ dropwatch -l kas
Initalizing kallsyms db
dropwatch> start
1 drops at __udp4_lib_rcv+0x3a8 (0xffffffff81a2c3a8)
3 drops at udp_queue_rcv_skb+0x1d0 (0xffffffff81a2b1d0)
7 drops at __udp_enqueue_schedule_skb+0x120 (0xffffffff81a2a520)

# 해석:
#   __udp4_lib_rcv: 소켓 없음 (NoPorts) 또는 체크섬 오류
#   udp_queue_rcv_skb: BPF 필터 거부
#   __udp_enqueue_schedule_skb: 수신 버퍼 오버플로우

# 커널 5.17+ perf 기반 드롭 추적 (dropwatch 대안)
$ perf trace --no-syscalls -e 'skb:kfree_skb' -a --duration 30 2>&1 | \
  awk '/reason:/ {print $NF}' | sort | uniq -c | sort -rn
     47 reason:UDP_RCVBUF
     12 reason:NO_SOCKET
      3 reason:UDP_CSUM
도구분석 대상오버헤드최적 사용 시점
ss -u -n -e소켓별 드롭/메모리매우 낮음초기 진단
/proc/net/snmp프로토콜 통계없음상시 모니터링
nstat증분 통계없음실시간 변화 추적
perf stattracepoint 카운트낮음특정 이벤트 빈도
perf tracekfree_skb 추적중간드롭 원인 분석
bpftrace커스텀 분석중간심층 프로파일링
dropwatch드롭 위치중간드롭 위치 특정
perf recordCPU 핫스팟중~높음CPU 병목 분석
ftrace함수 호출 흐름높음경로 추적
체계적 진단 순서: UDP 성능 문제는 아래에서 위로 진단하는 것이 효율적입니다: (1) ethtool -S로 NIC 드롭 확인, (2) softnet_stat으로 CPU backlog 오버플로우 확인, (3) /proc/net/snmp로 프로토콜 레벨 오류 확인, (4) ss -e로 소켓별 드롭/메모리 상태 확인, (5) perf/bpftrace로 CPU 핫스팟 분석.

sendmmsg/recvmmsg 배치 시스템콜

UDP 워크로드에서 시스템콜 오버헤드를 줄이는 핵심 기법인 sendmmsg()/recvmmsg() 배치 시스템콜의 커널 내부 구현과 최적 활용 패턴을 분석합니다.

sendmsg() x N sendmmsg() x 1 sendmsg #1 syscall 진입 syscall 복귀 sendmsg #2 syscall 진입 syscall 복귀 sendmsg #N syscall 진입 syscall 복귀 ... N번 시스템콜 경계 전환 (user/kernel) sendmmsg() msg #1 전송 msg #2 전송 ... msg #N 전송 1번 syscall 진입 N회 루프 1번 syscall 복귀 1번 시스템콜 경계 전환 (user/kernel) sendmsg() x 64 64번 syscall + context switch ~300K pps (작은 패킷 기준) sendmmsg() vlen=64 1번 syscall, 내부 64회 루프 ~500K pps (약 65% 향상) sendmmsg + recvmmsg + GSO/GRO 결합 시 최대 효과
/* 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 NNN~300K단순 구현
sendmmsg() vlen=64164~500K다수 소형 패킷
sendmsg() + GSO1~44~1.5M대형 전송
sendmmsg() + GSO1~2,800~3M+최대 처리량
MSG_WAITFORONE 플래그: 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:
Docker/Kubernetes 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_INDATAGRAMSInDatagrams정상 수신된 데이터그램 수udp_unicast_rcv_skb 성공
UDP_MIB_NOPORTSNoPorts수신 소켓 없는 패킷 수__udp4_lib_rcv (sk==NULL)
UDP_MIB_INERRORSInErrors수신 오류 (버퍼, 체크섬 등)여러 드롭 포인트
UDP_MIB_OUTDATAGRAMSOutDatagrams송신된 데이터그램 수udp_send_skb 성공
UDP_MIB_RCVBUFERRORSRcvbufErrors수신 버퍼 오버플로우__udp_enqueue_schedule_skb
UDP_MIB_SNDBUFERRORSSndbufErrors송신 버퍼 오류udp_sendmsg 실패
UDP_MIB_CSUMERRORSInCsumErrors체크섬 오류udp4_csum_init / udp_queue_rcv_skb
UDP_MIB_IGNOREDMULTIIgnoredMulti무시된 멀티캐스트수신자 없는 멀티캐스트
UDP_MIB_MEMERRORSMemErrors메모리 할당 실패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
프로덕션 ftrace 사용 주의: function_graph tracer는 모든 함수 호출에 계측 코드를 삽입하므로 프로덕션 환경에서 심각한 성능 저하를 유발할 수 있습니다. 프로덕션에서는 bpftrace의 kprobe나 perf trace의 tracepoint를 사용하세요. ftrace는 개발/테스트 환경에서의 상세 분석에만 사용하는 것이 안전합니다.

UDP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.

참고자료