들어가며
MSA 가 보편화되고 복잡한 비즈니스를 해쳐나가기 위하여 다양한 도메인 서버를 운영하게 되는데요.
도메인 서버가 많아지면 많아질수록 Network 통신은 그만큼 많아지게 됩니다.
그에 따라 Network 관련된 예외들을 경험해볼 수 있는데요. 하지만 Network 에러를 분석하고 다양한 상황을 파악하기에는 정말 쉽지 않죠.
그래서 보통은 Idempotency(멱등성) 를 지원하게 하는 API 를 제공하는 방향으로 설계를 하는데요.
멱등성을 도입하지 못한 레거시 코드나, 정황상 멱등성 설계가 어려운 상황에 처한 분들도 있을거에요.
이럴 때는 어쩔 수 없이 분석을 해서 헤쳐나가야 하는데요. java, kotlin 진영에서 발생하는 Network 에러 유형에 대해 간단하게 공부해볼까 합니다 ㅎㅎ
오늘 소개해드릴 항목은 크게 SocketException: Connection reset 과 ClientAbortException: broken pipe 이지만,
본격적인 분석은 SocketException: Connection reset 에 대해 분석해보겠습니다.
Network Exception
SocketException: Connection reset 은 왜 발생할까?
ClientAbortException: broken pipe 은 왜 발생할까?
둘 간의 exception 에 간의 차이는 무엇일까?
- SocketException: Connection reset Connection 이 Remote server 에 의해서 강제로 종료될 때 client 측에서 발생하는 예외. 이 경우는 주로 Server 측에서 TCP RST packet 을 보낸경우에 속한다.
- remote server application 이 crash 되었다
- remote server 가 해당 port 에서 listen 하고 있지 않다
- 네트워크 설정 오류
- connection 이 idle 상태가 되어 server 가 닫기를 판단하였다
- ...
- ClientAbortException: broken pipe Response 를 내려주던 와중에 Connection 이 client 에 의해 강제로 끊어졌을 때 server 측에서 발생되는 예외
- client application 이 crash 되거나 종료되었다
- server 가 response 주기 전에 user(client) 의 web page 이탈
- server 가 response 주기 전에 client 가 connection 종료
- ...
이외에도 위 Exception 이 발생하는 상황들은 엄~청 다양할 수 있습니다.
오늘은 SocketException: Connection reset 을 이해하고자 하는데요.
이것을 이해하려면, TCP RST 시나리오에 대해 먼저 이해할 필요가 있습니다.
TCP RST
TCP RST 라는게 도대체 무엇일까?
쉽게 이해해보자면, Server 측에서 TCP connection 을 강제로 close 하기 위하여 전송하는 flag 를 의미합니다.
TCP 는 기본적으로 상호간의 대화를 통해 통신을 이끌어나가는 것인데, TCP RST 는 server 가 client 에게
"나 너랑 더 이상 할말 없어. 잘가." 라고 통보하고 즉시 중단하는 것을 의미하죠.
TCP RST 가 발생하는 시나리오는 굉장히 다양합니다.
TCP RST 를 발생시킬 수 있는 시나리오는 무엇이 있을까요?
시나리오1
위 시나리오는 SYN 세그먼트가 지연되어서 ACK 를 잘못 인지한 경우 client 측에서 발생시키는 상황입니다.
200 SYN 에 대한 201 ACK 를 기대했지만, 91 ACK 가 와서 establish 전에 RST 를 client 측에서 발생하는 것이죠.
다행히도 이 경우는 connection 을 맺기전과정에 이루어진 것이기 때문에 abort 되는건 없다고 이해해볼 수 있습니다.
시나리오2
이 시나리오는 TCP A 가 crash 가 일어나서, 200 SYN 로 보냈지만, ACK 가 150 으로 오는 것을 보고 client 가 server 에게 RST packet 을 전달하게 됩니다.
위 시나리오에서는 TCP B 가 abort 를 맞이하게 되며, 기존에 응답 내리던 것을 강제로 중단하게 됩니다.
그 이후 SYN 200 에 대한 요청을 다시 하여 TCP B 에서는 200 에 대한 ACK 를 내려주는 프로세스를 타게 되겠죠.
시나리오3
이 경우도 마찬가지입니다. TCP A 는 crash 가 된 상태였고, 150 ACK 에 대하여 인지하지 못하기 때문에
TCP B 쪽으로 RST 를 보내고, TCP B 는 abort 를 맞이 하게 됩니다.
시나리오4
이 경우에는 establish 과정에서 서로의 seq, ack 가 맞지 않은 상황으로 되었는데요.
역시나 시나리오1 과 유사하게 client 측에서 RST 를 보내서 다시 연결을 맺기 위한 과정을 진행하게 됩니다.
전체 시나리오를 요약해보면 RST 는 정말 다양하게 발생합니다.
- Connection 을 맺기전에 Establish 과정에서도 발생 가능
- Data 를 내려주고 있는 와중에도 발생 가능
- Client or Server 측에서 Connection 을 강제로 close 하는 경우
- ...
위 시나리오 중에서 Connection 을 강제로 close 하는 경우는 어떤 경우인지 더 살펴볼게요.
Connection close RST
일반적으로 connection close 는 FIN 패킷을 주고 받고, close 를 호출해주는 것이 TCP 의 종료과정입니다.
단, 이 FIN 패킷을 주고받는 과정 없이 강제로 종료하는 경우들이 있는데, 이것을 Linger 설정(활성화후 0초설정) 을 통해 실험해볼 수 있습니다.
Linger 설정은 TCP 에서 socket 이 close 되었을 때 전송되지 않은 데이터를 어떻게 처리할 것인지 조정하는 옵션입니다. Socket buffer 에 남아있는 데이터 처리에 대한 정책 설정이라고 이해하면 편합니다. default 는 off, 남아있는 데이터를 항상 전송하도록 보장
이때 server 측에서 connection 을 바로 중단하게 되면 client 측에서는 FIN packet 을 받지 못했고, connection 이 종료되었다는 것은 인지하였기 때문에
client 측에서는 RST 를 인지하고 SocketException: Connection reset 을 발생하게 되는 것입니다.
한번 SocketException: Connection reset 을 Linger 설정을 임의로 주지 않은채로 재현해볼게요.
핵심은 server 측에서 바로 중단하는 것이 포인트일듯 하다.
Connection Reset 재현 시나리오
재현 시나리오는 SocketException: Connection reset 이 발생하는 한가지의 사나리오임을 인지합니다. 꼭!!
server2 에 아래와 같은 controller 코드를 만들고, server1 에서 10초 delay 를 주어 호출하도록 한다.
@RestController
@RequestMapping("/delay")
class DelayController {
@GetMapping("/time")
fun delay(
@RequestParam t: Long
): DelayTimeDto {
Thread.sleep(t)
return DelayTimeDto("$t ms 만큼 delay 되었습니다.")
}
}
위 API 를 호출하면서 아래와 같은 임의 조작을 가해보자.
- server1 에서 server2 로 호출하는 와중에 server2 를 갑자기 shutdown 한다.
- server1 에서는 server2 가 갑작스럽게 socket.close 를 호출하게 되므로, connection reset 을 응답받게 된다.
위 과정을 기반으로 TCP 흐름을 wireshark 를 통해서 분석해보도록 한다.
server1: port 8080
server2: port 8081
- 3-way handshake 는 seq 0, ack 1 기반으로 connection 을 맺게 된다.
- server1 는 seq1 기반으로 요청을 전달하고, 데이터를 수신받기를 PSH 기반으로 인지한다.
- server2 는 갑자기 중단되어 seq1 에 대한 RST 를 server1 에게 전달한다.
최종적으로 server2 는 갑작스러운 종료를 하게 되고, server1 의 예외 로그는 아래와 같이 찍힌다
Conclusion
따라서 client 에서 발생하는 SocketException: Connection reset 은 전적으로 server 측의 트리거링으로 발생합니다.
위 재현 시나리오처럼 볼 수 있듯이 서버가 요청을 올바르게 처리한 것인지 안한 것인지는 RST 발생 시나리오에 따라서 확정짓기가 어렵습니다.
SocketException: Connection reset 이 발생하면 직접 action 을 취해볼 수 있는 것은 상대방 server 측에게 확인요청을 하여 네트워크 에러 과정을 같이 진단해볼 수 밖에 없겠네요.
- 네트워크 설정에 오류가 있는 것은 아닌지
- application 이 임의로 죽었거나, 배포중인 상황은 아닌지
- 다른 네트워크 요소(방화벽)에 의해 RST 가 강제로 일어나게 된 것은 아닌지
Reference
Orderly Versus Abort Connection release in java
Connection Reset
TCP: Differences Between FIN and RST
'Developer > Kotlin & Java' 카테고리의 다른 글
[Kotlin] Coroutine - 6. Suspend 함수에 대해 deep dive 를 해보자 (3) | 2024.03.01 |
---|---|
Java21 Virtual thread vs Kotlin Coroutine - 가상쓰레드와 코루틴에 대해 고찰해보자 (2) | 2024.01.27 |
[Kotlin] Kotlin Annotation 에 대해 톧아보기 (0) | 2023.04.01 |
[Kotlin] Coroutine - 5. 코루틴의 Channel 의 모든 것 (0) | 2023.03.05 |
[Kotlin] Coroutine - 4. 코루틴에서의 예외(exception) 핸들링 (0) | 2023.02.04 |