카테고리 없음

확장성 있는 웹소켓 서버 구현 이야기

bknote71 2024. 10. 18. 23:27

대용량 연결 처리가 가능한 웹소켓 서버를 구현하는 것이 목표였다.

 

웹소켓 연결에 대해서 얼마까지 처리가 가능한지 확인해보았다.

go 로 간단하게 클라이언트 코드를 짜고 로컬(macos)에서 연결을 테스트해보니, 120개 정도가 넘어가니 Connection reset by peer 에러가 발생했다.

 

Connection reset by peer (RST)

Whireshark를 통해 확인해보니 연결이 established 되고 나서 HTTP GET 요청을 보낼 때 서버측에서 즉시 RST 플래그를 보내 연결을 종료시켰다.

 

1. ulimit -n 으로 FD 할당에 제한이 있는지 확인해보았다:

- 1048576 (ulimited)

- 제한은 없었다.

 

2. 검색을 통해서 syn 큐 혹은 accept 큐 오버플로우일 수 있겠다고 생각이 들었다.

- netstat -s | grep -i "listen" 으로 확인해보니 "0 listen queue overflow" 즉 오버플로우가 발생한 적이 없다고 했다.

- somaxconn 값을 확인해보니 512 였다.

- macos에서는 net.ipv4.tcp_max_syn_backlog 값을 확인할 수 없었다. (또한 syn drop 을 확인할 수 없음)

 

(나중에 알게된 사실인데 macos에서 관리자 권한으로 netstat -s를 실행하지 않는다면, 통계 정보를 제대로 볼 수 없다. 즉 위 0 listen queue overflow는 사실이 아닐 수도 있다.)

 

sysctl -w kern.ipc.somaxconn=4096 으로 accept 큐 사이즈를 설정했다.

그럼에도 동일한 문제(connection reset by peer)가 지속해서 발생했다.

 

3. 스프링부트에서는 server.tomcat.accept-count 값을 listen() 시스템 콜의 backlog 값으로 지정했고, 이 값의 기본값은 100이였다.

- 이 값을 동일하게 4096으로 변경

Net.listen(fd, backlog)에서 accept-count값으로 backlog 설정

 

4. 다시 로컬(macos)에서 1k 정도의 연결 테스트를 진행해보니 모두 성공하였다.

 

5. 도커를 통해 웹소켓 서버(우분투 기반)를 실행 후 다시 1k 연결 요청을 했지만 여전히 connection reset by peer 문제가 발생했다.

- (대략 120개 정도 이상의 연결 처리가 안됨)

- accept-queue 설정이 적용이 되지 않는 문제가 있었다. (매우 작은 수치로 설정해도 적용되지 않았다.)

- 이때 (SYN, accept) queue의 overflow를 다시 확인해보았다.

 

 

SYN 큐에서부터 패킷이 드롭된 것을 확인할 수 있었다.

somaxconn과 맥에서는 확인할 수 없었던 syn_backlog 값을 확인해보았다.

cat /proc/sys/net/core/somaxconn
4096

cat /proc/sys/net/ipv4/tcp_max_syn_backlog
512

 

syn 큐의 크기를 늘려 실행하도록 하였다.

docker run -d --network scheduler_app-network -p 8090:8090 \
        --name api-server bknote71/api-server \
        --privileged \
        --sysctl net.ipv4.tcp_max_syn_backlog=4096 \
        --ulimit nofile=1048576:1048576

 

docker exec로 접속하여 확인해보았더니 여전히 512 였다.

도커 공식문서에 따르면, (sysctl) net.* 설정은 host 네트워크 네임스페이스를 제외한 곳에 적용이 된다고 한다.

 

그러나 적용이 안되었다. 뭐가 문제였을까?

host 네트워크 모드로 실행하여 호스트의 네트워크 설정을 사용하려 했으나, macos에서는 host 네트워크 모드는 사용할 수 없었다.

 

그래서 이번에는 macos가 아닌 EC2-우분투 환경에서 도커 테스트를 진행했다.

여전히 --sysctl 옵션은 적용이 되지 않았고, EC2의 tcp_max_syn_backlog 값을 4096으로 설정 후 호스트 네트워크 모드로 실행해보았다. 

 

도커 인스턴스에 접속하여, 값을 확인하니 잘 반영된 것을 확인하였다.

다시 실행해보니 1k 연결 처리가 잘 되었다.

 

6. 3000개의 연결 요청 시도 실패

 

3000개의 연결 요청을 동시에 보냈을 때, 최초 시도 시 되었다가, 두 번째 이후 시도부터 대략 1000개 정도의 연결이 connection reset by peer 이슈로 종료되었다.

큐 사이즈(syn, accept)를 4096 정도로 늘려보았지만, 여전히 1000개 정도가 연결이 되지 않았다.

 

또 overflow 때문이었을까?

nstat -az TcpExtListenDrops
nstat -az TcpExtListenOverflows

 

위 두 값을 확인했지만 둘 다 0 이었다.

 

무엇이 원인이었을까?

nestat -s로 TcpExt를 확인해보니 많은 timeout과 재전송이 있었다.

많은 timeout과 재전송

 

혼잡 상황이었기 때문에, 네트워크 대역폭 혹은 리소스 부족 때문에 발생한게 아닌가 추측하고 리소스를 확인해봤다.

74.7MiB free

 

이용 가능한 메모리가 74.7 MiB으로 매우 적었지만, 정말 이것 때문에 연결 과정에서 처리를 못해 문제가 발생한건지 의문이었다.

 

7. 도커를 쓰지 않고 EC2 인스턴스(프리티어, 1GB Mem)에서 직접 자바 서버 실행

도커로 실행했을 때, 많은 메모리를 사용하기 때문에 최대한 리소스를 아끼기 위해 인스턴스에 직접 실행해보았다.

(우분투의 somaxconn 값을 8012로 설정 후 자바 서버 실행)

 

자바 서버를 실행 후 ss 로 accept 큐 사이즈를 확인해보니 8012로 반영이 된 것을 확인했다.

LISTEN Recv-Q(accept Q) 사이즈가 8012

 

4000 개의 동시 요청을 시도해보았다.

4000개 동시 요청

 

모두 연결 요청이 되었지만, 리소스 부족으로 자바 서버에서는 OutOfMemoryError가 발생했다.

OutOfMemoryError

 

ss -lt로 (0.3초 단위) 확인해보니 1912개의 연결 요청이 들어왔다가, OutOfMemoryError가 발생되고 나서, 0으로 줄어들었다.

즉 해당 Error가 발생하면 accept 큐를 비우는 것 같았다.

 

이후 다시 웹 소켓을 100개정도 요청해보니 톰캣 Acceptor 스레드에서 OutOfMemoryError 에러가 발생하면서, 99개의 요청이 처리되지 못하고 쌓여있게 되었다.

 

저렇게 처리되지 못한 요청 연결들이 accept 큐에 계속 쌓여있다가, 제한을 넘겼을 때 이슈가 발생한 것이다.

지속해서 100개씩 요청했을 때 처리되지 못하는 연결 요청
8013 이상 제한을 넘지 못한다
drop과 overflow 발생

 

9. 톰캣 Acceptor thread에서 OOM Error가 발생하고나서, 왜 다시 연결 요청을 처리하지 못했던 것일까?

결론부터 말하자면, (1) Acceptor thread는 오직 한 개만 실행되고 (2) OOM Error가 발생했을 때 스레드는 종료되기 때문에 더 이상 요청 처리를 못한 것이다.

 

기본 connector는 1개

 

위 1개 실행되는 커넥터부터 타고타고 내려가서 NioEndpoint(AbstractEndpoint)에서 Acceptor 스레드 하나를 실행한다.

in AbstractEndpoint.class

 

Acceptor.class (Runnable)

 

여기서 OutOfMemoryError, catch(Throwable)에 의해 잡히게 되는데,  ExceptionUtils.handleThrowable 내용을 보면 아래와 같다.

ExceptionUtils.handleThrowable 메서드

 

OutOfMemoryError는VirtualMachineError의 하위 클래스이기 때문에 다시 예외로 던져지게 된다.

즉 이때 던져진 Error는 더이상 Acceptor runnable 에서 처리할 수 없기 때문에 Acceptor 스레드는 종료된다.

처음에는 해당 OutOfMemoryError를 잡아서 다시 Acceptor를 실행해볼까 생각해보았지만, OOM 이슈가 해당 요청이 몰리는 상황에만 발생하지 않기 때문에 굉장히 위험하다고 판단했다.

 

10. scale-out 가능한 웹소켓 서버

우선 scale-up은 현재 상황에서 불가능(비용 이슈)하니, 대량의 웹소켓 처리를 위해서는 어쩔 수 없이 scale-out이 필요하다 생각이 들었다.

웹 소켓 서버의 메모리나 커넥션 개수 등의 지표를 수집하여, 적절한 타이밍에 도커로 scale-out을 하도록 구현했다. 그리고 사용자 요청이 왔을 때 최적의 웹소켓 서버 주소(ws)를 반환하도록 하는 모니터링 서버를 구현하였다.

 

특히 OOM 이슈가 발생하면, 서버는 그것을 기록하고 해당 서버 인스턴스(도커)를 종료했다 다시 실행시켰다.

 

아래 예시는 3.106.188.183 주소에 과도한 웹소켓 연결을 하여 자동으로 scale-out 된 상황이다.

(3.25.70.92 인스턴스에서 도커 컨테이너 실행)

그리고 웹소켓 주소를 요청했더니, 가장 적합한(부하가 없는) 3.25.70.92 인스턴스의 웹소켓 주소가 반환된 것을 확인할 수 있다.

 

이렇게 하여 확장성 있는 웹소켓 서버 구현기를 마무리하겠다.

 


출처

- https://www.alibabacloud.com/blog/tcp-syn-queue-and-accept-queue-overflow-explained_599203