迁移 Python 2 App Engine Cloud NDB 和从 Cloud Tasks 应用迁移到 Python 3 和 Cloud Datastore(模块 9)

1. 概览

无服务器迁移站系列 Codelab(自定进度的动手教程)和相关视频旨在帮助 Google Cloud 无服务器开发者通过一次或多次迁移(主要是从旧版服务迁移)来指导他们的应用现代化。这样做可让您的应用更易于移植,并为您提供更多选择和灵活性,使您能够与更多 Cloud 产品集成并访问这些产品,还能更轻松地升级到新的语言版本。虽然最初侧重于最早的 Cloud 用户(主要是 App Engine [标准环境] 开发者),但本系列文章的范围足够广,可涵盖其他无服务器平台(例如 Cloud FunctionsCloud Run),或者其他平台(如果适用)。

此 Codelab 的目的是将模块 8 示例应用移植到 Python 3,并将 Datastore(Datastore 模式的 Cloud Firestore)访问权限从使用 Cloud NDB 切换到原生 Cloud Datastore 客户端库,并升级到最新版本的 Cloud Tasks 客户端库。

我们在模块 7 中添加了对任务队列的使用,以处理推送任务,然后在模块 8 中将该使用方式迁移到了 Cloud Tasks。在第 9 模块中,我们将继续学习 Python 3 和 Cloud Datastore。如果使用 Task Queues 处理拉取任务,则需要迁移到 Cloud Pub/Sub,并应改为参阅模块 18-19。

在接下来的实验中

  • 将模块 8 示例应用移植到 Python 3
  • 将 Datastore 访问从 Cloud NDB 切换到 Cloud Datastore 客户端库
  • 升级到最新版本的 Cloud Tasks 客户端库

所需条件

调查问卷

您打算如何使用本教程?

仅通读 阅读并完成练习

您如何评价使用 Python 的体验?

新手水平 中等水平 熟练水平

您如何评价自己在使用 Google Cloud 服务方面的经验水平?

新手水平 中等水平 熟练水平

2. 背景

模块 7 演示了如何在 Python 2 Flask App Engine 应用中使用 App Engine 任务队列推送任务。在模块 8 中,您将该应用从任务队列迁移到 Cloud Tasks。在模块 9 中,您将继续这一迁移之旅,将该应用移植到 Python 3,并将 Datastore 访问权限从使用 Cloud NDB 切换到原生 Cloud Datastore 客户端库。

由于 Cloud NDB 同时适用于 Python 2 和 3,因此对于将应用从 Python 2 移植到 3 的 App Engine 用户来说,它就足够了。将客户端库迁移到 Cloud Datastore 是完全可选的,只有一个原因促使您考虑这样做:您有非 App Engine 应用(和/或 Python 3 App Engine 应用)已在使用 Cloud Datastore 客户端库,并且您希望将代码库整合为仅使用一个客户端库来访问 Datastore。Cloud NDB 专门为 Python 2 App Engine 开发者打造,用作 Python 3 迁移工具,因此如果您还没有使用 Cloud Datastore 客户端库的代码,则无需考虑此迁移。

最后,Cloud Tasks 客户端库的开发仅在 Python 3 中继续进行,因此我们将从最终的 Python 2 版本之一“迁移”到其 Python 3 同类版本。幸运的是,Python 3 中没有来自 Python 2 的重大更改,这意味着您无需在此处执行任何其他操作。

本教程包含以下步骤:

  1. 设置/准备工作
  2. 更新配置
  3. 修改应用代码

3. 设置/准备工作

本节介绍如何执行以下操作:

  1. 设置 Cloud 项目
  2. 获取基准示例应用
  3. (重新)部署并验证基准应用

这些步骤可确保您从可正常运行的代码开始,并确保代码已准备好迁移到云服务。

1. 设置项目

如果您已完成模块 8 Codelab,请重复使用同一项目(和代码)。或者,您可以创建一个全新的项目或重复使用另一个现有项目。确保项目具有有效的结算账号,并且已启用 App Engine 应用。找到您的项目 ID,因为您需要在本 Codelab 中随时使用它,每当遇到 PROJECT_ID 变量时,都要使用该 ID。

2. 获取基准示例应用

前提条件之一是有一个正常运行的模块 8 App Engine 应用:完成模块 8 Codelab(推荐)或从代码库复制模块 8 应用。无论您是使用自己的代码还是我们的代码,我们都能在模块 8 中开始(“START”)。本 Codelab 将引导您完成迁移,最后获得类似于模块 9 代码库文件夹(“FINISH”)中的代码。

无论您使用哪个模块 7 应用,该文件夹都应如下所示,可能还包含 lib 文件夹:

$ ls
README.md               appengine_config.py     requirements.txt
app.yaml                main.py                 templates

3. (重新)部署并验证基准应用

执行以下步骤来部署模块 8 应用:

  1. 删除 lib 文件夹(如果有),然后运行 pip install -t lib -r requirements.txt 以重新填充 lib。如果您的开发机器上同时安装了 Python 2 和 3,您可能需要改用 pip2
  2. 确保您已安装初始化 gcloud 命令行工具,并查看其用法
  3. (可选)使用 gcloud config set project PROJECT_ID 设置您的云项目,这样您就不必在每次发出 gcloud 命令时都输入 PROJECT_ID
  4. 使用 gcloud app deploy 部署示例应用
  5. 确认应用按预期正常运行。如果您已完成模块 8 的 Codelab,应用会显示热门访问者以及最近的访问(如下图所示)。底部会显示即将被删除的旧任务。

4aa8a2cb5f527079.png

4. 更新配置

requirements.txt

新的 requirements.txt 与模块 8 的几乎相同,只有一个重大变化:将 google-cloud-ndb 替换为 google-cloud-datastore。进行此更改后,您的 requirements.txt 文件应如下所示:

flask
google-cloud-datastore
google-cloud-tasks

requirements.txt 文件不包含任何版本号,这意味着系统会选择最新版本。如果出现任何不兼容情况,使用版本号锁定应用的有效版本是一种标准做法。

app.yaml

第二代 App Engine 运行时环境不支持内置第三方库(如 2.x 中的内置第三方库),也不支持复制内置库。对于第三方软件包,唯一的要求是在 requirements.txt 中列出它们。因此,可以删除 app.yaml 的整个 libraries 部分。

另一项更新是,Python 3 运行时要求使用自行进行路由的 Web 框架。因此,所有脚本处理程序都必须更改为 auto。不过,由于所有路由都必须更改为 auto,并且此示例应用中没有提供任何静态文件,因此拥有任何处理程序都是无关紧要的,因此也应移除整个 handlers 部分。

app.yaml 中唯一需要做的就是将运行时设置为受支持的 Python 3 版本,例如 3.10。进行此更改后,新的缩写 app.yaml 将仅包含以下单行代码:

runtime: python310

删除 appengine_config.py 和 lib

新一代 App Engine 运行时改进了第三方软件包的使用方式:

  • 内置库是指经过 Google 审核并可在 App Engine 服务器上使用的库,这可能是因为它们包含开发者不允许部署到云端的 C/C++ 代码,这些库在第 2 代运行时中不再可用。
  • 在第二代运行时中,不再需要复制非内置库(有时称为“供应商”或“自打包”)。相反,它们应列在 requirements.txt 中,以便构建系统在部署时自动为您安装它们。

由于对第三方软件包管理进行了这些更改,因此不再需要 appengine_config.py 文件和 lib 文件夹,请将其删除。在第二代运行时中,App Engine 会自动安装 requirements.txt 中列出的第三方软件包。总结:

  1. 没有自捆绑或复制的第三方库;请在 requirements.txt 中列出它们
  2. 没有 pip installlib 文件夹中,即无 lib 文件夹期限
  3. app.yaml 中未列出内置第三方库(因此没有 libraries 部分);在 requirements.txt 中列出这些库
  4. 如果您的应用无需引用任何第三方库,则无需 appengine_config.py 文件

requirements.txt 中列出所有所需的第三方库是开发者的唯一要求。

5. 更新应用文件

只有一个应用文件 main.py,因此本部分中的所有更改都只会影响该文件。下面是一个“差异比较”图,用于说明将现有代码重构为新应用需要做出的总体更改。读者无需逐行阅读代码,因为其目的是简单直观地了解此重构需要做出的更改(但如果需要,可以打开新标签页或下载并放大)。

5d043768ba7be742.png

更新导入和初始化

模块 8 的 main.py 中的导入部分使用 Cloud NDB 和 Cloud Tasks;它应如下所示:

之前

from datetime import datetime
import json
import logging
import time
from flask import Flask, render_template, request
import google.auth
from google.cloud import ndb, tasks

app = Flask(__name__)
ds_client = ndb.Client()
ts_client = tasks.CloudTasksClient()

在第二代运行时(例如 Python 3)中,日志记录功能得到了简化和增强:

  • 如需获得全面的日志记录体验,请使用 Cloud Logging
  • 对于简单的日志记录,只需通过 print() 发送到 stdout(或 stderr)即可
  • 无需使用 Python logging 模块(因此将其移除)

因此,请删除 logging 的导入,并将 google.cloud.ndb 替换为 google.cloud.datastore。同样,将 ds_client 更改为指向 Datastore 客户端,而不是 NDB 客户端。完成这些更改后,新应用的顶部现在应如下所示:

升级后

from datetime import datetime
import json
import time
from flask import Flask, render_template, request
import google.auth
from google.cloud import datastore, tasks

app = Flask(__name__)
ds_client = datastore.Client()
ts_client = tasks.CloudTasksClient()

迁移到 Cloud Datastore

现在,您可以将 NDB 客户端库用法替换为 Datastore。App Engine NDB 和 Cloud NDB 都需要数据模型(类);对于此应用,它是 Visitstore_visit() 函数在所有其他迁移模块中的工作方式都相同:它通过创建新的 Visit 记录来注册访问,并保存访问客户端的 IP 地址和用户代理(浏览器类型)。

之前

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    with ds_client.context():
        Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

不过,Cloud Datastore 使用数据模型类,因此请删除该类。此外,Cloud Datastore 不会在创建记录时自动创建时间戳,因此您需要手动创建时间戳,这可以通过 datetime.now() 调用来完成。

如果没有数据类,修改后的 store_visit() 应如下所示:

升级后

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    entity = datastore.Entity(key=ds_client.key('Visit'))
    entity.update({
        'timestamp': datetime.now(),
        'visitor': '{}: {}'.format(remote_addr, user_agent),
    })
    ds_client.put(entity)

关键函数是 fetch_visits()。它不仅会执行原始查询来获取最新的 Visit,还会获取所显示的最后一个 Visit 的时间戳,并创建调用 /trim(因此也调用 trim())的推送任务,以批量删除旧的 Visit。以下是使用 Cloud NDB 的示例:

之前

def fetch_visits(limit):
    'get most recent visits & add task to delete older visits'
    with ds_client.context():
        data = Visit.query().order(-Visit.timestamp).fetch(limit)
    oldest = time.mktime(data[-1].timestamp.timetuple())
    oldest_str = time.ctime(oldest)
    logging.info('Delete entities older than %s' % oldest_str)
    task = {
        'app_engine_http_request': {
            'relative_uri': '/trim',
            'body': json.dumps({'oldest': oldest}).encode(),
            'headers': {
                'Content-Type': 'application/json',
            },
        }
    }
    ts_client.create_task(parent=QUEUE_PATH, task=task)
    return (v.to_dict() for v in data), oldest_str

主要变更:

  1. 将 Cloud NDB 查询替换为 Cloud Datastore 等效查询;查询样式略有不同。
  2. Datastore 不需要使用上下文管理器,也不会像 Cloud NDB 那样要求您提取其数据(使用 to_dict())。
  3. 将日志记录调用替换为 print()

完成这些更改后,fetch_visits() 应如下所示:

升级后

def fetch_visits(limit):
    'get most recent visits & add task to delete older visits'
    query = ds_client.query(kind='Visit')
    query.order = ['-timestamp']
    visits = list(query.fetch(limit=limit))
    oldest = time.mktime(visits[-1]['timestamp'].timetuple())
    oldest_str = time.ctime(oldest)
    print('Delete entities older than %s' % oldest_str)
    task = {
        'app_engine_http_request': {
            'relative_uri': '/trim',
            'body': json.dumps({'oldest': oldest}).encode(),
            'headers': {
                'Content-Type': 'application/json',
            },
        }
    }
    ts_client.create_task(parent=QUEUE_PATH, task=task)
    return visits, oldest_str

这通常是必需的全部信息。遗憾的是,存在一个主要问题。

(可能)创建新的(推送)队列

在模块 7 中,我们将 App Engine taskqueue 的使用添加到现有的模块 1 应用中。将推送任务作为旧版 App Engine 功能的一项主要优势是,系统会自动创建“默认”队列。在第 8 模块中将该应用迁移到 Cloud Tasks 时,该默认队列已存在,因此我们当时仍然无需担心它。不过,在模块 9 中,这种情况有所改变。

需要考虑的一个关键方面是,新的 App Engine 应用不再使用 App Engine 服务,因此您不能再假设 App Engine 会在其他产品 (Cloud Tasks) 中自动创建任务队列。如代码所示,在 fetch_visits() 中创建任务(针对不存在的队列)将会失败。需要一个新函数来检查是否存在(“默认”)队列,如果不存在,则创建一个。

将此函数命名为 _create_queue_if(),并将其添加到应用中 fetch_visits() 的正上方,因为该函数是在此处调用的。要添加的函数的正文:

def _create_queue_if():
    'app-internal function creating default queue if it does not exist'
    try:
        ts_client.get_queue(name=QUEUE_PATH)
    except Exception as e:
        if 'does not exist' in str(e):
            ts_client.create_queue(parent=PATH_PREFIX,
                    queue={'name': QUEUE_PATH})
    return True

Cloud Tasks create_queue() 函数需要队列的完整路径名,但不包括队列名称。为简单起见,请创建另一个变量 PATH_PREFIX,表示 QUEUE_PATH 减去队列名称 (QUEUE_PATH.rsplit('/', 2)[0])。将其定义添加到顶部附近,以便包含所有常量赋值的代码块如下所示:

_, PROJECT_ID = google.auth.default()
REGION_ID = 'REGION_ID'    # replace w/your own
QUEUE_NAME = 'default'     # replace w/your own
QUEUE_PATH = ts_client.queue_path(PROJECT_ID, REGION_ID, QUEUE_NAME)
PATH_PREFIX = QUEUE_PATH.rsplit('/', 2)[0]

现在,修改 fetch_visits() 中的最后一行以使用 _create_queue_if(),首先创建队列(如有必要),然后创建任务:

    if _create_queue_if():
        ts_client.create_task(parent=QUEUE_PATH, task=task)
    return visits, oldest_str

现在,_create_queue_if()fetch_visits() 的总和应如下所示:

def _create_queue_if():
    'app-internal function creating default queue if it does not exist'
    try:
        ts_client.get_queue(name=QUEUE_PATH)
    except Exception as e:
        if 'does not exist' in str(e):
            ts_client.create_queue(parent=PATH_PREFIX,
                    queue={'name': QUEUE_PATH})
    return True

def fetch_visits(limit):
    'get most recent visits & add task to delete older visits'
    query = ds_client.query(kind='Visit')
    query.order = ['-timestamp']
    visits = list(query.fetch(limit=limit))
    oldest = time.mktime(visits[-1]['timestamp'].timetuple())
    oldest_str = time.ctime(oldest)
    print('Delete entities older than %s' % oldest_str)
    task = {
        'app_engine_http_request': {
            'relative_uri': '/trim',
            'body': json.dumps({'oldest': oldest}).encode(),
            'headers': {
                'Content-Type': 'application/json',
            },
        }
    }
    if _create_queue_if():
        ts_client.create_task(parent=QUEUE_PATH, task=task)
    return visits, oldest_str

除了需要添加此额外代码之外,其余 Cloud Tasks 代码与模块 8 中的代码基本相同。最后要查看的代码是任务处理程序。

更新(推送)任务处理程序

在任务处理程序 trim() 中,Cloud NDB 代码会查询比显示的最旧访问记录更旧的访问记录。它使用仅限键的查询来加快速度 - 如果您只需要访问会话 ID,为什么还要提取所有数据?获得所有访问 ID 后,使用 Cloud NDB 的 delete_multi() 函数批量删除它们。

之前

@app.route('/trim', methods=['POST'])
def trim():
    '(push) task queue handler to delete oldest visits'
    oldest = float(request.get_json().get('oldest'))
    with ds_client.context():
        keys = Visit.query(
                Visit.timestamp < datetime.fromtimestamp(oldest)
        ).fetch(keys_only=True)
        nkeys = len(keys)
        if nkeys:
            logging.info('Deleting %d entities: %s' % (
                    nkeys, ', '.join(str(k.id()) for k in keys)))
            ndb.delete_multi(keys)
        else:
            logging.info(
                    'No entities older than: %s' % time.ctime(oldest))
    return ''   # need to return SOME string w/200

fetch_visits() 类似,大部分更改都涉及将 Cloud NDB 代码替换为 Cloud Datastore、调整查询样式、移除对其上下文管理器的使用,以及将日志记录调用更改为 print()

升级后

@app.route('/trim', methods=['POST'])
def trim():
    '(push) task queue handler to delete oldest visits'
    oldest = float(request.get_json().get('oldest'))
    query = ds_client.query(kind='Visit')
    query.add_filter('timestamp', '<', datetime.fromtimestamp(oldest))
    query.keys_only()
    keys = list(visit.key for visit in query.fetch())
    nkeys = len(keys)
    if nkeys:
        print('Deleting %d entities: %s' % (
                nkeys, ', '.join(str(k.id) for k in keys)))
        ds_client.delete_multi(keys)
    else:
        print('No entities older than: %s' % time.ctime(oldest))
    return ''   # need to return SOME string w/200

主应用处理程序 root() 没有变化。

移植到 Python 3

此示例应用设计为可在 Python 2 和 3 上运行。本教程的相关部分已介绍所有特定于 Python 3 的更改。无需执行任何其他步骤,也无需使用任何兼容性库。

Cloud Tasks 更新

支持 Python 2 的 Cloud Tasks 客户端库的最终版本为 1.5.0。在撰写本文时,适用于 Python 3 的最新版客户端库与该版本完全兼容,因此无需进一步更新。

HTML 模板更新

HTML 模板文件 templates/index.html 中也不需要进行任何更改,这样一来,我们就完成了所有必要的更改,最终得到了模块 9 应用。

6. 总结/清理

部署并验证应用

完成代码更新(主要是移植到 Python 3)后,使用 gcloud app deploy 部署应用。输出应与模块 7 和 8 中的应用相同,只不过您已将数据库访问移至 Cloud Datastore 客户端库,并已升级到 Python 3:

模块 7 visitme 应用

此步骤完成了 Codelab。欢迎您将自己的代码与模块 9 文件夹中的代码进行比较。恭喜!

清理

常规

如果您暂时不想继续操作,建议您停用 App Engine 应用,以免产生结算费用。不过,如果您想进一步测试或实验,App Engine 平台有免费配额,因此只要您不超过该使用层级,就不会产生费用。这是计算费用,但相关 App Engine 服务也可能会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,则这些服务会单独计费。在任何一种情况下,如果适用,请参阅下文中的“本 Codelab 特有的问题”部分。

为了完全公开透明,我们在此说明,部署到 Google Cloud 无服务器计算平台(例如 App Engine)会产生少量 build 和存储费用Cloud BuildCloud Storage 都有各自的免费配额。存储该图片会占用部分配额。不过,您可能居住在没有此类免费层的地区,因此请注意存储空间用量,以尽可能减少潜在费用。您应查看的特定 Cloud Storage“文件夹”包括:

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • 上述存储链接取决于您的 PROJECT_ID 和 *LOC*ation,例如,如果您的应用托管在美国,则为“us”。

另一方面,如果您不打算继续学习此应用或其他相关迁移 Codelab,并且想要彻底删除所有内容,请关闭您的项目

此 Codelab 特有的

以下列出的服务是此 Codelab 特有的。如需了解详情,请参阅各个产品的文档:

后续步骤

至此,我们已完成从 App Engine 任务队列推送任务到 Cloud Tasks 的迁移。模块 3 中还介绍了从 Cloud NDB 到 Cloud Datastore 的可选迁移(不使用任务队列或 Cloud Tasks)。除了模块 3 之外,还有其他迁移模块也重点介绍了如何从 App Engine 旧版捆绑服务迁出,您可以考虑以下模块:

  • 模块 2:从 App Engine NDB 迁移到 Cloud NDB
  • 模块 3:从 Cloud NDB 迁移到 Cloud Datastore
  • 模块 12-13:从 App Engine Memcache 迁移到 Cloud Memorystore
  • 模块 15-16:从 App Engine Blobstore 迁移到 Cloud Storage
  • 模块 18-19:App Engine 任务队列(拉取任务)到 Cloud Pub/Sub

App Engine 不再是 Google Cloud 中唯一的无服务器平台。如果您有一个小型 App Engine 应用或功能有限的应用,并希望将其转换为独立的微服务,或者您希望将单体式应用拆分为多个可重用的组件,那么这些都是考虑迁移到 Cloud Functions 的充分理由。如果容器化已成为应用开发工作流的一部分,尤其是当它包含 CI/CD(持续集成/持续交付或部署)流水线时,请考虑迁移到 Cloud Run。以下模块涵盖了这些使用场景:

  • 从 App Engine 迁移到 Cloud Functions:请参阅模块 11
  • 从 App Engine 迁移到 Cloud Run:请参阅模块 4,了解如何使用 Docker 将应用容器化;或参阅模块 5,了解如何在不使用容器、Docker 知识或 Dockerfile 的情况下完成迁移

您可以选择改用其他无服务器平台,但我们建议您先考虑最适合您的应用和使用情形的选项,然后再进行任何更改。

无论您接下来考虑哪个迁移模块,都可以在 开源代码库中访问所有无服务器迁移站内容(Codelab、视频、源代码 [如有])。该代码库的 README 还提供了有关应考虑哪些迁移以及任何相关的迁移模块“顺序”的指南。

7. 其他资源

Codelab 问题/反馈

如果您发现本 Codelab 存在任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:

迁移时可参考的资源

下表中提供了指向模块 8(开始)和模块 9(完成)的 Repo 文件夹的链接。您还可以从所有 App Engine Codelab 迁移的代码库中访问这些示例,您可以克隆该代码库或下载 ZIP 文件。

Codelab

Python 2

Python 3

模块 8

代码

(不适用)

模块 9

(不适用)

代码

在线资源

以下是一些可能与本教程相关的在线资源:

App Engine

Cloud NDB

Cloud Datastore

Cloud Tasks

其他云信息

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。