Push 알림 기능 개발기 (3) ‐ 다중 서버를 고려한 SSE 개선 with Redis - YJGwon/connectruck GitHub Wiki
2편에서 언급했듯이 SseEmitter
instance의 method 호출로 event를 발송하기 때문에 다중 서버 환경에서는 문제가 발생한다.

Client가 1번 WAS와 SSE 연결을 맺었는데 2번 WAS에 주문이 발생한 경우다. 2번 WAS 입장에서는 알림을 보낼 SSE 연결이 없기 때문에 알림을 보내지 않는다. 1번 WAS는 주문이 발생한 것을 몰라서 알림을 보낼 수 없다.

client는 항상 활성화된 서버와 SSE 연결을 유지하기 위해 주기적으로 heart beat를 체크하고 재연결 요청을 보낸다. timeout이 발생했을 때에도 마찬가지다. 재연결이 발생했을 때, 그 찰나에 이벤트가 발생할 수 있다. 이러한 이벤트들의 유실을 방지하기 위해 client에서는 Last-Event-Id 값을 보내주게 된다.
처음 개발할 당시에는 다중 서버를 고려하지 않고 간략하게 구현했기 때문에 주문 이벤트 보관을 server에서 in-memory로 처리했다. 이를 개선하지 않으면 다중 서버 환경에서는 위와 같이 유실 이벤트가 발생한다. 재연결 요청을 2번 WAS에게 보내는 사이 1번 WAS가 주문을 받았다면 그 이벤트는 영영 전달받지 못하는 이벤트가 되어버린다.

2번 WAS와의 SSE연결을 통해 새로운 주문을 전달받았다면, 다음에 다시 1번 WAS에 재연결 요청을 보내게 되더라도 그 전에 유실되었던 이벤트가 전송되지는 않는다.
1번의 서버 간 이벤트 동기화 문제를 해결하기 위해서는 Publish/Subscribe Model의 도입을 고려해볼 수 있다.

주문 발생 시 모든 서버가 이 사실을 공유해야 한다. 해당 주문의 알림을 보낼 SSE 연결을 가진 서버가 있다면 event를 발송할 수 있을 것이다. 위 그림과 같은 통신 방식을 Publish/Subscribe Model
이라고 한다.
-
수신자를 특정하지 않는 비동기 메시징 패러다임
-
정해진 범주가 있고, 수신자는 특정 범주에 대해 구독을 신청하여 메시지를 전달받음
publisher는 약속된 방식으로 이벤트를 공표한다. 이벤트를 수신하기를 원한다면 알아서 와서 기다리고 있다가(subscribe) 알아서 듣고 할 일을 하면 된다. publisher는 subscriber를 신경 쓰지 않는다.
주문 발생 이벤트의 경우 어떤 서버가 sse 연결을 가지고 있을지 모른다. 때문에 특정 서버가 수신해야 하는데 발신하는 입장에서 수신자를 특정할 수는 없는 상황
이 되어버린다. 이럴 때 pub-sub model이 요긴하다. 주문 발생한 사실을 publish하고 각 서버가 알아서 sse 전송을 처리하게 할 수 있다.
pub-sub model을 구현하는 방식에는 여러가지가 있지만 나는 Redis Pub/Sub
을 선택했다. 주문 알림은 연성 real-time
이라고 생각했기 때문이다. mq 중 가장 대표적이라 할 수 있는 kafka의 경우 consumer가 일정 주기로 message를 조회해가는 polling 방식이다. 반면 Redis Pub/Sub
은 이벤트가 발생하는 즉시 consumer에게 알리는 push 방식으로 동작한다.
물론 높은 실시간성을 제공하는 대신 단점도 있다. Redis Pub/Sub의 치명적인 단점은 이벤트를 보관하지 않는다는 것이다. 이벤트가 push 될 때 활성 상태인 subscriber가 없다면 그 이벤트는 그대로 사라져버린다(파스스….).
이런 치명적인 단점에도 불구하고 선택한 이유는 위의 유실 이벤트 처리와도 연관이 있다. server간 유실 이벤트를 공유할 방안이 있어야 하는데, DB를 뒤져서 주문 데이터를 찾아내도 되지만 reconnection 할 때 마다 일정 기준 이후의 값을 탐색 하는 것은 부담스러운 작업이다. 따라서 분산 cache가 하나 있어야겠다고 생각을 했던 참이었다. Redis pub/sub
가 발송한 메시지가 유실되더라도 주문을 받은 WAS에서 cache한 유실 이벤트 정보가 메시지를 보관한 것과 같은 효과를 낼 수 있다고 생각했다.

원래 event를 수신했어야 했던 Server가 장애가 나서 이벤트가 유실된 경우를 상상해봤다. 이런 경우에는 redis pub/sub의 높은 실시간성을 누리기 어려워진다. 그러나 polling 방식과 비교해 1. 안정적인 대신 평소에 polling 주기만큼 delay
2. 평소에 완전 실시간인 대신 장애 상황에서는 client의 heartbeat check 주기만큼 지연
이 둘 중 뭐가 나을까 생각해봤을 때 주문 알림이라면 2번이 낫지 않을까 생각했다.
물론 Redis는 여전히 SPOF가 될 수 있다. 그나마 다행인것은 Redis cluster환경에서도 pub/sub이 지원된다는 점이다. 이와 관련해서도 조사한 내용들이 좀 있는데, Redis pub/sub과 관련된 더 많은 내용은 별도의 글로 다루도록 하겠다.