从 Cloud Run 连接到全代管式数据库

1. 概览

在本实验中,您将把无服务器数据库(Spanner 和 Firestore)与在 Cloud Run 中运行的应用(Go 和 Node.js)集成在一起。Cymbal Eats 应用包含多个在 Cloud Run 上运行的服务。在以下步骤中,您将配置服务以使用 Cloud Spanner 关系型数据库和 Cloud Firestore(一种 NoSQL 文档数据库)。利用无服务器产品来处理数据层和应用运行时,您可以将所有基础设施管理任务剥离出来,全力专注于构建应用,而不必担心开销的问题。

2. 学习内容

在本实验中,您将学习如何完成以下操作:

  • 集成 Spanner
  • 启用 Spanner 托管服务
  • 集成到代码中
  • 部署连接到 Spanner 的代码
  • 集成 Firestore
  • 启用 Firestore 受管服务
  • 集成到代码中
  • 部署连接到 Firestore 的代码

3. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用 PROJECT_ID 标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除项目。Google Cloud 新用户符合参与 300 美元免费试用计划的条件。

设置环境

  1. 创建项目 ID 变量
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
export SPANNER_INSTANCE=inventory-instance
export SPANNER_DB=inventory-db
export REGION=us-east1
export SPANNER_CONNECTION_STRING=projects/$PROJECT_ID/instances/$SPANNER_INSTANCE/databases/$SPANNER_DB
  1. 启用 Spanner、Cloud Run、Cloud Build 和 Artifact Registry API
gcloud services enable \
     compute.googleapis.com \
     spanner.googleapis.com \
     run.googleapis.com \
     cloudbuild.googleapis.com \
     artifactregistry.googleapis.com \
     firestore.googleapis.com \
     appengine.googleapis.com \
     artifactregistry.googleapis.com
  1. 克隆存储库
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git
  1. 前往该目录
cd cymbal-eats/inventory-service/spanner

4. 创建和配置 Spanner 实例

Spanner 是商品目录服务后端关系型数据库。您将在以下步骤中创建 Spanner 实例、数据库和架构。

创建实例

  1. 创建 Cloud Spanner 实例
gcloud spanner instances create $SPANNER_INSTANCE --config=regional-${REGION} \
--description="Cymbal Menu Inventory" --nodes=1

输出示例

Creating instance...done.   
  1. 验证 Spanner 实例是否已正确配置
gcloud spanner instances list

输出示例

NAME: inventory-instance
DISPLAY_NAME: Cymbal Menu Inventory
CONFIG: regional-us-east1
NODE_COUNT: 1
PROCESSING_UNITS: 100
STATE: READY

创建数据库和架构

创建新数据库,并使用 Google 标准 SQL 的数据定义语言 (DDL) 创建数据库架构。

  1. 创建 DDL 文件
echo "CREATE TABLE InventoryHistory (ItemRowID STRING (36) NOT NULL, ItemID INT64 NOT NULL, InventoryChange INT64, Timestamp TIMESTAMP) PRIMARY KEY(ItemRowID)" >> table.ddl
  1. 创建 Spanner 数据库
gcloud spanner databases create $SPANNER_DB \
--instance=$SPANNER_INSTANCE \
--ddl-file=table.ddl

输出示例

Creating database...done.

验证数据库状态和架构

  1. 查看数据库的状态
gcloud spanner databases describe $SPANNER_DB \
--instance=$SPANNER_INSTANCE

输出示例

createTime: '2022-04-22T15:11:33.559300Z'
databaseDialect: GOOGLE_STANDARD_SQL
earliestVersionTime: '2022-04-22T15:11:33.559300Z'
encryptionInfo:
- encryptionType: GOOGLE_DEFAULT_ENCRYPTION
name: projects/cymbal-eats-7-348013/instances/menu-inventory/databases/menu-inventory
state: READY
versionRetentionPeriod: 1h
  1. 查看数据库的架构
gcloud spanner databases ddl describe $SPANNER_DB \
--instance=$SPANNER_INSTANCE

输出示例

CREATE TABLE InventoryHistory (
  ItemRowID STRING(36) NOT NULL,
  ItemID INT64 NOT NULL,
  InventoryChange INT64,
  TimeStamp TIMESTAMP,
) PRIMARY KEY(ItemRowID);

5. 集成 Spanner

在本部分中,您将了解如何将 Spanner 集成到您的应用中。此外,SQL Spanner 还提供客户端库JDBC 驱动程序R2DBC 驱动程序REST APIRPC API,让您可以将 Spanner 集成到任何应用中。

在下一部分中,您将使用 Go 客户端库在 Spanner 中安装、验证和修改数据。

安装客户端库

Cloud Spanner 客户端库会自动使用应用默认凭证 (ADC) 查找您的服务账号凭证,从而更轻松地与 Cloud Spanner 集成

设置身份验证

Google Cloud CLI 和 Google Cloud 客户端库会自动检测它们何时在 Google Cloud 上运行并使用当前 Cloud Run 修订版本的运行时服务账号。此策略称为应用默认凭证,支持跨多个环境移植代码。

不过,最好为其分配用户管理的服务账号,而不是默认服务账号,从而创建专用身份。

  1. 向服务账号授予 Spanner Database Admin 角色
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
--role="roles/spanner.databaseAdmin"

输出示例

Updated IAM policy for project [cymbal-eats-6422-3462].
[...]

使用客户端库

Spanner 客户端库可抽象化与 Spanner 集成的复杂性,并支持多种常用编程语言。

创建 Spanner 客户端

Spanner 客户端是一种用于读取 Cloud Spanner 数据库中的数据以及向该数据库写入数据的客户端。客户端可以安全地并发使用,但其 Close 方法除外。

以下代码段会创建一个 Spanner 客户端

main.go

var dataClient *spanner.Client
...
dataClient, err = spanner.NewClient(ctx, databaseName)

您可以将 Client 视为数据库连接:您与 Cloud Spanner 的所有交互都必须通过 Client 进行。通常,您可以在应用启动时创建客户端,然后重复使用该客户端来读取、写入和执行事务。每个客户端均使用 Cloud Spanner 中的资源。

修改数据

您可以通过多种方式在 Spanner 数据库中插入、更新和删除数据。下面列出了可用的方法。

在此实验中,您将使用变更来修改 Spanner 中的数据。

Spanner 中的变更

Mutation 是用于变更操作的容器。Mutation 表示插入、更新和删除等一系列操作,Cloud Spanner 将这些操作以原子方式应用于 Cloud Spanner 数据库中不同的行和表。

main.go

m := []*spanner.Mutation{}

m = append(m, spanner.Insert(
        "inventoryHistory",
         inventoryHistoryColumns,
        []interface{}{uuid.New().String(), element.ItemID, element.InventoryChange, time.Now()}))

此代码段用于在商品目录历史记录表中插入新行。

部署和测试

现在,Spanner 已配置完毕,并且您已查看关键代码元素,接下来可以将应用部署到 Cloud Run。

将应用部署到 Cloud Run

Cloud Run 可以通过一个命令自动构建、推送和部署您的代码。在以下命令中,您将对 run 服务调用 deploy 命令,并传入正在运行的应用所使用的变量,例如您之前创建的 SPANNER_CONNECTION_STRING。

  1. 点击“打开终端”
  2. 将库存服务部署到 Cloud Run
gcloud run deploy inventory-service \
    --source . \
    --region $REGION \
    --update-env-vars SPANNER_CONNECTION_STRING=$SPANNER_CONNECTION_STRING \
    --allow-unauthenticated \
    --project=$PROJECT_ID \
    --quiet

输出示例

Service [inventory-service] revision [inventory-service-00001-sug] has been deployed and is serving 100 percent of traffic.
Service URL: https://inventory-service-ilwytgcbca-uk.a.run.app
  1. 存储服务网址
INVENTORY_SERVICE_URL=$(gcloud run services describe inventory-service \
  --platform managed \
  --region $REGION \
  --format=json | jq \
  --raw-output ".status.url")

测试 Cloud Run 应用

插入内容

  1. 在 Cloud Shell 中,输入以下命令。
POST_URL=$INVENTORY_SERVICE_URL/updateInventoryItem
curl -i -X POST ${POST_URL} \
--header 'Content-Type: application/json' \
--data-raw '[
    {
        "itemID": 1,
        "inventoryChange": 5
    }
]'

输出示例

HTTP/2 200
access-control-allow-origin: *
content-type: application/json
x-cloud-trace-context: 10c32f0863d26521497dc26e86419f13;o=1
date: Fri, 22 Apr 2022 21:41:38 GMT
server: Google Frontend
content-length: 2

OK

查询商品

  1. 查询商品目录服务
GET_URL=$INVENTORY_SERVICE_URL/getAvailableInventory
curl -i ${GET_URL}

示例响应

HTTP/2 200
access-control-allow-origin: *
content-type: text/plain; charset=utf-8
x-cloud-trace-context: b94f921e4c2ae90210472c88eb05ace8;o=1
date: Fri, 22 Apr 2022 21:45:50 GMT
server: Google Frontend
content-length: 166

[{"ItemID":1,"Inventory":5}]

6. Spanner 概念

Cloud Spanner 使用声明性 SQL 语句来查询其数据库。SQL 语句会指明用户想要什么结果,但不会说明如何获取结果。

  1. 在终端中,输入以下命令来查询之前创建的记录。
gcloud spanner databases execute-sql $SPANNER_DB \
--instance=$SPANNER_INSTANCE \
--sql='SELECT * FROM InventoryHistory WHERE ItemID=1'

输出示例

ItemRowID: 1
ItemID: 1
InventoryChange: 3
Timestamp: 

查询执行计划

查询执行计划是 Spanner 用于获取结果的一系列步骤。获取特定 SQL 语句的结果的方式可能有多种。您可以在控制台和客户端库中访问查询执行计划。如需了解 Spanner 如何处理 SQL 查询,请执行以下操作:

  1. 在控制台中,打开 Cloud Spanner 实例页面。
  2. 转到 Cloud Spanner 实例
  3. 点击 Cloud Spanner 实例的名称。在“数据库”部分中,选择要查询的数据库。
  4. 点击“查询”。
  5. 在查询编辑器中输入以下查询
SELECT * FROM InventoryHistory WHERE ItemID=1
  1. 点击“运行”
  2. 点击“说明”

Cloud 控制台将直观显示您查询的执行计划。

149f8bae468f8b34.png

查询优化器

Cloud Spanner 查询优化器会比较替代执行计划,并选择最有效的计划。随着时间的推移,查询优化器会不断发展,扩大了查询执行计划中的选项集,并提高了传达这些选择的估算值的准确性,从而产生更高效的查询执行计划。

Cloud Spanner 将以新查询优化器版本的形式发布优化器更新。默认情况下,每个数据库在该版本发布后的 30 天内开始使用最新版本的优化器。

如需查看在 gcloud spanner 中运行查询时使用的版本,请将 --query-mode 标志设置为 PROFILE

  1. 输入以下命令以查看优化器版本
gcloud spanner databases execute-sql $SPANNER_DB --instance=$SPANNER_INSTANCE \
--query-mode=PROFILE --sql='SELECT * FROM InventoryHistory'

输出示例

TOTAL_ELAPSED_TIME: 6.18 msecs
CPU_TIME: 5.17 msecs
ROWS_RETURNED: 1
ROWS_SCANNED: 1
OPTIMIZER_VERSION: 3
 RELATIONAL Distributed Union
 (1 execution, 0.11 msecs total latency)
 subquery_cluster_node: 1
    |
    +- RELATIONAL Distributed Union
    |  (1 execution, 0.09 msecs total latency)
    |  call_type: Local, subquery_cluster_node: 2
    |   |
    |   \- RELATIONAL Serialize Result
    |      (1 execution, 0.08 msecs total latency)
    |       |
    |       +- RELATIONAL Scan
    |       |  (1 execution, 0.08 msecs total latency)
    |       |  Full scan: true, scan_target: InventoryHistory, scan_type: TableScan
    |       |   |
    |       |   +- SCALAR Reference
    |       |   |  ItemRowID
    |       |   |
    |       |   +- SCALAR Reference
    |       |   |  ItemID
    |       |   |
    |       |   +- SCALAR Reference
    |       |   |  InventoryChange
    |       |   |
    |       |   \- SCALAR Reference
    |       |      Timestamp
    |       |
    |       +- SCALAR Reference
    |       |  $ItemRowID
    |       |
    |       +- SCALAR Reference
    |       |  $ItemID
    |       |
    |       +- SCALAR Reference
    |       |  $InventoryChange
    |       |
    |       \- SCALAR Reference
    |          $Timestamp
    |
    \- SCALAR Constant
       true

ItemRowID: 1
ItemID: 1
InventoryChange: 3
Timestamp:

更新优化器版本

本实验发布时的最新版本为版本 4。接下来,您将更新 Spanner 表,以使用版本 4 的查询优化器。

  1. 更新优化器
gcloud spanner databases ddl update $SPANNER_DB \
--instance=$SPANNER_INSTANCE \
--ddl='ALTER DATABASE InventoryHistory
SET OPTIONS (optimizer_version = 4)'

输出示例

Schema updating...done. 
  1. 输入以下命令以查看优化器版本更新
gcloud spanner databases execute-sql $SPANNER_DB --instance=$SPANNER_INSTANCE \
--query-mode=PROFILE --sql='SELECT * FROM InventoryHistory'

输出示例

TOTAL_ELAPSED_TIME: 8.57 msecs
CPU_TIME: 8.54 msecs
ROWS_RETURNED: 1
ROWS_SCANNED: 1
OPTIMIZER_VERSION: 4
[...]

在 Metrics Explorer 中直观呈现查询优化器版本

您可以使用 Cloud 控制台中的 Metrics Explorer 直观呈现数据库实例的查询计数。您可以查看每个数据库中使用的优化器版本。

  1. 在 Cloud 控制台中前往“监控”,然后在左侧菜单中选择 Metrics Explorer
  2. 资源类型字段中,选择“Cloud Spanner 实例”。
  3. 指标字段中,选择“查询次数”,然后点击“应用”。
  4. 分组依据字段中,选择 database、optimizer_version 和 status。

581b859c25790b21.png

7. 创建和配置 Firestore 数据库

Firestore 是一个 NoSQL 文档数据库,能够自动扩缩、具备出色的性能,并且易于进行应用开发。虽然 Firestore 界面具有许多与传统数据库相同的功能,但作为 NoSQL 数据库,它与传统数据库在描述数据对象间关系的方式方面有所不同。

以下任务将引导您创建一个由 Firestore 提供支持的订购服务 Cloud Run 应用。在开始处理订单之前,订购服务会调用上一部分中创建的库存服务来查询 Spanner 数据库。此服务将确保有足够的库存,并且可以完成订单。

6843abaf4263e112.png

8. Firestore 概念

数据模型

Firestore 数据库由集合和文档组成。

b60acd63d4793a6c.png

文档

每个文档都包含一组键值对。Firestore 经过优化,可用于存储小型文档的大型集合。

5571cb2f261d2dbe.png

集合

您必须将所有文档存储在集合中。文档可以包含子集合和嵌套对象,包括基本字段(如字符串)或复杂对象(如列表)。

5811378cb721e5ec.png

创建 Firestore 数据库

  1. 创建 Firestore 数据库
gcloud firestore databases create --location=$REGION

输出示例

Success! Selected Google Cloud Firestore Native database for cymbal-eats-6422-3462

9. 将 Firestore 集成到您的应用中

在本部分中,您将更新服务账号、添加 Firestore 访问服务账号、查看并部署 Firestore 安全规则,以及查看 Firestore 中的数据是如何修改的。

设置身份验证

  1. 向服务账号授予 Datastore 用户角色
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
  --role="roles/datastore.user"

输出示例

Updated IAM policy for project [cymbal-eats-6422-3462].

Firestore 安全规则

安全规则以简单明了的格式提供访问权限控制和数据验证。

  1. 前往 order-service/starter-code 目录
cd ~/cymbal-eats/order-service
  1. 在云编辑器中打开 firestore.rules 文件
cat firestore.rules

firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents { ⇐ All database
    match /{document=**} { ⇐ All documents
      allow read: if true; ⇐ Allow reads
    }
    match /{document=**} {
      allow write: if false; ⇐ Deny writes
    }
  }
}

警告:最佳实践是限制对 Firestore 存储空间的访问权限。在本实验中,允许所有读取操作。不建议在生产环境中采用此配置。

启用 Firestore 受管服务

  1. 点击“打开终端”
  2. 使用当前项目 ID 创建 .firebaserc 文件。部署目标的设置存储在项目目录下的 .firebaserc 文件中。

firebaserc.tmpl

sed "s/PROJECT_ID/$PROJECT_ID/g" firebaserc.tmpl > .firebaserc
  1. 下载 Firebase 二进制文件
curl -sL https://firebase.tools | upgrade=true bash

输出示例

-- Checking for existing firebase-tools on PATH...
Your machine already has firebase-tools@10.7.0 installed. Nothing to do.
-- All done!
  1. 部署 Firestore 规则。
firebase deploy 

输出示例

=== Deploying to 'cymbal-eats-6422-3462'...

i  deploying firestore
i  cloud.firestore: checking firestore.rules for compilation errors...
✔  cloud.firestore: rules file firestore.rules compiled successfully
i  firestore: uploading rules firestore.rules...
✔  firestore: released rules firestore.rules to cloud.firestore

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/cymbal-eats-6422-3462/overview

修改数据

集合和文档是在 Firestore 中隐式创建的。只需将数据分配给集合中的文档即可。如果集合或文档不存在,Firestore 会创建一个。

向 Firestore 添加数据

您可以通过几种方式将数据写入 Cloud Firestore:

  • 设置集合中的文档的数据,明确指定文档标识符。
  • 向集合添加新文档。在这种情况下,Cloud Firestore 会自动生成文档标识符。
  • 创建一个使用自动生成的标识符的空文档,并在以后向该文档分配数据。

下一部分将指导您使用 set 方法创建文档。

设置文档

使用 set() 方法创建文档。使用 set() 方法时,您必须为要创建的文档指定 ID。

请查看以下代码段。

index.js

const orderDoc = db.doc(`orders/123`);
await orderDoc.set({
    orderNumber: 123,
    name: Anne,
    address: 555 Bright Street,
    city: Mountain View,
    state: CA,
    zip: 94043,
    orderItems: [id: 1],
    status: 'New'
  });

此代码将创建一个指定用户生成的文档 ID 123 的文档。如需让 Firestore 代表您生成 ID,请使用 add()create() 方法。

更新文档

借助更新方法 update(),您可以更新某些文档字段,而无需覆盖整个文档。

在下面的代码段中,代码会更新订单 123

index.js

const orderDoc = db.doc(`orders/123`);
await orderDoc.update(name: "Anna");

删除文档

在 Firestore 中,您可以删除集合、文档或文档中的特定字段。如需删除文档,请使用 delete() 方法。

以下代码段会删除订单 123。

index.js

const orderDoc = db.doc(`orders/123`);
await orderDoc.delete();

10. 部署和测试

在本部分中,您将应用部署到 Cloud Run,并测试创建、更新和删除方法。

将应用部署到 Cloud Run

  1. 将网址存储在变量 INVENTORY_SERVICE_网址 中,以便与 Inventory Service 集成
INVENTORY_SERVICE_URL=$(gcloud run services describe inventory-service \
 --region=$REGION \
 --format=json | jq \
 --raw-output ".status.url")
  1. 部署订单服务
gcloud run deploy order-service \
  --source . \
  --platform managed \
  --region $REGION \
  --allow-unauthenticated \
  --project=$PROJECT_ID \
  --set-env-vars=INVENTORY_SERVICE_URL=$INVENTORY_SERVICE_URL \
  --quiet

输出示例

[...]
Done.
Service [order-service] revision [order-service-00001-qot] has been deployed and is serving 100 percent of traffic.
Service URL: https://order-service-3jbm3exegq-uk.a.run.app

测试 Cloud Run 应用

创建文档

  1. 将订单服务应用的网址存储到变量中以供测试
ORDER_SERVICE_URL=$(gcloud run services describe order-service \
  --platform managed \
  --region $REGION \
  --format=json | jq \
  --raw-output ".status.url")
  1. 构建订单请求并将新订单发布到 Firestore 数据库
curl --request POST $ORDER_SERVICE_URL/order \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Jane Doe",
         "email": "Jane.Doe-cymbaleats@gmail.com",
    "address": "123 Maple",
    "city": "Buffalo",
    "state": "NY",
    "zip": "12346",
    "orderItems": [
        {
            "id": 1
        }
    ]
}'

输出示例

{"orderNumber":46429}

保存订单号以备后用

export ORDER_NUMBER=<value_from_output>

查看结果

在 Firestore 中查看结果

  1. 前往 Firestore 控制台
  2. 点击“数据”

465ceca6198b2b88.png

更新文档

提交的订单未包含数量。

  1. 更新记录并添加数量键值对
curl --location -g --request PATCH $ORDER_SERVICE_URL/order/${ORDER_NUMBER} \
--header 'Content-Type: application/json' \
--data-raw '{
"orderItems": [
        {   
            "id": 1,
            "quantity": 1   
        }
    ]
}'

输出示例

{"status":"success"}

查看结果

在 Firestore 中查看结果

  1. 前往 Firestore 控制台
  2. 点击“数据”

cfcf78d200e15b84.png

删除文档

  1. 从 Firestore 订单集合中删除商品 46429
curl --location -g --request DELETE $ORDER_SERVICE_URL/order/${ORDER_NUMBER}

查看结果

  1. 前往 Firestore 控制台
  2. 点击“数据”

73e14d69211d1539.png

11. 恭喜!

恭喜,您已完成本实验!

后续步骤:

探索其他 Cymbal Eats Codelab:

清理

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

删除项目

若要避免产生费用,最简单的方法是删除您为本教程创建的项目。