Загружаем каталог…
Загружаем каталог…
1. 도입 — 거절이 아니라 멈춤 트래픽이 몰리면 클라이언트 로그에 connection refused 가 찍힐 거라고 생각했다. 그런데 실제로 보이는 건 connect() 는 성공했는데 첫 요청이 1초, 3초씩 멈추다가 응답이 오거나 타임아웃 나는 현상이다. 커널 소스를 따라가 보니 원인은 서버의 accept 큐가 넘쳤을 때 커널이 3번째 ACK를 RST 없이 그냥 무시한다 는 데 있었다. 이 글은 listen(fd, backlog) 의 backlog가 실제로 무엇을 세는지에서 시작해, 그 ACK가 어디서 버려지는지까지 따라간 기록이다. 2. backlog는 "완성된 연결" 큐의 길이다 listen(2) man page에 따르면 Linux 2.2부터 backlog는 반쯤 열린(SYN_RECV) 연결이 아니라 핸드셰이크가 끝났지만 아직 accept() 되지 않은 소켓 수의 상한이다. 3-way handshake 자체의 흐름은 TCP 3-way/4-way handshake 글 에서 정리했고, 여기서는 서버 쪽 큐만 본다. listen() 에 넘긴 값은 net.core.somaxconn (5.4부터 기본 4096, 이전은 128)으로 조용히 잘려 sk->sk_max_ack_backlog 에 저장된다. 커널은 이 한 값을 두 관문에 쓴다. 카운터 의미 비교 대상 qlen SYN_RECV 상태 요청 수 ("SYN 큐") sk_max_ack_backlog → 넘으면 syncookie 전환 sk_ack_backlog accept 대기 중인 완성 소켓 수 sk_max_ack_backlog → 넘으면 overflow 헷갈렸던 지점은 tcp_max_syn_backlog 다. 이 sysctl은 syncookies가 꺼져 있을 때 "마지막 1/4은 살아 있음이 입증된 목적지에게만"을 판단할 때만 쓰인다. 기본값( tcp_syncookies=1 )에서 SYN 단계의 실질 임계값은 앱의 backlog다. 그래서 tcp_max_syn_backlog 만 올리는 튜닝은 효과가 없다. 하나 더 있다. 두 비교가 모두 >= 가 아니라 > 라서 실제로는 backlog+1개 까지 들어간다. // include/net/sock.h static inline bool sk_acceptq_is_full(const struct sock *sk) { return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog); } sock.h 에는 " >= 여야 한다고 생각하면 commit 64a146513f8f를 보라"는 주석이 붙어 있다. off-by-one처럼 보이지만 의도적으로 되돌린 결과다. 3. accept 큐가 꽉 찼을 때 — 관문이 두 번 있다 accept 큐가 꽉 찬 상태에서 새 SYN 이 오면 tcp_conn_request() 가 ListenOverflows 를 올리고 SYN 자체를 버린다. 클라이언트는 SYN_SENT에 머물며 SYN을 재전송한다. 이 경우는 connect() 가 늦어질 뿐 성공처럼 보이지는 않는다. 문제는 SYN 시점엔 자리가 있었는데 3번째 ACK가 도착한 시점엔 큐가 찬 경우다. burst에서 여러 핸드셰이크가 동시에 진행되면 이렇게 된다. tcp_check_req() 가 child 소켓을 만들려다 실패하면 이 분기로 간다. // net/ipv4/tcp_minisocks.c — listen_overflow if (!READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_abort_on_overflow)) { inet_rsk(req)->acked = 1; // ACK를 받았다고 표시만 하고 return NULL; // 세그먼트는 버린다 — RST 없음 } 이때 양쪽 상태가 어긋난다. client: SYN-ACK 받음 → ESTABLISHED (connect() 성공, 요청 write) server: request_sock은 SYN_RECV 그대로, child 소켓 없음 └ reqsk 타이머 만료 → SYN-ACK 재전송 → client가 ACK 재전송 → 그때 큐에 자리가 있으면 child 생성 → tcp_synack_retries 횟수를 넘기면 요청 폐기 클라이언트 입장에선 연결이 열렸으니 바로 요청을 보낸다. 그 사이 서버엔 이 연결을 받을 소켓이 없으므로 요청은 처리되지 않는다. 요청 세그먼트도 ACK를 싣고 있어 같은 검사를 다시 거치지만, 큐가 여전히 차 있으면 똑같이 버려진다. 결국 서버의 SYN-ACK 재전송(또는 클라이언트의 데이터 재전송)이 다시 도착했을 때 큐에 자리가 나 있어야 비로소 연결이 서버에 생긴다. 이 재전송 간격은 초기 1초에서 지수적으로 늘어나는 것으로 알려져 있다. "첫 요청이 1초, 3초씩 멈춘다"는 증상이 이 간격과 맞아떨어진다. 타이머 백오프의 일반 원리는 TCP RTO 글 과 같은 구조다. tcp_abort_on_overflow=1 이면 대신 RST를 보내 즉시 실패시킨다. 하지만 ip-sysctl 문서는 기본값 0의 이유를 "burst로 인한 overflow라면 회복된다"고 설명한다. RST로 바꾸면 그 회복 기회를 버리는 셈이다. 4. 직접 확인하기 accept하지 않는 서버를 listen(1) 로 띄우고 연결을 여러 개 붙이면 큐가 차는 걸 볼 수 있다. # backlog_demo.py — listen(1) 후 accept()를 하지 않는 서버 import socket, time s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("127.0.0.1", 9999)) s.listen(1) time.sleep(600) python3 backlog_demo.py & nstat -n # 카운터 기준점 리셋 for i in 1 2 3 4; do (sleep 30 | nc 127.0.0.1 9999 &); done ss -lnt 'sport = :9999' # LISTEN 행: Recv-Q=sk_ack_backlog, Send-Q=sk_max_ack_backlog ss -tan 'dport = :9999' # 클라이언트 상태 nstat -az TcpExtListenOverflows TcpExtListenDrops LISTEN 행의 Send-Q는 1인데 Recv-Q가 2 까지 오르면 backlog+1이 보인다. 순차로 붙였으니 3·4번째는 SYN 단계에서 버려져 SYN-SENT로 남고 ListenOverflows 가 오른다. 3번째 ACK 무시 경로는 핸드셰이크가 동시에 겹쳐야 나와서 이 순차 루프로는 재현이 잘 안 된다. 운영에서는 ListenOverflows 가 증가하는지를 먼저 보는 편이 확실하다. 5. 정리 backlog는 완성된 연결의 큐 길이이고, 이 큐가 넘치면 Linux는 기본적으로 거절하지 않고 침묵한다 — 그래서 증상이 refused가 아니라 수 초짜리 멈춤으로 나타난다. "connect는 되는데 첫 응답이 느리다"면 ss -lnt 의 Recv-Q/Send-Q와 nstat 의 ListenOverflows 부터 보고, 앱의 backlog와 somaxconn 을 함께 올리는 게 순서다. 다음으로 파고들 만한 건 syncookie가 MSS·wscale을 쿠키에 싣는 방식과, 데이터가 올 때까지 child 생성을 미루는 TCP_DEFER_ACCEPT 다. 참고 자료 listen(2) — https://man7.org/linux/man-pages/man2/listen.2.html Linux Documentation/networking/ip-sysctl.rst — tcp_abort_on_overflow , tcp_max_syn_backlog , tcp_syncookies , tcp_synack_retries Linux 커널 소스(torvalds/linux) — include/net/sock.h , net/ipv4/tcp_input.c ( tcp_conn_request ), net/ipv4/tcp_minisocks.c ( tcp_check_req ), net/ipv4/tcp_diag.c
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
connect()는 성공했는데 첫 요청이 멈추는 이유 — Linux listen backlog와 accept 큐 오버플로. 1. 도입 — 거절이 아니라 멈춤 트래픽이 몰리면 클라이언트 로그에 connection refused 가 찍힐 거라고 생각했다. 그런데 실제로 보이는 건 connect() 는 성공했는데 첫 요청이 1초, 3초씩 멈추다가 응답이 오거나 타임아웃 나는 현상이다. 커널 소스를 따라가 보니 원인은 서버의 accept 큐가 넘쳤을 때 커널이 3번째 ACK를 RST 없이 그냥 무시한다 는 데 있었다. 이 글은 listen(fd, backlog) 의 backlog가 실제로 무엇을 세는지에서 시작해, 그 ACK가 어디서 버려지는지까지 따라간 기록이다. 2. backlog는 "완성된 연결" 큐의…
Открыть источник