본문 바로가기

사이드 프로젝트

웹 채팅 프로그램 개발 프로젝트 후기 - 3

안녕하세요.

채팅방 퇴장 기능, 성능 분석 등을 마지막으로 진행해보겠습니다.

 

3. 주요 기능 구현 방식

  • 5. 채팅방 퇴장 기능

최초 트랜잭션은 아래의 흐름으로 구현했습니다.

  • ChatRoom 존재여부 확인 : chatRoomRepository.findById
  • MemberChatRoom 삭제 : memberChatRoomRepository.delete(memberId, roomId)
  • ChatRoom에 참여 중인 Member 확인 : memberChatRoomRepository.findByRoomId
  • ChatRoom 참여 중인 Member가 없으면 삭제 : chatRoomRepository.delete(roomId)

 위 트랜잭션은 동시에 실행될 때 Isolation Level 때문에 문제가 발생함을 확인했습니다. MySQL의 기본 Isolation Level은 REPEATABLE READ이고, JPA 또한 영속성 컨텍스트를 통해 어플리케이션 단에서의 REPEATABLE READ 격리 수준을 제공합니다.

문제 상황은 아래와 같습니다.

  • Tr1 : ------------ . . . . -------------------------memberChatRoomRepository.findByRoomId---...----commit
  • Tr2 : -----------memberChatRoomRepository.delete--------- ---------------------------....---------commit

 Tr2에서 MemberChatRoom을 삭제하여도 이미 실행 중인 Tr1 트랜잭션에서는 해당 변경이 반영되지 않아 Phantom Read가 발생합니다. 현재 참여 중인 사용자를 조회하는 메소드 memberChatRoomRepository.findByRoomId가 어려움을 주고 있었습니다...

 

해결을 위해 생각해본 방법은 아래와 같습니다.

  • @Transactional 제거
    • 위 트랜잭션에서 @Transactional 어노테이션을 제거하여 단계별로 독립적인 트랜잭션으로 진행되도록 함
    • DML 관련 작업 실패 시 롤백이 필요하므로 채택하지 않았습니다.
  • 낙관적 락 또는 비관적 락
    • 관련 개념 정리 링크 : https://github.com/chrismrkr/WIL/blob/main/JPA/JPASummary.md#7-트랜잭션과-락-2차-캐시
    • Pessimistic_read Lock 상태에서의 현재 트랜잭션이 테이블 특정 Row를 Read 중이라면, 다른 트랜잭션은 해당 Row를 Write할 수 없습니다.
    • 트랜잭션 흐름을 아래와 같이 변경하고, Pessimistic Read를 추가했습니다.
      • ChatRoom 존재 여부 확인 : chatRoomRepository.findById
      • ChatRoom에 참여 중인 Member 확인 : memberChatRoomRepository.findByRoomId
        • 이 시점에 Pessimistic_read Lock을 걸어서 동시 트랜잭션을 제어하는 것을 목표하였음
      • MemberChatRoom 삭제 : memberChatRoomRepository.delete(memberId, roomId)
      • ChatRoom 참여 중인 Member가 없으면 삭제 : chatRoomRepository.delete(roomId)
    • 하지만, Dead Lock이 발생하는 문제가 발생하여 채택하지 않았습니다.
    • Trx1의 A. Pessismistic Read가 Trx2의 D. delete를 Block하고, Trx2의 C.Pessimistic Read가 Trx1의 B. Delete를 Block 하므로 Dead Lock이 발생합니다.

Pessimistic_read에서의 Dead Lock

  • 외래 키 제약 조건
    • ChatRoom과 MemberChatRoom은 1:N 관계에 있습니다.
    • ChatRoom 삭제 시, 매핑된 MemberChatRoom이 남아있으면 지우지 못하도록 외래키 제약 조건을 테이블에 추가할 수 있습니다.
    • 이를 통해 MemberChatRoom 엔티티를 RoomId로 조회하는 과정을 제거하므로 앞서 발생한 모든 문제를 피할 수 있습니다.
    • 다만, 채팅방을 퇴장할 때 마다 ChatRoom을 삭제하는 메소드가 동작하고, 이는 외래키 제약 조건을 발동한다는 단점이 있습니다.
  • 일부 프로세스 비동기 처리 (채택)
    • 정리하자면, MemberChatRoom 엔티티를 조회하는 메소드가 동일한 트랜잭션 내에서 Phantom Read 또는 Dead Lock을 일으키는 것이 문제입니다.
    • 이에 트랜잭션을 아래와 같이 분리해보았습니다.
      • 1. ChatRoom 존재 여부 확인 -> MemberChatRoom 삭제 
      • 2. ChatRoom에 참여 중인 Member 확인 -> 존재하지 않으면 ChatRoom 삭제 (비동기 처리)
    • 첫번째 과정을 통해 MemberChatRoom 테이블의 데이터 삭제까지는 완료되고 Commit 됩니다.
    • 채팅방이 삭제된 상태란 ChatRoom 엔티티가 실제로 없거나 ChatRoom 엔티티에 매핑된 MemberChatRoom이 0개일 때 입니다.
    • 그러므로, 사용자가 채팅방에 입장할 때는 ChatRoom이 존재하더라도 MemberChatRoom이 0개이면 채팅방이 삭제된 것으로 인식하도록 만들면 됩니다.
    • 이러한 경우에는 두번째 과정이 첫번째와 동일한 트랜잭션에서 진행된 필요가 없으므로 비동기 처리할 수 있습니다. 해당 방법으로 정상적으로 처리됨을 확인했습니다.
    • 비동기 처리를 위해 이벤트 브로커 Kafka를 활용했고, 반드시 실행될 것이 보장되어야 하므로 아래와 같이 설정했습니다. 
      • Producer: Ack=all, idempotence=true
      • Consumer: Manual Ack
    • Kafka 관련 정리 내용 : https://github.com/chrismrkr/WIL/blob/main/infrastructure/Kafka/kafka-summary.md
 

WIL/infrastructure/Kafka/kafka-summary.md at main · chrismrkr/WIL

what I Learned. Contribute to chrismrkr/WIL development by creating an account on GitHub.

github.com

 

 

4. 성능 분석

AWS EC2에서 아래 성능을 가진 가상 머신을 생성하여 성능 테스트를 실시하였습니다.

  • CPU: 4 Core CPU
  • Memory: 8GB
  • DISK: 30GB

 우선 얼마 만큼의 트래픽을 서버가 최대로 받을 수 있는지 확인하는 것이 필요했습니다. JMeter를 통해 100RPS에서 시작하여 점진적으로 RPS 높이면서 부하 테스트를 진행했습니다.

 2000RPS 까지는 에러율이 0% 였으나, 2000RPS를 초과하면서 부터 점차 에러율이 증가하기 시작했습니다. WAS에서 발생하는 에러는 확인이 되지 않았으나 Nginx에서 아래와 같은 에러를 발견하였습니다.

  • worker_connections are not enough, reusing connection
  • worker_connections are not enough, while connecting to upstream

클라이언트의 요청을 처리하기 위한 커넥션이 부족한 것으로 판단했습니다. 확인해본 내용은 아래와 같습니다.

  • Nginx Worker_connection
    • worker_process: 4, connection: 1024 -> 총 4096 connection 생성 가능
  • 파일 디스크립터 개수
    • cat /proc/sys/fs/file-ns : 충분한 수치로 확인하였음

 2000개의 클라이언트 요청 시, 클라이언트 <-> Nginx, 그리고 Nginx <-> WAS 사이 2가지 Connection이 필요하므로 총 4000 Connection이 필요합니다. 그러므로, 2000RPS를 초과할수록 에러율이 증가하는 현상은 현재 설정에서는 정상적인 상황으로 판단했습니다.

  Nginx 파라미터를 조정하지 않는 한, 서버에서 받을 수 있는 최대 트래픽은 2000RPS로 결정하였고, 해당 기준에 맞추어 모든 주요 기능을 테스트한 결과, 평균 1000ms 이내로 요청을 에러 없이 처리함을 확인했습니다.