IPVS L4 로드밸런싱
Linux IPVS(IP Virtual Server) 아키텍처, 스케줄링 알고리즘(rr/wrr/lc/wlc/lblc/sh/dh), 포워딩 모드(NAT/DR/TUN/FULLNAT), keepalived 고가용성, conntrack 연동, 성능 비교 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- 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) 선형 탐색입니다.
ip_vs_sync 멀티캐스트로 연결 상태를 실시간 동기화하여 페일오버 시 세션이 끊기지 않습니다.단계별 이해
- VIP와 Real Server 개념 잡기
클라이언트 → VIP(Director) → Real Server 흐름을 그림으로 익힙니다. Director가 패킷을 어떻게 전달하느냐가 포워딩 모드의 핵심입니다. - NAT 모드로 시작하기
가장 간단한 NAT 모드로 ipvsadm을 직접 실행해 패킷 흐름을 tcpdump로 확인합니다. dst IP가 Real Server로 바뀌는 과정을 눈으로 봅니다. - DR 모드 이해하기
Real Server가 VIP를 lo 인터페이스에 추가하고 ARP를 억제해야 하는 이유를 이해합니다. 응답 패킷이 Director를 거치지 않아 처리량이 2배 이상 늘어납니다. - 스케줄링 알고리즘 선택하기
균일 요청은 rr/wrr, 커넥션 유지 시간이 긴 서비스는 lc/wlc, 캐시 서버는 dh/sh를 선택합니다. - keepalived로 HA 구성하기
Active/Standby Director 두 대에 keepalived를 설치하고 VRRP와 헬스체크 설정을 적용합니다. - 모니터링 및 진단하기
ipvsadm -Ln --stats, /proc/net/ip_vs, conntrack -L로 연결 상태를 실시간 확인합니다. - 세션 동기화로 무중단 HA 구성하기
Primary Director에ipvsadm --start-daemon master --syncid 1, Backup에--start-daemon backup을 설정합니다. keepalived notify 스크립트로 역할 전환 시 자동 활성화합니다. - 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는 세 가지 주요 개념으로 구성됩니다.
- Virtual Service (가상 서비스): 클라이언트가 접속하는 VIP:Port:Protocol 조합.
ip_vs_service구조체로 표현됩니다. - Real Server (실제 서버): 실제 요청을 처리하는 백엔드 서버.
ip_vs_dest구조체로 표현됩니다. - Connection (연결): 클라이언트 ↔ VIP 연결을 추적하는
ip_vs_conn구조체. 해시 테이블로 관리됩니다.
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 패킷 처리 흐름은 다음 순서로 진행됩니다.
- NIC →
ip_rcv()→ routing → NF_INET_LOCAL_IN ip_vs_in()호출: 연결 해시 탐색 (ip_vs_conn_get())- 신규 연결이면 스케줄러 호출 → RS 선택 →
ip_vs_conn_new() - 포워딩 모드에 따라 패킷 변환 후 전달
- RS 응답 →
ip_vs_out()에서 역방향 변환
스케줄링 알고리즘
IPVS는 스케줄링 알고리즘을 커널 모듈로 분리하여 동적 로드가 가능합니다.
struct ip_vs_scheduler의 schedule() 콜백을 구현하면 됩니다.
현재 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_scheduler의 schedule() 콜백을 구현하면 커스텀 스케줄러도 모듈로 등록할 수 있습니다.
/* 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로 전달하는 네 가지 포워딩 모드를 지원합니다. 각 모드는 패킷 헤더를 어떻게 변환하느냐와 응답 경로가 어디를 통하느냐에서 차이가 납니다.
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_ignore와 arp_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.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의 고가용성을 위한 사실상의 표준 도구입니다. 두 가지 핵심 기능을 제공합니다.
- VRRP (Virtual Router Redundancy Protocol): Active/Standby Director 간 VIP 페일오버
- 헬스체크 (Health Check): RS 장애 감지 후 자동 제거/복구
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 로드밸런싱을 구현하는 방법은 여러 가지가 있습니다. 각 방식의 동작 레이어, 성능 특성, 적합한 사용 사례를 비교합니다.
| 항목 | 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 로드밸런서 구현입니다.
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 모드의 성능 우위가 뚜렷해집니다.
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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.