관리 메뉴

Mini

24. 12. 3. 개발일지 // 성능개선 본문

개발일지

24. 12. 3. 개발일지 // 성능개선

Mini_96 2024. 12. 27. 01:09

* 성능개선

  • 부하테스트 결과

1차
2차

  • 문제점
    • 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}`
    );
  }

 

이중 필요한 필드(gameId)만 가져오도록 개선

/**
   * 최적화된 플레이어 위치 업데이트 함수
   * @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();
  }

3379 -> 2931 약간의 개선
2900 -> 2700
최종 test

 

* 문제

  • 약 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. 최초 1회 조회: isAlivePlayer 확인을 위한 Redis 호출
  2. 플레이어가 관전자(dead)인 경우:
    • 1회 조회: 전체 플레이어 목록 조회 (players)
    • N회 조회: 각 플레이어마다 isAlive 상태 개별 조회

예를 들어, 게임에 10명의 플레이어가 있다면:

  • 1번 호출: 초기 isAlive 체크
  • 1번 호출: 플레이어 목록 조회
  • 10번 호출: 각 플레이어의 isAlive 상태 체크
  • 총 12번의 Redis 호출 발생

이렇게 수정하면:

  • 네트워크 왕복 횟수: 12회 → 3회로 감소
  • Redis 서버 부하 감소
  • 전체 처리 시간 단축

기존과 차이가 없다..

 

* 현재 구조의 가장 큰 문제

  • 가설1
    • 모든플레이어 들의 상태를 조회하는 부분이 병목일것이다.

바로 return 하도록 하고 임시로 test