将 App Engine Blobstore 迁移到 Cloud Storage

本指南介绍了如何从 App Engine Blobstore 迁移到 Cloud Storage

Cloud Storage 与 App Engine Blobstore 类似,您可以使用 Cloud Storage 传送大型数据对象 (blob),例如视频或图片文件,并让您的用户可上传大型数据文件。虽然 App Engine Blobstore 只能通过 App Engine 旧版捆绑服务访问,但 Cloud Storage 是独立的 Google Cloud 产品,可通过 Cloud 客户端库访问。Cloud Storage 可为您的应用提供更现代化的对象存储解决方案,并在之后让您可以灵活地迁移到 Cloud Run 或其他 Google Cloud 应用托管平台。

对于 2016 年 11 月之后创建的 Google Cloud 项目,Blobstore 在后台使用 Cloud Storage 存储桶。这意味着,将应用迁移到 Cloud Storage 后,这些现有 Cloud Storage 存储桶中的所有现有对象和权限都会保持不变。您还可以开始使用适用于 Cloud Storage 的 Cloud 客户端库访问这些现有存储桶。

主要区别和相似之处

Cloud Storage 排除了以下 Blobstore 依赖项和限制:

  • Python 2 版 Blobstore API 依赖于 webapp。
  • Python 3 版 Blobstore API 通过实用程序类来使用 Blobstore 处理程序
  • 对于 Blobstore,可以上传到 Blobstore 的文件数量的上限为 500。您可以在 Cloud Storage 存储桶中创建的对象数量不受限制。

Cloud Storage 不支持:

  • Blobstore 处理程序类
  • Blobstore 对象

Cloud Storage 与 App Engine Blobstore 的相似之处:

  • 能够在运行时环境中读取和写入大型数据对象,以及存储和传送静态大型数据对象,例如影片、图片或其他静态内容。Cloud Storage 的对象大小限制为 5 TiB。
  • 可让您将对象存储在 Cloud Storage 存储桶中。
  • 具有免费层级。

准备工作

  • 您应查看并了解 Cloud Storage 价格和配额:
  • 已有使用 Blobstore 的 Python 2 或 Python 3 App Engine 应用。
  • 本指南中的示例展示了一个使用 Flask 框架迁移到 Cloud Storage 的应用。请注意,在迁移到 Cloud Storage 时,您可以使用任何 Web 框架,包括保留 webapp2

概览

概括来讲,从 App Engine Blobstore 迁移到 Cloud Storage 的过程包括以下步骤:

  1. 更新配置文件
  2. 更新 Python 应用
    • 更新 Web 框架
    • 导入并初始化 Cloud Storage
    • 更新 Blobstore 处理程序
    • 可选:如果使用 Cloud NDB 或 App Engine NDB,请更新数据模型
  3. 测试和部署应用

更新配置文件

在修改应用代码以从 Blobstore 迁移到 Cloud Storage 之前,请更新配置文件以使用 Cloud Storage 库。

  1. 更新 app.yaml 文件。 按照适用于您的 Python 版本的说明操作:

    Python 2

    对于 Python 2 应用:

    1. 移除 handlers 部分以及 libraries 部分中任何不必要的 webapp 依赖项。
    2. 如果您使用 Cloud 客户端库,请添加最新版本的 grpciosetuptools 库。
    3. 添加 ssl 库,因为 Cloud Storage 要求这样做。

    以下是一个示例 app.yaml 文件,其中包含所做的更改:

    runtime: python27
    threadsafe: yes
    api_version: 1
    
    handlers:
    - url: /.*
      script: main.app
    
    libraries:
    - name: grpcio
      version: latest
    - name: setuptools
      version: latest
    - name: ssl
      version: latest
    

    Python 3

    对于 Python 3 应用,请删除除 runtime 元素之外的所有行。 例如:

    runtime: python310 # or another support version
    

    Python 3 运行时会自动安装库,因此您无需指定过往 Python 2 运行时中的内置库。如果 Python 3 应用在迁移到 Cloud Storage 时使用其他旧版捆绑服务,请保持 app.yaml 文件不变。

  2. 更新 requirements.txt 文件。 按照适用于您的 Python 版本的说明操作:

    Python 2

    将适用于 Cloud Storage 的 Cloud 客户端库添加到 requirements.txt 文件中的依赖项列表。

    google-cloud-storage
    

    然后,运行 pip install -t lib -r requirements.txt 以更新应用的可用库列表。

    Python 3

    将适用于 Cloud Storage 的 Cloud 客户端库添加到 requirements.txt 文件中的依赖项列表。

    google-cloud-storage
    

    App Engine 会在 Python 3 运行时的应用部署期间自动安装这些依赖项,因此如果存在 lib 文件夹,请将其删除。

  3. 对于 Python 2 应用,如果您的应用使用内置库或复制的库,则必须在 appengine_config.py 文件中指定以下路径:

    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set PATH to your libraries folder.
    PATH = 'lib'
    # Add libraries installed in the PATH folder.
    vendor.add(PATH)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(PATH)
    

更新 Python 应用

修改配置文件后,请更新 Python 应用。

更新 Python 2 Web 框架

对于使用 webapp2 框架的 Python 2 应用,建议迁移过时的 webapp2 框架。如需了解 Python 2 支持终止日期,请参阅运行时支持时间表

您可以迁移到其他 Web 框架,例如 Flask、Django 或 WSGI。由于 Cloud Storage 排除了 webapp2 的依赖项,并且 Blobstore 处理程序不受支持,因此您可以删除或替换其他与 webapp 相关的库。

如果您选择继续使用 webapp2,请注意本指南中的示例将 Cloud Storage 与 Flask 搭配使用。

如果除了 Cloud Storage 之外,您还想要使用 Google Cloud 服务,或者为了获取最新运行时版本的访问权限,您应考虑将应用升级到 Python 3 运行时。如需了解详情,请参阅 Python 2 到 Python 3 迁移概览

导入并初始化 Cloud Storage

通过更新导入和初始化行来修改应用文件:

  1. 移除 Blobstore import 语句,如下所示:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. 添加 Cloud Storage 和 Google 身份验证库的 import 语句,如下所示:

    import io
    from flask import (Flask, abort, redirect, render_template,
    request, send_file, url_for)
    from google.cloud import storage
    import google.auth
    

    需要使用 Google 身份验证库才能为 Cloud Storage 获取 Blobstore 中使用的项目 ID。导入 Cloud NBD 等其他库,前提是适用于您的应用。

  3. 为 Cloud Storage 创建新客户端并指定 Blobstore 中使用的存储桶。例如:

    gcs_client = storage.Client()
    _, PROJECT_ID = google.auth.default()
    BUCKET = '%s.appspot.com' % PROJECT_ID
    

    对于 2016 年 11 月之后的 Google Cloud 项目,Blobstore 会将数据写入以应用网址命名的 Cloud Storage 存储桶,并遵循 PROJECT_ID.appspot.com 格式。您可以使用 Google 身份验证获取项目 ID,以指定用于在 Blobstore 中存储 blob 的 Cloud Storage 存储桶。

更新 Blobstore 处理程序

由于 Cloud Storage 不支持 Blobstore 上传和下载处理程序,因此您需要结合使用 Cloud Storage 功能、io 标准库模块、Web 框架和 Python 实用程序来上传和下载 Cloud Storage 中的对象 (blob)。

下面演示了如何使用 Flask 作为示例 Web 框架来更新 Blobstore 处理程序:

  1. 将 Blobstore 上传处理程序类替换为 Flask 中的上传函数。按照适用于您的 Python 版本的说明操作:

    Python 2

    Python 2 中的 Blobstore 处理程序是 webapp2 类,如以下 Blobstore 示例所示:

    class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads()
            blob_id = uploads[0].key() if uploads else None
            store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
            self.redirect('/', code=307)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    如需使用 Cloud Storage,请执行以下操作:

    1. 将 Webapp 上传类替换为 Flask 上传函数。
    2. 将上传处理程序和路由替换为使用路由修饰的 Flask POST 方法。

    更新后的代码示例

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    在更新后的 Cloud Storage 代码示例中,应用现在通过其对象名称 (fname) 而不是 blob_id 来标识对象工件。路由也在应用文件的底部进行。

    为了获取上传的对象,Blobstore 的 get_uploads() 方法已替换为 Flask 的 request.files.get() 方法。在 Flask 中,您可以使用 secure_filename() 方法获取文件的名称,而不使用路径字符(例如 /),并通过使用 gcs_client.bucket(BUCKET).blob(fname) 指定存储桶名称和对象名称来标识对象。

    Cloud Storage upload_from_file() 调用会执行上传操作,如更新后的示例所示。

    Python 3

    Python 3 版 Blobstore 中的上传处理程序类是一个实用程序类,需要使用 WSGI environ 字典作为输入参数,如以下 Blobstore 示例所示:

    class UploadHandler(blobstore.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads(request.environ)
            if uploads:
                blob_id = uploads[0].key()
                store_visit(request.remote_addr, request.user_agent, blob_id)
            return redirect('/', code=307)
    ...
    @app.route('/upload', methods=['POST'])
    def upload():
        """Upload handler called by blobstore when a blob is uploaded in the test."""
        return UploadHandler().post()
    

    如需使用 Cloud Storage,请将 Blobstore 的 get_uploads(request.environ) 方法替换为 Flask 的 request.files.get() 方法。

    更新后的代码示例

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    在更新后的 Cloud Storage 代码示例中,应用现在通过其对象名称 (fname) 而不是 blob_id 来标识对象工件。路由也在应用文件的底部进行。

    为了获取上传的对象,Blobstore 的 get_uploads() 方法已替换为 Flask 的 request.files.get() 方法。在 Flask 中,您可以使用 secure_filename() 方法获取文件的名称,而不使用路径字符(例如 /),并通过使用 gcs_client.bucket(BUCKET).blob(fname) 指定存储桶名称和对象名称来标识对象。

    Cloud Storage upload_from_file() 方法会执行上传操作,如更新后的示例所示。

  2. 将 Blobstore 下载处理程序类替换为 Flask 中的下载函数。按照适用于您的 Python 版本的说明操作:

    Python 2

    以下下载处理程序示例展示了使用 webapp2 的 BlobstoreDownloadHandler 类的用法:

    class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    如需使用 Cloud Storage,请执行以下操作:

    1. 更新 Blobstore 的 send_blob() 方法以使用 Cloud Storage 的 download_as_bytes() 方法。
    2. 将路由从 webapp2 更改为 Flask。

    更新后的代码示例

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    在更新后的 Cloud Storage 代码示例中,Flask 会修饰 Flask 函数中的路由,并使用 '/view/<path:fname>' 标识对象。Cloud Storage 通过对象名称和存储桶名称来标识 blob 对象,并使用 download_as_bytes() 方法以字节形式下载对象,而不是使用 Blobstore 中的 send_blob 方法。如果未找到工件,则应用会返回 HTTP 404 错误。

    Python 3

    与上传处理程序一样,Python 3 版 Blobstore 中的下载处理程序类是一个实用程序类,需要使用 WSGI environ 字典作为输入参数,如以下 Blobstore 示例所示:

    class ViewBlobHandler(blobstore.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            if not blobstore.get(blob_key):
                return "Photo key not found", 404
            else:
                headers = self.send_blob(request.environ, blob_key)
    
            # Prevent Flask from setting a default content-type.
            # GAE sets it to a guessed type if the header is not set.
            headers['Content-Type'] = None
            return '', headers
    ...
    @app.route('/view/<blob_key>')
    def view_photo(blob_key):
        """View photo given a key."""
        return ViewBlobHandler().get(blob_key)
    

    如需使用 Cloud Storage,请将 Blobstore 的 send_blob(request.environ, blob_key) 替换为 Cloud Storage 的 blob.download_as_bytes() 方法。

    更新后的代码示例

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    在更新后的 Cloud Storage 代码示例中,blob_key 已替换为 fname,而 Flask 使用 '/view/<path:fname>' 网址来标识对象。gcs_client.bucket(BUCKET).blob(fname) 方法用于查找文件名和存储桶名称。 Cloud Storage 的 download_as_bytes() 方法以字节形式下载对象,而不是使用 Blobstore 中的 send_blob() 方法。

  3. 如果应用使用主处理程序,请将 MainHandler 类替换为 Flask 中的 root() 函数。按照适用于您的 Python 版本的说明操作:

    Python 2

    下面是一个使用 Blobstore 的 MainHandler 类的示例:

    class MainHandler(BaseHandler):
        'main application (GET/POST) handler'
        def get(self):
            self.render_response('index.html',
                    upload_url=blobstore.create_upload_url('/upload'))
    
        def post(self):
            visits = fetch_visits(10)
            self.render_response('index.html', visits=visits)
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    如需使用 Cloud Storage,请执行以下操作:

    1. 移除 MainHandler(BaseHandler) 类,因为 Flask 会为您处理路由。
    2. 使用 Flask 简化 Blobstore 代码。
    3. 移除末尾的 webapp 路由。

    更新后的代码示例

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Python 3

    如果您使用的是 Flask,则您没有 MainHandler 类,但如果使用 blobstore,则需要更新 Flask 根函数。以下示例使用 blobstore.create_upload_url('/upload') 函数:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = blobstore.create_upload_url('/upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    如需使用 Cloud Storage,请将 blobstore.create_upload_url('/upload') 函数替换为 Flask 的 url_for() 方法,以获取 upload() 函数的网址。

    更新后的代码示例

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload') # Updated to use url_for
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

测试和部署应用

通过本地开发服务器,您可以测试应用是否运行,但在部署新版本之前,您无法测试 Cloud Storage,因为所有 Cloud Storage 请求都需要通过互联网发送到实际的 Cloud Storage 存储桶。请参阅测试和部署应用,以了解如何在本地运行应用。然后部署新版本,以确认应用看起来与以前相同。

使用 App Engine NDB 或 Cloud NDB 的应用

如果您的应用使用 App Engine NDB 或 Cloud NDB 来添加与 Blobstore 相关的属性,则必须更新 Datastore 数据模型。

更新数据模型

由于 Cloud Storage 不支持 NDB 中的 BlobKey 属性,因此您需要修改与 Blobstore 相关的行以使用 NDB、Web 框架或其他位置的内置等效项。

如需更新数据模型,请执行以下操作:

  1. 在数据模型中找到使用 BlobKey 的行,如下所示:

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()
    
  2. ndb.BlobKeyProperty() 替换为 ndb.StringProperty()

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.StringProperty() # Modified from ndb.BlobKeyProperty()
    
  3. 如果您还要在迁移过程中从 App Engine NDB 升级到 Cloud NDB,请参阅 Cloud NDB 迁移指南,了解如何重构 NDB 代码以使用 Python 上下文管理器。

Datastore 数据模型的向后兼容性

在上一部分中,将 ndb.BlobKeyProperty 替换为 ndb.StringProperty 会导致应用向后不兼容,这意味着应用无法处理 Blobstore 创建的旧条目。如果您需要保留旧数据,请为新的 Cloud Storage 条目创建另一个字段,而不是更新 ndb.BlobKeyProperty 字段,并创建一个函数来对数据进行标准化。

根据前面部分中的示例进行以下更改:

  1. 定义数据模型时,创建两个单独的属性字段。使用 file_blob 属性可标识 Blobstore 创建的对象,使用 file_gcs 属性可标识 Cloud Storage 创建的对象:

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()  # backwards-compatibility
        file_gcs  = ndb.StringProperty()
    
  2. 找到引用新访问的行,如下所示:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_blob=upload_key).put()
    
  3. 更改代码,以使 file_gcs 用于近期的条目。例如:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_gcs=upload_key).put() # change file_blob to file_gcs for new requests
    
  4. 创建一个新函数来对数据进行标准化。以下示例展示如何使用提取、转换和加载 (ETL) 循环遍历所有访问,并采用访问者和时间戳数据来检查是否存在 file_gcsfile_gcs

    def etl_visits(visits):
        return [{
                'visitor': v.visitor,
                'timestamp': v.timestamp,
                'file_blob': v.file_gcs if hasattr(v, 'file_gcs') \
                        and v.file_gcs else v.file_blob
                } for v in visits]
    
  5. 找到引用 fetch_visits() 函数的行:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    
  6. fetch_visits() 封装在 etl_visits() 函数内,例如:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = etl_visits(fetch_visits(10)) # etl_visits wraps around fetch_visits
        return render_template('index.html', **context)
    

示例

后续步骤