教程:使用 Memorystore for Redis 作为游戏排行榜

本教程将向您介绍如何使用 Memorystore for Redis 构建在 Google Kubernetes Engine (GKE) 上运行的基于 ASP.NET 的排行榜应用,然后使用一个单独的基于 JavaScript 的示例游戏来发布和检索得分。本文档适用于希望在云端运营自己的排行榜的游戏开发者。

简介

排行榜是多人游戏中的一项常见功能。排行榜会实时显示玩家排名,这有助于鼓励玩家更积极地参与游戏。若要实时捕获玩家操作并对得分进行排名,可以选择 Redis 之类的内存数据库。

下图展示了排行榜的基础架构:

显示 VPC 内的 GKE 集群以及一个单独的 Memorystore for Redis 实例的示意图。

在 Google Cloud 中,排行榜由 VPC 网络内的 GKE 集群和一个单独的 Memorystore for Redis 实例组成。

下图显示了本教程的应用架构。客户端使用排行榜 API 与得分进行交互,这些得分保存在 Google Cloud 内运行的 Memorystore for Redis 实例中。

显示本教程中的应用架构的示意图

排行榜 API 的方法

排行榜应用的 API 包含以下方法:

  • PostScore(string playerName, double score)。此方法会为指定的玩家在排行榜上发布得分。
  • RetrieveScores(string centerKey, int offset, int numScores)。此方法会下载一组得分。如果您将玩家 ID 作为 centerKey 的值进行传递,则该方法将返回高于和低于指定玩家得分的得分。如果您没有为 centerKey 传递值,则该方法将返回前 N 个绝对得分,其中 N 是您在 numScores 中传递的值。例如,若要获得前 10 个得分,请调用 RetrieveScores('', 0, 10)。如需获得 5 个高于和低于某个玩家得分的得分,请调用 RetrieveScores('player1', -5, 10)

该示例的代码库包含一个模拟游戏和一个概念验证排行榜实现。在本教程中,您将部署游戏和排行榜,并验证排行榜 API 正常运行并且可以通过互联网访问。

目标

  • 创建一个 Memorystore for Redis 实例。
  • 使用端点创建无头服务,用于将请求定向到此实例。
  • 将排行榜应用部署到 GKE。
  • 使用进行 API 调用的已部署应用验证排行榜功能。

费用

本教程使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用量来估算费用。 Google Cloud 新用户可能有资格申请免费试用

准备工作

  1. 登录您的 Google Cloud 帐号。如果您是 Google Cloud 新手,请创建一个帐号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到“项目选择器”

  3. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  4. 启用 Memorystore for Redis and Google Kubernetes Engine API。

    启用 API

准备环境

在本教程中,您将在 Cloud Shell 中运行命令。Cloud Shell 可用于访问 Google Cloud 中的命令行,并且还包含您在 Google Cloud 中开发所需的 Cloud SDK 和其他工具。初始化 Cloud Shell 可能需要几分钟。

  1. 打开 Cloud Shell:

    打开 Cloud Shell

  2. 将默认 Compute Engine 区域设置为您要在其中创建 Google Cloud 资源的区域。在本教程中,您将使用 us-central1 地区中的 us-central1-a 区域。

    export REGION=us-central1
    export ZONE=us-central1-a
    gcloud config set compute/zone $ZONE
    
  3. 克隆包含示例代码的 GitHub 代码库:

    git clone https://github.com/GoogleCloudPlatform/memstore-gaming-leaderboard.git
    
  4. 转到克隆的目录:

    cd memstore-gaming-leaderboard
    
  5. 使用文本编辑器打开 leaderboardapp/k8s/appdeploy.yaml 文件,并将 [YOUR_PROJECT_ID] 占位符更改为您的 Google Cloud 项目 ID。

创建一个 Memorystore for Redis 实例

在本教程中,您将创建一个 Memorystore for Redis 的基本级实例,该实例适用于测试和本教程的范围。对于生产部署,我们建议您部署一个标准级实例,该实例可提供 99.9% 的服务等级协议 (SLA) 并且具有自动故障转移功能,以确保您的实例具有高可用性。如需详细了解标准级实例,请参阅 Memorystore for Redis 高可用性

  • 在 Cloud Shell 中,创建一个 1 GB 的 Memorystore for Redis 实例:

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

    此命令可能需要几分钟时间才能完成。

构建应用容器映像

在本教程中,您将使用 GKE 部署一个简单的排行榜应用。由于 Unity 和 C# 在游戏领域很流行,因此应用级使用 C# 和 ASP.NET 框架。

为了将排行榜 API 部署到 GKE 集群,您需要先将其上传到 Container Registry 之类的注册表。

  1. 在您克隆的 GitHub 项目中,打开 README.md 文件。
  2. 按照创建 Docker 映像并将其上传到 Container Registry 的说明进行操作。

在下一部分中创建集群后,您将使用此映像来部署排行榜应用。

创建或重复使用一个 GKE 集群

在部署排行榜应用之前,您必须先创建一个启用了别名 IP 地址范围的 GKE 集群。如果您已经有 GKE 集群(使用别名 IP 地址范围),则可以在本教程中使用该集群。如果您使用现有集群,则必须执行额外的步骤,以使用该集群的凭据配置 kubectl 命令行工具。

  1. 如果您要创建集群,请创建一个名为 leaderboard 并且具有两个节点的集群:

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

    此命令可能需要几分钟时间才能完成。

  2. 等待集群启动后,验证其正在运行:

    gcloud container clusters list
    

    当您看到一个名称为 leaderboard、状态为 RUNNING 的条目时,表示集群正在运行。

为现有集群配置凭据

  1. 如果您在本教程中使用的是现有 GKE 集群,请使用该集群的凭据配置 kubeconfig 文件。对于 name-of-existing-cluster,请使用您的集群名称。

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

将 Memorystore for Redis 实例映射到 Kubernetes 服务

当 Memorystore for Redis 实例准备就绪后,您将在 GKE 集群中创建一项服务,以便应用级与其连接。

  1. 验证 Memorystore for Redis 实例正在运行:

    gcloud redis instances list --region=$REGION
    

    您将看到如下所示的输出内容:

    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
    

    STATUS 列显示 READY 时,表示实例正在运行。如果 HOST 字段为空,则表示实例尚未完成启动过程。请稍等片刻,然后再次运行 redis instances list 命令。

  2. 将实例的 IP 地址存储在环境变量中:

    export REDIS_IP=$(gcloud redis instances list --filter="name:cm-redis" --format="value(HOST)" \
        --region=$REGION)
    
  3. 为 Redis 创建 Kubernetes 服务:

    kubectl apply -f k8s/redis_headless_service.yaml
    

    此服务没有 Pod 选择器,因为您希望它指向 Kubernetes 集群外部的 IP 地址。

  4. 创建一个端点,用于定义您之前检索的 Redis IP 地址:

    sed "s|REDIS_IP|${REDIS_IP}|g" k8s/redis_endpoint.yaml | kubectl apply -f -
    
  5. 验证服务和端点已正确创建:

    kubectl get services/redis endpoints/redis
    

    如果一切正常,您会看到如下所示的输出,其中包含 Redis 服务和 Redis 端点的条目:

    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
    

如需详细了解 Kubernetes 服务和端点,请参阅 Google Cloud 博客上的Kubernetes 最佳做法:映射外部服务

将排行榜应用和服务部署到 Kubernetes

现在可以通过部署在 GKE 集群中的应用访问 Redis 实例。因此,您可以部署排行榜应用了。

  • 按照 README.md 文件(该文件包含在您之前克隆的 GitHub 代码库的根目录中)中的说明进行操作。

验证部署

本教程提供的模拟游戏应用是用 JavaScript 编写的,可用于调用排行榜 API。以下代码段展示了模拟游戏在玩家完成游戏后如何发布得分:

                    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;
                    })();

此外,该应用还会通过 API 调用来检索排行榜得分(排名靠前的得分或玩家姓名周围的得分),例如:

                    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 []
                        }
                    })();

如需访问示例应用,请按以下步骤操作:

  1. 通过运行以下命令获取模拟游戏的 IP 地址:

    kubectl get ingress
    

    输出类似于以下内容:

    NAME                      HOSTS   ADDRESS        PORTS   AGE
    memstore-gaming-ingress   *       34.102.192.4   80      43s
    
  2. 浏览到以下网址,其中 ip_address_for_gke 是模拟游戏的地址:

    http://ip_address_for_gke.
    

此示例很简单,但足以演示基本的 API 使用情况。如果您直接从用户设备或机器上运行的游戏客户端应用发布或检索得分,则示例会使用分配给排行榜 API 的 Kubernetes 负载平衡器对象的公共 IP 地址调用该 API。对于此示例应用,排行榜 API 和 JavaScript 客户端都托管在您使用之前显示的 kubectl get ingress 命令获取的同一 IP 地址上。

如何实现这些方法

以 C# 编写的排行榜应用使用 StackExchange.Redis 库与 Redis 通信。以下代码段显示了如何使用 Redis 和 StackExchange.Redis 库实现 PostScoreRetrieveScores

以下代码段显示了 PostScore 方法:

        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);
        }

以下代码段显示了 RetrieveScores 方法:

        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;
        }

对示例游戏的补充说明

排行榜 API 遵循 REST 惯例,仅作为示例提供。在运行正式版游戏排行榜时,我们建议您集成身份验证流程,以便系统仅发布来自经过验证的用户的得分。

目前,Memorystore for Redis 不会永久保留玩家得分。因此,如果应用出现问题,您可能会丢失排行榜信息。如果您的游戏需要永久性排行榜,则您应定期在永久性数据库中备份排行榜得分。

清理

为避免因本教程中使用的资源导致您的 Google Cloud 帐号产生费用,请删除项目。

删除项目

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

后续步骤