* 성능개선
- 부하테스트 결과
- 문제점
- updatePosition, chatMessage의 응답시간이 매우 높다.
- 일단 updatePosition부터 개선해보자
- 원인가설
- player 필드를 가져올때, hgetall로 필요없는 필드까지 가져와서 느려진다.
- pipeline을 사용하지않고, 네트워크를 여러번 왕복하기때문에 느려진다.
//기존코드
async updatePosition(updatePosition: UpdatePositionDto, clientId: string) {
const { gameId, newPosition } = updatePosition;
const playerKey = REDIS_KEY.PLAYER(clientId);
const player = await this.redis.hgetall(playerKey);
this.gameValidator.validatePlayerInRoom(SocketEvents.UPDATE_POSITION, gameId, player);
await this.redis.set(`${playerKey}:Changes`, 'Position');
await this.redis.hset(playerKey, {
positionX: newPosition[0].toString(),
positionY: newPosition[1].toString()
});
this.logger.verbose(
`플레이어 위치 업데이트: ${gameId} - ${clientId} (${player.playerName}) = ${newPosition}`
);
}
/**
* 최적화된 플레이어 위치 업데이트 함수
* @param updatePosition - 업데이트할 위치 정보
* @param clientId - 플레이어 ID
* @returns Promise<void>
* @throws {Error} 플레이어가 게임에 속해있지 않은 경우
* @example
* await updatePosition({ gameId: '123', newPosition: [1, 2] }, 'player1');
*/
async updatePosition(updatePosition: UpdatePositionDto, clientId: string): Promise<void> {
const { gameId, newPosition } = updatePosition;
const playerKey = REDIS_KEY.PLAYER(clientId);
// 1. 먼저 검증
const playerGameId = await this.redis.hget(playerKey, 'gameId');
this.gameValidator.validatePlayerInRoomV2(
SocketEvents.UPDATE_POSITION,
gameId,
playerGameId?.toString()
);
// 2. 검증 통과 후 업데이트 수행
const pipeline = this.redis.pipeline();
pipeline.set(`${playerKey}:Changes`, 'Position');
pipeline.hmset(playerKey, {
positionX: newPosition[0].toString(),
positionY: newPosition[1].toString()
});
await pipeline.exec();
}
* 문제
- 약 20%의 성능개선(3300 ms-> 2800ms)을 얻었지만 여전히 부족하다.
- 추정원인
- Player의 변화를 구독받는 부분에 문제가 있을것이다.
- 먼저 현재 구현부를 시각화 해보자
private async handlePlayerPosition(playerId: string, playerData: any, server: Namespace) {
// ... 초기 코드 생략 ...
// 1️⃣ 첫 번째 Redis 호출
const isAlivePlayer = await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'isAlive');
if (isAlivePlayer === '1') {
server.to(gameId).emit(SocketEvents.UPDATE_POSITION, updateData);
} else if (isAlivePlayer === '0') {
// 2️⃣ 두 번째 Redis 호출
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
// 3️⃣ N번의 추가 Redis 호출 (N+1 문제 발생 지점)
const deadPlayers = await Promise.all(
players.map(async (id) => {
const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive');
return { id, isAlive };
})
);
// ... 이후 코드 생략 ...
}
}
N+1 문제가 발생하는 흐름입니다:
- 최초 1회 조회: isAlivePlayer 확인을 위한 Redis 호출
- 플레이어가 관전자(dead)인 경우:
- 1회 조회: 전체 플레이어 목록 조회 (players)
- N회 조회: 각 플레이어마다 isAlive 상태 개별 조회
예를 들어, 게임에 10명의 플레이어가 있다면:
- 1번 호출: 초기 isAlive 체크
- 1번 호출: 플레이어 목록 조회
- 10번 호출: 각 플레이어의 isAlive 상태 체크
- 총 12번의 Redis 호출 발생
이렇게 수정하면:
- 네트워크 왕복 횟수: 12회 → 3회로 감소
- Redis 서버 부하 감소
- 전체 처리 시간 단축
* 현재 구조의 가장 큰 문제
- 가설1
- 모든플레이어 들의 상태를 조회하는 부분이 병목일것이다.
'개발일지' 카테고리의 다른 글
24. 11. 28. 개발일지 // hotfix (0) | 2024.12.27 |
---|---|
24. 11. 27. 개발일지 // jest --runInBand --detectOpenHandles --forceExit 오류 해결, socket io handshake error (0) | 2024.11.28 |
24. 11. 26. 개발일지 // 모니터링 구현, rss, heap, admin ui , namespace vs server (0) | 2024.11.27 |
24. 11 . 25. 개발일지 // 강퇴구현, 핀포인트 도입 (0) | 2024.11.26 |
24. 11. 21. 개발일지 // 자동배포 수정, 인터셉터 버그수정(redis event trigger 이해) (0) | 2024.11.22 |