art run 맵매칭 개발기 (2/2)

art run 맵매칭 개발기 (2/2)

Tags
Socket
Kafka
redis
Published
Apr 19, 2022
Property
해당 기능의 코드는 모두 github에서 확인할 수 있습니다!

intro

아티클의 작성 목적은 기술 사용의 근거들에 대해 다시 정리해보는 것이다.
면접 때 많이 물어보는데 매끄럽게 대답하지 못했던 것이 아쉽다.

변경사항

message broadcaster 역할을 redis에 맡기다.

메시지 브로드캐스팅을 위한 서비스로는 redis를 사용하기로 했다.
달리기를 할 때마다 kafka에서 토픽을 즉시 생성한 뒤, 달리기가 끝나면 토픽을 제거하는 식으로 개발할 계획이었다. 그러나, 마땅한 레퍼런스가 없었는데, kafka는 dynamic topic이 권장되지 않기 때문이었다. 간단히 설명하자면 메모리 관리 이슈 때문에 그렇다고 한다. Kafka의 파티션은 쉽게 축소할 수 없다. 구조에 대해 좀 더 깊이 공부할 필요가 있을 것 같다.

consume을 담당하는 worker를 분리하다.

맵매칭을 담당하는 별도의 worker를 분리하기로 했다.
메시지 서버를 두었기 때문에 가능한 확장이다. pub/sub을 분리하므로써, 높은 Scalability를 획득할 수 있다.

동작 성공!

http://jxy.me/websocket-debug-tool/ 를 통해 간단하게 테스트해본 결과, 개발 서버에서 의도한 대로 작동함을 확인하였다.
notion image
위도, 경도가 함께 전달한 경로값을 기준으로 보정된 것을 확인할 수 있다.
위도, 경도가 함께 전달한 경로값을 기준으로 보정된 것을 확인할 수 있다.

총정리

맵매칭 시퀀스 다이어그램
맵매칭 시퀀스 다이어그램
  1. 달리기 시작 단계에서 최초 1번 socket 연결과 해당 routeId에 대한 redis 구독을 한다.
  1. GPS 보정 요청을 socket을 통해 전송한다. socket은 메시지를 받으면 kafka match.req 토픽에 전송한다.
    1. 워커에서는 match.req 토픽에서 메시지를 consume하여 GPS 값을 보정한 뒤, 워커에서 match.res 토픽으로 전송한다.
  1. 서버는 Kafka match.res 토픽을 구독하고 있다가, 메시지가 오면 consume 한 뒤 routeId의 redis 토픽에 메시지를 그대로 전송한다.
    1. redis는 구독하고 있는 서버(1번에서 구독한 서버)에 보정값을 전송하고, 해당 서버는 socket으로 클라이언트에게 결과를 전달한다.
 
맵매칭 기능 아키텍처 다이어그램
맵매칭 기능 아키텍처 다이어그램
이해하기 쉽게 흐름도를 하나 더 만들었다. 로드밸런싱에 대한 내용은 생략되어 있다.
[검정색] Client들은 Load Balancing 되어 Sticky하게 각각의 서버에 붙어있다.
[보라색] 웹소켓을 통해 전송된 요청은 Kafka를 거쳐서 워커에 (하나의 컨슈머 그룹) 도착한다.
[노란색] 워커에서는 보정 처리를 하여 Kafka로 전송한다. 서버는 (하나의 컨슈머 그룹) Kafka를 통해 보정값을 받는다. 이 때, 보정값을 받는 서버는 알 수 없다. (이를 해결하기 위해 redis가 있다.)
[빨간색] 서버는 redis의 routeId 토픽에 메시지를 그대로 전송한다. 그러면 이를 구독하고 있던 서버에서 읽을 수 있다.
[검정색] 받은 결과를 다시 클라이언트에 돌려준다.
 

왜 이렇게 만들었나요?

설계하는 것도 어렵지만 설명하는 건 더 어려운 것 같아서 궁금할 만한 포인트들을 간단하게 Q&A 형식으로 정리해보았다.

Q. Kafka를 왜 사용했나요?

Kafka를 메시지 브로커 목적으로 사용했다. (카카오 모빌리티 맵매칭 기능 레퍼런스) 프로젝트에서 로그 수집 겸용으로 Kafka를 선택하였는데, 결과적으로 로그 용도로는 쓰지는 않았다. 메시지 큐 목적이었기 때문에 다른 메시지 큐 서비스로 대체할 수도 있다.(RabbitMQ, ActiveMQ 등)

Q. Redis를 왜 사용했나요? (Kafka만 써도 되지 않나요?)

Kafka는 Dynamic Topic 생성이 권장되지 않는다. (위의 설명 참조)
Redis는 pub/sub 기능을 사용하여 메시지 브로커로 이용하였다. (카카오톡 실시간 댓글 아키텍처 레퍼런스)

Q. Dynamic Topic 생성이 왜 필요한가요?

단일 Consumer Group이기 때문에, match.res를 원하는 서버에서 Consume하지 못할 수 있다.
보정 응답을 타겟 서버로 전달해주어야 하기 때문에, 시작 단계에서 redis pub/sub 파이프라인을 구축해놓고 사용한다.
(나름대로 해당 방식이 가장 간단하다고 생각하였는데, 더 좋은 방법이 있다면 알고 싶다!)

Q. Web Socket 연결이 한 서버로 몰리지는 않습니까?

GCP에서 LoadBalancer를 설정했기 때문에 분산이 이루어지고 있다. sessionAffinity를 설정하여 클라이언트는 지속적으로 WebSocket 연결이 가능하다.

Q. Redis Kafka 없이도 기능 구현이 가능할 것 같은데 왜 이렇게 해야하는가?

메시지 서버 없이 Server, Worker 간 단순 HTTP 통신을 할 경우 Blocking이 생길 수 밖에 없다.
메시지 서버가 있어서 Non-Blocking 처리가 가능하다.

Q. Kafka만 장애상황일 때 어떻게 되나요?

장애 케이스까지 구현하지는 않았지만, 클라이언트에서 쌓아두고 있다가 Kafka가 정상화 되면 서버로 보내주도록 하면 좋을 것 같다!
 

여전히 의문이 남는 점

테스트

이번 프로젝트에서 가장 쓰는 기술도 많고, 의존성이 큰 기능이다. 테스트 코드를 짜기도 어렵고, 테스트를 하더라도 비즈니스 로직이 아니라, (이미 잘 작동하는 것이 보장되어 있는) 라이브러리를 테스트하는 코드를 쓰게 되는 것 같다. 실제 서비스에서 어떻게들 테스트 코드를 쓰는지 궁금하다. @MessageMapping 의 경우도 테스트 할 수 있는 방법이 현재로서 없는 듯 하다.

Kafka에 대한 이해

Kafka를 단순히 메시지큐라고 생각하고 썼기 때문에, 아직 Kafka를 쓸 줄 안다고 말하기는 어려울 것 같다. 메시지 브로커와 이벤트 브로커의 차이, 파티션 등 공부할 부분이 꽤 많아 보인다. 더 동시성이 중요한, 순차 처리가 중요한 기능을 개발하게 된다면 기술에 대한 더 깊이 있는 이해가 필요할 것 같다.