IPVS L4 로드밸런싱

Linux IPVS(IP Virtual Server) 아키텍처, 스케줄링 알고리즘(rr/wrr/lc/wlc/lblc/sh/dh), 포워딩 모드(NAT/DR/TUN/FULLNAT), keepalived 고가용성, conntrack 연동, 성능 비교 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: 네트워크 스택, Netfilter 프레임워크, NAT 심화 문서를 먼저 읽으세요. IPVS는 Netfilter 훅 위에서 동작하며, NAT/DR/TUN 포워딩 모드를 이해하려면 IP 라우팅과 주소 변환 개념이 필수입니다.
일상 비유: IPVS는 교통 분산기(교통경찰)와 같습니다. VIP(가상 IP)로 들어오는 모든 차량(패킷)을 여러 차선(Real Server)으로 골고루 분배하되, 한 번 배정된 차량은 같은 차선을 계속 이용하도록 세션 지속성을 보장합니다. 교통경찰은 차량 내부를 검사하지 않고 목적지만 바꿔주므로(L4), 처리 속도가 매우 빠릅니다.

핵심 요약

  • VIP / Real Server — 클라이언트는 VIP(가상 IP)로 접속하고, IPVS가 실제 서버(Real Server)로 분산합니다.
  • L4 커널 내 처리 — Netfilter LOCAL_IN/FORWARD 훅에서 패킷을 가로채 유저스페이스를 거치지 않고 전달합니다.
  • 스케줄링 알고리즘 — rr(라운드로빈), wrr(가중치), lc(최소연결), wlc(가중최소연결) 등 10가지를 커널 모듈로 제공합니다.
  • 포워딩 모드 — NAT(주소변환), DR(Direct Routing, MAC만 교체), TUN(IP 터널), FULLNAT(src+dst 모두 변환) 4가지 모드를 지원합니다.
  • conntrack 연동 — 세션 지속성(Persistence)과 상태 추적을 conntrack과 협력하여 처리합니다.
  • keepalived HA — VRRP로 VIP 자동 페일오버, 헬스체크로 장애 Real Server 자동 제거를 수행합니다.
  • Kubernetes kube-proxy — iptables 모드 대신 IPVS 모드를 선택하면 수천 개 서비스를 O(1)로 처리합니다.
  • 성능 우위 — iptables DNAT 대비 10만 PPS 이상 고부하에서 IPVS가 선형 확장성을 유지합니다.
  • 세션 동기화 HA — Active/Backup Director 간 ip_vs_sync 멀티캐스트로 연결 상태를 실시간 동기화하여 페일오버 시 세션이 끊기지 않습니다.
  • XDP/Katran 가속 — NIC 드라이버 수준의 XDP 훅에서 패킷을 처리하면 Netfilter를 완전히 우회하여 IPVS 대비 2~5배 높은 처리량을 달성합니다.
  • Kubernetes IPVS 모드 — kube-proxy IPVS 모드는 서비스 수 10,000개에서도 O(1) 해시 룩업으로 균일한 지연을 유지합니다. iptables 모드는 O(n) 선형 탐색입니다.

단계별 이해

  1. VIP와 Real Server 개념 잡기
    클라이언트 → VIP(Director) → Real Server 흐름을 그림으로 익힙니다. Director가 패킷을 어떻게 전달하느냐가 포워딩 모드의 핵심입니다.
  2. NAT 모드로 시작하기
    가장 간단한 NAT 모드로 ipvsadm을 직접 실행해 패킷 흐름을 tcpdump로 확인합니다. dst IP가 Real Server로 바뀌는 과정을 눈으로 봅니다.
  3. DR 모드 이해하기
    Real Server가 VIP를 lo 인터페이스에 추가하고 ARP를 억제해야 하는 이유를 이해합니다. 응답 패킷이 Director를 거치지 않아 처리량이 2배 이상 늘어납니다.
  4. 스케줄링 알고리즘 선택하기
    균일 요청은 rr/wrr, 커넥션 유지 시간이 긴 서비스는 lc/wlc, 캐시 서버는 dh/sh를 선택합니다.
  5. keepalived로 HA 구성하기
    Active/Standby Director 두 대에 keepalived를 설치하고 VRRP와 헬스체크 설정을 적용합니다.
  6. 모니터링 및 진단하기
    ipvsadm -Ln --stats, /proc/net/ip_vs, conntrack -L로 연결 상태를 실시간 확인합니다.
  7. 세션 동기화로 무중단 HA 구성하기
    Primary Director에 ipvsadm --start-daemon master --syncid 1, Backup에 --start-daemon backup을 설정합니다. keepalived notify 스크립트로 역할 전환 시 자동 활성화합니다.
  8. Kubernetes IPVS 모드 활용하기
    kube-proxy ConfigMap에서 mode: "ipvs"로 설정하고, ipvsadm -Ln으로 ClusterIP/NodePort 서비스가 IPVS Virtual Service로 등록되는지 확인합니다.

개요: IPVS와 Linux Virtual Server

IPVS(IP Virtual Server)는 리눅스 커널 내에서 동작하는 L4 로드밸런서입니다. 1998년 Wensong Zhang이 개발한 LVS(Linux Virtual Server) 프로젝트의 핵심 컴포넌트로, 커널 2.6.10부터 메인라인에 포함되어 있습니다(net/netfilter/ipvs/). Netfilter 훅을 통해 패킷을 가로채고, 유저스페이스를 전혀 거치지 않으므로 수십만 CPS(Connections Per Second) 규모에서도 안정적으로 동작합니다.

IPVS는 세 가지 주요 개념으로 구성됩니다.

Kubernetes IPVS 모드: kube-proxy는 iptables 모드 대신 IPVS 모드를 지원합니다. 서비스 수가 1,000개를 넘으면 iptables 규칙 탐색이 O(n)으로 성능이 급락하는 반면, IPVS 해시 테이블은 O(1) 룩업으로 10,000개 서비스도 균일한 지연을 유지합니다.
IPVS 전체 아키텍처 Client A 192.168.1.10 Client B 192.168.1.20 Director (IPVS) VIP: 10.0.0.1:80 Netfilter Hooks LOCAL_IN / FORWARD ip_vs_conn hash 연결 추적 테이블 Scheduler rr / wrr / lc / wlc … ipvsadm / netlink RS1 10.0.0.11:80 RS2 10.0.0.12:80 RS3 10.0.0.13:80 keepalived VRRP + Health Check

IPVS 아키텍처와 Netfilter 통합

IPVS는 Netfilter 프레임워크의 두 훅 지점에 등록됩니다. NF_INET_LOCAL_IN(우선순위 -1)에서 VIP로 향하는 패킷을 포착하고, NF_INET_FORWARD(우선순위 -1)에서 기존 연결의 후속 패킷을 처리합니다. 패킷이 훅에 도달하면 IPVS는 ip_vs_in() 함수를 호출합니다.

핵심 자료구조

/* include/net/ip_vs.h */

/* Virtual Service: VIP:Port:Protocol */
struct ip_vs_service {
    struct hlist_node   s_list;     /* 서비스 해시 테이블 노드 */
    atomic_t            refcnt;
    u32                 flags;      /* IP_VS_SVC_F_* */
    __u16               protocol;  /* TCP/UDP/SCTP */
    __be32              addr;       /* VIP */
    __be16              port;       /* 포트 */
    struct ip_vs_scheduler *scheduler;  /* 스케줄러 포인터 */
    struct list_head    destinations;   /* Real Server 목록 */
    __u32               num_dests;
    struct ip_vs_stats  stats;
    /* ... */
};

/* Real Server: 실제 백엔드 서버 */
struct ip_vs_dest {
    struct list_head    n_list;     /* 서비스 내 RS 목록 */
    __be32              addr;       /* RS IP */
    __be16              port;       /* RS 포트 */
    volatile unsigned   flags;      /* IP_VS_DEST_F_* */
    atomic_t            weight;     /* 가중치 */
    atomic_t            activeconns;  /* 활성 연결 수 */
    atomic_t            inactconns;   /* 비활성 연결 수 */
    struct ip_vs_stats  stats;
    /* ... */
};

/* Connection: 클라이언트-VIP 연결 상태 */
struct ip_vs_conn {
    struct hlist_node   c_list;     /* 연결 해시 테이블 노드 */
    /* 5-tuple */
    __be32              caddr;      /* 클라이언트 IP */
    __be32              vaddr;      /* VIP */
    __be32              daddr;      /* RS IP (Real Server) */
    __be16              cport;
    __be16              vport;
    __be16              dport;
    __u16               protocol;
    /* 상태 */
    volatile __u16      state;      /* IP_VS_TCP_S_*, IP_VS_UDP_S_* */
    unsigned long       timeout;
    atomic_t            refcnt;
    struct ip_vs_dest   *dest;      /* 배정된 RS 포인터 */
    /* NAT용 포트/IP 변환 정보 */
    union nf_inet_addr  nat_addr;
    __be16              nat_port;
    /* ... */
};

Netfilter 훅 등록

/* net/netfilter/ipvs/ip_vs_core.c */

static const struct nf_hook_ops ip_vs_ops4[] = {
    /* 인바운드: VIP로 들어오는 신규 연결 처리 */
    {
        .hook     = ip_vs_in,
        .pf       = NFPROTO_IPV4,
        .hooknum  = NF_INET_LOCAL_IN,
        .priority = NF_IP_PRI_NAT_DST + 1,  /* conntrack DNAT 이후 */
    },
    /* 아웃바운드: Director → RS 포워딩 경로 */
    {
        .hook     = ip_vs_out,
        .pf       = NFPROTO_IPV4,
        .hooknum  = NF_INET_FORWARD,
        .priority = NF_IP_PRI_NAT_DST - 1,
    },
    /* 아웃바운드 LOCAL_OUT: DR/TUN 반환 경로 */
    {
        .hook     = ip_vs_local_reply4,
        .pf       = NFPROTO_IPV4,
        .hooknum  = NF_INET_LOCAL_OUT,
        .priority = NF_IP_PRI_NAT_DST + 1,
    },
};

IPVS 패킷 처리 흐름은 다음 순서로 진행됩니다.

  1. NIC → ip_rcv() → routing → NF_INET_LOCAL_IN
  2. ip_vs_in() 호출: 연결 해시 탐색 (ip_vs_conn_get())
  3. 신규 연결이면 스케줄러 호출 → RS 선택 → ip_vs_conn_new()
  4. 포워딩 모드에 따라 패킷 변환 후 전달
  5. RS 응답 → ip_vs_out()에서 역방향 변환

스케줄링 알고리즘

IPVS는 스케줄링 알고리즘을 커널 모듈로 분리하여 동적 로드가 가능합니다. struct ip_vs_schedulerschedule() 콜백을 구현하면 됩니다. 현재 10가지 공식 알고리즘이 있습니다.

알고리즘 약어 모듈 설명 적합한 상황
Round Robin rr ip_vs_rr 순환 방식으로 RS를 선택. 가장 단순 서버 성능이 동일하고 요청 처리 시간이 균일할 때
Weighted RR wrr ip_vs_wrr 가중치 비율로 분배. 고성능 서버에 더 많이 할당 서버 성능이 불균일할 때
Least Connection lc ip_vs_lc 활성 연결 수가 가장 적은 RS 선택 요청 처리 시간이 불균일할 때
Weighted LC wlc ip_vs_wlc lc + 가중치 결합. (activeconns×256+inactconns)/weight 최솟값 선택 실무 범용 권장 알고리즘
Locality-Based LC lblc ip_vs_lblc 동일 클라이언트 IP → 동일 RS (캐시 친화). 과부하 시 재배분 웹 캐시 서버
LBLC with Replication lblcr ip_vs_lblcr lblc + RS 집합 복제. 핫스팟 방지 캐시 서버 클러스터
Destination Hash dh ip_vs_dh 목적지 IP 해시로 RS 고정 배정 정방향 프록시, 캐시 일관성
Source Hash sh ip_vs_sh 소스 IP 해시로 RS 고정 배정 (세션 지속성) NAT 뒤 클라이언트 세션 유지
Shortest Expected Delay sed ip_vs_sed 예상 대기 시간 최소 RS 선택. (activeconns+1)/weight 응답 시간 편차가 큰 서비스
Never Queue nq ip_vs_nq 활성 연결 없는 RS 우선 배정, 없으면 sed로 폴백 유휴 서버를 즉시 활용해야 할 때

스케줄러 내부 구현 (wlc 예시)

각 스케줄러는 net/netfilter/ipvs/ 디렉터리 내 별도 파일로 구현됩니다. struct ip_vs_schedulerschedule() 콜백을 구현하면 커스텀 스케줄러도 모듈로 등록할 수 있습니다.

/* net/netfilter/ipvs/ip_vs_wlc.c */

static struct ip_vs_dest *
ip_vs_wlc_schedule(struct ip_vs_service *svc, const struct sk_buff *skb,
                   struct ip_vs_iphdr *iph)
{
    struct ip_vs_dest *dest, *least = NULL;
    unsigned long loh = ULONG_MAX;

    list_for_each_entry_rcu(dest, &svc->destinations, n_list) {
        if (dest->flags & IP_VS_DEST_F_OVERLOAD)
            continue;
        if (!atomic_read(&dest->weight))
            continue;

        /* overhead = (activeconns * 256 + inactconns) / weight */
        unsigned long oh =
            (atomic_read(&dest->activeconns) * 256UL
             + atomic_read(&dest->inactconns))
            / atomic_read(&dest->weight);

        if (oh < loh) {
            loh   = oh;
            least = dest;
        }
    }
    return least;
}

LBLCR 스케줄러 — Locality-Based LC with Replication

ip_vs_lblcr 스케줄러는 웹 캐시 클러스터에 특화된 알고리즘입니다. 동일 목적지 IP는 동일 RS 집합(set)으로 보내 캐시 히트율을 높이되, 특정 RS가 과부하 상태이면 집합에 다른 RS를 추가하여 핫스팟을 방지합니다.

/* net/netfilter/ipvs/ip_vs_lblcr.c — 핵심 로직 */

/* 목적지 IP별 RS 집합 캐시 */
struct ip_vs_lblcr_entry {
    struct hlist_node       list;
    __be32                  addr;           /* 목적지 IP */
    struct ip_vs_dest_set   set;            /* RS 집합 */
    unsigned long           lastuse;        /* 마지막 사용 시각 */
};

static struct ip_vs_dest *
ip_vs_lblcr_schedule(struct ip_vs_service *svc, const struct sk_buff *skb,
                     struct ip_vs_iphdr *iph)
{
    struct ip_vs_lblcr_table *tbl = svc->sched_data;
    struct ip_vs_lblcr_entry *en;
    struct ip_vs_dest *dest;

    /* 1. 목적지 IP로 캐시 룩업 */
    en = ip_vs_lblcr_get(tbl, iph->daddr);
    if (!en) {
        /* 2. 새 항목: wlc 알고리즘으로 RS 선택 후 집합 생성 */
        dest = ip_vs_wlc_schedule(svc, skb, iph);
        en   = ip_vs_lblcr_new(iph->daddr, dest);
        ip_vs_lblcr_add(tbl, en);
        return dest;
    }

    /* 3. 캐시 히트: 집합 내 최소 연결 RS 선택 */
    dest = ip_vs_dest_set_min(&en->set);

    /* 4. 과부하 시 새 RS를 집합에 추가 (복제) */
    if (is_overloaded(dest, svc)) {
        struct ip_vs_dest *new_dest = ip_vs_wlc_schedule(svc, skb, iph);
        ip_vs_dest_set_add(&en->set, new_dest);
        dest = new_dest;
    }
    return dest;
}

SED와 NQ 알고리즘 상세

SED(Shortest Expected Delay)는 각 RS에 새 연결을 추가했을 때 예상되는 대기시간을 계산하여 최솟값을 선택합니다. 수식: (activeconns + 1) / weight. 기존 lc보다 가중치를 더 잘 활용하지만, 모든 RS가 바쁠 때 새 연결 1개짜리 RS를 무조건 선택하는 문제가 있습니다.

NQ(Never Queue)는 SED의 이 문제를 보완합니다. 활성 연결이 0인 RS(완전 유휴 서버)가 있으면 무조건 그 서버로 배정하고, 없을 때만 SED 알고리즘으로 폴백합니다. 유휴 서버를 즉시 활용해야 하는 오토스케일링 환경에 적합합니다.

/* net/netfilter/ipvs/ip_vs_nq.c */

static struct ip_vs_dest *
ip_vs_nq_schedule(struct ip_vs_service *svc, const struct sk_buff *skb,
                  struct ip_vs_iphdr *iph)
{
    struct ip_vs_dest *dest, *least = NULL;
    unsigned long loh = ULONG_MAX, doh;

    list_for_each_entry_rcu(dest, &svc->destinations, n_list) {
        if (!atomic_read(&dest->weight))
            continue;

        /* 활성 연결 0 = 완전 유휴 서버 → 즉시 반환 */
        if (atomic_read(&dest->activeconns) == 0 &&
            dest->flags & IP_VS_DEST_F_AVAILABLE)
            return dest;

        /* SED 수식: (activeconns + 1) * 256 / weight */
        doh = (atomic_read(&dest->activeconns) + 1) * 256UL
              / atomic_read(&dest->weight);

        if (doh < loh) {
            loh   = doh;
            least = dest;
        }
    }
    return least;
}

알고리즘별 상태 저장 오버헤드 비교

알고리즘 소스 파일 추가 상태 메모리 오버헤드 Lock 경합
rr ip_vs_rr.c 현재 RS 포인터 최소 (포인터 1개) spinlock/RCU
wrr ip_vs_wrr.c 현재 RS + 현재 가중치 카운터 낮음 spinlock
lc / wlc ip_vs_lc.c / ip_vs_wlc.c RS별 activeconns/inactconns (atomic) 낮음 (RS당 2 atomic) RCU (읽기 잠금 없음)
lblc ip_vs_lblc.c 목적지 IP → RS 해시 캐시 중간 (캐시 크기 × 엔트리) spinlock (캐시 갱신 시)
lblcr ip_vs_lblcr.c 목적지 IP → RS 집합 캐시 높음 (집합 크기 가변) spinlock (집합 수정 시)
sh / dh ip_vs_sh.c / ip_vs_dh.c IP → RS 해시 테이블 (고정 256 버킷) 중간 (256 × 포인터) RCU
sed / nq ip_vs_sed.c / ip_vs_nq.c RS별 activeconns (atomic) 낮음 RCU

포워딩 모드 (NAT / DR / TUN / FULLNAT)

IPVS는 패킷을 Real Server로 전달하는 네 가지 포워딩 모드를 지원합니다. 각 모드는 패킷 헤더를 어떻게 변환하느냐와 응답 경로가 어디를 통하느냐에서 차이가 납니다.

IPVS 포워딩 모드 비교 NAT 모드 Client→VIP: dst=RS IP 응답: src=VIP (Director 경유) Client ──→ Dir ──→ RS Client ←── Dir ←── RS RS GW = Director 사설망 RS 사용 가능 DR 모드 MAC만 변경 (IP 불변) 응답: RS→Client 직접 Client ──→ Dir ──→ RS Client ←────────── RS RS: lo에 VIP 추가 ARP 응답 억제 필수 TUN 모드 IP-in-IP 캡슐화 응답: RS→Client 직접 Client ──→ Dir═══→ RS Client ←────────── RS RS 원격 가능 (WAN) RS: tunl0 인터페이스 FULLNAT 모드 src+dst 모두 변환 응답: RS→Dir→Client Client ──→ Dir ──→ RS Client ←── Dir ←── RS RS GW 무관 비표준 패치 필요 포워딩 모드 특성 비교 항목 NAT DR TUN FULLNAT 응답 경로 Director 경유 RS → Client 직접 RS → Client 직접 Director 경유 RS 위치 같은 서브넷 같은 서브넷 원격 가능 같은 서브넷 RS 설정 GW=Director VIP + ARP 억제 tunl0 + VIP 없음 처리량 한계 Director 대역폭 RS 대역폭 합산 RS 대역폭 합산 Director 대역폭 표준 여부 표준 표준 표준 비표준(패치)

NAT 모드

NAT 모드는 Director가 목적지 IP를 VIP에서 RS IP로 변경합니다. 응답 패킷도 Director를 경유하므로 RS의 기본 게이트웨이를 Director로 설정해야 합니다.

# Director 설정
# 1. IP 포워딩 활성화
sysctl -w net.ipv4.ip_forward=1

# 2. 가상 서비스 추가 (VIP:80 TCP, wlc 알고리즘)
ipvsadm -A -t 10.0.0.1:80 -s wlc

# 3. Real Server 추가 (NAT 모드: -m)
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.11:80 -m -w 1
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.12:80 -m -w 1

# RS 서버에서: 기본 게이트웨이를 Director로 설정
ip route add default via 192.168.100.1  # Director 내부 IP

DR 모드 (Direct Routing)

DR 모드는 패킷의 MAC 주소만 변경하여 RS로 전달합니다. 응답 패킷은 RS에서 클라이언트로 직접 전송되므로 Director가 병목이 되지 않습니다. RS는 VIP를 lo 인터페이스에 추가하고 ARP 응답을 억제해야 합니다.

# Director 설정
ipvsadm -A -t 10.0.0.1:80 -s wlc
# DR 모드: -g (gateway/direct routing)
ipvsadm -a -t 10.0.0.1:80 -r 10.0.0.11:80 -g -w 1
ipvsadm -a -t 10.0.0.1:80 -r 10.0.0.12:80 -g -w 1

# ─────────────────────────────────────────
# 각 RS 서버에서 실행 (ARP 억제 + VIP 추가)
# ─────────────────────────────────────────

# ARP 요청을 lo 인터페이스에서 응답하지 않도록 설정
sysctl -w net.ipv4.conf.all.arp_ignore=1
sysctl -w net.ipv4.conf.all.arp_announce=2

# lo에 VIP 추가 (prefix /32: 브로드캐스트 없음)
ip addr add 10.0.0.1/32 dev lo

# /etc/sysctl.conf에 영구 적용
cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
EOF

TUN 모드

TUN 모드는 원본 패킷을 IP-in-IP로 캡슐화하여 RS에 전달합니다. RS가 물리적으로 다른 데이터센터에 있어도 됩니다. RS에서는 ipip 모듈을 로드하고 tunl0 인터페이스에 VIP를 할당합니다.

# RS 서버에서
modprobe ipip
ip addr add 10.0.0.1/32 dev tunl0
ip link set tunl0 up

sysctl -w net.ipv4.conf.tunl0.arp_ignore=1
sysctl -w net.ipv4.conf.tunl0.rp_filter=0

# Director
ipvsadm -A -t 10.0.0.1:80 -s rr
# TUN 모드: -i
ipvsadm -a -t 10.0.0.1:80 -r 203.0.113.11:80 -i -w 1

TUN 모드 캡슐화 옵션 (IPIP / GRE)

커널 4.9부터 ip_vs_tunnel_encap_type sysctl로 캡슐화 방식을 선택할 수 있습니다. 기본값은 IPIP(0)이며, GRE(1)를 선택하면 GRE 키를 통해 다중 VIP를 구분할 수 있습니다.

# TUN 캡슐화 방식 설정
# 0 = IPIP (기본), 1 = GRE, 2 = GUE (Generic UDP Encapsulation)
sysctl -w net.ipv4.vs.tunnel_encap_type=0

# GRE 모드 사용 시 RS에서 GRE 인터페이스 설정
ip tunnel add gre1 mode gre local RS_IP remote DIRECTOR_IP
ip addr add 10.0.0.1/32 dev gre1
ip link set gre1 up

# GUE(UDP 캡슐화) 모드: NAT 방화벽 통과 가능
sysctl -w net.ipv4.vs.tunnel_encap_type=2
sysctl -w net.ipv4.vs.tunnel_encap_dport=6080  # GUE 기본 포트

DR 모드 ARP 억제 심화

DR 모드에서 RS는 VIP를 lo 인터페이스에 추가하는데, 이때 ARP 요청에 응답하지 않도록 arp_ignorearp_announce 두 파라미터를 반드시 설정해야 합니다. 이 설정 없이는 RS가 ARP 요청에 응답하여 VIP 트래픽이 Director가 아닌 RS로 직접 흘러들어 갑니다.

# ── arp_ignore 값의 의미 ──
# 0 (기본): 어떤 인터페이스에서도 로컬 IP로 ARP 응답 (위험)
# 1: ARP 요청이 들어온 인터페이스에 설정된 IP에 대해서만 응답
# 2: ARP 요청 수신 인터페이스 IP + 같은 서브넷에만 응답
# 8: ARP 요청에 절대 응답 안 함

# ── arp_announce 값의 의미 ──
# 0 (기본): 어떤 소스 IP도 ARP 광고에 사용
# 1: 목적지 서브넷에 없는 IP는 가급적 사용 안 함
# 2: 항상 해당 인터페이스의 최적 로컬 IP를 소스로 사용 (권장)

# DR 모드 RS 설정 완전 예시
# 1. 인터페이스별 + all 양쪽 설정 필수
sysctl -w net.ipv4.conf.all.arp_ignore=1
sysctl -w net.ipv4.conf.eth0.arp_ignore=1
sysctl -w net.ipv4.conf.all.arp_announce=2
sysctl -w net.ipv4.conf.eth0.arp_announce=2

# 2. VIP를 lo에 추가 (/32: 서브넷 광고 없음)
ip addr add 10.0.0.1/32 dev lo label lo:vip

# 3. rp_filter 비활성화 (소스 IP 검증 완화)
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.eth0.rp_filter=0

# 4. 확인
ip addr show dev lo
arping -I eth0 10.0.0.1  # RS에서 응답 없어야 정상

FULLNAT 모드 심화

FULLNAT은 표준 커널에 포함되지 않은 비표준 패치입니다. Alibaba Cloud의 LVS 패치 또는 kube-nat 프로젝트에서 구현을 찾을 수 있습니다. FULLNAT은 NAT 모드와 달리 소스 IP도 Director의 IP로 변경하므로 RS의 기본 게이트웨이를 Director로 설정할 필요가 없고, RS가 서로 다른 서브넷 또는 VPC에 분산되어 있어도 동작합니다.

/* FULLNAT 패킷 흐름 */

/* 인바운드 (Client → Director → RS) */
원본:   src=Client_IP:Cport  dst=VIP:80
FULLNAT 변환: src=DIP:Eport   dst=RS_IP:80
/* DIP = Director의 내부 IP, Eport = 임시 포트 */

/* RS 응답 (RS → Director) */
원본:   src=RS_IP:80  dst=DIP:Eport
FULLNAT 역변환: src=VIP:80  dst=Client_IP:Cport

/* RS에서 원본 클라이언트 IP를 알아야 하는 경우: TOA (TCP Option Address) */
/* TOA는 TCP 옵션 필드(Kind=254)에 원본 클라이언트 IP를 삽입 */
/* RS 커널에 toa.ko 모듈 설치 후 getsockopt(SO_GET_REAL_IP)로 획득 */
TOA(TCP Option Address): FULLNAT 환경에서 RS가 원본 클라이언트 IP를 알 수 없는 문제를 해결하기 위해, Director가 TCP SYN 패킷의 TCP 옵션 필드에 원본 클라이언트 IP를 삽입합니다. RS에 toa.ko 커널 모듈을 설치하면 getsockopt(TCP_REAL_CLIENT_ADDR)로 원본 IP를 획득할 수 있습니다. Nginx, HAProxy 등 대부분의 L7 프록시는 TOA 모듈 패치를 지원합니다.

IPVS + conntrack 연동

IPVS는 자체 연결 추적(ip_vs_conn)을 사용하지만, NAT 모드에서는 Netfilter conntrack과도 협력합니다. CONFIG_IP_VS_NFCT 옵션을 활성화하면 IPVS가 conntrack 항목을 생성/갱신합니다.

conntrack 바인딩

/* net/netfilter/ipvs/ip_vs_nfct.c */

/* IPVS 연결을 conntrack에 바인딩 */
void ip_vs_nfct_expect_related(struct sk_buff *skb, struct nf_conn *ct,
                                struct ip_vs_conn *cp, u_int8_t proto,
                                const __be16 dport, int from_rs)
{
    struct nf_conntrack_expect *exp;
    exp = nf_ct_expect_alloc(ct);
    if (!exp)
        return;
    /* 기대 연결 등록 */
    nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
                      nf_ct_l3num(ct), NULL, NULL, proto, NULL, &dport);
    nf_ct_expect_related(exp, 0);
    nf_ct_expect_put(exp);
}

/* 새 IPVS 연결 생성 시 conntrack 생성 */
static void ip_vs_nfct_bind_dest(struct ip_vs_conn *cp)
{
    if (!(cp->flags & IP_VS_CONN_F_NFCT))
        return;
    /* cp → nf_conn 연결 */
}

세션 지속성 (Persistence)

IPVS는 동일 클라이언트의 연속 연결을 동일 RS로 유도하는 세션 지속성을 지원합니다. -p 옵션으로 지속 시간(초)을 설정합니다.

# 세션 지속성: 300초 동안 같은 클라이언트는 같은 RS로
ipvsadm -A -t 10.0.0.1:80 -s lc -p 300

# 지속성 확인
ipvsadm -Lcn | grep PERSIST

# conntrack 상태 확인
conntrack -L | grep "dport=80"

# IPVS 연결 테이블 확인
cat /proc/net/ip_vs_conn

conntrack 관련 sysctl

# IPVS + conntrack 연동 활성화
sysctl -w net.ipv4.vs.conntrack=1

# IPVS 연결 해시 테이블 크기 (2의 지수)
sysctl -w net.ipv4.vs.conn_reuse_mode=1

# 연결 타임아웃 조정
sysctl -w net.ipv4.vs.tcp_timeout_established=900
sysctl -w net.ipv4.vs.tcp_timeout_fin_wait=30
sysctl -w net.ipv4.vs.udp_timeout=300

# 대규모 환경: 해시 테이블 버킷 수 (기본 4096)
# 커널 파라미터: ip_vs_conn_tab_bits=20 (2^20 = 1M 버킷)

ip_vs_nfct_expect_related() 전체 흐름

FTP, SIP 같은 ALG(Application Layer Gateway) 프로토콜은 데이터 연결을 동적으로 생성합니다. IPVS는 ip_vs_nfct_expect_related()를 통해 conntrack의 expect 메커니즘을 활용하여 이러한 연관 연결을 자동으로 추적합니다.

/* net/netfilter/ipvs/ip_vs_nfct.c */

/*
 * 흐름: FTP 제어 연결(21) → 데이터 연결(20) 추적
 *
 * 1. FTP 클라이언트가 PORT 명령으로 데이터 포트를 알림
 * 2. ip_vs_ftp.c의 ALG가 포트 번호를 파싱
 * 3. ip_vs_nfct_expect_related() 호출 → conntrack expect 등록
 * 4. 데이터 연결이 도착하면 conntrack이 IPVS에 통보
 * 5. IPVS가 같은 RS로 데이터 연결을 유도
 */
void ip_vs_nfct_expect_related(struct sk_buff *skb, struct nf_conn *ct,
                                struct ip_vs_conn *cp, u_int8_t proto,
                                const __be16 dport, int from_rs)
{
    struct nf_conntrack_expect *exp;

    /* 1. expect 구조체 할당 */
    exp = nf_ct_expect_alloc(ct);
    if (!exp)
        return;

    /* 2. expect 초기화: 프로토콜 + 목적지 포트 지정 */
    nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
                      nf_ct_l3num(ct),
                      from_rs ? &cp->daddr : NULL,   /* RS IP */
                      from_rs ? NULL : &cp->vaddr,   /* VIP */
                      proto, NULL, &dport);

    /* 3. NAT 헬퍼: expect 연결의 DNAT 처리를 IPVS에 위임 */
    exp->expectfn = ip_vs_nfct_expect_callback;
    exp->flags   |= NF_CT_EXPECT_PERMANENT;

    /* 4. conntrack에 expect 등록 */
    nf_ct_expect_related(exp, 0);
    nf_ct_expect_put(exp);
}

/* FTP ALG 모듈 로드 */
// modprobe ip_vs_ftp
// modprobe nf_conntrack_ftp

conntrack 없이 IPVS 운용

대규모 환경에서는 conntrack의 오버헤드를 제거하기 위해 비활성화할 수 있습니다. 단, 이 경우 IPVS 자체 연결 테이블(ip_vs_conn)만 사용하므로 FTP/SIP ALG, SNAT 자동 처리 등 일부 기능이 동작하지 않습니다.

# conntrack 없이 IPVS 운용 (DR 모드에서 주로 사용)
sysctl -w net.ipv4.vs.conntrack=0

# conntrack 모듈 언로드 (DR/TUN 모드 전용 환경)
modprobe -r nf_conntrack_ipv4
modprobe -r nf_conntrack

# IPVS 자체 연결 해시 테이블 크기 조정
# 부팅 파라미터: ip_vs.conn_tab_bits=20 → 2^20 = 1,048,576 버킷
# /etc/modprobe.d/ipvs.conf
echo "options ip_vs conn_tab_bits=20" > /etc/modprobe.d/ipvs.conf

# 현재 연결 테이블 크기 확인
cat /proc/sys/net/ipv4/vs/conn_tab_size

NAT 모드: conntrack SNAT/DNAT 협력

IPVS NAT 모드는 Netfilter conntrack의 DNAT/SNAT와 협력합니다. 패킷이 NF_INET_LOCAL_IN 훅에 도달하면 먼저 conntrack이 DNAT(VIP→RS)를 처리하고, 이어서 IPVS가 연결 테이블을 갱신합니다. 응답 패킷은 conntrack이 역방향 SNAT(RS→VIP)를 수행합니다.

/* IPVS NAT + conntrack 협력 패킷 흐름 */

/* ── 인바운드 SYN (Client → Director) ── */
// 1. NF_INET_PRE_ROUTING: conntrack이 NEW 상태로 등록
// 2. 라우팅 결정: VIP가 로컬 → NF_INET_LOCAL_IN
// 3. conntrack DNAT (ip_tables: DNAT 규칙이 있는 경우)
// 4. IPVS ip_vs_in(): 스케줄러 → RS 선택 → ip_vs_conn_new()
// 5. ip_vs_nat_xmit(): dst IP를 VIP→RS IP로 변환
// 6. NF_INET_LOCAL_OUT → NF_INET_POST_ROUTING → RS로 전송

/* ── 아웃바운드 SYN-ACK (RS → Director) ── */
// 1. NF_INET_PRE_ROUTING (Director에서 수신)
// 2. IPVS ip_vs_out(): src IP를 RS IP→VIP로 변환
// 3. conntrack이 ESTABLISHED로 상태 갱신
// 4. NF_INET_POST_ROUTING → Client로 전송

keepalived와 고가용성 (HA)

keepalived는 IPVS Director의 고가용성을 위한 사실상의 표준 도구입니다. 두 가지 핵심 기능을 제공합니다.

keepalived는 내부적으로 libipvs를 통해 ipvsadm 규칙을 직접 관리하므로, keepalived 설정만으로 IPVS 서비스 전체를 선언적으로 관리할 수 있습니다.

keepalived.conf 전체 예시

# /etc/keepalived/keepalived.conf

global_defs {
    router_id LVS_DIRECTOR_1
    script_user root
    enable_script_security
}

# ── VRRP 인스턴스: VIP 페일오버 ──
vrrp_instance VI_1 {
    state MASTER          # Standby는 BACKUP
    interface eth0
    virtual_router_id 51  # 0-255, 같은 그룹끼리 동일
    priority 100          # Standby는 90 (낮을수록 우선순위 낮음)
    advert_int 1          # VRRP 광고 주기 (초)
    authentication {
        auth_type PASS
        auth_pass secretkey
    }
    virtual_ipaddress {
        10.0.0.1/24 dev eth0  # VIP
    }
}

# ── Virtual Server 선언 ──
virtual_server 10.0.0.1 80 {
    delay_loop 6          # 헬스체크 주기 (초)
    lb_algo wlc           # 스케줄링 알고리즘
    lb_kind NAT           # 포워딩 모드: NAT | DR | TUN
    persistence_timeout 300  # 세션 지속성 (초, 0=비활성)
    protocol TCP

    # ── Real Server 1 ──
    real_server 192.168.100.11 80 {
        weight 1
        TCP_CHECK {
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
            connect_port 80
        }
    }

    # ── Real Server 2 ──
    real_server 192.168.100.12 80 {
        weight 1
        HTTP_GET {
            url {
                path /healthz
                status_code 200
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }
    }

    # ── Real Server 3 (고가중치) ──
    real_server 192.168.100.13 80 {
        weight 3
        HTTP_GET {
            url {
                path /healthz
                digest 9b3a0c85a887a0e3cf30b19abe2eadca
            }
            connect_timeout 3
        }
    }
}

# ── UDP 서비스 예시 ──
virtual_server 10.0.0.1 53 {
    delay_loop 10
    lb_algo rr
    lb_kind DR
    protocol UDP

    real_server 192.168.100.11 53 {
        weight 1
        DNS_CHECK {
            type A
            name example.com
        }
    }
}

HA 페일오버 동작

# keepalived 서비스 관리
systemctl enable --now keepalived

# VRRP 상태 확인
journalctl -u keepalived -f

# 현재 IPVS 규칙 확인 (keepalived가 자동 적용)
ipvsadm -Ln

# Active Director 장애 시 시뮬레이션
systemctl stop keepalived   # Standby가 MASTER로 전환, VIP 인계

# 헬스체크 실패 시 RS 자동 제거 확인
watch -n1 ipvsadm -Ln

ipvsadm 설정 실전

ipvsadm은 IPVS 커널 모듈과 netlink 소켓으로 통신하는 CLI 도구입니다. 규칙은 /etc/ipvsadm.rules 또는 ipvsadm-save로 영구 저장합니다.

기본 명령어

# 커널 모듈 로드
modprobe ip_vs
modprobe ip_vs_rr ip_vs_wrr ip_vs_lc ip_vs_wlc

# ── 서비스 관리 ──
# TCP 서비스 추가
ipvsadm -A -t VIP:PORT -s SCHEDULER [-p TIMEOUT]
# UDP 서비스 추가
ipvsadm -A -u VIP:PORT -s SCHEDULER

# 서비스 수정
ipvsadm -E -t VIP:PORT -s NEW_SCHEDULER

# 서비스 삭제
ipvsadm -D -t VIP:PORT

# ── Real Server 관리 ──
# RS 추가 (-m NAT, -g DR, -i TUN)
ipvsadm -a -t VIP:PORT -r RS_IP:RS_PORT -m -w WEIGHT

# RS 수정 (가중치 변경)
ipvsadm -e -t VIP:PORT -r RS_IP:RS_PORT -m -w NEW_WEIGHT

# RS 삭제
ipvsadm -d -t VIP:PORT -r RS_IP:RS_PORT

# ── 조회 ──
# 전체 규칙 (숫자 포트)
ipvsadm -Ln

# 연결 통계 포함
ipvsadm -Ln --stats

# 연결 속도 포함
ipvsadm -Ln --rate

# 현재 연결 목록
ipvsadm -Lcn

# ── 저장/복원 ──
ipvsadm-save -n > /etc/ipvsadm.rules
ipvsadm-restore < /etc/ipvsadm.rules

# ── 전체 초기화 ──
ipvsadm -C

고급 설정 예시

# HTTPS 서비스 + 세션 지속성 300초
ipvsadm -A -t 10.0.0.1:443 -s wlc -p 300
ipvsadm -a -t 10.0.0.1:443 -r 192.168.100.11:443 -m -w 2
ipvsadm -a -t 10.0.0.1:443 -r 192.168.100.12:443 -m -w 1

# 퍼시스턴스 넷마스크: /24 서브넷 단위 세션 유지
ipvsadm -A -t 10.0.0.1:80 -s sh -p 300 -M 255.255.255.0

# 방화벽 마크 기반 가상 서비스 (포트 무관 로드밸런싱)
# iptables로 마크 설정
iptables -t mangle -A PREROUTING -d 10.0.0.1 -j MARK --set-mark 1
# IPVS에서 마크로 서비스 정의
ipvsadm -A -f 1 -s wlc
ipvsadm -a -f 1 -r 192.168.100.11:0 -m -w 1

# IPv6 지원
ipvsadm -A -t [2001:db8::1]:80 -s rr
ipvsadm -a -t [2001:db8::1]:80 -r [2001:db8::11]:80 -m

성능 튜닝 파라미터

# /etc/sysctl.d/99-ipvs.conf

# IP 포워딩 (NAT 모드 필수)
net.ipv4.ip_forward = 1

# 연결 추적 테이블 크기 (대규모 환경)
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_buckets = 262144

# IPVS 연결 재사용 모드
# 0: 비활성, 1: TIME_WAIT 재사용, 2: 완전 재사용
net.ipv4.vs.conn_reuse_mode = 1

# TIME_WAIT 상태 연결 제어
net.ipv4.vs.expire_nodest_conn = 1
net.ipv4.vs.expire_quiescent_template = 1

# TCP 타임아웃 (기본값 900초는 길다)
net.ipv4.vs.tcp_timeout_established = 300
net.ipv4.vs.tcp_timeout_time_wait = 30

# 동기화 데몬 (백업 Director와 연결 상태 동기화)
# ipvsadm --start-daemon master --mcast-interface eth0
net.ipv4.vs.sync_threshold = "3 50"

# SYN 쿠키 (SYN 플러드 방어)
net.ipv4.tcp_syncookies = 1

# 로컬 포트 범위 확장
net.ipv4.ip_local_port_range = 1024 65535

IPVS vs nftables nat vs HAProxy 비교

L4 로드밸런싱을 구현하는 방법은 여러 가지가 있습니다. 각 방식의 동작 레이어, 성능 특성, 적합한 사용 사례를 비교합니다.

처리량 비교 (10만 연결 기준, 상대적 수치) 100% 75% 50% 25% 100% IPVS DR 85% IPVS NAT 60% nftables DNAT 45% iptables DNAT 70% HAProxy L4 * 상대 비교값. 실제 수치는 하드웨어/설정에 따라 다릅니다.
항목 IPVS (DR) IPVS (NAT) nftables DNAT HAProxy (L4) nginx upstream
동작 레이어 L4 커널 L4 커널 L4 커널 L4 유저스페이스 L7 유저스페이스
컨텍스트 스위치 없음 없음 없음 있음 있음
룩업 복잡도 O(1) 해시 O(1) 해시 O(n) 규칙 O(1) 해시 O(n) 업스트림
TLS 오프로딩 불가 불가 불가 가능 가능
L7 라우팅 불가 불가 불가 제한적 가능
헬스체크 keepalived 별도 keepalived 별도 없음 내장 내장
10만 서비스 O(1) 유지 O(1) 유지 성능 저하 O(1) 유지 메모리 증가
Kubernetes 지원 kube-proxy IPVS kube-proxy IPVS kube-proxy iptables 별도 Ingress Ingress Controller
설정 복잡도 중간 낮음 낮음 낮음 낮음
권장 용도 고성능 LB 범용 LB 소규모 L4 프록시 L7 프록시

세션 지속성(Persistence) 심화

세션 지속성(Persistence)은 동일 클라이언트의 여러 연결을 항상 동일 RS로 유도하는 기능입니다. HTTP/1.1 Keep-Alive, 데이터베이스 연결 풀, SIP 통화처럼 여러 TCP 연결이 동일 백엔드를 사용해야 하는 경우에 필수입니다. IPVS는 지속성 엔진(Persistence Engine)을 플러그인 형태로 지원합니다.

기본 지속성 엔진 (ip_vs_pe_default)

기본 지속성 엔진은 클라이언트 IP + 넷마스크를 키로 사용하여 지속성 테이블(ip_vs_pe_data)에 RS 매핑을 저장합니다. 키 계산은 ip_vs_conn_hashkey_conn() 함수가 담당합니다.

/* include/net/ip_vs.h — 지속성 엔진 인터페이스 */
struct ip_vs_pe {
    struct list_head    n_list;
    char                *name;          /* 엔진 이름 (예: "sip") */
    struct module       *module;

    /* 지속성 키 계산: skb에서 키를 추출하여 param에 저장 */
    int (*fill_param)(struct ip_vs_conn_param *p, struct sk_buff *skb);

    /* 지속성 체크: 기존 연결과 현재 패킷의 키가 일치하는지 확인 */
    bool (*ct_match)(const struct ip_vs_conn_param *p,
                     struct ip_vs_conn *ct);

    /* 해시값 계산 */
    u32 (*hashkey_raw)(const struct ip_vs_conn_param *p, u32 initval,
                       bool inverse);
    int (*show_pe_data)(const struct ip_vs_conn *cp, char *buf);
};

/* 기본 엔진 등록 */
static struct ip_vs_pe ip_vs_pe_default = {
    .name           = "",
    .fill_param     = ip_vs_fill_iph_skb,
    .hashkey_raw    = ip_vs_conn_hashkey_param,
    /* ct_match 없음: IP만 비교 */
};

SIP Call-ID 기반 지속성 (ip_vs_pe_sip)

SIP 프로토콜은 동일 통화(Call)의 여러 UDP 패킷이 서로 다른 소스 포트를 사용할 수 있습니다. 클라이언트 IP만으로는 지속성을 보장할 수 없으므로, SIP Call-ID 헤더를 키로 사용합니다. ip_vs_pe_sip.ko 모듈이 SIP 페이로드를 파싱하여 Call-ID를 추출합니다.

# SIP 지속성 설정
modprobe ip_vs_pe_sip

# SIP UDP 443 서비스에 SIP 지속성 엔진 + 360초 타임아웃
ipvsadm -A -u 10.0.0.1:5060 -s rr -p 360 --pe sip

ipvsadm -a -u 10.0.0.1:5060 -r 192.168.100.11:5060 -m -w 1
ipvsadm -a -u 10.0.0.1:5060 -r 192.168.100.12:5060 -m -w 1

# 확인: SIP Call-ID별 지속성 엔트리
ipvsadm -Lcn | grep PERSIST

FWM(Firewall Mark) 기반 지속성

FWM 기반 지속성은 iptables/nftables로 패킷에 마크를 붙여 포트 무관하게 동일 RS로 유도합니다. HTTPS(443)와 HTTP(80)를 동시에 처리하는 서버에서 두 포트의 클라이언트를 같은 RS로 보내야 할 때 유용합니다.

# ── FWM 기반 지속성 설정 완전 예시 ──

# 1. iptables로 HTTP + HTTPS 패킷에 마크 1 설정
iptables -t mangle -A PREROUTING -d 10.0.0.1 -p tcp --dport 80  -j MARK --set-mark 1
iptables -t mangle -A PREROUTING -d 10.0.0.1 -p tcp --dport 443 -j MARK --set-mark 1

# 2. IPVS 마크 기반 가상 서비스 정의 (포트 0 = 모든 포트)
ipvsadm -A -f 1 -s wlc -p 360

# 3. Real Server 추가
ipvsadm -a -f 1 -r 192.168.100.11:0 -m -w 1
ipvsadm -a -f 1 -r 192.168.100.12:0 -m -w 1

# 4. 확인: 마크 기반 서비스 목록
ipvsadm -Ln
# FWM  1 wlc persistent 360
#   -> 192.168.100.11:0     Masq    1      0          0
#   -> 192.168.100.12:0     Masq    1      0          0

지속성 + DR/TUN 모드 주의점

DR/TUN 모드에서 세션 지속성을 사용할 때는 다음 사항에 주의해야 합니다.

상황 문제 해결책
DR + Persistence + NAT 방화벽 클라이언트 IP가 NAT IP로 마스킹 → 지속성 키 충돌 넷마스크 축소(-M 255.255.255.255) 또는 FWM 사용
TUN + IPv6 지속성 IPIP 터널이 IPv6를 캡슐화하지 못하는 경우 GUE 캡슐화 타입으로 전환
지속성 타임아웃 만료 중 RS 제거 활성 지속성 엔트리가 남아 있어 새 연결이 삭제된 RS로 향할 수 있음 expire_quiescent_template=1 sysctl 활성화
sh 스케줄러 + Persistence 중복 sh 알고리즘 자체가 IP 해시 기반 → -p 옵션과 기능 중복 sh는 -p 없이 사용, 지속성 필요 시 lc/wlc + -p 조합 선호

HTTPS 서비스 지속성 완전 예시

# HTTPS 로드밸런서: 300초 세션 지속성
# 같은 클라이언트의 새 HTTPS 연결은 항상 같은 RS로
ipvsadm -A -t 10.0.0.1:443 -s wlc -p 300

ipvsadm -a -t 10.0.0.1:443 -r 192.168.100.11:443 -m -w 2
ipvsadm -a -t 10.0.0.1:443 -r 192.168.100.12:443 -m -w 1
ipvsadm -a -t 10.0.0.1:443 -r 192.168.100.13:443 -m -w 1

# 지속성 엔트리 확인
ipvsadm -Lcn
# PERSIST  10.0.0.1:443  192.168.100.11:443   300   CIP.CIP.CIP.CIP

# 지속성 + 서브넷 단위 (/24 서브넷의 모든 클라이언트를 같은 RS로)
ipvsadm -A -t 10.0.0.1:443 -s wlc -p 300 -M 255.255.255.0

# ip_vs_persist_conn_out() — 지속성 엔트리 생성 경로
# ip_vs_in() → 신규 연결 → ip_vs_conn_new() → ip_vs_persist_conn_out() 호출
# → persist_template 생성 → 해시 테이블에 (CIP, VIP) 키로 저장

세션 동기화 프로토콜 심화

IPVS 세션 동기화는 Active(Master) Director의 연결 상태를 Backup Director에 실시간으로 전송하여, Active 장애 발생 시 기존 세션이 끊기지 않도록 합니다. ip_vs_sync.c에 구현되어 있으며, 멀티캐스트 또는 유니캐스트로 동기화 패킷을 전송합니다.

동기화 프로토콜 v1 vs v2

v2 프로토콜(커널 3.14+)은 IPv6 지원과 확장 속성(PE 데이터, 타임아웃 등)을 추가했습니다.

/* net/netfilter/ipvs/ip_vs_sync.c */

/* v1 동기화 메시지 구조 (IPv4 전용) */
struct ip_vs_sync_conn_v0 {
    __u8        reserved;
    __u8        protocol;       /* TCP/UDP */
    __be16      cport;          /* 클라이언트 포트 */
    __be16      vport;          /* VIP 포트 */
    __be16      dport;          /* RS 포트 */
    __be32      fwmark;         /* FW 마크 */
    __be32      timeout;        /* 타임아웃 (초) */
    __be32      caddr;          /* 클라이언트 IPv4 */
    __be32      vaddr;          /* VIP IPv4 */
    __be32      daddr;          /* RS IPv4 */
    __be16      flags;          /* IP_VS_CONN_F_* */
    __be16      state;          /* 연결 상태 */
};

/* v2 동기화 메시지 헤더 */
struct ip_vs_sync_mesg {
    __u8        reserved;
    __u8        syncid;         /* 동기화 그룹 ID (0-255) */
    __be16      size;           /* 전체 메시지 크기 */
    __u8        nr_conns;       /* 포함된 연결 수 (최대 255) */
    __u8        version;        /* 프로토콜 버전 (1 or 2) */
    __u16       spare;
    /* 뒤에 ip_vs_sync_v4/v6 구조체가 nr_conns개 이어짐 */
};

/* v2: IPv6 + 확장 속성 지원 */
struct ip_vs_sync_v6 {
    __u8        type;           /* STYPE_F_INET6 */
    __u8        protocol;
    __be16      ver_size;       /* 버전 + 크기 */
    __be32      flags;
    __be16      state;
    __be16      cport, vport, dport;
    __be32      fwmark;
    __be32      timeout;
    struct in6_addr caddr, vaddr, daddr;  /* IPv6 주소 */
    /* 확장 속성: PE 데이터, 세션 지속성 정보 등 */
};

동기화 데몬 설정

# ── Master Director 설정 ──
# syncid=1, eth1 인터페이스로 멀티캐스트 전송
ipvsadm --start-daemon master --syncid 1 --mcast-interface eth1

# ── Backup Director 설정 ──
ipvsadm --start-daemon backup --syncid 1 --mcast-interface eth1

# 기본 멀티캐스트 그룹: 224.0.0.81 (IPv4), ff02::12 (IPv6)
# 기본 포트: UDP 8848

# 유니캐스트 동기화 (방화벽 환경, 커널 4.5+)
ipvsadm --start-daemon master --syncid 1 \
    --mcast-interface eth1 \
    --sync-maxlen 1500 \
    --mcast-port 8848 \
    --mcast-ttl 1

# 동기화 상태 확인
ipvsadm --list-daemon

# 동기화 통계
cat /proc/net/ip_vs_stats | grep -i sync

동기화 데몬 내부 스레드

/* net/netfilter/ipvs/ip_vs_sync.c — 스레드 구조 */

/*
 * Master: sync_thread_master()
 *   - ipvs->sync_queue에서 동기화할 연결 꺼내기
 *   - ip_vs_sync_conn() 호출: 연결 → 동기화 패킷 변환
 *   - 멀티캐스트 소켓으로 전송
 *   - 배치 처리: sync_send_mesg_maxlen(기본 1460 bytes)까지 묶어 전송
 *
 * Backup: sync_thread_backup()
 *   - 멀티캐스트 소켓에서 동기화 패킷 수신
 *   - ip_vs_process_message() 호출
 *   - 각 ip_vs_sync_conn_v0 / v2 파싱
 *   - ip_vs_conn_new() 또는 ip_vs_conn_update() 호출
 */

/* 동기화 전송 임계값 튜닝 */
// 연결 상태가 변할 때마다 즉시 전송하지 않고 임계값 초과 시 전송
// net.ipv4.vs.sync_threshold = "sync_threshold sync_period"
sysctl -w net.ipv4.vs.sync_threshold="3 50"
// → activeconns가 3 이상이거나, 50개 이상 연결이 대기 중이면 전송

// 동기화 패킷 최대 크기 조정 (기본 1460 bytes = MTU - IP/UDP 헤더)
// 크게 설정하면 묶음 전송으로 효율 향상
sysctl -w net.ipv4.vs.sync_sock_size=65536

세션 동기화 없이 HA 구성: ECMP + Maglev Hash

세션 동기화는 멀티캐스트 트래픽과 CPU 오버헤드를 수반합니다. 초고성능 환경에서는 ECMP(Equal-Cost Multi-Path) + Maglev 일관성 해시를 조합하여 상태 동기화 없이도 동일 클라이언트를 항상 동일 Director로 유도합니다. Director 장애 시 해당 Director의 연결만 끊기고 나머지는 유지됩니다.

# ECMP + Maglev Hash (BGP ECMP 환경)
# 1. 각 Director가 동일 VIP를 BGP로 광고
# 2. 업스트림 라우터가 ECMP로 분산 (Maglev hash 선택 시 동일 5-tuple → 동일 Director)
# 3. Director별로 독립적 IPVS 운영 (세션 동기화 불필요)

# IPVS Maglev Hash 스케줄러 (커널 4.18+, ip_vs_mh 모듈)
modprobe ip_vs_mh
ipvsadm -A -t 10.0.0.1:80 -s mh
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.11:80 -g -w 1
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.12:80 -g -w 1

# mh 알고리즘: RS 추가/제거 시 기존 연결의 95% 이상 유지
# (일관성 해시: 버킷 테이블 크기 기본 2503)

Primary + Backup IPVS HA 클러스터 완전 예시

# ── Primary Director (10.0.0.2): keepalived.conf ──
vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    virtual_ipaddress {
        10.0.0.1/24 dev eth0
    }
    notify_master "/usr/local/bin/ipvs-start-master.sh"
    notify_backup "/usr/local/bin/ipvs-start-backup.sh"
}

virtual_server 10.0.0.1 80 {
    delay_loop 2
    lb_algo wlc
    lb_kind DR
    persistence_timeout 300
    protocol TCP
    real_server 192.168.100.11 80 {
        weight 1
        TCP_CHECK { connect_timeout 3 connect_port 80 }
    }
    real_server 192.168.100.12 80 {
        weight 1
        TCP_CHECK { connect_timeout 3 connect_port 80 }
    }
}

# ── /usr/local/bin/ipvs-start-master.sh ──
#!/bin/bash
ipvsadm --start-daemon master --syncid 1 --mcast-interface eth1

# ── /usr/local/bin/ipvs-start-backup.sh ──
#!/bin/bash
ipvsadm --stop-daemon master 2>/dev/null
ipvsadm --start-daemon backup --syncid 1 --mcast-interface eth1

XDP/eBPF 기반 IPVS 가속

전통적인 IPVS는 Netfilter 훅에서 동작하여 커널 네트워크 스택을 부분적으로 거칩니다. XDP(eXpress Data Path)를 활용하면 NIC 드라이버 수준에서 패킷을 처리하여 IPVS 대비 2~5배 높은 처리량을 달성할 수 있습니다. Facebook의 Katran이 대표적인 XDP 기반 L4 로드밸런서 구현입니다.

Katran/XDP L4 로드밸런서 아키텍처 Client src: Client IP XDP Director (Katran) NIC XDP hook (XDP_TX) BPF 프로그램 (katran.bpf.c) VIP 룩업 → Maglev Hash vip_map BPF Hash 맵 lru_hash_map 세션 테이블 IPIP 캡슐화 (XDP_TX) bpf_fib_lookup() 라우팅 Backend 1 IPIP 디캡슐화 Backend 2 VIP로 직접 응답(DSR) Backend 3 VIP로 직접 응답(DSR) Client DSR 응답 IPVS vs Katran/XDP 성능 비교 항목 IPVS (DR) Katran (XDP) 비고 처리량 (10GbE) ~8 Mpps ~14 Mpps XDP가 약 75% 높음 latency (p99) ~50 μs ~20 μs conntrack 미우회 conntrack 지원 지원 미지원 XDP는 LRU 맵으로 대체

Facebook Katran 아키텍처

Katran은 Facebook이 공개한 오픈소스 XDP L4 로드밸런서입니다. IPVS와 달리 Netfilter를 전혀 거치지 않고 NIC 드라이버 레벨의 XDP 훅에서 패킷을 처리합니다. 세션 상태는 BPF LRU 해시 맵에 저장하며, 백엔드 선택은 Maglev 일관성 해시를 사용합니다.

/* Katran XDP BPF 프로그램 핵심 로직 (C pseudocode) */

/* BPF 맵 정의 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key,   struct vip_definition);   /* VIP + 포트 + 프로토콜 */
    __type(value, struct vip_meta);         /* 백엔드 정보 + Maglev 테이블 인덱스 */
    __uint(max_entries, MAX_VIPS);
} vip_map SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __type(key,   struct flow_key);         /* 5-tuple */
    __type(value, struct real_pos_lru);     /* 선택된 백엔드 인덱스 */
    __uint(max_entries, DEFAULT_LRU_SIZE);  /* 기본 8M 엔트리 */
} lru_map SEC(".maps");

SEC("xdp")
int xdp_katran_lb(struct xdp_md *ctx) {
    struct flow_key fkey = {};
    struct real_def *real;

    /* 1. 패킷 파싱: IP 헤더 + 5-tuple 추출 */
    parse_packet(ctx, &fkey);

    /* 2. VIP 맵 룩업 */
    struct vip_meta *vip = bpf_map_lookup_elem(&vip_map, &fkey.vip);
    if (!vip)
        return XDP_PASS;  /* 비-VIP 패킷은 커널 스택으로 */

    /* 3. LRU 세션 테이블 룩업 (기존 세션 재사용) */
    struct real_pos_lru *pos = bpf_map_lookup_elem(&lru_map, &fkey);
    if (!pos) {
        /* 4. 신규 세션: Maglev hash로 백엔드 선택 */
        __u32 real_pos = maglev_hash(&fkey, vip->reals_cnt);
        /* 5. LRU에 저장 */
        bpf_map_update_elem(&lru_map, &fkey, &real_pos, BPF_ANY);
        pos = &real_pos;
    }

    /* 6. 백엔드 정보 조회 */
    real = get_real_by_pos(*pos);

    /* 7. IPIP 캡슐화: 원본 패킷을 백엔드 IP로 캡슐화 */
    encapsulate_ipip(ctx, real->dst);

    /* 8. XDP_TX: NIC에서 직접 전송 (커널 스택 우회) */
    return XDP_TX;
}

Google Maglev 일관성 해시

Maglev 해시는 Google이 개발한 일관성 해시 알고리즘으로, 백엔드 추가/제거 시 기존 세션의 95% 이상을 동일 백엔드로 유지합니다. ECMP 환경에서 여러 Director가 동일한 선택을 하도록 결정론적(deterministic) 해시를 사용합니다.

/* Maglev 해시 테이블 구성 (백엔드 3개, 테이블 크기 7) */
/* 각 백엔드는 순환 시퀀스로 테이블 슬롯을 채움 */

백엔드 B0: preference = [3, 0, 4, 1, 5, 2, 6]
백엔드 B1: preference = [0, 2, 4, 6, 1, 3, 5]
백엔드 B2: preference = [3, 4, 5, 6, 0, 1, 2]

/* 순서대로 빈 슬롯 채우기 */
슬롯 3 → B0 (B0 첫 번째 선택)
슬롯 0 → B1 (B1 첫 번째 선택)
슬롯 4 → B2 (B2 첫 번째 선택)
슬롯 1 → B0 (슬롯 3 점유, 다음 선택 0 → B1 점유, 4 → B2 점유, 1 → 빈 슬롯)
슬롯 2 → B1
슬롯 5 → B2
슬롯 6 → B0

/* 최종 테이블: [B1, B0, B1, B0, B2, B2, B0] */
/* 패킷 해시 % 7 → 테이블 인덱스 → 백엔드 선택 */

/* ip_vs_mh 커널 구현 (net/netfilter/ipvs/ip_vs_mh.c) */
// 테이블 크기: 기본 2503 (소수), 변경: ipvsadm --mh-port 명령
ipvsadm -A -t 10.0.0.1:80 -s mh --mh-port 2503
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.11:80 -g -w 1
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.12:80 -g -w 1
ipvsadm -a -t 10.0.0.1:80 -r 192.168.100.13:80 -g -w 1

XDP L4 LB 한계

항목 IPVS XDP/Katran
conntrack 지원 지원 (CONFIG_IP_VS_NFCT) 미지원 (LRU 맵으로 대체)
ALG (FTP/SIP) 모듈로 지원 BPF 파서 직접 구현 필요
완전한 NAT 지원 제한적 (주로 IPIP/DSR)
NIC 호환성 모든 NIC XDP Native 지원 NIC 필요
디버깅 도구 ipvsadm, procfs, conntrack bpftool, bpftrace, XDP 통계
운영 복잡도 낮음 높음 (BPF 프로그램 관리 필요)

UDP/QUIC 로드밸런싱

IPVS는 TCP와 달리 UDP 로드밸런싱에서 추가적인 고려사항이 있습니다. UDP는 비연결 지향 프로토콜이므로 IPVS는 타임아웃 기반으로만 세션을 관리합니다.

UDP 처리 특수성

/* net/netfilter/ipvs/ip_vs_proto_udp.c */

/* UDP 연결 상태 머신 (매우 단순) */
static const struct ip_vs_timeout_table udp_timeouts[] = {
    [IP_VS_UDP_S_NORMAL]    = { "UDP", 300 },  /* 기본 300초 */
    [IP_VS_UDP_S_LAST]      = { "UDP_LAST", 2 },
};

/* UDP 타임아웃은 TCP보다 훨씬 짧아야 함 */
// 기본값 300초는 DNS 쿼리(수십ms)에 과도하게 길다

# UDP 타임아웃 조정
sysctl -w net.ipv4.vs.udp_timeout=30      # 30초로 단축
sysctl -w net.ipv4.vs.udp_timeout_established=30

# UDP 세션 없이 로드밸런싱하는 경우 (상태 추적 불필요)
# --persistence 옵션 없으면 UDP 패킷마다 스케줄러 재호출
ipvsadm -A -u 10.0.0.1:53 -s rr
ipvsadm -a -u 10.0.0.1:53 -r 192.168.100.11:53 -m -w 1
ipvsadm -a -u 10.0.0.1:53 -r 192.168.100.12:53 -m -w 1

QUIC(UDP 443) 로드밸런싱 과제

QUIC 프로토콜은 UDP 443 포트를 사용하며, 클라이언트가 IP를 변경해도 Connection ID를 통해 세션을 유지하는 특성이 있습니다. 클라이언트 IP 기반 세션 지속성만으로는 IP 변경 시나리오(모바일 핸드오버, NAT 변경)를 처리할 수 없습니다.

/* QUIC Connection ID 기반 로드밸런싱 BPF 프로그램 패턴 */

/* QUIC 헤더 파싱: Long Header에서 Connection ID 추출 */
struct quic_long_header {
    __u8    first_byte;     /* 비트 7: 1 = Long Header */
    __be32  version;
    __u8    dcil;           /* Destination Connection ID 길이 */
    /* dcil 바이트의 Destination Connection ID */
    /* ... */
};

/* XDP BPF: QUIC Connection ID → 백엔드 선택 */
SEC("xdp")
int xdp_quic_lb(struct xdp_md *ctx) {
    __u8 *conn_id = extract_quic_conn_id(ctx);
    if (!conn_id)
        return XDP_PASS;

    /* Connection ID의 특정 바이트에 서버 ID 인코딩 (QUIC LB 초안) */
    /* RFC 9000 + draft-ietf-quic-load-balancers 참고 */
    __u8 server_id = conn_id[1];  /* 서버 ID 바이트 */

    return forward_to_backend(ctx, server_id);
}

# 현실적 대안: IPVS persistence + 충분히 긴 타임아웃
ipvsadm -A -u 10.0.0.1:443 -s sh -p 3600
# sh(Source Hash) = 클라이언트 IP 해시 → 같은 클라이언트는 항상 같은 서버로

DNS UDP 로드밸런싱

# DNS UDP(53) + TCP(53) 동시 로드밸런싱
# UDP DNS: 짧은 타임아웃 + rr 알고리즘
ipvsadm -A -u 10.0.0.1:53 -s rr
ipvsadm -a -u 10.0.0.1:53 -r 192.168.100.11:53 -m -w 1
ipvsadm -a -u 10.0.0.1:53 -r 192.168.100.12:53 -m -w 1

# TCP DNS: 지속성 필요 없음 (보통 1회성 쿼리)
ipvsadm -A -t 10.0.0.1:53 -s rr
ipvsadm -a -t 10.0.0.1:53 -r 192.168.100.11:53 -m -w 1
ipvsadm -a -t 10.0.0.1:53 -r 192.168.100.12:53 -m -w 1

# DNS 헬스체크 (keepalived)
real_server 192.168.100.11 53 {
    weight 1
    DNS_CHECK {
        type A
        name healthcheck.example.com
        # 응답이 있으면 RS 정상으로 판단
    }
}

SIP UDP 로드밸런싱

# SIP(5060/UDP) 로드밸런싱: ip_vs_pe_sip 모듈 사용
modprobe ip_vs_pe_sip

# SIP Call-ID 기반 지속성 (같은 통화는 항상 같은 SIP 서버로)
ipvsadm -A -u 10.0.0.1:5060 -s rr -p 3600 --pe sip

ipvsadm -a -u 10.0.0.1:5060 -r 192.168.100.11:5060 -m -w 1
ipvsadm -a -u 10.0.0.1:5060 -r 192.168.100.12:5060 -m -w 1

# RTCP(5061)도 같은 SIP 서버로: FWM 활용
iptables -t mangle -A PREROUTING -d 10.0.0.1 -p udp --dport 5060 -j MARK --set-mark 10
iptables -t mangle -A PREROUTING -d 10.0.0.1 -p udp --dport 5061 -j MARK --set-mark 10
ipvsadm -A -f 10 -s rr -p 3600 --pe sip
ipvsadm -a -f 10 -r 192.168.100.11:0 -m -w 1
ipvsadm -a -f 10 -r 192.168.100.12:0 -m -w 1

게임 서버 UDP 로드밸런싱

# 게임 서버: 짧은 지연시간 + 세션 지속성 필수
# 클라이언트가 방에 입장하면 같은 게임 서버 유지

# 전략 1: sh(Source Hash) 알고리즘 — IP 기반 고정 배정
ipvsadm -A -u 10.0.0.1:7777 -s sh
ipvsadm -a -u 10.0.0.1:7777 -r 192.168.100.11:7777 -m -w 1
ipvsadm -a -u 10.0.0.1:7777 -r 192.168.100.12:7777 -m -w 1

# 전략 2: DR 모드 + persistence (응답 지연 최소화)
# 게임 서버가 같은 L2 서브넷에 있는 경우
ipvsadm -A -u 10.0.0.1:7777 -s wlc -p 1800
ipvsadm -a -u 10.0.0.1:7777 -r 10.0.0.11:7777 -g -w 1
ipvsadm -a -u 10.0.0.1:7777 -r 10.0.0.12:7777 -g -w 1

# DR 모드 RS 설정 (UDP도 동일)
# lo에 VIP 추가 + arp_ignore/arp_announce 설정 필수

Kubernetes/컨테이너 환경 IPVS

Kubernetes kube-proxy는 서비스 트래픽을 처리하는 두 가지 주요 모드를 제공합니다: iptables 모드와 IPVS 모드. 서비스 수가 많을수록 IPVS 모드의 성능 우위가 뚜렷해집니다.

Kubernetes IPVS kube-proxy 서비스 처리 Pod / Client ClusterIP 접속 External NodePort 접속 IPVS Virtual Services (kube-proxy가 자동 생성) ClusterIP: 10.96.0.1:443 TCP wlc → Pod1 10.244.0.2:8443 → Pod2 10.244.1.3:8443 NodePort: 0.0.0.0:32443 TCP wlc → Pod1 10.244.0.2:8443 → Pod2 10.244.1.3:8443 kube-ipvs0 dummy interface ClusterIP 주소 바인딩 (ARP 응답) iptables KUBE-MARK-MASQ 외부→NodePort SNAT 처리 Pod1 (Node A) 10.244.0.2:8443 Pod2 (Node B) 10.244.1.3:8443 Pod3 (Node C) 10.244.2.4:8443 K8s 서비스 수: 100개 → iptables vs IPVS 차이 미미 | 1,000개 → IPVS 2배 빠름 | 10,000개 → IPVS 10배+ 빠름 (O(1) vs O(n) 룩업) iptables 규칙 수: 서비스당 ~10개 → 10,000 서비스 = 100,000 규칙 (선형 탐색)

kube-proxy IPVS 모드 활성화

# kube-proxy ConfigMap 설정 (kubeadm 클러스터)
kubectl edit configmap kube-proxy -n kube-system

# 다음 설정 추가/변경:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  scheduler: "wlc"          # 스케줄링 알고리즘
  syncPeriod: 30s           # IPVS 규칙 동기화 주기
  minSyncPeriod: 5s         # 최소 동기화 주기
  strictARP: false          # true: kube-ipvs0 ARP 응답 억제

# kube-proxy DaemonSet 재시작
kubectl rollout restart daemonset kube-proxy -n kube-system

# IPVS 모드 확인
kubectl logs -n kube-system -l k8s-app=kube-proxy | grep "Using ipvs"

# 노드에서 IPVS 규칙 확인
ipvsadm -Ln
# TCP  10.96.0.1:443 wlc persistent 10800
#   -> 10.244.0.2:8443              Masq    1      0          0
#   -> 10.244.1.3:8443              Masq    1      0          0

kube-proxy가 생성하는 IPVS 규칙 패턴

# ClusterIP 서비스 → IPVS Virtual Service
# kubectl get svc my-service → ClusterIP: 10.96.100.1, Port: 80
ipvsadm -Ln | grep -A3 "10.96.100.1"
# TCP  10.96.100.1:80 rr
#   -> 10.244.0.5:80                Masq    1      0          0
#   -> 10.244.1.6:80                Masq    1      0          0

# NodePort 서비스 → 모든 노드 IP + NodePort
ipvsadm -Ln | grep -A3 "32080"
# TCP  192.168.1.10:32080 rr       ← 노드 IP
# TCP  127.0.0.1:32080 rr          ← loopback
# TCP  10.244.0.1:32080 rr         ← Pod CIDR 게이트웨이

# sessionAffinity: ClientIP → IPVS persistence (-p 10800초)
# kubectl patch svc my-svc -p '{"spec":{"sessionAffinity":"ClientIP"}}'
ipvsadm -Ln | grep "10.96.100.1"
# TCP  10.96.100.1:80 rr persistent 10800

kube-proxy IPVS vs Cilium kube-proxy replacement 비교

항목 kube-proxy IPVS Cilium kube-proxy replacement
구현 IPVS + iptables (MASQ) eBPF (XDP + TC hook)
연결 추적 conntrack 사용 eBPF 맵 (conntrack 없이)
NodePort 처리 IPVS + SNAT XDP 레벨 (NIC에서 직접)
서비스 룩업 O(1) (IPVS 해시) O(1) (BPF 해시 맵)
10,000 서비스 지연 ~50 μs ~10 μs
네트워크 정책 별도 도구 필요 eBPF 네트워크 정책 내장
설치 복잡도 낮음 (기본 포함) 높음 (CNI 교체 필요)
관찰 가능성 ipvsadm, conntrack Hubble (L7까지 가시화)

컨테이너 오버레이 네트워크에서 FULLNAT 필요성

Kubernetes에서 오버레이 네트워크(VXLAN, GENEVE)를 사용할 때, kube-proxy IPVS NAT 모드는 Pod IP로 DNAT 후 오버레이로 전송합니다. 이 경우 RS(Pod)는 패킷의 소스 IP가 클라이언트 IP 대신 노드 IP로 보이는 문제가 발생합니다. FULLNAT 또는 SNAT를 통해 일관된 소스 IP를 보장해야 합니다.

# K8s IPVS NAT 모드에서 SNAT 동작 확인
# 외부 클라이언트(203.0.113.1) → NodePort(192.168.1.10:32080) → Pod(10.244.1.6:80)

# 노드에서 iptables POSTROUTING 규칙 확인
iptables -t nat -L POSTROUTING -n | grep KUBE-POSTROUTING
# KUBE-POSTROUTING ... 0x4000/0x4000 MASQUERADE

# IPVS + iptables 협력 흐름:
# 1. 패킷 수신: src=203.0.113.1, dst=192.168.1.10:32080
# 2. IPVS: dst → 10.244.1.6:80 (DNAT)
# 3. iptables KUBE-MARK-MASQ: 마크 0x4000 설정
# 4. POSTROUTING: 마크 있으면 MASQUERADE → src=10.244.X.1 (노드 Pod IP)
# 5. Pod에서 수신: 클라이언트 IP가 노드 IP로 마스킹됨

# X-Forwarded-For 또는 PROXY Protocol로 원본 IP 전달 필요

커널 소스 구조

IPVS 소스는 net/netfilter/ipvs/ 디렉터리에 집중되어 있습니다. 총 약 50개 파일, 6만 줄 규모입니다.

파일 역할
ip_vs_core.c 핵심 패킷 처리, Netfilter 훅 등록, ip_vs_in() / ip_vs_out()
ip_vs_conn.c 연결 해시 테이블, ip_vs_conn_new() / ip_vs_conn_get() / ip_vs_conn_put()
ip_vs_ctl.c netlink 제어 인터페이스, ipvsadm 통신, sysctl 등록
ip_vs_sched.c 스케줄러 등록/해제 API, ip_vs_scheduler_bind()
ip_vs_rr.c Round Robin 스케줄러
ip_vs_wlc.c Weighted Least Connection 스케줄러
ip_vs_lblc.c Locality-Based Least Connection 스케줄러 (캐시 테이블 포함)
ip_vs_nat.c NAT 모드 패킷 변환, ip_vs_nat_xmit()
ip_vs_tunnel.c TUN 모드 IP-in-IP 캡슐화, ip_vs_tunnel_xmit()
ip_vs_xmit.c DR 모드 MAC 변환 및 전송, ip_vs_dr_xmit()
ip_vs_nfct.c conntrack 연동, ip_vs_nfct_bind_dest()
ip_vs_sync.c 멀티캐스트 연결 상태 동기화 (Active/Standby Director)
ip_vs_proto_tcp.c TCP 특화 처리, 상태 기계, 포트 변환
ip_vs_proto_udp.c UDP 특화 처리, 타임아웃 관리
ip_vs_est.c 통계 추정기 (연결 속도, 바이트 레이트)
include/net/ip_vs.h 전체 자료구조 정의 (ip_vs_conn, ip_vs_service, ip_vs_dest 등)

커널 빌드 설정

# .config 관련 옵션
CONFIG_IP_VS=m                    # IPVS 코어
CONFIG_IP_VS_IPV6=y               # IPv6 지원
CONFIG_IP_VS_DEBUG=n              # 디버그 (성능 저하)
CONFIG_IP_VS_NFCT=y               # conntrack 연동
CONFIG_IP_VS_PROTO_TCP=y
CONFIG_IP_VS_PROTO_UDP=y
CONFIG_IP_VS_PROTO_SCTP=y

# 스케줄러
CONFIG_IP_VS_RR=m
CONFIG_IP_VS_WRR=m
CONFIG_IP_VS_LC=m
CONFIG_IP_VS_WLC=m
CONFIG_IP_VS_LBLC=m
CONFIG_IP_VS_LBLCR=m
CONFIG_IP_VS_DH=m
CONFIG_IP_VS_SH=m
CONFIG_IP_VS_SED=m
CONFIG_IP_VS_NQ=m

진단 및 모니터링

IPVS 문제 진단은 /proc 인터페이스, ipvsadm 통계, conntrack, 그리고 eBPF/ftrace를 활용합니다.

procfs 인터페이스

# ── /proc 인터페이스 ──

# 현재 IPVS 서비스 및 RS 목록
cat /proc/net/ip_vs

# 현재 활성 연결 (5-tuple 전체)
cat /proc/net/ip_vs_conn

# IPVS 통계 (패킷/바이트/연결 수)
cat /proc/net/ip_vs_stats

# 상세 통계 (CPU별)
cat /proc/net/ip_vs_stats_percpu

ipvsadm 모니터링

# 실시간 통계 (1초 갱신)
watch -n1 'ipvsadm -Ln --stats'

# 연결 속도 (CPS, 초당 새 연결)
ipvsadm -Ln --rate

# 특정 서비스만
ipvsadm -Ln -t 10.0.0.1:80 --stats

# 현재 연결 수 (활성 + 비활성)
ipvsadm -Lcn | wc -l

# RS별 연결 분포 확인
ipvsadm -Ln | awk '/->/{print $2, $5}'

conntrack 진단

# IPVS 관련 conntrack 항목 확인
conntrack -L -p tcp --dport 80 | head -20

# 연결 수 집계
conntrack -C

# 연결 테이블 포화 확인 (drops > 0 이면 문제)
conntrack -S | grep drop

# 실시간 이벤트 추적
conntrack -E -p tcp --dport 80

eBPF/ftrace 진단

# bpftrace: ip_vs_in 함수 진입 추적
bpftrace -e '
kprobe:ip_vs_in {
    @calls = count();
}
interval:s:1 {
    print(@calls);
    clear(@calls);
}'

# ftrace: IPVS 함수 추적
echo 'ip_vs_*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe

# perf: IPVS 관련 심볼 샘플링
perf top -p $(pgrep -x keepalived) --kallsyms=/proc/kallsyms

# 연결 해시 충돌 확인 (해시 테이블 부족 신호)
bpftrace -e '
kprobe:ip_vs_conn_get {
    @gets = count();
}
kretprobe:ip_vs_conn_get {
    if (retval == 0) @misses = count();
}
interval:s:5 {
    print(@gets); print(@misses);
    clear(@gets); clear(@misses);
}'

자주 발생하는 문제와 해결책

증상 원인 해결책
RS로 연결이 안 됨 (NAT) RS의 기본 게이트웨이가 Director가 아님 ip route add default via DIRECTOR_IP
DR 모드에서 응답 없음 RS의 ARP 억제 미설정 또는 VIP 미설정 arp_ignore=1, arp_announce=2, lo에 VIP 추가
연결이 갑자기 끊김 conntrack 테이블 포화 (nf_conntrack: table full) nf_conntrack_max 증가, 타임아웃 단축
특정 RS에만 연결 집중 conn_reuse_mode=0으로 TIME_WAIT 연결 재사용 sysctl net.ipv4.vs.conn_reuse_mode=1
keepalived 재시작 후 연결 끊김 IPVS 연결 상태 동기화 미설정 ipvsadm --start-daemon master/backup 설정
SYN 패킷이 RS에 도달하지 않음 rp_filter가 소스 IP를 차단 sysctl net.ipv4.conf.all.rp_filter=0

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