Connection Reset by Peer 문제 해결

Jeongmin Yeo (Ethan)·2023년 2월 14일
4

문제 해결

목록 보기
1/1

Connection Reset by Peer 문제란 뭘까?

Client 가 요청을 보냈는데 서버쪽에서 연결이 닫혔다고 다시 연결하라는 RST (Reset) 패킷을 보내는 경우에 이 에러가 발생한다.

Client-Server 연결에서 한 쪽만 연결되어있는 좀비 커넥션이 생겼을 때 발생한다.

이 에러가 난 상황은 우리 쪽 Crawler 에서 Straw 에 적재할 때 발생한 에러이다. Straw 쪽에 적재를 하려고 요청을 보냈는데 Straw 쪽에서 연결을 닫은 경우에 발생했다.

  • (사내 문제를 해결한 거라서 용어가 다소 어색할 수 있습니다. Crawler 는 저희 팀이 개발한 어플리케이션이고, Straw 는 여기에 적재할 저장소입니다.)

닫힌 이유 자체는 많을 수 있다.

  • network connection.
  • Firewall and VPN.
  • any proxies and load balancers
    • idle timeout configuration (the connection is closed when there is no incoming data for a certain period of time)
  • the target server.
    • idle timeout (the connection is closed when there is no incoming data for a certain period of time)
    • limit for buffering data in memory
    • multipart exceeds the max file size limit
    • bad request
    • max keep alive requests (the connection is closed when the requests reach the configured maximum number)
  • 참고: [Reactor Netty References]

왜 한쪽만 닫히지?

TCP 의 연결 종료 과정을 보면 다음과 같다.

  • 종료하는 쪽에선 FIN 패킷을 보내면서 종료를 시작한다. 그리고 성공적으로 종료되면 클라이언트-서버 둘 다 CLOSED 상태가 된다. 그러면 한쪽만 닫히면 안되는거 아닌가!!
  • 실제로 현실의 네트워크 상황에서는 FIN 패킷이 전달 안되는 경우도 있다고 한다.
    • 스위치가 강제로 뽑혀졌을 때.
    • Client 와 Sever 간의 네트워크 컴포넌트들이 껴있을 때 FIN 을 전달하지 못할 수 있다. (i.e 로드밸런서, 프록시 등)

실제로 한쪽만 닫히는 경우 사례

  • DSR (Direct Server Return) 구조라고 가정. 클라이언트에서는 로드밸런서로 요청을 보내고, 서버는 클라이언트로 직접 전송.
  • 1) Client 는 Server 와의 TCP 연결을 위해서 Load Balancer 의 VIP 로 요청을 보낸다. 그러면 Load Balancer 는 Server 1 로 요청을 라우팅하고 이 세션 정보를 기억하기 위해서 세션 테이블에 기록한다. 이 과정을 통해서 Client 와 Server 1 는 TCP Handshake 를 통해서 연결이 맺어진다.
    • 이 때문에 Client 는 Load Balancer 로 요청을 보내면 계속 Server 1 과 통신이 가능하다.
  • 2) 근데 Client 에서 한동안 요청을 보내지 않으면 Load Balancer 는 해당 세션 정보를 지워버린다. 이때 중요한 점은 Server 와 Client 의 TCP 연결은 아직 유지되어있다는 점이다.
  • 3) 이렇게 Load Balancer 에서 세션 정보가 지워진 상황에서 Client 가 다시 요청을 보내면 Load Balancer 는 Server 1 이 아닌 Server 2 나 3 에게 요청을 라우팅 할 수 있다. 그러면 해당 서버들은 Client 에게 RST 패킷을 전송하게 된다. TCP 연결 없이 요청을 보냈기 떄문에.

2번에서 이렇게 한동안 요청을 보내지 않으면 연결을 지워버리는 걸 idle Timeout 이라고 한다.

실제론 이것말고도 더 다양한 사례가 있다.

  • 1) Client → Server 로 대용량 데이터 적재.
  • 2) Server 에서 이를 처리하는데 시간이 꽤 걸림.
  • 3) Load Balancer → Server 로 HealthCheck 하는데 실패함. 해당 Server 다운시킴.
  • 4) 서버가 다운되면서 소켓이 닫힘.
  • 5) 이후 해당 소켓으로 요청이 또 들어오면 RST 패킷이 나감.

그럼 우리는 왜 이 문제가 발생했을까?

Straw 도 앞단에 Load Balancer 를 쓰고 있다. 그래서 위의 Load Balancer 사례와 유사하다.

결론적으로 idle Timeout 이 발생할 확률이 높다고 생각했다.

  • Crawler 에서는 Straw 와 여러 커넥션을 맺는다. 그리고 크롤링 한 문서의 상태가 Delete 일 때 문서를 지울려고 Straw 에서 Delete 요청을 보내는데 Bulk Delete 를 지원하지 않아서 parallelStream() 을 써서 단건으로 지우는 요청을 병렬로 보낸다. 이 때문에 유휴 커넥션이 생길 가능성이 높다. (실제로 Netty Debug 모드에서 꽤 보면 생긴다.)
  • 에러가 발생할 당시의 로그를 보면 커넥션을 얻기까지 시간이 3분정도 걸렸었다. (Straw 쪽의 Load Balancer 의 maxIdleTimeout 은 120초라고 한다.)

해결 방법

1) Naive 한 접근. TCP 연결을 유지하지말고, 요청 보낼 때마다 TCP 연결을 하면 되지 않을까?

  • TCP Handshake 의 비용 문제가 발생한다.
  • 연결을 먼저 끊는 쪽에서의 소켓은 TIME_WAIT 라는 상태에 도달하게 된다. 이 상태는 완전히 소켓이 닫히기 전에 30초동안 대기하게 된다. 즉 소켓을 쓸 수 없는 문제가 생겨서 포트 고갈의 문제가 생길 수 있다.

2) TCP Keep-Alive probe 를 통해서 요청이 끊겼는지 계속 조사하는 방법.

  • 연결이 끊겼는지 안끊겼는지 알 수 있는 방법이 없다. 즉 조사를 계속해야한다.
  • TCP Keep-Alive 는 주기적으로 연결이 되어있는지 확인을 하는 패킷을 보내서 연결을 유지해준다.
  • 관련 파라미터
    • TCP_KEEPIDLE(300) : 300 초 동안 유휴 상태가 되어있다면 TCP Keep-Alive 조사를 한다.
    • TCP_KEEPCNT(8) : Keep-Alive 가 실패할 경우 최대 8번 조사를 한다.
    • TCP_KEEPINTVL(60) : 개별 Keep-Alive 조사를 60초 간격으로 한다.

3) Client 의 Connection Pool 에서 설정하는 옵션으로 maxIdleTimeout 을 서버 쪽 maxIdleTimeout 보다 작게 주는 방법.

  • Straw 쪽 Load Balncer 의 maxIdleTimeout 은 120 초라고 한다.
  • 이것보다 Client 쪽 maxIdleTimeout 을 작게 줘서 우리쪽에서도 대응할 수 있도록 하는 방법.

정리하자면 연결을 받는 서버쪽의 maxIdleTimeout 을 미리 알고 있다면 3번의 방법이 가장 나을듯하다. 이걸 알지 못한다면 2번으로 해결해야하고.

  • TCP Keep-Alive 의 해결 방법은 연결을 확인해야하는 패킷을 계속 보내야하니까. 네트워크 대역폭을 차지할 것이니 3번보다 좋지 않은듯.

번외로 Connection 관리에 대한 팁

커넥션을 기본 설정으로 쓰면 최대 개수가 500 개까지 늘어날 수 있다고 하고, 1000 개 까지 커넥션을 얻을려고 시도하는 Pending 상태가 될 수 있다고 한다.

Reference 에서는 high load 가 예상된다면 너무 많은 커넥션이 생기지 않도록 조심하라고 함. 트래픽이 증가해서 커넥션 개수가 늘어났는데, 트래픽이 줄어들면 그만큼 유휴 커넥션이 생길 수 있으니까 그런 듯.

Reactor Netty References
When you expect a high load, be cautious with a connection pool with a very high value for maximum connections. You might experience reactor.netty.http.client.PrematureCloseException exception with a root cause "Connect Timeout" due to too many concurrent connections opened/acquired.

profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

0개의 댓글