Kafka, Redis, Web Socket, Stomp 를 활용한 채팅 서버 회고

강혜성·2023년 2월 20일
4

ZZALU 프로젝트

목록 보기
10/11

기술 적용 이유

  • 채팅 이라는 것은 우리에게 익숙하고 기술적으로도 많이 알려져 있어 기술적으로 우위를 가져가기 어렵다는 생각이 들었다.

  • 따라서 기술적인 도전을 위해 대용량, 확장성, 안정성 이라는 키워드를 가지고 아키텍처를 설계했다.

Web Socket, Stomp

  • 웹 소켓 통신을 하기 위해서 가장 기초가 되는 Web Socket과 STOMP이다.

  • Web Socket은 서버와의 Socket 연결을 담당하고 STOMP는 메세지를 관리하고 PUB/SUB 기능을 제공해 특정 토픽을 구독 하고 있는 사용자는 특정 토픽의 발행자가 하는 메시지를 수신할 수 있다.

  • 채팅의 경우 채팅을 보내는 사람이 발행자(PUB)가 되고 채팅을 받는 사람이(SUB)가 된다.

  • 간단한 만큼 문제점이 존재한다. 서로 다른 서버가 있을 경우 채팅이 되지 않는다. 동일한 서버 2개; A 서버 B 서버가 있다면 A서버의 사용자가 채팅을 보내면 (PUB) 해당 서버에서 구독하고 있는 사용자들만 수신한다. 따라서 같은 채팅방일지라도 서버가 다르면 메세지를 정상적으로 수신할 수 없다.

  • 이는 우리가 처음 생각했던 대용량, 확장성 이라는 키워드를 만족시키지 못했다.

Redis

  • REDIS는 No Sql DB, In-Memory, PUB/SUB 등 다양하게 사용된다. 우선적으로 서로 다른 서버에서의 채팅 메시지를 수신하기 위해서 사용한 방법을 설명한다.(PUB/SUB)

  • 한 채팅방에 2명의 사용자가 채팅을 하고 있다고 가정한다. A 사용자는 A 서버에 접속 중이고 B 사용자는 B 서버에 접속하고 있는 중이다.
    Web Socket과 STOMP만을 사용한다면 A 서버 사용자가 메세지를 보내도 B 서버 사용자는 수신할 수 없다. 반대도 마찬가지다.

  • REDIS DB(Server)의 PUB/SUB을 이용한다면 이를 해결 할 수 있다. STOMP가 서버 내에서(사용자 끼리) PUB/SUB를 담당한다면 REDIS는 서버 외부에서(서버 끼리) PUB/SUB를 담당한다.

  • 채팅방에서 A 사용자가 메세지를 전송하면 STOMP를 이용하여 서버에서 다시 REDIS로 발행한다. REDIS에서는 이를 구독하고 있는 다른 서비스들에 이를 발행하고 발행된 결과를 STOMP가 다시 PUB을 하게 된다면 서로 다른 서버에 있는 모든 사용자가 SUB(수신)을 할 수 있다.

  • 이렇게 된다면 정상적으로 서비스가 수행되는 것 처럼 보이지만 단점이 있다.

  • REDIS 서버가 죽어버리면 안에 있는 내용이 위험하고 채팅방의 채팅이 많아질 경우 채팅의 순서 보장 및 유실 가능성이 커진다.
    이는 우리가 처음 생각했던 안정성이란 키워드를 만족시키지 못했다.

Kafka

  • Web Socket과 STOMP를 이용해서 서버와 클라이언트간 소켓 연결을 활성화 하고, 서버 내부에서 사용자들간 메세지를 송수신 했다.

  • Redis를 이용하여 다른 서버들과 통신을 할 수 있게 했으며, 채팅 내역을 채팅 내역을 불러올 때 Redis에도 저장해 Key-Value 구조의 빠른 read를 사용했다.
    여기까지만 보면 정상적인 구동 시 채팅 서버가 원활하게 돌아감을 알 수 있다.

  • 하지만 서버에 문제가 발생했을 경우 Redis는 순차 처리를 보장하지 않고 메세지의 유실이 생길 수 있다. 이를 해결하기 위해 Kafka를 사용했다.

  • Kafka가 어떻게 적용되었는지 설명하기 전에 우선적으로 Kafka가 무엇인지 설명을 한다.

  • Kafka는 PUB/SUB 모델의 메시지 큐로 분산환경에 특화되어 있는 특징을 가지고 있는 서버이다.

  • 위의 정리를 이용해 단순하게 설명을 하자면 다수의 서버가 하나의 서비스를 이루고 있는 경우 kafka를 이용하여 분산처리를 하는데 각 서버가 통신하는 방식은 PUB/SUB를 이용하고, 이 PUB/SUB 메세지들은 메시지 큐의 형태로 순차적으로 처리된다. 그리고 모든 메세지들은 로그로 처리된다. 위의 특성을 이용한다면 서비스의 안정성을 확보할 수 있다.
    처리 방식은 다음과 같다.

  • A 사용자가 채팅방에 메세지를 보낼경우 이를 A 서버가 Kafka에 지정된 Topic으로 Kafka Producer를 이용하여 PUB을 한다. PUB을 하게 되면 Kafka서버에 전송이되고 Kafka는 연결되어 있는 Consumer(Kafka Listener)에게 해당 메세지를 보낸다. 수신 받은 서버 측에서는 특정 토픽에 대한 메세지를 받았을 때, 처리해야 할 로직을 호출하여 처리한다.
    채팅의 경우 수신 받은 메세지를 이용하여 Redis로 발행해 모든 서버가 공유하게 하고 이후 STOMP를 이용하여 해당 서버에 있는 모든 사용자와 공유한다.

  • 이러한 구조로 모든 채팅 메세지들은 Kafka의 메시지 큐를 이용하여 순차적인 처리를 보장받을 수 있으며 모든 내역을 로그로 기록되기 때문에 유실이 되더라도 복구할 수 있으며, Consumer에 장애가 생길경우 Kafka가 해당 메시지를 다른 Consumer에게 보낼 수 있고, 처리할 수 있는 Consumer가 없을 경우에는 Kafka가 해당 메세지를 가지고 있다 Consumer가 복구되는데로 처리되어 안정성을 확보할 수 있다.

  • 또한 Kafka는 Producer와 Consumer가 분리되어 있기 때문에 확장성이 보장받는다. 서버의 확장이 필요할 때, 확장된 서버에 Producer와 Consumer를 설정하고 처리 로직만 추가하면 되기 때문에 확장에도 유리하다.

  • 이러한 장점으로 인해 Kafka는 Event 기반 실시간 처리가 필요한 대규모 실시간 서비스 (카카오 댓글, LINE 등)에 사용되며 첫 설계 시 생각했던 안정성, 확장성을 가진 Event 기반 대규모 실시간 채팅 서버를 개발 할 수 있었다.

CI/CD

빌드/ 배포 자동화 (CI/CD) Continuous Integration/ Continuous Delivery

CI/CD 설계

  • FE / BE 협업 구조 상 BE가 생성한 API들을 FE가 받아서 사용해야 하며 수정 또는 새로운 API 생성 시 서버에 계속해서 반영되어야 하며, DB를 모두 공유해야 유기적인 연결이 가능하므로 개발 서버가 필요했고, 실제 FE / BE가 테스트할 수 있는 통합 테스트 서버가 필요해 2개의 개발용 서버를 구축했다.

실제 설계

  • 첫 번째 API용 서버로는 Git Lab의 BE 브랜치를 이용하여 구축했다. EC2안에 Docker를 설치하고, Docker를 이용하여 Jenkins를 설치한다. 이후 Jenkins의 Pipeline을 이용해 Git Lab BE 브랜치를 클론하고 이를 Gralde을 이용해 빌드하여 SSHPublisher를 이용해 EC2에 전송하고 실행한다. Webhook을 이용해 Git Lab repo에 Push, Merge가 일어날 때 마다 자동 빌드되므로 BE 개발 사항을 빠르게 반영할 수 있었다.
  • 두 번째 통합 테스트 용 서버로는 Git Lab의 Dev 브랜치를 이용하여 구축했다. 마찬가지로 Jenkins Pipeline을 이용하여 Git Lab Dev 브랜치를 클론하고 FE는 Npm을 이용하여 빌드하고 빌드된 dist 파일을 back에 이동시킨후 Gradle을 이용해 빌드했고 빌드된 결과를 SSHPublisher를 이용해 EC2에 전송하고 실행한다. 마찬가지로 Push, Merge가 일어날 때 마다 자동 빌드되므로 테스트가 필요할 때 마다 BE, FE를 Dev에 머지하여 테스트가 가능했다.
  • 추가적으로 빌드완료 시 Mattermost에 빌드 완료 메세지를 보내도록 구성했다.

API Script

Giphy Script 작성

  • Giphy API를 이용해서 불러오는 것 보다 미리 호출한 Giphy API 결과를 DB에 저장하고 이를 이용하는 것이 성능에 좋다는 판단. API를 호출하기 위해서 Python을 사용했다.
  • 노션에 작성했던 태그들을 기반으로 API를 호출하고 검색한 상위 태그를 모두 담아야 했다. #을 이용하여 상위 태그를 구분하고 -를 이용하여 태그의 끝임을 구분하여 검색 태그들이 변하더라도 Script를 수정하지 않아도 되게 구성했다.
  • DB에 저장할 때는 pymysql을 이용하여 Connection을 유지하였고 이미 있는 데이터인지 판단하는 구문, 업데이트 구문, 삽입 구문을 각 각 상황에 맞게 사용해 에러 발생을 줄였다.

시스템 구성

  • Server 구조
    1. EC2에는 Docker가 설치되어 있다. 설치되어 있는 항목은 다음과 같다.
      1. Jenkins
      2. Redis
      3. Kafka/Zookeeper
      4. MariaDB
    2. Jenkins는 Git Lab의 main, dev, be 브랜치와 Web Hook 연결이 되어 있으며 Push 또는 Merge가 발생할 경우 해당 브랜치를 빌드하고 빌드 결과를 EC2에 SSHPublisher를 이용하여 전송한다.
    3. 이후 nohup java -jar 명령어를 이용하여 Tomcat을 실행시킨다.

  • 채팅 메세지 Kafka, Redis 적용 구조
    1. 사용자가 Action 또는 Event를 발생 시켰을 경우 해당 서버의 Kafka Producer가 해당 Topic으로 발행한다.
    2. Kafka Server는 이를 수신하고 Kafka Consumer에 전달한다.
    3. Redis Publisher에 Message를 Consume 할 수 있도록 설정한다.
    4. Redis Publisher는 Redis에 발행하고 (ConvertAndSend) 이후 모든 구독자에게 해당 메세지가 전달된다.
    5. 그림에는 생략되어 있지만 Web Socket과 STOMP가 사용되었다.

0개의 댓글