연결 하나가 느려지자 나머지가 전부 멈췄다 · 쓰기 준비인데 접속 실패: 비차단 연결 수립 · 이론
쓰기 준비인데 접속 실패: 비차단 연결 수립 이해하기
한 줄 요약
쓰기 준비는 연결 성공 도장이 아닙니다. 연결 중 상태에서 완료 준비를 관찰한 뒤 SO_ERROR를 한 번 읽고, 최초 결과를 애플리케이션 상태에 보관합니다.
왜 이게 필요했나
서버가 연결을 받아 주는 예제만 시험하면 EVENT_WRITE가 오자마자 connected로 표시하는 코드도 통과합니다. 같은 코드를 닫힌 포트에 적용하면 접속이 거절됐는데도 초록불이 들어옵니다. 더 이상 기다릴 필요가 없다는 것과 성공했다는 것을 같은 의미로 썼기 때문입니다. '실패도 완료 이벤트를 만든다'는 관점이 있어야 진단기의 성공률을 믿을 수 있습니다.
여기서 만들 것은 여러 대상의 TCP 연결 수립 결과를 모으는 진단기입니다. 연결되면 곧 닫으며 HTTP 요청이나 TLS 인증서를 검사하지 않습니다. 그러므로 connected는 로그인 가능, API 정상, 데이터 저장 성공을 뜻하지 않습니다. 사용자에게 보이는 결과 이름이 실제 검증 범위를 넘으면 정확한 코드도 부정확한 운영 도구가 됩니다.
어떻게 동작하나
[connect(2)](https://man7.org/linux/man-pages/man2/connect.2.html)에 따라 Linux의 비차단 TCP 연결은 즉시 끝나거나 진행 중일 수 있습니다. Python connect_ex의 정수 결과를 상태 전이에 사용합니다. 이 실습은 AF_INET/SOCK_STREAM에 한정합니다. AF_UNIX의 진행 중 EAGAIN 규칙을 숫자 IPv4 TCP에 그대로 가져오지 않습니다.
| 현재 상태 | 관찰 | 다음 상태 |
| --- | --- | --- |
| new | connect_ex 결과 0 또는 EISCONN | connected, 오류 0 |
| new | EINPROGRESS 또는 EALREADY | pending, 결과 미정 |
| new | 그 밖의 오류 | failed, 해당 errno |
| pending | WRITE 뒤 SO_ERROR가 0 | connected |
| pending | WRITE 뒤 SO_ERROR가 오류 | failed |
| new/pending | 절대 마감 또는 취소 | timed_out 또는 cancelled |
pending이라고 connect_ex를 매번 다시 호출하지 않습니다. 한 번 시작한 시도의 결과를 기다리는 것입니다. 실패한 소켓의 재사용을 일반적인 재시도 방법으로 삼지 않고 닫은 뒤 새 시도를 만듭니다. 이번 진단기는 자동 재시도 자체를 하지 않아 결과 하나가 연결 시도 하나와 대응합니다.
[socket(7)](https://man7.org/linux/man-pages/man7/socket.7.html)의 SO_ERROR는 오류를 얻으면서 지웁니다. 로그를 남기는 함수가 먼저 읽고, 상태 판정 함수가 두 번째로 읽으면 둘의 결과가 달라질 수 있습니다. Dial.error를 두고 최초 완료 판정의 값을 보존하세요. 읽을 때마다 '최신 오류'를 요청하는 것이 정확도를 높이는 방법이 아닙니다.
python3 /opt/fixtures/reactor/connect_probe.py실험은 한 소켓에는 listen을 호출하고 다른 소켓은 bind만 합니다. 둘 다 포트를 잡고 있으므로 '빈 포트를 찾고 닫은 뒤 다시 접속'하는 경합이 없습니다. Linux 컨테이너에서 관찰한 진행 중 연결의 경우 둘 다 writable=true였지만 SO_ERROR는 각각 0과 ECONNREFUSED였습니다. 거절 결과를 다시 읽으면 0이었습니다. 숫자 111 같은 errno 상수 값은 OS마다 다를 수 있으므로 코드에는 errno.ECONNREFUSED를 씁니다. 즉시 완료한 경우에는 connect_ex의 최초 반환값으로 분류합니다.
이름 해석을 몰래 끼우지 않는다
숫자 주소 대신 도메인을 넣으면 연결 이전에 이름 해석 단계가 생깁니다. 소켓을 비차단으로 바꿨다는 사실만으로 이름 해석까지 비차단이 되었다고 할 수 없습니다. 이 실습의 host는 숫자 IPv4로 검증하며 localhost도 거절합니다. IPv6 주소 후보 경쟁, DNS 캐시, Happy Eyeballs는 추가 설계가 필요한 별도 주제입니다. 범위를 표시하는 것은 오류를 숨기기 위해서가 아니라 측정한 단계가 무엇인지 분명히 하기 위해서입니다.
마감은 시도 시작 때가 아니라 Dial 생성 때 정합니다. 대기열에 오래 머문 연결도 총시간을 소비하기 때문입니다. connect_ex를 호출하기 전에 이미 마감이면 네트워크 시도를 시작하지 않고 timed_out으로 끝냅니다. 종료 상태는 이후 cancel이나 expire가 덮지 않습니다. 성공 직후 전체 취소가 와도 이미 확인한 성공을 취소로 바꾸지 않습니다.
현장에서 만나는 모습
2026-09-13 확인한 [Cloudflare Software Engineer, Spectrum 공고](https://job-boards.greenhouse.io/cloudflare/jobs/8094826?gh_jid=8094826-1)는 Linux 소켓과 TCP 연결 상태, 수명주기 문제의 디버깅을 다룹니다. 여기서는 그중 연결 수립과 실패 원인 보존을 연습합니다. 해당 기업의 공식 교육이나 채용 보증은 아니며 Go·Rust 및 대규모 운영 역량은 별도로 학습해야 합니다.
다음 확인에서 할 것
다음 퀴즈에서 관찰과 판정의 차이를 확인합니다. 이후 종합 실습에서는 Dial의 new→pending→종료 전이를 만들고 실제 loopback TCP의 성공과 거절을 시험합니다. 정답처럼 보이는 'WRITE이면 성공' 구현과 'SO_ERROR를 계속 재조회' 구현이 왜 틀리는지 자기 말로 설명한 뒤 퀴즈로 확인하세요.