遷移 Python 2 App Engine Cloud NDB &;將 Cloud Tasks 應用程式遷移至 Python 3 和 Cloud Datastore (單元 9)

1. 總覽

無伺服器遷移站系列的程式碼研究室系列 (自助式實作教學課程) 和相關影片,旨在引導 Google Cloud 無伺服器開發人員透過一或多種遷移作業 (主要用於遷移舊版服務) 逐步翻新應用程式。這麼一來,您的應用程式就能更具可攜性,並提供更多選擇和使用彈性,進而整合及使用更多 Cloud 產品,也更容易升級至較新的語言版本。本系列課程一開始將著重在最早的 Cloud 使用者 (主要是 App Engine (標準環境) 開發人員),但涵蓋其他無伺服器平台,包括 Cloud FunctionsCloud Run 或其他無伺服器平台 (如適用)。

本程式碼研究室的用途,是將模組 8 範例應用程式移植至 Python 3,也能將 Datastore (Cloud Firestore (Datastore 模式)) 存取從使用 Cloud NDB 切換至原生 Cloud Datastore 用戶端程式庫,並升級至最新版的 Cloud Tasks 用戶端程式庫。

我們針對單元 7 中的「推送」工作新增了工作佇列,然後在單元 8 中將該用量遷移至 Cloud Tasks。在單元 9 中,我們會繼續探討 Python 3 和 Cloud Datastore。使用工作佇列執行「提取」工作的使用者會遷移至 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,以及從使用 Cloud NDB 切換至原生 Cloud Datastore 用戶端程式庫。

Cloud NDB 適用於 Python 2 和 Python 3,因此 App Engine 使用者可將應用程式從 Python 2 移植至 Python 3。完全選擇性將用戶端程式庫遷移至 Cloud Datastore,但只有一個理由可以考慮這麼做:您有非 App Engine 應用程式 (和/或 Python 3 App Engine 應用程式) 已經使用 Cloud Datastore 用戶端程式庫,並想要整合程式碼集,以僅透過一個用戶端程式庫存取 Datastore。Cloud NDB 是專為 Python 3 App Engine 開發人員建立的 Python 3 遷移工具,所以如果您尚未擁有使用 Cloud Datastore 用戶端程式庫的程式碼,就不需要考慮這項遷移。

最後,Cloud Tasks 用戶端程式庫的開發作業只會在 Python 3 中繼續進行,因此我們要進行「遷移」工作。從最終的 Python 2 版本到 Python 3 當代風幸運的是,Python 2 沒有破壞性變更,這表示您在此不需要進行其他操作。

本教學課程包含下列步驟:

  1. 設定/事前作業
  2. 更新設定
  3. 修改應用程式程式碼

3. 設定/事前作業

本節說明如何:

  1. 設定 Cloud 專案
  2. 取得基準範例應用程式
  3. (重新) 部署及驗證基準應用程式

以下步驟可確保您從有效的程式碼開始著手,並準備好遷移至 Cloud 服務。

1. 設定專案

如果您已完成單元 8 程式碼研究室,請重複使用相同的專案和程式碼。或者,您可以建立新的專案,或是重複使用其他現有專案。請確認專案具備有效的帳單帳戶和已啟用的 App Engine 應用程式。在本程式碼研究室中,您需要先用到這組 ID 來尋找專案 ID,每次遇到 PROJECT_ID 變數時都會用到。

2. 取得基準範例應用程式

必要條件之一是正常運作的 Module 8 App Engine 應用程式:完成單元 8 程式碼研究室 (建議) 或從存放區複製模組 8 應用程式。無論您使用自有或我們的模型,我們都會從單元 8 程式碼開始 (「START」)。本程式碼研究室將引導您逐步完成遷移作業,最後會產生與單元 9 存放區資料夾 (「FINISH」) 類似的程式碼。

無論使用哪個 Module 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 指令中輸入 PROJECT_ID,請使用 gcloud config set project PROJECT_ID 設定 Cloud 專案。
  4. 使用 gcloud app deploy 部署範例應用程式
  5. 確認應用程式運作正常,不會出現任何問題。如果您已完成單元 8 程式碼研究室,應用程式會顯示熱門訪客以及最近的造訪 (如下圖所示)。畫面底部會顯示舊有工作將遭到刪除。

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 執行階段必須使用可自行轉送的網路架構。因此,所有指令碼處理常式都必須變更為 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 代執行階段已無法使用。
  • 第 2 代執行階段不再需要複製非內建程式庫 (有時稱為「供應商」或「自行組合」)。應改為列在 requirements.txt 中,建構系統會在部署時自動代替您安裝。

也由於這些變更對第三方套件管理作業有所調整,因此您不需要使用 appengine_config.py 檔案和 lib 資料夾,因此請刪除這些資料夾。在第 2 代執行階段中,App Engine 會自動安裝 requirements.txt 中列出的第三方套件。總結:

  1. 沒有自行組合或複製的第三方程式庫;在「requirements.txt」中列出
  2. 沒有 pip install 放入 lib 資料夾,意味著沒有任何 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

現在要改用 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 Datastore 對應的 Cloud NDB 查詢;查詢樣式稍有不同
  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 中,我們為現有的模組 1 應用程式新增 App Engine taskqueue。將推送工作視為舊版 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() 函式需要佇列的完整路徑名稱,但「除了」佇列名稱「除外」。為求簡單起見,請建立另一個代表 QUEUE_PATH 減去佇列名稱 (QUEUE_PATH.rsplit('/', 2)[0]) 的變數 PATH_PREFIX。在接近頂端處加入其定義,讓所有常數指派的程式碼區塊看起來如下所示:

_, 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() 函式批次刪除所有造訪 ID。

變更前:

@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 Datastore 的 Cloud NDB 程式碼、調整查詢樣式、移除使用內容管理工具,以及將記錄呼叫變更為 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 應用程式

這個步驟完成了程式碼研究室。建議您將程式碼與模組 9 資料夾中的程式碼進行比較。恭喜!

清除所用資源

一般

如果您現階段已完成設定,建議您停用 App Engine 應用程式,以免產生帳單費用。不過,如果您想測試或進行其他測試,App Engine 平台提供免費配額,而且只要不超出用量限制,就不需支付任何費用。這適用於運算,但相關 App Engine 服務可能也會產生費用,詳情請參閱定價頁面。如果這項遷移作業涉及其他 Cloud 服務,我們會另外計費。無論採用哪種情況,請參閱「本程式碼研究室的專屬」以下章節。

如要完整揭露,部署至 Google Cloud 無伺服器運算平台 (如 App Engine) 會產生少許建構和儲存空間費用Cloud Build 提供的免費配額與 Cloud 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*,例如「us」如果應用程式是由美國代管

另一方面,如果您不打算繼續使用這個應用程式或其他相關的遷移程式碼研究室,且想要徹底刪除所有項目,請關閉專案

本程式碼研究室的專屬功能

下列服務專屬於本程式碼研究室。詳情請參閱每項產品的說明文件:

後續步驟

以上就是從 App Engine 工作佇列推送工作遷移至 Cloud Tasks 的作業。從 Cloud NDB 遷移至 Cloud Datastore 的程序,也會在模組 3 中自行說明 (不含工作佇列或 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 Blob 遷移至 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 的情況下執行應用程式

您可以選擇是否要改用其他無伺服器平台,在進行任何變更前,建議您考量應用程式和用途的最佳選擇。

無論接下來選擇使用哪個遷移模組,所有無伺服器遷移站內容 (程式碼研究室、影片、原始碼 [如有]) 都可以透過其開放原始碼存放區存取。存放區的 README 還針對應考慮遷移的項目和任何相關的「訂單」提供指引接下來介紹遷移模組

7. 其他資源

程式碼研究室問題/意見回饋

如果您在本程式碼研究室中發現任何問題,請先搜尋您的問題再提出申請。搜尋及建立新問題的連結:

遷移資源

下表提供模組 8 (START) 和單元 9 (FINISH) 的存放區資料夾連結。您也可以透過所有 App Engine 程式碼研究室遷移作業的存放區存取這些資料,可以複製或下載 ZIP 檔案。

Codelab

Python 2

Python 3

單元 8

程式碼

(不適用)

單元 9

(不適用)

程式碼

線上資源

以下為可能與本教學課程相關的線上資源:

App Engine

Cloud NDB

Cloud Datastore

Cloud Tasks

其他 Cloud 資訊

授權

這項內容採用的是創用 CC 姓名標示 2.0 通用授權。