백그라운드 작업 #

일반적인 엔지니어링 실무에서 복잡하거나 긴 작업은 예약된 작업으로 백그라운드에서 실행합니다. 이런 방식은 게임 서버 컨텍스트에서 데이터 유지 관리를 수행하거나 특정 시간에 전체 플레이어 기반으로 플레이어 일일 보상을 나눠주는 것과 같은 작업을 실행하는 데 주로 사용됩니다. 이러한 작업은 플레이어 기반이 작을 때는 대체로 빠르고 문제가 없습니다. 그러나 플레이어 기반이 증가하기 시작하면 이러한 작업을 수행하는 데 필요한 시간도 늘어납니다. 이로 인해 하루 중 특정 시간에 서버에 높은 CPU 및 데이터베이스 로드가 급증할 수 있으며, 이로 인해 해당 시간 동안 성능과 플레이어 경험이 떨어질 수 있습니다.

이에 대한 해결책은 대신 JIT(Just-In-Time) 접근 방식을 사용하는 것입니다. 해당 날짜에 게임에 로그인했는지 여부에 관계없이 모든 플레이어가 하루에 100금화를 받는 예를 살펴보겠습니다.

백그라운드 작업 접근 방식 #

백그라운드 작업 접근 방식을 사용하면 플레이어 기반에 가장 영향이 적은 시간(예: 자정)에 작업을 예약하고 모든 플레이어에 대해 반복하고 100금화를 제공하는 작업을 실행합니다.

여기서 합리적인 필터링을 수행하여 특정 기간 내에 활동한 플레이어에 대해서만 반복할 수 있지만, 지금은 최근에 플레이했는지 여부에 관계없이 모든 플레이어에게 금화를 제공하려고 합니다.

최상의 시나리오에서 이 방법은 시간 복잡도의 O(n) 순서가 될 것입니다. 이에 따라 플레이어 기반이 늘어나면 이 작업을 수행하는 데 필요한 시간도 늘어납니다.

상상할 수 있듯이 시간이 지남에 따라 이 작업은 서버의 CPU와 데이터베이스에 점점 더 많은 부담을 줄 것이며 결국 이 시간 동안 플레이어에게는 네트워크 성능이 좋지 않게 됩니다.

JIT 접근 방식 #

매일 자정에 대량의 금화를 배포하는 대신 Just-In-Time 접근 방식은 플레이어가 다음에 게임에 로그인할 때마다 적절한 금화를 배포합니다.

이를 구현하려면 플레이어가 로그인할 때 다음 단계를 수행합니다:

  • 사용자 메타데이터의 속성 또는 저장소 엔진의 값을 통해 플레이어가 마지막으로 로그인한 시간을 확인합니다.
  • 마지막 로그인이 없으면 플레이어에게 100금화를 줍니다.
  • 마지막 로그인이 있다면 플레이어가 마지막으로 로그인한 이후 경과한 시간을 기준으로 계산된 금화를 플레이어에게 제공합니다.
  • 마지막 로그인 타임스탬프를 현재 시간으로 저장

어떤 사용자가 로그인하고 하루 중 몇 시에 로그인하는지에 관계없이 이 작업은 항상 시간 복잡도의 O(1) 순서입니다.

이 접근 방식의 이점은 세 가지입니다:

  1. 이 작업에 대한 CPU/데이터베이스 수요는 작은 청크로 필요할 때 하루 종일 분산됩니다.
  2. 대규모 플레이어 기반을 대상으로 작업을 수행하는 경우와 달리 플레이어는 네트워크 성능 저하의 급증을 경험하지 않습니다.
  3. 게임에 다시 로그인하지 않는 플레이어는 CPU/데이터베이스 처리 시간에 비용을 들이지 않습니다.

JIT 접근 방식을 구현하는 방법 #

매일 자정에 각 플레이어에게 100 금화를 나눠주는 예를 다시 사용하여 위에서 설명한 JIT 접근 방식으로 구현해 보겠습니다.

사용자가 게임에 로그인할 때마다 이를 트리거되도록 하기 위해 사용자가 장치 ID를 사용하여 인증할 때마다 사후 후크를 설정합니다. 이것을 InitModule 함수 안에 등록합니다.

1
2
3
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerAfterAuthenticateDevice(afterAuthenticateDevice)
}

그런 다음 실행할 함수를 정의합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let afterAuthenticateDevice : AfterHookFunction<Session, AuthenticateDeviceRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, session: nkruntime.Session, request: nkruntime.AuthenticateDeviceRequest) {
  // Define a constant for how many milliseconds are in a day
  const msInADay = 1000 * 60 * 60 * 24;

  // Get previous midnight
  const previousMidnight = new Date().setHours(0, 0, 0, 0);

  // Get the user's account so that we can check their metadata for a lastLoginTimestamp
  const account = nk.accountGetId(ctx.userId);
  const lastLoginTimestamp : number = account.user.metadata.lastLoginTimestamp;

  let rewardCoins = 0;

  if (!lastLoginTimestamp) {
    // If the user has never logged in, give them 100 coins
    rewardCoins = 100;
    logger.debug("Player has never logged in before, giving them 100 coins.");
  } else if(lastLoginTimestamp < previousMidnight) {
    // If the user's last login was before midnight, we're going to give them at least 100 coins
    rewardCoins = 100;

    // Calculate how many full days have passed since midnight and the time they last logged in
    const timeBetweenMidnightAndLastLogin = previousMidnight - account.user.metadata.lastLoginTimestamp;
    const totalFullDaysSinceLastLogin = Math.floor(timeBetweenMidnightAndLastLogin / msInADay);

    // Increase the reward by 100 for each full day since last login
    rewardCoins += totalFullDaysSinceLastLogin * 100;

    logger.debug(`Player has logged in for the first time today and it has been a further ${totalFullDaysSinceLastLogin} days; giving them ${rewardCoins} coins.`);
  } else {
    logger.debug("Player has already logged in today, no coins are being rewarded.");
  }

  // Update the user's lastLoginTimestamp
  const metadata = account.user.metadata;
  metadata.lastLoginTimestamp = Date.now();
  nk.accountUpdateId(ctx.userId, null, null, null, null, null, null, metadata);

  // Give the player their coins
  nk.walletUpdate(ctx.userId, { coins: rewardCoins });
};

위의 예에서 볼 수 있듯이 사용자가 로그인하면 afterAuthenticateDevice 함수가 후크로 실행됩니다. 이 함수는 사용자가 마지막으로 로그인한 후 얼마나 지났는지 확인하고(한 번이라도 로그인한 적이 있는 경우) 당일의 첫 번째 로그인에 대해 100개의 코인을 보상하고 마지막 로그인 이후 추가 1일당 100개의 추가 코인을 보상합니다.

이 예제에서는 사용자가 인증한 후 실행되는 후크를 사용하지만 다른 것도 가능합니다. 사용자 세션의 다양한 지점에서 원하는 로직을 수행할 수 있습니다. 사용자가 대체 인증 방법을 연결하기 전이나 친구를 추가한 후를 예로 들 수 있습니다. 사용자가 대신 사용자 지정 RPC 함수를 호출하도록 만들 수도 있습니다. 사용 가능한 후크의 전체 목록은 문서의 메시지 이름 섹션을 참조하세요.