Tutorial: Using Memorystore for Redis as a game leaderboard


This tutorial shows you how to use Memorystore for Redis to build an ASP.NET-based leaderboard application running on Google Kubernetes Engine (GKE), and then post and retrieve scores using a separate JavaScript-based sample game. This document is intended for game developers who want to operate their own leaderboards in the cloud.

Introduction

Leaderboards are a common feature in multiplayer games. A leaderboard shows player rankings in real time, which helps encourage players to be more engaged in the game. To capture player actions and rank the score in real time, an in-memory database like Redis is a great choice.

The following diagram shows the infrastructure of the leaderboard:

Diagram showing GKE cluster
inside a VPC, and a separate Memorystore for Redis instance.

In Google Cloud, the leaderboard consists of a GKE cluster inside a VPC network, and a separate Memorystore for Redis instance.

The following diagram shows the application architecture for this tutorial. Clients use the leaderboard API to interact with the scores that are maintained in a Memorystore for Redis instance running in Google Cloud.

Diagram showing the architecture for the application in this tutorial

Methods of the leaderboard API

The API for the leaderboard application includes the following methods:

  • PostScore(string playerName, double score). This method posts a score to the leaderboard for the specified player.
  • RetrieveScores(string centerKey, int offset, int numScores). This method downloads a set of scores. If you pass a player ID as the value for centerKey, the method returns the scores that are above and below those of the specified player. If you don't pass a value for centerKey, the method returns the top N absolute scores, where N is the value you pass in numScores. For example, to get the top 10 scores, call RetrieveScores('', 0, 10). To get 5 scores above and below a player's score, call RetrieveScores('player1', -5, 10).

The code repository for the example includes a mock game and a proof-of-concept leaderboard implementation. During this tutorial, you deploy the game and leaderboard and validate that the leaderboard API works correctly and can be accessed over the internet.

Objectives

  • Create a Memorystore for Redis instance.
  • Create a headless service with an endpoint that directs requests to this instance.
  • Deploy the leaderboard application to GKE.
  • Verify leaderboard functionality using the deployed application that makes API calls.

Costs

In this document, you use the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use the pricing calculator. New Google Cloud users might be eligible for a free trial.

Before you begin

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the Memorystore for Redis and Google Kubernetes Engine APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Make sure that billing is enabled for your Google Cloud project.

  7. Enable the Memorystore for Redis and Google Kubernetes Engine APIs.

    Enable the APIs

Preparing your environment

In this tutorial, you run commands in Cloud Shell. Cloud Shell gives you access to the command line in Google Cloud, and includes the Google Cloud CLI and other tools that you need for Google Cloud development. Cloud Shell can take several minutes to initialize.

  1. Open Cloud Shell:

    Open Cloud Shell

  2. Set the default Compute Engine zone to the zone where you're going to create your Google Cloud resources. In this tutorial, you use the us-central1-a zone in the us-central1 region.

    export REGION=us-central1
    export ZONE=us-central1-a
    gcloud config set compute/zone $ZONE
    
  3. Clone the GitHub repository that contains the sample code:

    git clone https://github.com/GoogleCloudPlatform/memstore-gaming-leaderboard.git
    
  4. Go to the cloned directory:

    cd memstore-gaming-leaderboard
    
  5. Using a text editor, open the leaderboardapp/k8s/appdeploy.yaml file and change the [YOUR_PROJECT_ID] placeholder to your Google Cloud project ID.

Creating a Memorystore for Redis instance

For this tutorial, you create a Basic Tier instance of Memorystore for Redis, which is appropriate for testing and the scope of this tutorial. For a production deployment, we recommend that you deploy a Standard Tier instance, which provides a 99.9% SLA with automatic failover to ensure that your instance is highly available. For details on Standard Tier instances, see high availability for Memorystore for Redis.

  • In Cloud Shell, create a 1 GB Memorystore for Redis instance:

    gcloud redis instances create cm-redis --size=1 \
      --tier=basic \
      --region=$REGION \
      --zone=$ZONE
    

    This command might take a few minutes to complete.

Building the application container images

For this tutorial, you deploy a simple leaderboard application using GKE. Because Unity and C# are popular in gaming, the application tier uses C# and the ASP.NET framework.

To deploy the leaderboard API to a GKE cluster, you need to first upload it to a registry like Container Registry.

  1. Open the README.md file in the GitHub project that you cloned.
  2. Follow the instructions for creating a Docker image and uploading it to Container Registry.

You'll use this image to deploy the leaderboard application after you create the cluster in the next section.

Creating or reusing a GKE cluster

Before you deploy the leaderboard application, you must create a GKE cluster with alias IP ranges enabled. If you already have a GKE cluster (using alias IP ranges), you can use that cluster for this tutorial. If you use an existing cluster, you must perform an additional step to configure the kubectl command-line tool with credentials for that cluster.

  1. If you want to create a cluster, create one named leaderboard that has two nodes:

    gcloud container clusters create leaderboard --num-nodes=2 --enable-ip-alias
    

    This command can take a few minutes to complete.

  2. After waiting for the cluster to start, verify that it's running:

    gcloud container clusters list
    

    The cluster is running when you see an entry that has the name leaderboard whose status is RUNNING.

Configure credentials for an existing cluster

  1. If you're using an existing GKE cluster for this tutorial, configure the kubeconfig file with the credentials for that cluster. For name-of-existing-cluster, use the name of your cluster.

    gcloud container clusters get-credentials name-of-existing-cluster
    

Mapping the Memorystore for Redis instance to a Kubernetes service

When the Memorystore for Redis instance is ready, you make a service inside the GKE cluster so that the application tier can connect.

  1. Verify that the Memorystore for Redis instance is running:

    gcloud redis instances list --region=$REGION
    

    You see output like the following:

    INSTANCE_NAME  VERSION    REGION           TIER       SIZE_GB    HOST       PORT  NETWORK  RESERVED_IP  STATUS    CREATE_TIME
    cm-redis       REDIS_4_0  us-central1      STANDARD  1            10.0.0.3  6379  default  10.0.0.0/29  READY     2019-05-10T04:37:45
    

    The instance is running when the STATUS column shows READY. If the HOST field is empty, the instance has not completed the startup process. Wait a moment and run the redis instances list command again.

  2. Store the IP address of the instance in an environment variable:

    export REDIS_IP=$(gcloud redis instances list --filter="name:cm-redis" --format="value(HOST)" \
        --region=$REGION)
    
  3. Create the Kubernetes service for Redis:

    kubectl apply -f k8s/redis_headless_service.yaml
    

    This service doesn't have a Pod selector, because you want it to point at an IP address that's outside of the Kubernetes cluster.

  4. Create an endpoint that defines the Redis IP address that you retrieved earlier:

    sed "s|REDIS_IP|${REDIS_IP}|g" k8s/redis_endpoint.yaml | kubectl apply -f -
    
  5. Verify that the service and endpoint were created correctly:

    kubectl get services/redis endpoints/redis
    

    If everything is working correctly, you see output like the following, with an entry for the Redis service and the Redis endpoint:

    NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    service/redis      ClusterIP   10.59.241.103   none        6379/TCP   5m
    
    NAME               ENDPOINTS       AGE
    endpoints/redis    10.0.0.3:6379   4m
    

For more information about Kubernetes services and endpoints, see Kubernetes best practices: mapping external services on the Google Cloud blog.

Deploying the leaderboard app and service to Kubernetes

The Redis instance is now reachable from an application that's deployed in the GKE cluster. Therefore, you're ready to deploy the leaderboard app.

  • Follow the instructions in the README.md file included in the root of the GitHub repo that you cloned earlier.

Validating the deployment

The mock game application provided, written in JavaScript, can be used to make calls to the leaderboard API. The following code snippet shows how the mock game posts scores after the player finishes playing:

                    var scoreInfo = {
                        playerName: this.username,
                        score: this.calculateScore().toFixed(2)
                    };

                    var pThis = this;

                    var postUrl = "/api/score";
                    (async () => {
                        try {
                            await axios.post(postUrl, scoreInfo)
                        } catch (error) {
                            console.error(error);
                        }

                        var lbPromise = pThis.fetchLeaderboard();
                        var qPromise = pThis.fetchQuestions();

                        pThis.questions = await qPromise;
                        await lbPromise;
                    })();

In addition, the app makes an API call to retrieve the leaderboard scores—either the top scores or scores centered around the player's name, like this:

                    var pThis = this;
                    var getUrl = "/api/score/retrievescores";

                    (async () => {
                        try {
                            var params = {
                                centerKey: '',
                                offset: 0,
                                numScores: 11
                            };

                            if (pThis.centered) {
                                params.centerKey = pThis.username;
                                params.offset = -5;
                                params.numScores = 11;
                            }

                            const response = await axios.get(getUrl, { params: params });
                            pThis.leaderboard = response.data;
                        } catch (error) {
                            console.error(error);
                            return []
                        }
                    })();

To access the sample application, follow these steps:

  1. Get the IP address of the mock game by running the following command:

    kubectl get ingress
    

    The output is similar to the following:

    NAME                      HOSTS   ADDRESS        PORTS   AGE
    memstore-gaming-ingress   *       34.102.192.4   80      43s
    
  2. Browse to the following URL, where ip_address_for_gke is the address for the mock game:

    http://ip_address_for_gke.
    

This sample is simple, but it's adequate for demonstrating basic API usage. When you post or retrieve scores directly from a game client app that's running on a user device or machine, the sample calls the leaderboard API at the public IP address assigned to its Kubernetes Load Balancer object. For this sample application, both the leaderboard API and the JavaScript client are hosted at the same IP address that you get using the kubectl get ingress command shown earlier.

How the methods are implemented

The leaderboard application, written in C#, uses the StackExchange.Redis library for communicating with Redis. The code snippets that follow show how PostScore and RetrieveScores are implemented using Redis and the StackExchange.Redis library.

The following code snippet shows the PostScore method:

        public async Task<bool> PostScoreAsync(ScoreModel score)
        {
            IDatabase db = _redis.GetDatabase();

            // SortedSetAddAsync corresponds to ZADD
            return await db.SortedSetAddAsync(LEADERBOARD_KEY, score.PlayerName, score.Score);
        }

The following code snippet shows the RetrieveScores method:

        public async Task<IList<LeaderboardItemModel>> RetrieveScoresAsync(RetrieveScoresDetails retrievalDetails)
        {
            IDatabase db = _redis.GetDatabase();
            List<LeaderboardItemModel> leaderboard = new List<LeaderboardItemModel>();

            long offset = retrievalDetails.Offset;
            long numScores = retrievalDetails.NumScores;

            // If centered, get rank of specified user first
            if (!string.IsNullOrWhiteSpace(retrievalDetails.CenterKey))
            {
                // SortedSetRankAsync corresponds to ZREVRANK
                var rank = await db.SortedSetRankAsync(LEADERBOARD_KEY, retrievalDetails.CenterKey, Order.Descending);

                // If specified user is not present, return empty leaderboard
                if (!rank.HasValue)
                {
                    return leaderboard;
                }

                // Use rank to calculate offset
                offset = Math.Max(0, rank.Value + retrievalDetails.Offset);

                // Account for number of scores when we're attempting to center
                // at element in rank [0, abs(offset))
                if(offset <= 0)
                {
                    numScores = rank.Value + Math.Abs((long)retrievalDetails.Offset) + 1;
                }
            }

            // SortedSetRangeByScoreWithScoresAsync corresponds to ZREVRANGEBYSCORE [WITHSCORES]
            var scores = await db.SortedSetRangeByScoreWithScoresAsync(LEADERBOARD_KEY,
                skip: offset,
                take: numScores,
                order: Order.Descending);

            var startingRank = offset;
            for (int i = 0; i < scores.Length; i++)
            {
                var lbItem = new LeaderboardItemModel
                {
                    Rank = startingRank++,
                    PlayerName = scores[i].Element.ToString(),
                    Score = scores[i].Score
                };
                leaderboard.Add(lbItem);
            }

            return leaderboard;
        }

Additions to the sample game

The leaderboard API follows REST conventions and is provided only as an example. When you run a production game leaderboard, we recommend integrating an authentication flow so that only scores that are from validated users can be posted.

Currently, Memorystore for Redis doesn't provide persistence for player scores. Therefore, if the app experiences an issue, you could lose the leaderboard information. If your game requires a persistent leaderboard, you should back up the leaderboard scores periodically in a persistent database.

Clean up

To avoid incurring charges to your Google Cloud account for the resources used in this tutorial, delete the project.

Delete the project

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

What's next