이슈
팀장님과 같은 버스를 타고 지하철에서 갈라졌는데, 서비스 장애 모니터링 슬랙 채널에서 알림이 쏟아지기 시작했다.

핸드폰으로 영향 범위(Blast Radius)를 파악했다. 불행 중 다행으로 장애 도메인은 웹 서버 티어(Tier)와 해당 DB 인스턴스에 국한되었고, Authoritative DNS 와 기타 서비스들은 정상 응답을 하고 있었다. 장애 인지까지 걸린 MTTD(Mean Time To Detect)는 약 3~5분이었다.
자취방 도착 후 곧바로 노트북을 열고 VPN을 연결하여 복구 작업을 시작했다. MTTR(Mean Time To Recover) 약 20분 만에 모든 서비스 복구를 완료했다. 이후 1시간 동안은 서비스 안정성 모니터링 및 Docker Swarm 워크로드 재배치(Redistribution) 작업을 진행했다.
이번에는 IDC 온프레미스 베어메탈 서버 위에서 Docker Swarm으로 운영 중인 인프라에서 발생한 연쇄 장애를 단계별로 트러블슈팅을 기록해봤다.
용어 정리
- Blast Radius (영향 범위): 장애가 실제로 영향을 미치는 시스템의 범위. 좁을수록 복구가 쉽다.
- MTTD (Mean Time To Detect): 장애가 발생한 시점부터 인지하는 데까지 걸린 평균 시간. 모니터링 시스템의 성능 지표로 쓰인다.
- MTTR (Mean Time To Recover): 장애 인지 후 서비스가 완전히 복구되기까지 걸린 평균 시간. 운영팀의 대응 역량을 나타내는 핵심 SRE 지표다.
- Authoritative DNS: 특정 도메인에 대한 최종적인 IP 주소 원본 데이터를 보유하고 응답하는 권한 있는 네임서버.
- 온프레미스 베어메탈 (On-Premises Bare Metal): AWS/GCP 같은 퍼블릭 클라우드의 가상머신(VM)이 아닌, 직접 OS를 설치해 제어하는 물리 서버. 자유도와 가성비가 높지만 하단 인프라 관리를 직접 해야 한다.
전체 장애 흐름 한눈에 보기

인프라 구성 개요
본격적인 트러블슈팅에 앞서, 해당 서버의 네트워크 및 오케스트레이션 환경을 간략히 정리한다.

용어 정리
- 사설 L2 세그먼트 (Private L2 Segment): 공인 인터넷과 격리된 내부 전용 네트워크. 서버 간 통신(동기화, DB 복제, Swarm heartbeat 등)을 외부 노출 없이 처리한다. 별도의 물리 NIC(Network Interface Card)를 통해 구성되어 있다.
- Docker Swarm: Docker 내장 컨테이너 오케스트레이터. k8s보다 운영 복잡도가 낮아 소규모 베어메탈 환경에서 가성비와 효율성이 좋다.
1단계: dockerd 데몬 크래시 — overlay2 그래프드라이버 마운트 실패
증상
SSH 접속 후 가장 먼저 도커 데몬 프로세스 상태를 조회했는데 실행에 실패하고 있는 상태였다.
$ systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled)
Active: failed (Result: exit-code)
Process: ... ExecStart=/usr/bin/dockerd -H fd:// (code=exited, status=1/FAILURE)
용어 정리
- systemd: 현대 리눅스 배포판의 PID 1 프로세스. 서비스의 시작/종료/재시작 정책을 관리한다.
failed상태는 설정된 재시작 횟수를 모두 소진하고 완전히 죽었다는 뜻이다. - dockerd: Docker 데몬 프로세스. 컨테이너 런타임, 이미지 레이어 관리, 네트워크 네임스페이스 등을 총괄하는 백그라운드 서비스다.
원인 확인
journalctl 로그는 원인 파악이 불편해 데몬을 직접 포그라운드 모드(foreground mode)로 실행해 스택 트레이스를 확인했다.
$ sudo dockerd
# ...
ERRO[0000] failed to start daemon: error initializing graphdriver:
driver not supported: overlay2
진짜 문제는 overlay2 스토리지 드라이버 초기화 실패에 있었다. Docker가 레이어 마운트 드라이버 자체를 지원하지 않는다고 반환하는 상황이었다.
용어 정리
- 그래프드라이버 / overlay2 (Graph Driver / overlay2): Docker가 컨테이너 레이어(이미지 + 쓰기 레이어)를 호스트 파일시스템에 마운트하는 방식. 최신 리눅스 커널의 OverlayFS를 사용하는데, 이 모듈이 커널에서 로드되지 않으면 Docker는 아예 실행되지 못한다.
원인 분석
overlayfs 모듈이 왜 없는지 부팅된 커널과 로그를 분석한 결과, 범인은 OS 자동 업데이트(Unattended-upgrades)로 인한 커널 버전 이슈였다. 서버 재부팅 시점과 맞물려 OS 코어 엔진(커널)은 자동 업데이트를 통해 최신 버전으로 올라갔으나, 그에 맞는 서브 모듈 패키지들이 제대로 로드되지 못하면서 '엔진과 모듈의 버전 불일치'가 발생한 것이다.
2단계: 방화벽 서브시스템 동반 마비
문제는 Docker뿐만이 아니었다. 사설망과 공인망 트래픽을 통제하는 방화벽 스크립트(/etc/rc.firewall)를 수동으로 실행했더니 아래와 같은 에러가 쏟아졌다.
$ sudo /etc/rc.firewall
ip6tables v1.8.9 (nf_tables): Protocol not supported (nft)
ip6tables v1.8.9 (nf_tables): Protocol not supported (nft)
# ......
용어 정리
- iptables / nftables: 리눅스 커널 내부의 패킷 필터링 프레임워크. 최신 리눅스는 nftables 백엔드를 기본으로 사용하며, nf_tables 커널 모듈이 로드되어 있어야 명령어 처리가 가능하다.
방화벽 스크립트 자체의 문법 오류가 아니었다. 1단계의 Docker 이슈와 동일하게, 커널 모듈 미지원(버전 불일치)으로 인해 방화벽 서브시스템까지 함께 마비된 것이다. 증상별로 우회(Workaround)하기보다는, 근본 원인인 커널 모듈 동기화를 진행하기로 결정했다.
3단계: 커널 모듈 패키지 재설치 및 동기화
우선 서버의 커널 버전을 확인했다.
$ uname -r
6.8.0-51-generic
커널 모듈 패키지 누락 확인
버전은 확인되었으나, 실제 모듈 파일이 디스크에 존재하지 않거나 로드할 수 없는 상태였다.
$ sudo modprobe overlay
modprobe: FATAL: Module overlay not found in directory /lib/modules/6.8.0-51-generic
용어 정리
- modprobe: 커널 모듈을 로드/언로드하는 명령어. linux-modules-extra: Ubuntu 커널 패키지 중 OverlayFS, 네트워크 드라이버 등 필수 확장 모듈을 담고 있는 패키지. 베어메탈에서는 이 패키지가 없으면 Docker와 방화벽이 정상 작동하지 않는다.
현재 부팅된 커널 버전에 맞춰 모듈 패키지를 명시적으로 재설치했다.
# 현재 부팅된 커널 버전에 맞는 모듈 패키지 강제 설치
$ sudo apt-get install --reinstall -y \
linux-modules-$(uname -r) \
linux-modules-extra-$(uname -r)
# 모듈 의존성 갱신 후 재부팅
$sudo depmod -a$ sudo reboot
용어 정리
- depmod: 커널 모듈 의존성 데이터베이스를 재생성하는 명령어. 새 모듈 설치 후 반드시 실행해야 modprobe가 올바른 순서로 모듈을 로드할 수 있다.
재부팅 후 Docker 데몬 및 overlay 모듈이 정상적으로 로드되는 것을 확인했다.
$ sudo systemctl start docker && systemctl status docker
● docker.service - Docker Application Container Engine
Active: active (running) # 정상 복구
$ lsmod | grep overlay
overlay 155648 0 # 커널 모듈 정상 로드
4단계: iptables 백엔드 충돌 해결 + 방화벽 스크립트 현행화
Docker는 살아났지만, 방화벽 스크립트를 재실행하자 이번엔 다른 에러가 발생했다.
증상
$ sudo /etc/rc.firewall
iptables: Bad argument '3'
원인 분석 — 두 가지 함정
첫 번째: iptables 백엔드 불일치 잔여물. 커널 복구 후 nft 백엔드가 활성화되었는데, 이전에 legacy 백엔드로 작성되어 메모리에 남아있던 잔존 룰셋(Stale Ruleset)이 충돌을 일으켰다. 두 번째: Docker 체인 구조 변경. 최신 Docker 버전에 맞춰 DOCKER-ISOLATION-STAGE-1/2 체인이 폐기되고 DOCKER-USER 체인으로 재설계되었으나, 기존 스크립트 내부의 grep 파싱 로직이 구형 체인을 찾고 있어 파라미터 에러를 유발했다.
용어 정리
- DOCKER-USER 체인: 외부 트래픽이 컨테이너로 진입하기 전에 관리자가 방화벽 규칙을 적용할 수 있도록 Docker가 공식적으로 제공하는 진입점(Hook)이다.
해결 — 클린슬레이트 초기화 + 스크립트 리팩토링
모든 방화벽 룰을 모두 초기화하고 다시 설정했다.
# 모든 테이블의 체인/룰 완전 플러시 (Clean Slate)
$sudo iptables -F && sudo iptables -X$ sudo iptables -t nat -F && sudo iptables -t nat -X
$sudo iptables -t mangle -F && sudo iptables -t mangle -X$ sudo ip6tables -F && sudo ip6tables -X
이후, 구형 체인 파싱 코드를 버리고 Docker 권장 방식인 DOCKER-USER 기반으로 트래픽 제어 로직을 재작성했다.
# 레거시 방식 (Docker 업데이트 시 동작 보장 안 됨)
# DOCKER_CHAIN=$(iptables -L FORWARD | grep -o 'DOCKER-ISOLATION-STAGE-[0-9]')
# iptables -I $DOCKER_CHAIN ...
# 모던 방식 (DOCKER-USER 체인 직접 사용)
# 기본적으로 모든 외부 인바운드 차단
iptables -I DOCKER-USER -s 0.0.0.0/0 -j DROP
# 사설 세그먼트(192.168.0.0/24) 노드 간 통신 허용
iptables -I DOCKER-USER -s 192.168.0.0/24 -j ACCEPT
5단계: 소실된 사설 L2 인터페이스 복구 (Netplan)
이제 다 끝났나 싶었는데, ip link show를 확인하니 사설 세그먼트용 NIC(enp3s0f1np1)가 DOWN 상태였고 IP도 없었다.
$ ip addr show enp3s0f1np1
3: enp3s0f1np1: <BROADCAST,MULTICAST> mtu 9000 qdisc noop state DOWN
# IP 할당 안 됨
원인
과거에 사설망을 세팅할 때 영구 설정 파일(Netplan)을 작성해두었던걸로 보이는데 파일이 보이지 않았다. ip addr add 같은 임시 명령어(Ephemeral Configuration)로만 IP를 밀어 넣고 설치했을 가능성을 두었다면 재부팅했을 때 기술 부채로 함께 터진 것 같다.
해결 — Netplan 퍼시스턴트 설정
정상 운영 중인 web01 노드를 참고하여, IP 충돌이 없는 주소를 할당하는 Netplan YAML 파일을 작성했다.
# /etc/netplan/60-vrack.yaml
network:
version: 2
renderer: networkd
ethernets:
enp3s0f1np1:
addresses:
- 192.168.0.3/24 # 사설 세그먼트 고정 IP
routes:
- to: 192.168.0.0/24
via: 192.168.0.254 # 사설 세그먼트 게이트웨이
mtu: 9000 # 점보 프레임 (서버 간 내부 통신용)
# 설정 적용 및 L3 라우팅 도달성 검증
$ sudo netplan apply
$ ping -c 3 192.168.0.1
64 bytes from 192.168.0.1: icmp_seq=0 ttl=64 time=0.4 ms # 통신 성공
6단계: Docker Swarm 파티션 복구 및 라벨 재부여
사설 네트워크가 복구되면서, 단절되었던 Swarm 클러스터를 다시 이어붙일 차례였다.

사설망 단절로 네트워크 파티션(Network Partition)이 발생하여 web03 노드가 클러스터에서 이탈(Unreachable)한 상태였다.
Swarm 노드 강제 퇴출 및 재가입
노드를 깔끔하게 클러스터에서 뺀 뒤, 다시 조인(Join)시켰다.
# [Manager 노드] 비정상 노드 강제 퇴출
$ docker node rm --force web03
# [web03 노드] 기존 Swarm 참여 정보 초기화 후 재가입
$docker swarm leave --force$ docker swarm join --token SWMTKN-1-xxxx 192.168.0.1:2377
$ docker node ls # web03 Ready 상태 확인
플레이스먼트 라벨(Label) 복구
노드가 재가입하면 Node ID가 새로 발급되기 때문에, 기존에 부여했던 메타데이터(라벨)가 모두 초기화된다. 이 때문에 letsencrypt-proxy 같은 전역(Global) 서비스들이 web03에 스케줄링되지 않고 있었다.
# web03에 누락된 라벨 일괄 복구
$ docker node update \
--label-add db-slave=true \
--label-add general=true \
--label-add letsencrypt-proxy=true \
web03
# 서비스 복제본 수 정상화(2/2) 확인
$ docker service ls
ID NAME MODE REPLICAS IMAGE
xxxxxxxx letsencrypt-proxy global 2/2 ... # ok
사후 분석 (Post-Mortem): 재부팅 한 방에 도미노가 된 이유

이번 장애는 여러 계층의 기술 부채가 "재부팅"이라는 트리거를 만나 연쇄 폭발을 일으킨 전형적인 도미노 장애였다.
- 커널 계층 (Kernel Layer): Ubuntu의
unattended-upgrades가 작동해 커널 엔진은 최신으로 업데이트되었으나 모듈은 동기화되지 않는 버전 불일치 발생. 이로 인해 재부팅 직후 Docker와 방화벽이 기동 실패함. - 네트워크 계층 (Network Layer): 사설망 설정이 Netplan에 영구 기록되지 않고 메모리(Ephemeral)에만 떠 있었음. 재부팅과 동시에 사설망 단절.
- 오케스트레이션 계층 (Orchestration Layer): 사설망 단절 → Swarm 노드 이탈 → 복구를 위한 재가입 → 노드 ID 변경에 따른 라벨 증발.
이후 해야할 운영 체크리스트
이번 장애를 계기로 시스템 신뢰성을 높이기 위해 수립한 액션 아이템들이다.
1. 커널 및 패키지 관리
unattended-upgrades정책을 재검토하고, 커널 업데이트 시linux-modules-extra동기화 보장 스크립트 추가.- 커널 업그레이드는 반드시 스테이징 환경에서 방화벽/Docker 기동 테스트 후 프로덕션 적용.
2. 네트워크 영구화 (Persistence)
- 모든 NIC 설정은 Netplan YAML(또는 /etc/network/interfaces) 파일에 선언적으로 기록.
- 임시
ip명령어는 단순 디버깅 용도로만 제한하며, 적용 후 반드시reboot테스트 수행.
3. 방화벽 및 스크립트 관리
rc.local기반의 방화벽 스크립트를systemd서비스 유닛으로 전환하여 부팅 시퀀스와 의존성(네트워크 로드 후 실행 등) 보장.- 레거시 iptables 체인 파싱 코드 제거 및 공식 권장 Hook(
DOCKER-USER) 전면 적용.
4. Swarm 노드 메타데이터 (IaC)
docker node update수동 텍스트 타이핑 근절. 노드 라벨 부여 작업을 Ansible Playbook으로 코드화(IaC)하여 장애 발생 시 명령어 한 줄로 멱등성 있게 복구 가능하도록 개선.
결론: 온프로미스 환경에서는 시스템의 모든 책임을 져야한다. 자동업데이트, 하드웨어 결함 문제 등, 시스템 인프라에서 수동 조작(Manual Intervention)과 임시방편(Workaround)은 언젠가 다시 재발할 수 있는 문제들이다.
모든 설정은 코드(IaC)와 파일로 남겨야 한다.
'실무 경험 > 실무 개발 & 협업' 카테고리의 다른 글
| [블록체인] - 솔라나 메인넷 노드, RPC 트러블슈팅 (0) | 2026.04.01 |
|---|---|
| [트러블슈팅] - 유지보수가 중단된 클라이언트와의 TLS 핸드셰이크 (0) | 2026.02.02 |
| 휴먼에러를 방지하는 방법 1 - git add -p(git add partial (or patch) (0) | 2025.04.24 |
| [블록체인] - Integer overflow 와 underflow (0) | 2025.04.21 |
| 2022 - 휴가만 쓰면 발생하는 우마카세가 삼겹살로 바뀌던 날: 크리스마스 전전날의 악몽 (TLS 이슈) (0) | 2024.02.29 |