앞단에 클라우드 로드밸런서를 하나 더 세우는 게 부담스러운 규모의 서비스가 꽤 많다. 트래픽은 크지 않은데 웹 서버 한 대가 죽으면 서비스 전체가 멈추는 구조, 이런 곳에서 가장 자주 꺼내 드는 카드가 Keepalived다. 예전에 L4 스위치로 VIP를 잡던 걸 소프트웨어로 대신한다고 보면 된다. 이 글에서는 Nginx 두 대에 VIP 하나를 걸어 한 대가 죽으면 다른 대로 자동 절체되도록 만드는 순서를 정리한다.
결론부터 말하자면, Keepalived를 두 노드에 설치하고 vrrp_instance로 MASTER/BACKUP 우선순위를 나눈 뒤, vrrp_script로 Nginx 상태를 감시하도록 묶으면 된다. MASTER의 Nginx가 죽으면 우선순위가 떨어지거나 FAULT 상태로 빠지고, VIP는 BACKUP으로 넘어간다. 클라우드 LB 비용 없이 L4 수준의 이중화가 완성된다.
1. VRRP와 VIP가 무엇인지부터
VRRP(Virtual Router Redundancy Protocol)는 여러 대의 라우터나 서버가 하나의 가상 IP를 공유하면서, 그중 한 대만 실제로 그 IP를 소유하도록 조율하는 프로토콜이다. IP 프로토콜 번호 112번을 쓰고, 같은 그룹에 속한 노드끼리 주기적으로 advertisement 패킷을 주고받는다.
VIP(Virtual IP)는 물리 서버 고유 IP와 별개로 떠다니는 가상 주소다. 평소에는 우선순위가 높은 MASTER 노드의 인터페이스에 붙어 있다가, MASTER가 죽으면 BACKUP 노드로 옮겨 붙는다. 클라이언트와 DNS는 이 VIP만 바라보므로, 뒤에서 어느 서버가 실제로 응답하는지는 알 필요가 없다.
Keepalived는 이 VRRP를 리눅스에서 구현한 데몬이다. 원래 LVS 로드밸런서의 헬스체크용으로 나왔지만, 지금은 VIP 이중화 용도로 더 많이 쓴다.
2. 구성 개요
같은 서브넷에 있는 Nginx 두 대와 VIP 한 개로 구성한다. VRRP는 같은 L2 세그먼트(동일 서브넷)에서 gratuitous ARP로 VIP의 MAC 위치를 광고하는 방식이라, 두 노드가 같은 서브넷에 있어야 정상 동작한다.
- node1 (MASTER):
10.0.0.11, priority 150 - node2 (BACKUP):
10.0.0.12, priority 100 - VIP:
10.0.0.100 - 인터페이스:
eth0(환경에 따라ens192등)
AWS 같은 퍼블릭 클라우드는 멀티캐스트와 임의 IP의 ARP 광고를 막는 경우가 많다. 이 경우는 6절에서 unicast와 notify 스크립트로 우회하는 방법을 따로 다룬다.
3. Keepalived 설치
RHEL 계열은 yum(또는 dnf)으로, Debian/Ubuntu는 apt로 설치한다. 두 노드에 동일하게 깐다.
# RHEL 9 / Rocky / AlmaLinux sudo dnf install -y keepalived # Ubuntu / Debian sudo apt update && sudo apt install -y keepalived # 버전 확인 (버전 번호는 저장소 상태에 따라 다를 수 있음) keepalived --version Keepalived v2.2.8 (04/04,2023)
설정 파일은 /etc/keepalived/keepalived.conf 하나다. 배포판에 따라 기본 파일이 없을 수 있으니 새로 작성한다.
4. MASTER/BACKUP 설정
먼저 node1(MASTER)의 /etc/keepalived/keepalived.conf다.
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2 # 2초마다 실행
timeout 3 # 3초 넘으면 실패로 간주
fall 2 # 연속 2회 실패하면 FAULT
rise 2 # 연속 2회 성공하면 복귀
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass s3cr3tpw # 최대 8자, 두 노드 동일
}
virtual_ipaddress {
10.0.0.100/24
}
track_script {
chk_nginx
}
notify_master "/etc/keepalived/notify.sh master"
notify_backup "/etc/keepalived/notify.sh backup"
notify_fault "/etc/keepalived/notify.sh fault"
}
node2(BACKUP)는 두 줄만 다르다. state BACKUP, priority 100. 나머지 virtual_router_id, auth_pass, virtual_ipaddress는 반드시 양쪽이 같아야 한다. virtual_router_id가 다르면 서로를 같은 그룹으로 인식하지 못해 둘 다 MASTER가 된다.
# node2 에서 다른 부분만
state BACKUP
priority 100
주요 키워드를 한 줄씩 정리한다.
priority
VRRP 그룹 안에서 누가 VIP를 가질지 결정하는 숫자다(1~254). 값이 큰 쪽이 MASTER가 된다. 여기서는 150 대 100으로 벌려 뒀다.
advert_int
MASTER가 살아 있다고 알리는 advertisement 전송 간격(초). 기본 1초다. BACKUP은 이 패킷이 약 3배 주기 동안 안 오면 MASTER가 죽었다고 판단하고 VIP를 가져온다.
virtual_router_id
VRRP 그룹 식별자(0~255). 같은 네트워크에 여러 VRRP 그룹이 있으면 겹치지 않게 배정한다. 겹치면 서로 간섭한다.
authentication
auth_type PASS는 평문 비밀번호로 advertisement를 구분한다. 암호화는 아니고 오설정 방지 수준이다. 최대 8자까지만 유효하다.
5. health check 스크립트
Keepalived 기본 동작은 노드 자체가 죽거나 네트워크가 끊겼을 때만 절체한다. 서버는 살아 있는데 Nginx 프로세스만 죽은 경우는 감지하지 못한다. 그래서 vrrp_script로 Nginx 상태를 직접 확인하고, track_script로 인스턴스에 연결한다.
/etc/keepalived/check_nginx.sh를 만든다. 프로세스 존재만 보지 말고 실제 응답까지 확인하는 편이 정확하다.
#!/bin/bash
# 프로세스 확인
if ! systemctl is-active --quiet nginx; then
exit 1
fi
# 로컬 응답 확인
curl -sf -o /dev/null http://127.0.0.1/ || exit 1
exit 0
sudo chmod +x /etc/keepalived/check_nginx.sh
여기서 한 번 짚고 넘어갈 게 있다. vrrp_script에 weight를 지정하지 않으면 기본값이 0이고, 이때는 fall 횟수만큼 연속 실패하면 인스턴스가 FAULT 상태로 빠져 VIP를 완전히 내려놓는다. 그러면 BACKUP이 VIP를 확실하게 가져간다.
반대로 weight에 값을 주면 동작이 달라진다. 예를 들어 weight -20이면 스크립트 실패 시 priority가 150에서 130으로만 떨어진다. BACKUP의 100보다 여전히 높아서 절체가 안 일어난다. 이게 초보자가 자주 걸리는 함정이다. 확실하게 넘기고 싶으면 weight를 빼서 FAULT 방식으로 가거나, BACKUP priority 아래로 떨어질 만큼 충분히 큰 음수를 줘야 한다. 나는 단순하게 weight 없이 FAULT로 넘기는 쪽을 권한다.
6. 실행과 절체 확인
두 노드에서 데몬을 올린다.
sudo systemctl enable --now keepalived sudo systemctl status keepalived
MASTER에서 VIP가 붙었는지 확인한다. VIP는 별도 인터페이스로 안 보이고 eth0의 secondary 주소로 붙는다.
ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
inet 10.0.0.11/24 brd 10.0.0.255 scope global eth0
inet 10.0.0.100/24 scope global secondary eth0
BACKUP에서 같은 명령을 치면 10.0.0.100이 안 보여야 정상이다. 둘 다 보이면 split-brain이니 7절로 간다.
이제 MASTER의 Nginx를 죽여서 절체를 확인한다.
# node1 (MASTER) sudo systemctl stop nginx # node2 (BACKUP) 로그 journalctl -u keepalived -f keepalived[1234]: (VI_1) Backup received priority 0 advertisement keepalived[1234]: (VI_1) Entering MASTER STATE
node2에서 ip addr show eth0를 다시 보면 VIP가 넘어와 있다. 외부에서 curl http://10.0.0.100/를 계속 때려 보면 절체 순간 짧게 끊겼다가 다시 응답한다. 끊기는 시간은 환경에 따라 다르지만 대략 몇 초 수준이다.
node1의 Nginx를 다시 살리면 priority가 회복되면서 VIP가 되돌아온다. 이 되돌림이 싫으면 nopreempt를 쓴다(7절).
7. split-brain 방지
split-brain은 두 노드가 서로의 VRRP advertisement를 못 받아서 양쪽 다 자기가 MASTER라고 착각하는 상태다. VIP가 두 노드에 동시에 붙어 ARP가 충돌하고 연결이 널뛴다. 원인은 대부분 방화벽이 VRRP 패킷을 막거나, 멀티캐스트가 차단된 네트워크다.
방화벽에서 VRRP 허용
VRRP는 IP 프로토콜 112번을 쓴다. TCP/UDP 포트가 아니라 프로토콜 자체를 열어야 한다.
# firewalld (RHEL 계열) sudo firewall-cmd --permanent --add-protocol=vrrp sudo firewall-cmd --reload # 멀티캐스트 주소 224.0.0.18 이 기본 대상이다 # 확인: 상대 노드에서 VRRP 패킷이 잡히는지 sudo tcpdump -i eth0 proto 112 -n
tcpdump에 상대 노드발 패킷이 안 잡히면 그 구간이 막힌 것이다.
멀티캐스트가 막힌 환경은 unicast로
AWS 같은 클라우드나 일부 스위치는 멀티캐스트를 막는다. 이때는 advertisement를 상대 노드로 직접 unicast 전송하도록 바꾼다. node1 설정에 다음을 추가한다.
unicast_src_ip 10.0.0.11
unicast_peer {
10.0.0.12
}
node2는 unicast_src_ip 10.0.0.12, peer는 10.0.0.11로 반대로 적는다. unicast를 쓰면 멀티캐스트 그룹에 의존하지 않아 split-brain 위험이 크게 준다.
불필요한 되돌림 막기
MASTER가 복구될 때마다 VIP가 되돌아오면 그 순간 또 짧게 끊긴다. 장애가 잦으면 flapping이 된다. BACKUP이 MASTER를 물고 있을 때 굳이 되뺏지 않게 하려면 BACKUP 쪽 인스턴스에 nopreempt를 넣고 state를 BACKUP으로 둔다.
vrrp_instance VI_1 {
state BACKUP
nopreempt
priority 100
...
}
8. 오류와 해결책
둘 다 MASTER가 된다
virtual_router_id나 auth_pass가 노드 간에 다르면 서로를 같은 그룹으로 인식하지 못한다. 두 값을 양쪽에서 똑같이 맞춘다. 그래도 그러면 방화벽에서 프로토콜 112가 막힌 것이니 7절대로 tcpdump proto 112로 확인한다.
Nginx가 죽어도 절체가 안 된다
vrrp_script에 weight를 줬는데 그 값이 작아서 priority가 BACKUP 위에 머물러 있는 경우다. 5절대로 weight를 빼서 FAULT 방식으로 바꾼다. 스크립트가 실행되는지 자체가 의심되면 로그를 본다.
journalctl -u keepalived | grep -i script # 스크립트가 안 돌면 실행 권한(chmod +x)이나 경로를 먼저 의심한다
HAProxy에서 VIP 바인딩 실패
HAProxy를 특정 VIP에 직접 bind 10.0.0.100:80으로 묶으면, VIP가 없는 BACKUP 노드에서 기동 자체가 실패한다. 로컬에 없는 IP에도 바인딩할 수 있게 커널 옵션을 켠다.
echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee /etc/sysctl.d/99-haproxy.conf sudo sysctl -p /etc/sysctl.d/99-haproxy.conf
Nginx를 listen 80처럼 0.0.0.0으로 열면 이 옵션은 필요 없다.
클라우드에서 VIP로 트래픽이 안 온다
AWS 같은 환경은 VRRP로 VIP를 노드에 붙여도 클라우드 네트워크가 임의 IP의 ARP를 신뢰하지 않아 외부 트래픽이 도달하지 않을 수 있다. 이때는 notify_master 스크립트에서 AWS CLI로 secondary private IP나 EIP를 현재 MASTER 노드로 재할당하는 방식을 쓴다.
# notify.sh 안에서 (master 로 전환될 때) aws ec2 assign-private-ip-addresses \ --network-interface-id eni-xxxx \ --private-ip-addresses 10.0.0.100 \ --allow-reassignment
이 경우 VRRP는 어느 노드가 MASTER인지 선출하는 역할만 하고, 실제 IP 이동은 클라우드 API가 담당한다.
마무리
Keepalived는 별도 하드웨어나 클라우드 LB 없이 소프트웨어만으로 L4 이중화를 만든다. 핵심은 세 가지다. priority로 MASTER/BACKUP을 나누고, vrrp_script와 track_script로 Nginx 상태를 직접 감시하며, 방화벽에서 프로토콜 112를 열고 필요하면 unicast로 split-brain을 막는다. 같은 서브넷의 온프레미스라면 위 설정 그대로 동작하고, 클라우드라면 unicast와 notify 스크립트를 얹어 API로 IP를 옮기면 된다.