- 이기적
- 오라클
- python
- 코딩테스트 python
- 부스트코스
- unsub
- 기초다지기
- AI 플랫폼을 활용한 데이터 분석
- 인공지능기초다지기
- 네이버부스트캠프
- 데이터베이스
- 빅데이터 분석 기반 에너지 운영 관리자 양성 및 취업과정
- DB
- 이것이 취업을 위한 코딩테스트다 with 파이썬
- 정보처리기사
- r
- 파이썬
- 빅데이터분석기사
- boostcourse
- Ai
- Oracle
- [멀티잇]데이터 시각화&분석 취업캠프(Python)
- 프로그래머스
- Django
- boostcoures
- SQL
- 코딩테스트
- 빅분기
- PY4E
- 데이터 분석 기반 에너지 운영 관리자 양성 및 취업과정
- Today
- Total
매일공부
[장애 대응 기록] Synology NAS 공유폴더 접속 불가 사태 — 원인 분석과 재발 방지 (2026.07.30) 본문
요약
- 증상: 점심시간 직후(7/29 12:51, 7/30 13:19) 전 직원 공유폴더(SMB) 접속 불가. 운영 웹(prod)과 SSH는 "정상"으로 확인됐지만 실제로는 새 연결 자체가 간헐적으로 거부됨
- 조치: 서버 재부팅으로 응급 복구, Docker 컨테이너별 메모리 제한 설정, 상시 감시(watchdog) 스크립트 도입
- 근본 원인: 워커 타임아웃은 이후 로그 분석으로 확정(Gunicorn sync 워커의 idle keep-alive 오판 문제, gthread로 전환해 해결). 다만 실제 신고 증상(특정 볼륨의 특정 폴더 접속 불가)의 진짜 원인은 재부팅으로 증거가 소실되어 끝내 미궁으로 남음
- 가장 큰 교훈: 사고 당시 진단 순서와 명령어가 뒤죽박죽이었음. 이 글은 다음에 같은 일이 생겼을 때 재부팅하기 전에 순서대로 확인할 체크리스트로 쓰기 위해 정리함
1. 타임라인
시각 사건
| 7/29 12:51 | 직원 접속 불가 신고 → 서버 재부팅으로 해결 (원인 미상) |
| 7/30 12:37:48 | prod 컨테이너 마지막 정상 요청 로그 |
| 7/30 12:40:54 | prod Gunicorn 워커 WORKER TIMEOUT → SIGKILL (Perhaps out of memory?) |
| 7/30 12:41~44 | 리소스 모니터에 CPU/메모리/네트워크/디스크 스파이크 관측 |
| 7/30 12:47 | dev 컨테이너 수동 종료 (문제 의심되어 조치, 결과적으로 무관했음) |
| 7/30 13:19 | 직원 "공유폴더 접속 안 됨" 신고 — 이게 진짜 증상 |
| 7/30 13:2x~ | SSH 연결도 간헐적으로 거부(refused)되기 시작. 이미 접속된 세션은 유지, 새 접속만 실패 |
| 7/30 ~14:00 | 서버 재부팅으로 응급 복구 |
| 7/30 14:46 | 메모리 제한 설정 + watchdog 스크립트 가동 완료 |
2. 진단 과정에서 확인한 것들 (배제된 원인 포함)
같은 시행착오를 반복하지 않기 위해, 이번에 조사했다가 "무관함"으로 결론 낸 것들을 먼저 기록해둔다.
❌ OOM Killer (커널 레벨)
sudo dmesg -T | grep -i "killed process"
→ 결과 없음. 커널이 강제로 프로세스를 죽인 흔적은 없었음.
❌ 예약된 백업/스케줄 작업
sudo cat /var/log/synobackup.log
sudo cat /var/log/synoscheduler.log
→ 사고 시각(12:41~44)대에 스케줄러 로그 없음. 정기 백업과 무관.
❌ HA 페일오버 문제
sudo synohactl status # 명령어 자체가 없음 (DSM GUI 방식 HA가 아니라 DRBD+Corosync 직접 구성)
cat /proc/drbd # cs:Connected ro:Primary/Secondary ds:UpToDate/UpToDate → 완전 정상
→ DRBD 동기화 정상, split-brain 없음. HA 인프라 자체는 건강했음.
❌ 백업 회선(LTE) flapping
sudo grep -c "if_link_down_hook_event.*ppp201" /var/log/messages # 6회
sudo grep "ppp201" /var/log/messages | grep "12:4" # 결과 없음
→ 오늘 6번 끊긴 건 사실이지만 사고 시각과 안 겹침. USB LTE 백업 회선(wvdial, usbmodem)이라 평소에도 간헐적으로 발생하는 것으로 추정.
❌ Auto Block (IP 차단)
DSM 제어판 → 보안 → 자동 차단 목록 확인 → 차단 목록에 없었고, 오히려 내부 서브넷을 허용 목록에 추가함.
❌ Storage Pool 용량 부족
Storage Manager 확인 결과 두 볼륨 모두 여유 있음. "Warning" 표시는 RAID 0/1의 씬 프로비저닝(thin provisioning) 특성상 스토리지 풀 레벨에서 뜨는 만성 경고였을 뿐, 실제 사고와는 무관. 드라이브 상태도 전부 Healthy.
❌ SMB 서비스 자체 장애
ps aux | grep smbd | grep -v grep | wc -l # 55개 프로세스 정상 가동
sudo smbstatus # 여러 직원 세션 정상 유지 중이었음
→ smbd 데몬은 살아있었고 이미 붙어있던 사용자 세션도 정상 유지됨. 서비스 자체가 죽은 게 아니라 "새 연결"만 문제였다.
🟡 dev 컨테이너 종료 (12:47) — 무관, 우연히 겹침
사고 발생(12:40~44)보다 늦은 시각에 조치한 것이라 인과관계상 원인일 수 없음. 다만 선제적으로 종료한 것.
✅ prod 컨테이너 WORKER TIMEOUT — 이후 로그로 확정된 원인
[CRITICAL] WORKER TIMEOUT (pid:32)
[ERROR] Worker (pid:32) was sent SIGKILL! Perhaps out of memory?
당시엔 "유력하지만 미확정"으로 남겨뒀는데, 이후 로그를 다시 들여다보면서 확정됐다. 근거:
12:37:48 — 마지막 access log (정적 파일 304, 별다른 무거운 요청 없음)
12:40:54 — [CRITICAL] WORKER TIMEOUT (pid:32)
12:40:55 — Worker was sent SIGKILL! Perhaps out of memory?
타임아웃 직전 access log에 느린 요청이나 대용량 처리 흔적이 전혀 없었다. 정적 파일 응답 이후 바로 조용해졌다가, 정확히 timeout 설정값(120초) 근처에서 죽었다. 이건 뷰 로직이 느려서가 아니라 sync 워커의 구조적 한계였다.
원인 메커니즘: sync 워커는 클라이언트 소켓을 accept한 뒤 다음 요청(keep-alive)을 기다리는 동안 블로킹 recv 상태에 들어가면, 그 사이 arbiter(마스터 프로세스)에게 heartbeat(notify())를 못 보낸다. 브라우저가 탭을 열어둔 채 keep-alive 연결만 유지하고 새 요청을 안 보내면, 워커는 계속 블로킹 상태로 있다가 timeout(120초)을 넘게 heartbeat가 없으면 arbiter가 "행 걸렸다"고 판단해 SIGKILL로 죽인다. 실제로는 멈춘 게 아니라 단순히 idle 연결을 기다리던 것뿐인데도 죽는 것.
Gunicorn이 SIGKILL 시 항상 찍는 정형 문구인 "Perhaps out of memory?"에 낚여서 처음엔 메모리 문제로 의심했지만, 실제로는 idle keep-alive 연결 때문에 워커가 오판당해 죽은 것이었다.
해결책 — gthread 워커로 전환: gthread 워커는 이벤트 루프(selector) 기반으로 여러 연결을 동시에 감시하면서 주기적으로 heartbeat를 보낼 수 있어서, 특정 연결이 idle 상태여도 워커 전체가 "무응답"으로 오판되지 않는다. 이건 Gunicorn 공식 문서에서도 언급하는 sync 워커의 대표적인 keep-alive 이슈이고, gthread가 표준적인 해결책이다.
worker_class = "sync" → worker_class = "gthread", threads = 4로 변경.
추가로 같이 조정한 것:
- workers와 threads를 곱하면 총 동시 처리량이 되므로(예: 3×4=12), 컨테이너 리소스에 맞춰 workers 수를 재산정해야 한다
- timeout = 120은 그대로 둬도 된다 — idle 문제였지 실제 느린 요청 문제가 아니었으므로
- 필요하면 graceful_timeout도 함께 검토
다만 워커 3개 중 1개만 3분간 재시작된 것만으로는 "전 직원 접속 불가"까지 설명하긴 어렵다 — 이건 여전히 배경 노이즈에 가깝고, 아래 "진짜 신고 증상"과는 별개의 문제로 남는다.
⚠️ 진짜 신고 증상(공유폴더 접속 불가)은 결국 원인 미확정
가장 뼈아픈 부분. 재부팅을 먼저 해버려서 그 순간의 볼륨/폴더 상태를 증거로 남기지 못함. 다음에는 반드시 아래 체크리스트의 "재부팅 전 증거 수집"부터 실행할 것.
3. 다음에 같은 일이 생기면 — 진단 체크리스트 (순서대로)
0단계. 증상부터 정확히 특정하기
"서버 접속 안 됨"은 실제로 아래 중 무엇인지 먼저 구분한다. 이번 사고에서 가장 큰 시행착오는 이걸 뒤늦게 물었다는 점이었다.
- [ ] 웹(운영 사이트)이 안 열리는가?
- [ ] SSH가 안 되는가?
- [ ] 파일 탐색기에서 공유폴더(SMB) 접속이 안 되는가? → 어느 볼륨/어느 폴더인지 구체적으로
- [ ] 전체 직원이 다 안 되는가, 일부만 그런가?
1단계. 클라이언트 vs 서버 구분
# Windows 클라이언트에서
ping [NAS IP]
Test-NetConnection -ComputerName [NAS IP] -Port 445 # SMB
Test-NetConnection -ComputerName [NAS IP] -Port 22 # SSH
- ping 성공 + 포트 열림(TcpTestSucceeded: True) → 서버는 살아있음, 서비스/인증 레벨 문제
- ping도 실패 → 네트워크/물리 문제, 아래는 스킵하고 물리 확인으로
2단계. (SMB 문제라면) 재부팅 전에 반드시 증거부터 수집
이번 사고에서 놓친 부분. 재부팅하면 증거가 사라지니 재부팅 버튼 누르기 전에 아래부터 실행:
# 그 폴더가 실제로 파일시스템 레벨에서 열리는지 (에러 메시지 종류가 중요)
ls -la "/volume2/문제의폴더명" 2>&1
# → Permission denied / Input-output error / No such file or directory 중 무엇인지로 원인 갈림
# 마운트 상태
mount | grep volume2
# 그 순간 smbd 로그 (다른 창에서 재현 시도하면서 tail -f)
sudo tail -f /var/log/samba/log.smbd
# 권한/소유권
stat "/volume2/문제의폴더명"
getfacl "/volume2/문제의폴더명"
# dmesg 최근 에러
sudo dmesg -T | tail -50 | grep -iE "volume2|drbd1|error"
3단계. 리소스 상태 확인
free -h
docker stats --no-stream
top -bn1 | head -20
여유 메모리(available)가 500MB 이하면 메모리 압박 의심.
4단계. prod 컨테이너 로그 확인
sudo docker logs <prod-container-name> --since 1h 2>&1 | grep -iE "sigkill|timeout|memory|killed"
5단계. Storage Manager (DSM 웹) 확인
DSM 웹 관리 화면 → Storage Manager → 볼륨별 상태, 경고 문구 확인.
"Warning: running low on space"는 RAID 0/1 씬 프로비저닝 만성 경고일 수 있으니 실제 free 용량(TB)도 같이 볼 것
6단계. HA 상태 확인 (DRBD+Corosync 구성인 경우)
cat /proc/drbd # cs:Connected ro:Primary/Secondary ds:UpToDate/UpToDate 여야 정상
또는 DSM 웹 → High Availability Manager 화면에서 Heartbeat 상태, 스토리지 경고 확인.
7단계. 그래도 원인 불명 + 급하면 재부팅
증거 수집 후에도 급하면 재부팅. 단, HA(DRBD) 구성이므로 가능하면 DSM 웹 정상 재시작을 쓰고, 물리 강제 종료는 최후 수단으로.
4. 오늘 적용한 재발 방지 조치
(1) Docker 컨테이너별 메모리 제한
제한된 메모리에 컨테이너 여러 개(prod, dev, DB, 기타 서비스)가 무제한으로 경쟁하던 게 근본적 리스크였음.
docker update --memory=3g --memory-swap=4g <prod-container-name>
docker update --memory=1g --memory-swap=1.5g <dev-container-name>
docker update --memory=1g --memory-swap=1.5g <db-container-name>
→ 하나가 문제를 일으켜도 그 컨테이너만 재시작되고, 시스템 전체가 마비되는 걸 막는 게 목적.
(2) 상시 감시(watchdog) 스크립트 도입
/volume1/<내부보안경로>/synology/server_watchdog.sh
- 5분마다 메모리/컨테이너 상태를 summary.log에 가볍게 기록
- 아래 조건 중 하나라도 걸리면 상세 스냅샷을 incident.log에 자동 기록:
- 여유 메모리 500MB 미만
- 최근 5분 내 prod WORKER TIMEOUT 발생
- 특정 볼륨 접근성 자체가 5초 이상 응답 없음 (timeout 5 ls /volume2/)
- nohup으로 백그라운드 실행, SSH 세션 끊겨도 유지됨
#!/bin/bash
# server_watchdog.sh
umask 022 # root로 돌릴 경우 다른 계정도 로그 읽을 수 있게
LOG_DIR="/volume1/<내부경로>/synology/logs"
mkdir -p "$LOG_DIR"
SUMMARY_LOG="$LOG_DIR/summary.log"
INCIDENT_LOG="$LOG_DIR/incident.log"
while true; do
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
MEM_INFO=$(free -m | awk '/^Mem:/{print $3"MB used /"$2"MB total, "$7"MB available"}')
PROD_MEM=$(docker stats <prod-container-name> --no-stream --format "{{.MemUsage}}" 2>/dev/null)
echo "$TIMESTAMP | Memory: $MEM_INFO | Docker (prod): $PROD_MEM" >> "$SUMMARY_LOG"
AVAILABLE_MB=$(free -m | awk '/^Mem:/{print $7}')
TIMEOUT_NOW=$(docker logs <prod-container-name> --since 300s 2>&1 | grep -c "WORKER TIMEOUT")
if [ "$AVAILABLE_MB" -lt 500 ] || [ "$TIMEOUT_NOW" -gt 0 ]; then
{
echo "########################################"
echo "### INCIDENT DETECTED: $TIMESTAMP"
echo "### Trigger: available_mem=${AVAILABLE_MB}MB, timeouts_5min=${TIMEOUT_NOW}"
echo "########################################"
echo "--- free -h ---"; free -h
echo "--- docker stats ---"; docker stats --no-stream
echo "--- prod recent logs ---"; docker logs <prod-container-name> --since 300s 2>&1 | tail -50
echo "--- volume2 check ---"; mount | grep volume2
echo "--- dmesg recent ---"; dmesg -T | tail -30
echo ""
} >> "$INCIDENT_LOG"
fi
VOLUME2_TEST=$(timeout 5 ls "/volume2/" 2>&1)
VOLUME2_STATUS=$?
if [ "$VOLUME2_STATUS" -ne 0 ]; then
{
echo "########################################"
echo "### VOLUME2 ACCESS FAILURE: $TIMESTAMP"
echo "### Error: $VOLUME2_TEST"
echo "########################################"
mount | grep volume2
df -h /volume2
sudo tail -50 /var/log/samba/log.smbd
dmesg -T | tail -30
echo ""
} >> "$INCIDENT_LOG"
fi
sleep 300
done
실행 확인:
ps aux | grep server_watchdog | grep -v grep
cat "<로그경로>/summary.log"
5. 남은 숙제
- [ ] DSM 작업 스케줄러에 watchdog 부팅 시 자동 실행 등록 (root 계정, umask 022 포함)
- [ ] 다음 재발 시 incident.log 우선 확인 → 특히 볼륨 접근성 실패 항목이 뜨는지가 핵심
- [x] prod Gunicorn worker_class를 sync → gthread(threads=4)로 변경 완료 — 로그로 원인 확정 후 적용, idle keep-alive로 인한 불필요한 워커 재시작 방지
- [ ] RAID 0 볼륨은 디스크 1개 장애 시 전체 데이터 손실 구조 — 하드웨어 업그레이드/RAID 구성 변경 제안서에 이번 반복 재부팅 이력을 실증 사례로 포함
- [ ] 제한된 램에 컨테이너 4개(prod/dev/DB/기타) 상시 구동은 여유가 빠듯한 구성 → 램 증설 필요성 재확인
'IT > 작업기록' 카테고리의 다른 글
| [작업 기록] NAS 원격개발 → 로컬개발 전환 기록 (0) | 2026.08.04 |
|---|---|
| [작업 기록] NAS 호스트 메모리 진단 — VS Code Remote-SSH가 먹는 몫 (0) | 2026.08.03 |
| [NAS] SSH 접속 → 컨테이너 진입 → 안 쓰는 프로세스 정리, 명령어 모음 (0) | 2026.07.30 |
| [Linux] SSH 연결이 끊겨도 계속 도는 모니터링 — nohup / screen (0) | 2026.07.30 |
| [Git] NAS에서 GitHub 조직 저장소로 push하기 — Deploy Key로 저장소별 분리 (0) | 2026.07.30 |