안녕하세요.
채팅방 퇴장 기능, 성능 분석에 대해서 정리하였습니다.
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이 발생합니다.
- 외래 키 제약 조건
- 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를 활용하는 것은 오버 엔지니어링이라고 판단하였습니다.
- 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
- @Transactional Propagation 속성을 활용한 트랜잭션 분리(채택)
- @Transactional Propagation 속성 중 REQUIRED_NEW를 활용하여 트랜잭션을 2개로 나누어 처리했습니다.
- 이를 통해 1번째 트랜잭션이 완료되면 커밋하고, 2번째 트랜잭션 실행할 때 새로운 스냅샷을 불러오도록 하였습니다.
- 물론, 2번째 트랜잭션이 실패하는 경우를 대비하여 Rollback을 위한 보상 트랜잭션을 구현하였습니다.
- Spring Framework @Transactional Propagation 속성 관련 내용 : https://github.com/chrismrkr/WIL/blob/main/JPA/JPASummary.md#71-transactional
WIL/JPA/JPASummary.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 이내로 요청을 에러 없이 처리함을 확인했습니다.
'사이드 프로젝트 > 채팅 프로그램 개발 프로젝트' 카테고리의 다른 글
웹 채팅 프로그램 개발 프로젝트 후기 - 1 (3) | 2024.12.02 |
---|---|
웹 채팅 프로그램 개발 프로젝트 후기 - 4 (2) | 2024.11.24 |
웹 채팅 프로그램 개발 프로젝트 후기 - 2 (2) | 2024.11.23 |