SSE(Server-Sent Events) 트러블슈팅

김건우·2024년 8월 1일
0

트러블슈팅

목록 보기
3/5

개요

SSE를 이용하는 통신의 개요는 다음과 같다.

출처 https://gong-check.github.io/dev-blog/BE/%EC%96%B4%EC%8D%B8%EC%98%A4/sse/sse/

SSE를 사용하는 방법이나 기본적인 내용은 여러 블로그에 잘 나와있으니 생략하기로하고, 구체적인 정보나 배포상황시에 생기는 문제점/주의할점에 대해서 설명하고자 한다.


Flow

Client의 SSE Subscribe 요청

GET /subscribe HTTP/1.1
Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

다음처럼 미디어 타입이 text/event-stream 으로 정해져있다.

또한 HTTP/1.1 을 사용하는 것이 KeepAlive 설정으로 지속적 연결을 사용해야하기 때문이다.

Server의 Subscription에 대한 응답

HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8
Transfer-Encoding: chunked

응답의 미디어 타입은 text/event-stream 으로 Transfer-Encoding의 헤더 값을 chunked로 설정한다. 서버는 동적으로 생성된 컨텐츠를 스트리밍하기 때문에 본문의 크기를 미리 알 수 없기 때문이다.

이벤트 전달

id:myemail_1722451875258
event:file-encoding-event
data:EventStream Created. [userEmail=myemail]

id:myemail_1722451888467
event:file-encoding-event
data:{"id":"2","nickname":"test","content":"영상 업로드가 완료되었습니다!","isRead":false,"type":"ENCODING_FINISH","createdAt":"2024-08-01T03:51:28","videoUrl":"https://d2761ttk8ct150.cloudfront.net/user/1/post/9/hls/cc1f885d-93fc-4a38-adea-27353af0116b/pathtest.m3u8","thumbnailUrl":"https://d2761ttk8ct150.cloudfront.net/user/1/post/9/thumbnails/cc1f885d-93fc-4a38-adea-27353af0116b/pathtestthumbnails.0000000.jpg"}

내가 구현한 SSE 의 이벤트로는 먼저 503 에러를 방지하기 위한 더미 이벤트 전송한다. EventStream Created. 부분이다.

서로 다른 이벤트는 (\n\n) 으로 구분되며, id, event, data 순서대로 구성된다.


주의할 점

1. Thread Safe 한 자료구조 사용

다음처럼 SseEmitter 객체를 사용할 때 비동기 요청이 완료되거나 타임아웃 발생 시 실행할 콜백을 등록할 수 있는데, 이때 새로운 Emitter 객체를 다시 생성하기 때문에 기존의 Emitter를 제거해주어야 한다.

이 콜백이 Sse Emitter를 관리하는 다른 스레드에서 실행되기에 thread-safe한 자료구조를 사용하지 않으면 ConcurrnetModificationException이 발생한다고 한다.

나는 ConcurrentHashMap을 사용했다.

ConcurrentHashMap이 Thread-safe한 이유 외부 블로그 참조

2. JPA 사용시 Connection 고갈 문제

SSE 통신을 하는 동안은 HTTP Connection이 계속 열려있다. 만약 SSE 연결 응답 API에서 JPA를 사용하고 open-in-view 속성이 true로 되어있다면, HTTP connection이 열려있는 동안 DB Connection도 같이 열려있게 되는 문제가 있다.

그렇기에 yml 설정 파일에서 open-in-view은 default로 끄는걸로 설정해두자.

3. Nginx 사용시 주의할 점

이번에 리버스 프록시로 Nginx를 통해 배포를 했었다.

로컬에서는 잘 동작했는데, 배포를 했더니 SSE 통신이 동작하지 않는 문제가 발생했다.

원인은 Nginx는 기본적으로 Upstream으로 요청을 보낼 때 HTTP/1.0 버전을 사용함을 확인했다.

HTTP/1.1은 지속 연결이 기본이기 때문에 헤더를 따로 설정해줄 필요가 없지만, Nginx에서 백엔드 WAS로 요청을 보낼 때는 HTTP/1.0을 사용하고 Connection: close 헤더를 사용하게 된다.

또 Nginx 의 proxy buffering 기능 또한 조심해야 한다.

SSE 통신에서 서버는 기본적으로 응답에 Transfer-Encoding의 헤더 값을 chunked로 사용한다. SSE는 서버에서 동적으로 생성된 컨텐츠를 스트리밍하기 때문에 본문의 크기를 미리 알 수 없기 때문이다.

그래서 Nginx는 서버의 응답을 버퍼에 저장해두었다가 버퍼가 차거나 서버가 응답 데이터를 모두 보내면 클라이언트로 전송하게 된다.

문제는 버퍼링 기능을 활성화 하면 SSE 통신 시 원하는 대로 동작하지 않거나 실시간성이 떨어지게 된다는 것이다. 그렇기에 비활성화 해주는 것으로 결정했다.

추가적으로 버퍼링을 비활성화하면 다른 모든 API 응답에 대해서도 버퍼링을 하지 않기에 비효율적일 수 있다. 백엔드의 응답 헤더에 X-accel로 시작하는 헤더가 있으면 Nginx가 이 정보를 이용해 내부적인 처리를 따로 하도록 만들 수 있다. 그렇기에 SSE 응답을 반환하는 API의 헤더에 X-Accel-Buffering: no 를 붙여주는 SSE 응답만 버퍼링을 하지 않도록 설정할 수 있다고 한다. (이 부분은 추후 확인해보고 적용하는걸로..)

4. 스케일 아웃

이 부분은 아직 직접 격어보진 못했지만, 서버가 늘어나서 로드밸런싱을 적용한다면 제대로 동작하지 않을 것이다. ConcurrentHashMap 자료구조를 사용해 SseEmitter 객체를 서버의 메모리에 저장하고 있기에 각 서버들끼리의 공유가 불가능 할 것이라고 생각한다.

가장 간단히 생각하면 Redis 에 저장하는 것으로 해결할 수 있을 것 같다.


궁금했던 점

SSE가 HTTP의 지속적인 연결을 이용해서 통신하는데, 이때 Spring boot의 Thread를 잡아먹고 있는게 아닌가?? 그럼 Thread 개수 만큼의 사용자만 이용할 수 있는 건가??

물론 당연히 그건 아니겠다 생각했지만, 어떻게 SSE의 지속적인 연결이 Thread를 잡아먹지 않을까 라는 의문이 들었다.

https://www.inflearn.com/community/questions/909440/%EC%A7%80%EC%86%8D-%EC%97%B0%EA%B2%B0%EA%B3%BC-sse

위의 인프런 커뮤니티에서 그 해답을 찾을 수 있었다.

요약하자면

  1. Thread와 Connection은 다른 개념이다. Connection은 말 그대로 연결을 열어주는 통로 역할을 하고, Thread는 작업자라 생각하면 된다.
  2. SSE 에서는 Thread와 Connection과의 연관점이 없다고 볼 수 있다. 하지만, Blocking IO를 사용하는 TCP 통신에서는 연관점이 있을 수도 있다.

얕게나마 사용해본 WebSocket은 내가 구현해본 바로는 내부 Message Broker를 통해 pub/sub 기능을 구현했었다. 이 또한 채팅방을 최대 Thread 개수만큼 만들면 서버가 터지는건가? 라고 생각했었는데, WebSocket 기술도 이처럼 Connection만 사용하고 Thread는 소비하지 않을 것 같다. 이또한 테스트 해봐야 알겠지만..


마무리

SSE 는 서버에서 먼저 정보를 보내야 할때, 나같은 경우 알림을 구현할 때 사용했다. 편리한 기능이지만, 문제가 생길 수 있는 점을 확실히 확실히 체크하고, 추후 확장성을 고려하는 것을 기본 마인드로 갖고 가는게 좋을 것 같다.

이번엔 미리 트러블슈팅을 겪은 다른 개발자분의 지식을 통해 해결하긴 했지만, 추후 처음보는 문제에 대해서도 해결할 수 있는 능력을 기르는 것이 중요하다 생각한다.

참조

profile
공부 정리용

0개의 댓글

Powered by GraphCDN, the GraphQL CDN