시스템 관리_29 고동시성 대량 트래픽 환경의 Broken pipe 및 Connection reset by peer 오류 원인 분석과 완벽 대응 가이드

 
  • 출처 / 참고: Linux man-pages (tcp(7), listen(2)), RFC 793 (Transmission Control Protocol), Nginx / Netty Performance Tuning Guide

명령어: ss, netstat, nstat, dmesg, sysctl, tcpdump
키워드: Broken pipe, Connection reset by peer, ECONNRESET, EPIPE, TCP RST, somaxconn, tcp_max_syn_backlog, ListenDrop
사용처: 대규모 동시 접속 프로모션 이벤트, API 게이트웨이 및 WAS 간 통신 시 간헐적 커넥션 단절 및 502/500 장애 분석과 튜닝


실행예제

고동시성(High Concurrency) 트래픽 인입 시 백엔드 WAS 로그에 java.io.IOException: Broken pipe가 폭증하거나, Nginx/클라이언트 로그에 (104: Connection reset by peer)가 발생하는 원인을 파악하기 위해 TCP 소켓 백로그(Backlog) 큐 오버플로우와 TCP RST 패킷 카운터를 점검합니다.

# 1. 애플리케이션 및 웹 서버 에러 로그 확인
$ tail -n 20 /opt/tomcat/logs/catalina.out
# [출력 예시 - WAS]
# org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe
# 또는
# java.io.IOException: Connection reset by peer

# [출력 예시 - Nginx 프록시 에러 로그]
# [error] 14502#14502: *982121 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 203.0.113.50, upstream: "http://127.0.0.1:8080/api/v1/orders"

# 2. 커널 백로그 큐 오버플로우로 인해 드롭된 TCP 연결 카운터 확인
$ nstat -az TcpExtListenOverflows TcpExtListenDrops
# TcpExtListenOverflows         12450              0.0
# TcpExtListenDrops             12450              0.0
# -> 카운터가 0이 아니고 지속 증가 중이라면 수신 큐(Accept Queue)가 꽉 차서 커널이 연결을 끊고 RST를 보낸 상태임

# 3. 리스닝 소켓의 현재 백로그 수용 한도(Send-Q)와 현재 대기열 크기(Recv-Q) 점검
$ ss -lnt '( sport = :80 or sport = :8080 )'
# State      Recv-Q Send-Q Local Address:Port  Peer Address:Port
# LISTEN     129    128    0.0.0.0:8080        0.0.0.0:*
# -> Recv-Q가 Send-Q(128) 한도를 초과하여 차 있는 경우 신규 SYN/ACK 연결이 거부되어 RST 발생

# 4. 현재 커널 파라미터(somaxconn, syn_backlog) 값 확인
$ sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
# net.core.somaxconn = 128 (기본값이 너무 작음)
# net.ipv4.tcp_max_syn_backlog = 512

# 5. 비정상 종료를 유발하는 TCP RST 패킷 발생 실시간 캡처
$ sudo tcpdump -nn -i any "tcp[tcpflags] & tcp-rst != 0" and port 8080

   

스크립트

현재 리눅스 서버의 시스템 레벨 TCP 수신 대기 큐(somaxconn, tcp_max_syn_backlog), 애플리케이션 소켓 버퍼 크기, 그리고 커널 레벨의 TCP RST 및 백로그 드롭 통계를 전수 점검하여 고동시성 병목을 진단하는 쉘 스크립트입니다.

#!/bin/bash
# diagnose_high_concurrency_resets.sh - Diagnose TCP Backlog drops, RST packets, and Broken pipe causes

echo "=========================================="
echo " [고동시성 TCP 연결 단절 및 백로그 진단]"
echo "=========================================="

# 1. 커널 소켓 대기열 파라미터 점검
echo -e "\n[1] 시스템 글로벌 커널 백로그 설정 점검:"
SOMAXCONN=$(sysctl -n net.core.somaxconn 2>/dev/null)
SYN_BACKLOG=$(sysctl -n net.ipv4.tcp_max_syn_backlog 2>/dev/null)
TCP_ABORT=$(sysctl -n net.ipv4.tcp_abort_on_overflow 2>/dev/null)

echo "  - net.core.somaxconn          : ${SOMAXCONN}"
echo "  - net.ipv4.tcp_max_syn_backlog : ${SYN_BACKLOG}"
echo "  - net.ipv4.tcp_abort_on_overflow: ${TCP_ABORT}"

if [ "$SOMAXCONN" -lt 1024 ]; then
    echo "  [경고] somaxconn(현재: ${SOMAXCONN})이 1024 미만으로 너무 작습니다."
    echo "  -> 대량 트래픽 급증 시 Accept Queue 오버플로우로 Connection reset 발생 위험 높음."
fi

# 2. 커널 ListenDrops 통계 분석 (nstat)
echo -e "\n[2] 커널 레벨 연결 드롭 및 거절 통계:"
if command -v nstat &>/dev/null; then
    OVERFLOWS=$(nstat -az TcpExtListenOverflows | awk 'NR>1 {print $2}')
    DROPS=$(nstat -az TcpExtListenDrops | awk 'NR>1 {print $2}')
    RESETS_SENT=$(nstat -az TcpOutRsts | awk 'NR>1 {print $2}')
    
    echo "  - Listen Overflow 횟수 : ${OVERFLOWS:-0}"
    echo "  - Listen Drop 횟수     : ${DROPS:-0}"
    echo "  - 송신된 TCP RST 수     : ${RESETS_SENT:-0}"
    
    if [ "${OVERFLOWS:-0}" -gt 0 ]; then
        echo "  [위험] 백로그 큐 초과로 유실된 연결이 감지되었습니다! (Reset의 직접 원인)"
    else
        echo "  [정상] 백로그 오버플로우가 감지되지 않았습니다."
    fi
else
    echo "  [안내] nstat 명령어가 없어 상세 통계를 건너뜁니다."
fi

# 3. 주요 웹/WAS 포트의 Accept Queue 점유율 진단
echo -e "\n[3] 주요 리스닝 포트의 큐 포화도 점검 (Recv-Q vs Send-Q):"
ss -lnt | awk 'NR>1 && $4 ~ /:(80|443|8080|9000)$/ {printf "  포트: %-15s Recv-Q(현재대기): %-5s Send-Q(최대한도): %-5s\n", $4, $2, $3}'

# 4. 파일 디스크립터(FD) 한도 여유 점검
echo -e "\n[4] 시스템 및 프로세스 파일 디스크립터 한도:"
SYS_FILE_MAX=$(sysctl -n fs.file-max)
SYS_FILE_NR=$(cat /proc/sys/fs/file-nr | awk '{print $1}')
echo "  - 전체 파일 오픈 수 / 한도: ${SYS_FILE_NR} / ${SYS_FILE_MAX}"

echo -e "\n=========================================="
echo " [진단 완료]"
echo "=========================================="

   

해설

동시 접속자가 급증할 때 발생하는 Broken pipe와 Connection reset by peer는 네트워크 계층에서 TCP RST(Reset) 패킷이 송수신될 때 애플리케이션에 전달되는 소켓 I/O 예외입니다.

1. “Broken pipe”와 “Connection reset by peer”의 원리적 차이

구분 시스템 에러 코드 발생 시점 및 원리 대표적 상황
Connection reset by peer ECONNRESET (104) 상대방이 비정상적으로 연결을 끊거나 RST를 보낸 상태에서 내가 read()를 호출할 때 발생 • 서버의 Accept Queue가 가득 차 신규 요청 거절
• 상대방 프로세스가 크래시되어 OS가 RST 회신
Broken pipe EPIPE (32) 상대방이 연결을 닫았거나 이미 RST를 수신했음에도, 이를 무시하고 내가 계속 write()(데이터 전송)를 시도할 때 발생 • 사용자가 로딩을 못 참고 브라우저를 닫았으나, WAS가 쿼리 완료 후 본문을 쓰려고 할 때 (ClientAbortException)

2. 고동시성 환경에서의 주된 병목 원인

  1. 커널 백로그(Backlog) 큐 포화 및 tcp_abort_on_overflow:
    • 리눅스는 3-Way Handshake를 처리하는 SYN Queue(미완성 연결)와 핸드셰이크가 끝난 후 애플리케이션의 accept()를 기다리는 Accept Queue(완성 연결)가 있습니다.
    • Accept Queue의 최대 크기는 min(net.core.somaxconn, 애플리케이션의 listen backlog)으로 결정됩니다.
    • 트래픽이 몰려 애플리케이션 스레드가 accept()를 제때 못 꺼내가면 큐가 차게 되며, 커널은 신규 패킷을 드롭하거나 즉시 RST를 반환(ECONNRESET)합니다.
  2. 클라이언트 측의 사전 타임아웃 종료:
    • 백엔드 처리 지연(DB 락, 롱 트랜잭션)으로 클라이언트(또는 앞단 Nginx 프록시)의 proxy_read_timeout이 먼저 만료되어 연결을 끊어버리면, 뒤늦게 처리를 마친 백엔드가 클라이언트 소켓에 응답을 쓰려다 Broken pipe를 냅니다.
  3. Keep-Alive 만료 타이밍 불일치 (Race Condition):
    • Nginx와 WAS 사이의 Keep-Alive 유지 시간 설정이 다를 때 발생합니다. WAS가 유휴 시간이 지나 먼저 소켓을 닫는 순간 Nginx가 요청을 보내면 Connection reset by peer가 발생합니다.

3. 해결 및 튜닝 가이드

1) 리눅스 커널 백로그 대역폭 확장 (/etc/sysctl.conf)

대규모 동시 연결을 수용할 수 있도록 커널 소켓 큐를 상향 조정합니다.

# /etc/sysctl.d/99-network-concurrency.conf
# 완성 연결 수신 대기 큐 크기 (기본값 128/4096 -> 65535 권장)
net.core.somaxconn = 65535

# SYN Handshake 미완성 연결 큐 크기
net.ipv4.tcp_max_syn_backlog = 65535

# 큐 오버플로우 시 무조건 RST를 보내지 않고 패킷을 재전송하도록 유지 (기본값 0 유지)
net.ipv4.tcp_abort_on_overflow = 0

# 시스템 전체 가용 포트 확장
net.ipv4.ip_local_port_range = 10240 65535

적용: sudo sysctl --system

2) 애플리케이션(WAS / Web) 백로그 동기화

커널의 somaxconn을 올려도 톰캣이나 Nginx 자체의 backlog 설정이 작으면 효과가 없습니다.

  • Tomcat (server.xml):
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               maxThreads="400"
               acceptCount="2048" />  <!-- acceptCount가 Tomcat의 backlog 크기임 -->
    
  • Nginx (nginx.conf):
    listen 80 backlog=65535;
    

3) Keep-Alive 타임아웃 정렬

항상 앞단(Nginx)의 유휴 타임아웃보다 뒷단(WAS)의 유휴 타임아웃을 더 길게 설정해야 중간 연결 단절을 방지할 수 있습니다.

  • Nginx keepalive_timeout 60s; 라면 Tomcat keepAliveTimeout="65000" (65초)로 설정합니다.

   

주의사항

  1. ClientAbortException은 정상적인 네트워크 현상일 수 있음:
    • 톰캣 로그에 찍히는 단순한 ClientAbortException: Broken pipe는 모바일 사용자가 페이지 로딩 도중 뒤로 가기를 누르거나 다른 탭으로 이동할 때 발생하는 지극히 정상적인 예외일 수 있습니다. 전체 요청 대비 빈도가 극소수라면 시스템 오류가 아니므로 무리한 튜닝이 필요하지 않습니다.
  2. tcp_abort_on_overflow = 1 함부로 켜지 말 것:
    • 이 파라미터를 1로 켜면 Accept Queue가 찼을 때 즉각 클라이언트에 RST를 꽂아 에러를 냅니다. 기본값 0으로 두어야 클라이언트의 지수 백오프(Exponential Backoff) 재전송을 통해 큐가 비워질 때까지 세션이 복구될 기회를 얻습니다.
  3. SO_LINGER 설정 주의:
    • 애플리케이션 레벨에서 소켓 종료 시 SO_LINGER 타임아웃을 0초(l_onoff=1, l_linger=0)로 설정하면 정상적인 TCP 4-Way Handshake(FIN) 대신 즉시 RST를 송출하여 상대방 서버에 Connection reset by peer를 유발하므로 특별한 이유가 없는 한 활성화하지 않아야 합니다.