웹 소켓 끊긴 후 재 연결 관련 이슈 분석

이동욱·2025년 6월 19일
0

TroubleShooting

목록 보기
9/9

개요


웹 소켓이 끊기는데 백엔드 로그에 disconnect가 안 남고 client에서의 재 연결 로직도 10분 정도 후에 타서 지속적으로 이슈가 올라왔음

stack


  • 백엔드 : Spring Boot
    • stomp, sockjs 사용
  • 프론트엔드 : Vue2
    • sockjs-client 1.6.1, webstomp-client 1.2.6 사용

분석


  • 모든 사용자가 같은 일시에 웹소켓이 동시에 끊기는걸 확인하고 네트워크 이슈로 파악했음
  • 어플리케이션 쪽에서 재 연결 로직도 있고 서버에서 heartbeat도 보내고 있는 상황이었음
  • 어플리케이션에서의 문제는 아닐 것으로 보고 패킷 분석을 위해 와이어샤크로 분석 진행

원인


  1. 서버에서 클라이언트에게 보내는 하트비트가 갑자기 멈춤

    • 중간 네트워크 장비 이슈로 추정
    • 찾아보니 sockjs, stomp를 사용해 웹 소켓 서버를 구현한 상태에선 sockjs, stomp level의 하트비트가 다르다고 함
    • 현재 적용되어 있는 하트비트는 sockjs level의 하트비트로 중간 네트워크 장비로 끊겼을 땐 서버 / 클라이언트가 웹 소켓이 끊긴 것을 감지하지 못해 재연결 로직을 타지 않았음
    • 실제 해당 웹소켓의 TCP connection은 연결되어 있는 상태에서 데이터는 도달하지 못하는 상황 (Dead connection)
  2. OS level의 리눅스 커널에서는 오래 사용되지 않은 TCP connection에 Keep-Alive 패킷을 보내 해당 connection을 끊을지 결정한다고 함

    • tcp_keepalive_time : 연결이 아무 활동도 없는 상태로 유지되는 시간, 이후 첫 keep-alive 패킷 전송
    • tcp_keepalive_intvl : keep-alive 패킷 전송 주기
    • tcp_keepalive_probes : 최대 재시도 횟수
  3. 하트비트가 끊긴 시점에 주기적으로 TCP Keep-Alive 패킷을 전송하고 있었고 최대 횟수 보내자마자 해당 TCP connection을 끊는 패킷이 전송됨(FIN, ACK)

  4. 그제서야 클라이언트는 웹소켓에 대한 TCP connection 끊김을 감지하고 재 연결 시도

해결


  • 네트워크 이슈기 때문에 중간 네트워크 장비 모두를 포함한 전체적인 패킷에 대한 분석을 하기 어려운 상황이라 어플리케이션 레벨에서의 조치가 필요했음
  • 현재 문제는 하트비트가 멈추면서 TCP connection이 실제로 끊겨 재 연결까지의 텀이 길기 때문에 사용자 입장에서 느끼는 문제라고 파악
  • 그래서 sockjs level -> stomp level의 하트비트로 수정해 클라이언트 -> 서버, 서버 -> 클라이언트에서 하트비트를 주기적으로 받지 못하면 재 연결 시도하도록 수정함

Stomp level heartbeat


  • sockjs level의 heartbeat는 단순 connection이 살아있는지 확인하는 정도의 수준으로 heartbeat가 오지 않더라도 재 연결 시도는 하지 않음
  • stomp level의 heartbeat는 client to server, server to client의 양쪽 방향의 heartbeat를 모두 전송하고 서로 살아있는지 확인/감지함
stomp level heartbeat는 stomp 1.1 부터 기능 추가됨
  • frontend에서 웹 소켓 연결 방식

    const url = "websocket server url";
    const socket = new SockJS(url);
    const stompClient = Stomp.over(socket, {protocols: Stomp.VERSIONS.supportedProtocols()};
    
    ...
    
    • stomp level heartbeat 사용 위해 1.1 이상 프로토콜을 사용
    { protocols: ['v11.stomp', 'v12.stomp'] }
  • backend에서 웹 소켓 서버 방식

    • 기존
    ...
    
    @Configuration
    @EnableWebSocketMessageBroker
    public class webSocketConfig implements WebSocketMessageBrokerConfigurer {
      @Override
      public void registerStompEndpoints(StompEndpointRegistry registry)
      {
          registry.addEndpoint("/secured/ws-stomp").addInterceptors(talkStompHandshakeInterceptor).setAllowedOrigins("*").withSockJS();
      }
      
      @Override
      public void configureMessageBroker(MessageBrokerRegistry registry)
      {
          registry.enableSimpleBroker("/chat", "/ready");
          registry.setApplicationDestinationPrefixes("/event");
      }
      
      ...
      
    }
    • tobe
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry)
    {
      registry.enableSimpleBroker("/chat", "/ready")
              .setHeartbeatValue(new long[] {3000, 3000})
              .setTaskScheduler(heartBeatScheduler());
    }
    
    @Bean
    public ThreadPoolTaskScheduler heartBeatScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(1);
        scheduler.setThreadNamePrefix("stomp-heartbeat-thread-");
        scheduler.initialize();
        return scheduler;
    }

고찰


  • 와이어샤크로 패킷 분석은 실제 처음 해봤는데 처음 해보면서 해결하지 못할 것 같았던 문제를 해결하니 많은 걸 배울 수 있었다. 특히 이번 문제의 쟁점이었던 TCP connection Keep-Alive를 os level에서 보내 connection 연결을 끊는다는 새로운 사실을 알게 되어서 좋았다.
  • 그리고 중간 네트워크 장비에서 웹 소켓 연결이 끊길 때 Dead connection이 발생할 수도 있다는 것을 알게 되었다.
  • Dead connection으로 인한 문제를 해결하는 과정에서 sockjs, stomp level heartbeat의 차이점이 존재한다는 것을 배울 수 있었다.
profile
lduk 웹 개발자(back)

0개의 댓글