infra
Platform

모듈 맵

[Network] 통신 장애를 실시간으로 탐지하는 쉘 스크립트 작성

0 / 35 완료

펼치기
0 / 35 완료0%

네트워크 트러블슈팅 · 32 / 35

[Network] 통신 장애를 실시간으로 탐지하는 쉘 스크립트 작성

bash 스크립트로 여러 서버의 포트 통신을 자동 점검하고 이상 시 로그를 남깁니다

🚨INCIDENT ALERT
HIGH

배포 전마다 여러 서버의 DB, Redis, API 포트를 사람이 손으로 확인합니다. 어느 날 한 포트를 빼먹어 장애가 났고, 체크리스트는 있었지만 자동 증거는 없었습니다.

반복 점검은 스크립트로 남겨야 합니다. 성공과 실패를 같은 형식으로 기록하면 장애 전후 비교가 쉬워집니다.

네트워크 통신 점검 쉘 스크립트

"어젯밤에 DB 연결이 잠깐 끊겼는데 언제 어느 서버인지 모르겠다." 자동화된 점검 스크립트가 있다면 이런 문제를 사전에 감지하고 기록할 수 있습니다. 이 챕터에서는 nc와 bash를 활용해 여러 서버의 포트 통신을 자동 점검하는 스크립트를 단계적으로 완성합니다.


1. 핵심 도구와 개념

이번 챕터에서 배울 것
  • 1nc -zv -w 옵션과 종료 코드($?)를 이용한 포트 점검 원리를 이해할 수 있다
  • 2타임스탬프 로그 기록과 bash 배열 루프를 활용할 수 있다
  • 3단일 포트 점검을 다중 서버 배열 루프 점검으로 확장할 수 있다
  • 4crontab 주기 표현식으로 자동 실행을 등록할 수 있다
  • 5장애 감지 시 슬랙·이메일 알림을 연동할 수 있다
  • 6cron 환경 PATH 문제와 스크립트 실행 권한 문제를 트러블슈팅할 수 있다
실습 환경 준비
nc 설치 여부 및 경로 확인
which nc
스크립트 디렉터리 생성
mkdir -p /opt/scripts
cron 서비스 상태 확인
systemctl status crond || systemctl status cron
로그 디렉터리 확인
ls -la /var/log/netcheck.log 2>/dev/null || echo '로그 없음 (첫 실행 후 생성됨)'
💡개념

nc(netcat)와 종료 코드($?)를 이용한 포트 점검

매일 아침 출근하면 외부 API 연동 서비스가 죽어있습니다. 새벽에 외부 포트가 일시적으로 막혔다가 복구됐지만 서비스는 재시작되지 않은 것입니다. 이런 상황을 자동으로 감지하고 알람을 보내려면 포트 점검 스크립트가 필요합니다. nc의 종료 코드를 활용하면 조건 분기 없이 포트 열림/닫힘을 판별하는 스크립트를 깔끔하게 만들 수 있습니다.

nc와 종료 코드로 포트 점검 자동화 — nc -z host port는 연결 성공 시 종료 코드 0, 실패 시 0이 아닌 값을 반환. 이 $?를 if로 분기해 포트 열림/닫힘을 판별하고, 닫혔으면 알람·재시작을 트리거. cron에 걸어 주기적으로 외부 연동 포트를 자동 감시확대

네트워크 점검 스크립트의 핵심은 nc종료 코드를 활용하는 것입니다.

nc -zv -w 옵션 설명

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part6/exam_32 && cd /tmp/networking/part6/exam_32

nc -zv -w 3 192.168.1.200 3306
🔍실행 후 확인할 것
  • exit code 우선 확인echo $? 출력이 0이면 성공, 1이면 실패 — 스크립트 분기의 기준값입니다
  • 응답 시간 판단ping RTT가 100ms 이상이면 지연 의심, curl -w "%{time_total}" 결과가 3초 초과면 타임아웃 원인 조사
  • HTTP 상태코드 조합200이면 정상, 5xx이면 서버 오류(서비스 점검), 000이면 TCP 연결 자체가 실패한 것
옵션의미
-z데이터 전송 없이 연결 가능 여부만 확인 (zero I/O)
-v상세 출력 (verbose)
-w 3연결 타임아웃 3초 (wait)

종료 코드($?)로 성공/실패 판단

로컬 터미널
# 포트 점검 후 종료 코드 확인
nc -zv -w 3 192.168.1.200 3306
echo $?
# 0   → 연결 성공 (포트 열려 있음)
# 1   → 연결 실패 (포트 닫힘, 방화벽 차단, 서버 다운 등)

if문과 결합

로컬 터미널
nc -zv -w 3 192.168.1.200 3306 2>/dev/null
if [ $? -eq 0 ]; then
  echo "SUCCESS: DB 포트 정상"
else
  echo "FAIL: DB 포트 응답 없음"
fi

# 축약 표현 (더 일반적)
if nc -zv -w 3 192.168.1.200 3306 2>/dev/null; then
  echo "SUCCESS"
else
  echo "FAIL"
fi

2>/dev/null 의미

nc -v는 상세 메시지를 stderr(2)로 출력합니다. 2>/dev/null은 stderr를 /dev/null로 버려서 화면 출력을 억제합니다. 스크립트 로그에는 직접 작성한 메시지만 남도록 할 때 유용합니다.


💡개념

타임스탬프 로그와 배열 루프

포트 점검 스크립트가 실패했습니다. 그런데 언제 실패했는지 로그에 시간이 없어서 새벽 3시에 발생한 장애인지 아침 9시에 발생한 건지 알 수 없습니다. 또 점검 대상이 늘어날수록 서버 목록을 일일이 하드코딩해야 합니다. 타임스탬프 로깅과 배열 루프를 활용하면 스크립트를 실무에서 실제로 쓸 수 있는 수준으로 만들 수 있습니다.

타임스탬프 기록

로컬 터미널
# date 명령으로 현재 시각 형식 지정
date '+%Y-%m-%d %H:%M:%S'
# 출력: 2024-01-15 14:30:25

# 변수에 저장
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] 점검 시작"

# 로그 파일에 기록 (>>: 이어쓰기, >: 덮어쓰기)
echo "[$TIMESTAMP] 포트 점검 결과" >> /var/log/netcheck.log

배열 선언과 루프

로컬 터미널
# bash 배열 선언
SERVERS=("192.168.1.100" "192.168.1.200" "192.168.1.201")
PORTS=(3306 6379 8080)

# 배열 순회
for server in "${SERVERS[@]}"; do
  echo "점검 중: $server"
done

# 인덱스와 함께 순회
for i in "${!SERVERS[@]}"; do
  echo "서버 $i: ${SERVERS[$i]}"
done

# 배열 길이
echo "서버 수: ${#SERVERS[@]}"

로그 파일 관리

로컬 터미널
LOG_FILE="/var/log/netcheck.log"

# 로그 디렉터리 생성 (없으면)
mkdir -p $(dirname $LOG_FILE)

# 로그 크기 제한 (7일 이상 된 항목 삭제)
find /var/log/ -name "netcheck*.log" -mtime +7 -delete

# logrotate 설정 파일 예시
# /etc/logrotate.d/netcheck
# /var/log/netcheck.log {
#     daily
#     rotate 7
#     compress
#     missingok
#     notifempty
# }
💡개념

점검 스크립트가 한 번 돌 때 무슨 일이 일어나나 — 대상 목록부터 호출자 조치까지 5단계

netcheck.sh 한 번 실행. 로그에 OK/FAIL이 쌓이고 종료코드가 남습니다. 이 사이에 스크립트는 대상 목록을 읽고 → 각 대상을 점검하고 → 성공/실패를 판정하고 → 종료코드·리포트로 요약하고 → 호출자(cron·모니터링)가 조치하는 5단계를 밟습니다. 이 흐름을 단계로 알면 "왜 스크립트가 멈췄지", "왜 거짓 OK지", "왜 cron이 안 잡지"가 각각 다른 단계의 문제임을 알 수 있습니다. 점검이 "잘 돈다"는 것은 아래 다섯 단계가 모두 성립했다는 뜻입니다.

TEXT
[호출자]  cron: */5 * * * * netcheck.sh
   │
   ① 대상 목록 로드            (배열/파일: "host:port:desc" 순회 준비)
   │
   ② 각 대상 점검(순차)        (nc -zw3 · ping · dig · curl /health)
   │    → -w 타임아웃으로 무응답 대상에서 매달리지 않음
   │
   ③ 결과 판정                (직후 $? 로 성공/실패 · 연속 실패 카운트)
   │
   ④ 종료코드·리포트           (로그에 타임스탬프+대상+결과 · exit N)
   │
   ⑤ 호출자 조치              (cron 메일 · Slack/메일 알림 · Nagios 코드 규약)
   ▼
[운영자]  "03:47 App-Server FAIL" 로그·알림 수신

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 목록 로드점검 대상을 배열이나 파일로 읽어 순회 준비. bash 배열은 TARGETS=("h:p:desc" ...) 소괄호 구조만 정상 순회파이썬·JS식 대괄호·콤마 표기 → 문법 오류로 루프가 한 덩어리로 돌거나 아예 안 돎
② 각 점검(순차)대상마다 점검을 순차 실행. nc -zw3로 포트, ping으로 호스트, dig로 DNS, curl로 HTTP. -w 타임아웃이 핵심-w 누락 + 방화벽 DROP → 그 대상에서 수십 초 매달려 다음 점검·다음 주기와 겹침 / ICMP만 막힌 곳을 ping으로 재면 서비스는 멀쩡한데 거짓 실패
③ 결과 판정점검 직후 $?로 성공(0)·실패(비0) 판정. 오탐 줄이려 연속 N회 실패를 카운트nc와 판정 사이에 echo 등이 끼면 $?가 그 명령 코드로 덮여 오판 / nc -z OK를 서비스 정상으로 오인(L4≠L7, UDP 거짓 OK)
④ 코드·리포트타임스탬프+대상+결과를 로그에 남기고, 실패 건수를 종료코드로 반환(exit $FAIL_COUNT)상태 전이(OK↔FAIL) 무시하고 매번 알림 → 폭주 / 종료코드 규약(0·1·2) 안 지키면 Nagios·모니터링이 상태를 오해
⑤ 호출자 조치cron·모니터링이 종료코드·리포트를 받아 메일·Slack·에스컬레이션. 복구(FAIL→OK)도 알림cron PATH에 nc 없음·실행권한 없음 → 스크립트가 아예 안 돎(로그 무변화)

"점검이 제대로 돌았다"는 다섯 단계가 모두 성립한 결과입니다 — 목록을 읽었고(①), 타임아웃 안에 각 대상을 쟀고(②), $?를 정확히 잡아 판정했고(③), 규약대로 요약해(④) 호출자가 받았습니다(⑤). 고장은 층으로 갈립니다 — 로그가 아예 없으면 ⑤(cron·권한·PATH), 스크립트가 멈추면 ②(타임아웃 누락), FAIL이 거짓이면 ③($? 덮임), OK가 거짓이면 ②③(L4만 확인·UDP)입니다. 점검 스크립트의 진짜 실력은 "연결되나"가 아니라 "이 다섯 단계 중 어디서 속는가"를 아는 데서 나옵니다.

점검 스크립트 판단 — 언제 알리고 언제 침묵하나
1회 실패로 바로 알릴까아니다 — 일시적 패킷 손실로 오탐 폭주. '연속 N회(예: 3회) 실패'를 기준으로. 단발 실패는 카운트만, 연속 임계 도달 시 알림. '깜빡임은 무시, 지속은 경보'
점검 주기(cron)를 얼마로중요 서비스 1분, 일반 5분. 너무 잦으면 대상 부하·로그 폭증, 너무 길면 감지 지연. 'MTTR 목표 ÷ 2' 정도가 출발점
복구되면 어떻게장애 알림만큼 '복구 알림'도 보낸다 — 안 보내면 이미 끝난 장애로 계속 대응. 상태 전이(OK→FAIL, FAIL→OK)에서만 알림, 같은 상태 반복은 침묵
알림이 폭주한다같은 장애 반복 발송 금지 — 상태 파일로 '이미 알린 장애'를 기억(플랩 디텍션). 또는 N분 쿨다운. '한 장애=한 알림'
nc가 멈춰 점검이 정지반드시 -w 타임아웃 — 없으면 한 대가 죽었을 때 그 자리에서 스크립트가 매달려 나머지 점검도 멈춘다. '점검이 장애에 같이 죽으면 안 됨'
무엇을 로그로 남길까타임스탬프+대상+결과+소요시간을 구조적으로(logger 또는 파일). 사후 분석은 로그가 전부 — '언제부터 언제까지 얼마나'를 복원할 수 있어야

2. 기본 5줄 점검 스크립트

다중 호스트 자동 점검 루프 — SERVERS=("192.168.1.100" "192.168.1.200") 배열을 for로 순회하며 nc -z -w2로 각 포트를 시도하고, 종료코드 $?가 0이면 정상 로그만 남기고 0이 아니면 logger로 syslog에 경고를 남긴 뒤 Slack·메일 알림과 선택적 자동 조치(restart)로 이어진다. cron으로 5분마다 실행해 무인 감시하며, 배열은 소괄호 구조만 정상 순회한다확대

쉘 스크립트 작성 시 필수적인 Bash 리스트 핸들링과 logger 활용

네트워크 점검 자동화 스크립트를 작성할 때 다수의 호스트 IP를 일괄 루프로 돌리기 위해 SRE는 다음과 같은 문법적 디테일을 준수해야 합니다.

  1. 올바른 Bash 1차원 배열 선언: SERVERS=("192.168.1.100" "192.168.1.200")
    • 공백으로 항목을 구분하고 소괄호 ()로 감싸 선언해야 루프 내부에서 정상 순회합니다.
    • SERVERS=["192.168.1.100", "192.168.1.200"] (파이썬/JS 리스트 표기법) 또는 SERVERS:192.168.1.100,192.168.1.200 등은 문법 오류를 일으키며, 오직 SERVERS=("값1" "값2") 처럼 소괄호 구조만 정상 인식합니다.
  2. logger 명령어를 통한 시스템 로그 수집: 스크립트 진단 결과나 타임스탬프가 가미된 경고 메시지를 로컬 파일 외에 시스템 표준 중앙 로그 데몬에 전송할 때는 logger 명령어를 사용해 /var/log/syslog (또는 /var/log/messages)에 영구 기록시킵니다.
    로컬 터미널
    # 시스템 syslog에 실시간 네트워크 진단 경고 기록
    $ logger "SYSTEM-ALERT: Network ping check failed for database server!"
    
로컬 터미널
mkdir -p /tmp/networking/part6/scripts && cd /tmp/networking/part6/scripts
cat > targets.txt << 'EOF'
8.8.8.8 Google DNS
1.1.1.1 Cloudflare DNS
192.168.1.1 Gateway
EOF
cat > check_base.sh << 'EOF'
#!/bin/bash
# 네트워크 점검 기본 템플릿
TARGETS_FILE="${1:-targets.txt}"
LOG_FILE="network_check_$(date +%Y%m%d_%H%M%S).log"

check_host() {
    local ip="$1"
    local name="$2"
    if ping -c 1 -W 2 "$ip" &>/dev/null; then
        echo "✓ $name ($ip): reachable"
    else
        echo "✗ $name ($ip): UNREACHABLE"
    fi
}

echo "=== Network Check $(date) ===" | tee "$LOG_FILE"
while read -r ip name; do
    check_host "$ip" "$name" | tee -a "$LOG_FILE"
done < "$TARGETS_FILE"
EOF
chmod +x check_base.sh
1단계: 최소한의 포트 점검 스크립트 작성

먼저 가장 단순한 형태의 포트 점검 스크립트를 작성합니다.

로컬 터미널
#!/bin/bash
# 파일: /opt/scripts/netcheck-simple.sh
# 기본 5줄 포트 점검 스크립트

TARGET_HOST="192.168.1.200"
TARGET_PORT="3306"
LOG_FILE="/var/log/netcheck.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

nc -zv -w 3 "$TARGET_HOST" "$TARGET_PORT" 2>/dev/null \
  && echo "[$TIMESTAMP] OK   ${TARGET_HOST}:${TARGET_PORT}" >> "$LOG_FILE" \
  || echo "[$TIMESTAMP] FAIL ${TARGET_HOST}:${TARGET_PORT}" >> "$LOG_FILE"

실행 권한 부여 및 테스트

로컬 터미널
chmod +x /opt/scripts/netcheck-simple.sh

# 직접 실행
/opt/scripts/netcheck-simple.sh

# 로그 확인
cat /var/log/netcheck.log
# [2024-01-15 14:30:25] OK   192.168.1.200:3306
# [2024-01-15 14:35:25] FAIL 192.168.1.200:3306  ← 장애 발생 시

&& 와 || 연산자 이해

로컬 터미널
명령A && 명령B   # 명령A가 성공(0)일 때만 명령B 실행
명령A || 명령B   # 명령A가 실패(non-0)일 때만 명령B 실행

# nc 성공 → && 이후 OK 기록
# nc 실패 → || 이후 FAIL 기록
2단계: 다중 서버 배열 루프 점검 스크립트

여러 서버와 포트를 배열로 관리하는 확장 버전을 작성합니다.

로컬 터미널
#!/bin/bash
# 파일: /opt/scripts/netcheck.sh
# 다중 서버 포트 점검 스크립트

LOG_FILE="/var/log/netcheck.log"
TIMEOUT=3

# 점검 대상: "호스트:포트:설명" 형식
TARGETS=(
  "192.168.1.200:3306:MySQL-DB"
  "192.168.1.200:6379:Redis-Cache"
  "192.168.1.100:8080:App-Server"
  "192.168.1.100:80:Nginx"
  "10.0.0.10:5432:PostgreSQL"
)

# 로그 디렉터리 생성
mkdir -p $(dirname "$LOG_FILE")

TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
FAIL_COUNT=0

echo "[$TIMESTAMP] ===== 네트워크 점검 시작 =====" >> "$LOG_FILE"

for target in "${TARGETS[@]}"; do
  # "호스트:포트:설명" 파싱
  HOST=$(echo "$target" | cut -d: -f1)
  PORT=$(echo "$target" | cut -d: -f2)
  DESC=$(echo "$target" | cut -d: -f3)

  # 포트 점검
  if nc -zv -w "$TIMEOUT" "$HOST" "$PORT" 2>/dev/null; then
    echo "[$TIMESTAMP] OK   ${HOST}:${PORT} (${DESC})" >> "$LOG_FILE"
  else
    echo "[$TIMESTAMP] FAIL ${HOST}:${PORT} (${DESC})" >> "$LOG_FILE"
    FAIL_COUNT=$((FAIL_COUNT + 1))
  fi
done

echo "[$TIMESTAMP] ===== 점검 완료 (실패: ${FAIL_COUNT}건) =====" >> "$LOG_FILE"

# 실패가 있으면 비정상 종료 코드 반환 (cron에서 메일 발송 트리거 가능)
exit $FAIL_COUNT

실행 및 로그 확인

로컬 터미널
chmod +x /opt/scripts/netcheck.sh
/opt/scripts/netcheck.sh

# 로그 확인
tail -20 /var/log/netcheck.log
# [2024-01-15 14:30:00] ===== 네트워크 점검 시작 =====
# [2024-01-15 14:30:00] OK   192.168.1.200:3306 (MySQL-DB)
# [2024-01-15 14:30:00] OK   192.168.1.200:6379 (Redis-Cache)
# [2024-01-15 14:30:00] FAIL 192.168.1.100:8080 (App-Server)
# [2024-01-15 14:30:00] OK   192.168.1.100:80 (Nginx)
# [2024-01-15 14:30:00] FAIL 10.0.0.10:5432 (PostgreSQL)
# [2024-01-15 14:30:00] ===== 점검 완료 (실패: 2건) =====
3단계: crontab에 등록해 자동 실행

5분마다 자동으로 점검이 실행되도록 cron에 등록합니다.

로컬 터미널
# crontab 편집
crontab -e

# 다음 내용 추가:
# 5분마다 점검 스크립트 실행
*/5 * * * * /opt/scripts/netcheck.sh

# 매시간 정각에 실행
# 0 * * * * /opt/scripts/netcheck.sh

# 매일 오전 9시에 실행
# 0 9 * * * /opt/scripts/netcheck.sh

cron 표현식 이해

*/5 * * * * 명령은 5분마다 실행을 의미합니다. 다섯 자리는 각각 다음 시간 단위입니다.

위치필드범위
1번째0-59
2번째0-23
3번째1-31
4번째1-12
5번째요일0-7 (0과 7은 일요일)

*/5는 5의 배수 분마다(0, 5, 10, 15 …)를 뜻합니다.

cron 등록 후 확인

로컬 터미널
# 현재 crontab 확인
crontab -l
# */5 * * * * /opt/scripts/netcheck.sh

# cron 서비스 상태 확인
systemctl status crond   # CentOS
systemctl status cron    # Ubuntu

# cron 실행 로그 확인
grep netcheck /var/log/cron        # CentOS
grep netcheck /var/log/syslog      # Ubuntu
journalctl -u cron | grep netcheck # systemd 환경

cron 실행 환경 주의사항

로컬 터미널
# cron은 PATH가 제한되어 있어 전체 경로 사용 필수
# /etc/nc → nc 대신 /usr/bin/nc 또는 /bin/nc
which nc   # nc의 전체 경로 확인

# 스크립트 상단에 PATH 명시하거나 스크립트 내에서 절대 경로 사용
NC=/usr/bin/nc
$NC -zv -w 3 "$HOST" "$PORT"
4단계: 장애 시 알림 추가 (슬랙/이메일)

장애 감지 시 즉시 알림이 가도록 스크립트를 확장합니다.

로컬 터미널
#!/bin/bash
# 파일: /opt/scripts/netcheck-alert.sh
# 장애 감지 시 알림 포함 버전

LOG_FILE="/var/log/netcheck.log"
TIMEOUT=3
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
ALERT_EMAIL="ops-team@company.com"

TARGETS=(
  "192.168.1.200:3306:MySQL-DB"
  "192.168.1.200:6379:Redis-Cache"
  "192.168.1.100:8080:App-Server"
)

TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
HOSTNAME=$(hostname)
FAILED_SERVICES=""

for target in "${TARGETS[@]}"; do
  HOST=$(echo "$target" | cut -d: -f1)
  PORT=$(echo "$target" | cut -d: -f2)
  DESC=$(echo "$target" | cut -d: -f3)

  if nc -zv -w "$TIMEOUT" "$HOST" "$PORT" 2>/dev/null; then
    echo "[$TIMESTAMP] OK   ${HOST}:${PORT} (${DESC})" >> "$LOG_FILE"
  else
    echo "[$TIMESTAMP] FAIL ${HOST}:${PORT} (${DESC})" >> "$LOG_FILE"
    FAILED_SERVICES="${FAILED_SERVICES}\n- ${DESC} (${HOST}:${PORT})"
  fi
done

# 장애 발생 시 알림
if [ -n "$FAILED_SERVICES" ]; then
  ALERT_MSG="[$HOSTNAME] 네트워크 장애 감지 (${TIMESTAMP}):\n${FAILED_SERVICES}"

  # 슬랙 알림
  if command -v curl &>/dev/null; then
    curl -s -X POST "$SLACK_WEBHOOK" \
      -H 'Content-type: application/json' \
      --data "{\"text\":\"$ALERT_MSG\"}" > /dev/null
  fi

  # 이메일 알림 (mailx 필요)
  if command -v mail &>/dev/null; then
    echo -e "$ALERT_MSG" | mail -s "[ALERT] 네트워크 장애 감지" "$ALERT_EMAIL"
  fi
fi

3. 장애 사례 분석

증상

로컬 터미널
/opt/scripts/netcheck.sh
# [2024-01-15 14:30:00] 점검 시작
# [2024-01-15 14:30:00] OK   192.168.1.200:3306 (MySQL-DB)
# (여기서 스크립트가 수 분 동안 멈춤...)

원인 분석

-w 타임아웃 옵션 없이 nc를 사용하면, 방화벽이 패킷을 DROP하는 서버에 대해 기본 타임아웃(수십 초)만큼 대기합니다.

로컬 터미널
# 잘못된 예 — 타임아웃 없음
nc -zv 192.168.1.201 3306   # DROP 구간에서 멈춤

# 올바른 예 — 3초 타임아웃
nc -zv -w 3 192.168.1.201 3306

스크립트 실행 시간 측정

로컬 터미널
# 타임아웃 없이 5개 서버 점검 시
# → 각 DROP 서버에서 ~30초 × 5 = 150초 소요

# -w 3 설정 시
# → 각 DROP 서버에서 3초 × 5 = 15초 소요
# → 10배 빠른 점검 완료

수정 후 스크립트

로컬 터미널
# TIMEOUT 변수를 환경에 맞게 조정
TIMEOUT=3  # 내부망: 3초, 인터넷 구간: 5-10초

nc -zv -w "$TIMEOUT" "$HOST" "$PORT" 2>/dev/null

교훈: 네트워크 점검 스크립트에서 -w(타임아웃)는 선택이 아닌 필수입니다. cron에서 실행되는 스크립트가 멈추면 다음 실행 시 겹쳐서 프로세스가 쌓입니다.

증상

로컬 터미널
# crontab에 등록했는데 로그가 쌓이지 않음
crontab -l
# */5 * * * * /opt/scripts/netcheck.sh

# 5분 후에도 /var/log/netcheck.log 변화 없음

원인 확인 및 해결

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# [원인 1] 실행 권한 없음
ls -la /opt/scripts/netcheck.sh
# -rw-r--r-- 1 root root ...   ← 실행 권한 없음!

# 해결
chmod +x /opt/scripts/netcheck.sh

# [원인 2] cron 서비스 미실행
systemctl status crond
# inactive (dead)

# 해결
systemctl start crond
systemctl enable crond

# [원인 3] nc가 cron 환경 PATH에 없음
# cron은 /usr/bin:/bin 정도만 PATH에 포함
# 스크립트 첫 줄에 PATH 추가
head -3 /opt/scripts/netcheck.sh
#!/bin/bash
export PATH=/usr/bin:/bin:/usr/local/bin:$PATH
# ...

# [원인 4] cron 실행 오류 로그 확인
grep CRON /var/log/syslog | tail -20    # Ubuntu
journalctl -u crond | tail -20          # CentOS systemd

# [원인 5] cron 출력을 파일로 리다이렉션
# crontab 항목에 출력 리다이렉션 추가
# */5 * * * * /opt/scripts/netcheck.sh >> /var/log/netcheck-cron.log 2>&1

cron 디버깅 팁

로컬 터미널
# cron 환경에서 수동 테스트
env -i HOME=/root SHELL=/bin/bash /bin/bash /opt/scripts/netcheck.sh

# 이렇게 실행해서 오류가 나면 cron에서도 동일하게 오류 발생

교훈: cron 등록 후 반드시 실제로 실행되는지 로그로 확인하세요. 실행 권한, PATH, cron 서비스 상태를 순서대로 점검합니다.

심화 — 점검이 찍는 OK를 의심하라

💡개념

심화: 포트가 열렸다 ≠ 서비스가 정상 — 체크가 만드는 착시

점검 스크립트의 진짜 위험은 '멈추는 것'보다 '거짓으로 OK를 찍는 것'입니다. 멈추면 눈에 띄지만, 거짓 OK는 장애를 조용히 덮어 정작 필요한 순간에 알림이 오지 않게 합니다. nc -z 성공이 실제로 무엇을 증명하는지 한 겹 더 파고들어야 합니다.

  • L4 성공은 L7 정상을 보장하지 않는다: nc -z가 OK여도 그 위 앱이 커넥션 풀 고갈·데드락으로 요청을 못 받을 수 있습니다. 진짜 헬스체크는 실제 프로토콜 요청(HTTP GET /health, DB SELECT 1)까지 보내 응답 내용을 봐야 합니다.
  • UDP는 핸드셰이크가 없어 거짓 OK가 난다: nc -zu로 DNS(53)·NTP(123)·syslog(514) 같은 UDP 포트를 점검하면, 서버가 죽어 있어도 대개 성공으로 뜹니다 — UDP는 연결 확립 절차가 없어 ICMP port-unreachable 같은 명시적 거부가 없으면 nc가 성공으로 판정하기 때문입니다.
  • 규모가 커지면 점검이 부작용을 만든다: 수백 대를 짧은 주기로 두드리면, 대상의 연결 레이트 제한·SYN 방어·fail2ban이 점검을 공격으로 오인해 차단하거나, 점검 호스트의 로컬 임시 포트가 TIME_WAIT로 쌓여 점검 자신이 실패합니다.
  • 닫힘의 종류를 구분하라: Connection refused(RST)는 서비스만 없다는 신호로 즉시 실패하고, 무응답(DROP)은 방화벽·호스트 다운으로 타임아웃까지 대기합니다. 둘을 같은 FAIL로 뭉치면 원인 판단이 흐려집니다.

그래서 성숙한 점검은 '연결이 되나'를 넘어 '정확한 응답이 오나'까지, 프로토콜에 맞는 실제 질의로 검증합니다.

상황

TCP 포트 점검 스크립트를 확장해 DNS(53/udp)·NTP(123/udp)도 nc -zu로 점검하도록 했습니다. 그런데 실제로 named를 내려도 스크립트는 계속 OK를 찍어, 장애를 며칠 동안 놓쳤습니다.

원인

UDP는 3-way handshake가 없습니다. nc -zu는 UDP 패킷을 하나 보내고, ICMP port-unreachable 같은 명시적 거부가 돌아오지 않는 한 성공(종료 코드 0)으로 판정합니다. 서버가 죽어도 방화벽이 조용히 DROP하거나 응답이 없으면 nc는 이를 '열림'으로 착각합니다. TCP식 열림 판정 논리를 UDP에 그대로 옮긴 것이 원인입니다.

진단

로컬 터미널
# 서버를 내린 상태에서도 0을 반환하는지 확인 (거짓 OK 재현)
nc -zu 192.168.1.53 53; echo "exit=$?"
# exit=0  ← 서비스가 죽었는데도 성공

# 실제 프로토콜 질의로 대조하면 진짜 상태가 드러난다
dig +short +time=2 +tries=1 @192.168.1.53 health.example.com
# (응답 없음 → 실제로는 죽어 있음)

해결

UDP 서비스는 실제 프로토콜 질의로 점검합니다 — DNS는 dig +time=2 +tries=1 @대상의 응답 유무, NTP는 ntpdate -q 대상, 그 밖의 UDP는 애플리케이션 레벨 프로브로 응답 내용까지 검증합니다. TCP 서비스도 가능하면 L4(nc) 대신 HTTP GET /health 같은 L7 요청으로 승격해 '진짜 살아 있음'을 확인합니다.


4. 실무 현장 관점

💼
실무 맥락
현업 패턴

실무에서 이 스크립트가 필요한 순간

  1. 정기 배포/패치 후 확인: 배포 후 모든 서비스 간 연결이 정상인지 자동으로 확인하는 스모크 테스트로 활용

  2. 24시간 모니터링: 모니터링 도구(Zabbix, Prometheus 등)가 없는 소규모 환경에서 간단한 가용성 모니터링 역할

  3. 장애 보고서 근거: 장애 발생 시 정확한 시각과 영향 받은 서버를 로그 파일에서 바로 확인 가능

완성된 프로덕션 수준 스크립트

로컬 터미널
#!/bin/bash
# /opt/scripts/netcheck-production.sh
# 프로덕션 수준 네트워크 점검 스크립트

set -euo pipefail

# === 설정 ===
LOG_FILE="/var/log/netcheck.log"
LOG_MAX_DAYS=30
TIMEOUT=3
HOSTNAME=$(hostname -f)

# === 점검 대상 (환경에 맞게 수정) ===
TARGETS=(
  "db-primary.internal:3306:MySQL-Primary"
  "db-replica.internal:3306:MySQL-Replica"
  "cache.internal:6379:Redis"
  "app1.internal:8080:App-Server-1"
  "app2.internal:8080:App-Server-2"
  "lb.internal:80:Load-Balancer"
)

# === 함수 정의 ===
log() {
  local level=$1
  shift
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOG_FILE"
}

check_port() {
  local host=$1
  local port=$2
  nc -zv -w "$TIMEOUT" "$host" "$port" 2>/dev/null
  return $?
}

cleanup_old_logs() {
  find "$(dirname "$LOG_FILE")" -name "netcheck*.log" -mtime +"$LOG_MAX_DAYS" -delete 2>/dev/null
}

# === 메인 로직 ===
mkdir -p "$(dirname "$LOG_FILE")"
cleanup_old_logs

FAIL_COUNT=0
FAIL_LIST=""

log "INFO" "=== 점검 시작 (호스트: $HOSTNAME, 대상: ${#TARGETS[@]}개) ==="

for target in "${TARGETS[@]}"; do
  HOST=$(echo "$target" | cut -d: -f1)
  PORT=$(echo "$target" | cut -d: -f2)
  DESC=$(echo "$target" | cut -d: -f3)

  if check_port "$HOST" "$PORT"; then
    log "OK  " "${HOST}:${PORT} [${DESC}]"
  else
    log "FAIL" "${HOST}:${PORT} [${DESC}]"
    FAIL_COUNT=$((FAIL_COUNT + 1))
    FAIL_LIST="${FAIL_LIST} ${DESC}(${HOST}:${PORT})"
  fi
done

if [ "$FAIL_COUNT" -gt 0 ]; then
  log "WARN" "점검 완료 — 실패 ${FAIL_COUNT}건:${FAIL_LIST}"
else
  log "INFO" "점검 완료 — 전체 정상"
fi

exit "$FAIL_COUNT"

Nagios/Zabbix 등과의 연동

로컬 터미널
# Nagios 플러그인 형태로 변환
# 종료 코드 규칙: 0=OK, 1=WARNING, 2=CRITICAL, 3=UNKNOWN

if [ "$FAIL_COUNT" -eq 0 ]; then
  echo "OK: 전체 ${#TARGETS[@]}개 서버 정상"
  exit 0
elif [ "$FAIL_COUNT" -le 1 ]; then
  echo "WARNING: ${FAIL_COUNT}개 서버 응답 없음"
  exit 1
else
  echo "CRITICAL: ${FAIL_COUNT}개 서버 응답 없음"
  exit 2
fi

5. 핵심 명령어 요약

목적명령어/표현
타임아웃 포함 포트 점검nc -zv -w 3 192.168.1.200 3306
종료 코드 확인echo $? (0=성공, 비0=실패)
타임스탬프 생성$(date '+%Y-%m-%d %H:%M:%S')
로그 파일에 기록echo "메시지" >> /var/log/netcheck.log
bash 배열 선언TARGETS=("host1:port1:desc1" "host2:port2:desc2")
배열 순회for item in "${TARGETS[@]}"; do ...; done
문자열 파싱echo "$item" | cut -d: -f1
cron 5분 주기*/5 * * * * /opt/scripts/netcheck.sh
cron 등록 확인crontab -l

정리

  • nc -zv -w 3-w 3은 타임아웃 3초 설정 — 스크립트 무한 대기 방지 필수
  • $?로 nc의 성공/실패 판단 (0=성공, 1=실패)
  • 배열(TARGETS=())과 for 루프로 여러 서버를 간결하게 점검
  • $(date '+%Y-%m-%d %H:%M:%S')로 타임스탬프를 로그에 함께 기록
  • crontab */5 * * * *으로 5분마다 자동 실행
  • cron 실행 전 실행 권한, PATH, 로그 리다이렉션 확인 필수

관련 모듈로 더 깊이:

다음 모듈에서는 웹 서버와 DB 사이의 통신 장애를 단계별로 고립·진단하는 troubleshooting-isolation 방법론을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

5분마다 10개 DB 서버의 3306 포트를 자동 점검하는 스크립트를 작성했습니다. 실제 운영에서 `nc 192.168.1.200 3306` 명령을 타임아웃 옵션 없이 사용하면 어떤 문제가 발생할 수 있습니까?

Q2

네트워크 점검 스크립트에서 `nc -zw3 $host $port`를 실행하고 바로 `if [ $? -ne 0 ]`으로 분기합니다. 그런데 실제로는 연결이 잘 되는 서버인데 스크립트가 계속 FAIL을 기록합니다. 원인으로 가장 가능성 높은 것은?

Q3

crontab 항목 `*/5 * * * * /opt/netcheck.sh`의 실행 주기는?

Q4

스크립트에서 여러 서버를 배열로 저장하고 루프로 점검할 때 올바른 bash 배열 문법은?

Q5

네트워크 점검 스크립트에서 로그에 타임스탬프를 추가하는 올바른 방법은?

Q6

포트 점검 스크립트에서 ping이 아니라 nc -z(또는 nc -zw3)로 점검해야 하는 이유는?

Q7

[심화] TCP 포트 점검 스크립트를 UDP 서비스(DNS 53)로 확장하려고 nc -zu를 썼다. 이 방식이 위험한 이유는?

Q8

[심화] nc -z 점검은 성공(OK)인데 사용자는 그 서비스가 느리거나 오류가 난다고 신고한다. 점검을 어떻게 고쳐야 이 간극을 없앨 수 있나?

0 / 8 답변

🧪 실습으로 확인하기

포트는 열렸다는데 왜 안 되지? — ss/netstat/telnet으로 TCP 진단

초급

"포트 8080 열었는데요?"와 "왜 안 돼요?" 사이의 간극을 메우는 실습. ss로 바인딩 상태를 확인하고, telnet/nc으로 원격 연결을 테스트하고, iptables 방화벽을 진단하고, 바인딩 주소(0.0.0.0 vs 127.0.0.1)까지 수정하는 4단계 TCP 포트 진단 플로우를 완성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요