迁移出站请求

默认情况下,Python 2.7 运行时使用 URL Fetch 服务处理出站 HTTP(S) 请求,即使您使用 urlliburllib2httplib Python 库发出这些请求也是如此。除非明确启用,否则 URL Fetch 不会处理来自 requests 库的请求。

Python 3 运行时无需中间服务来处理出站请求。如果您想停止使用 URL Fetch API,但仍需要类似的功能,则应迁移这些请求以使用标准 Python 库,例如 requests

URL Fetch 与标准 Python 库之间的主要区别

  • 由 URL Fetch 处理的请求的大小限制配额由 URL Fetch 处理的请求的大小限制配额不同。

  • 通过 URL Fetch,当您的应用向另一个 App Engine 应用发送请求时,URL Fetch 会添加 X-Appengine-Inbound-Appid 请求标头声明应用的身份。接收请求的应用可以使用该身份来确定是否应处理该请求。

    如果您的应用使用 URL Fetch,则此标头仅在从您的应用发送的请求中可用。如果您或第三方将标头添加到请求,App Engine 会移除该标头。

    如需了解如何在不使用 URL Fetch 的情况下声明和验证身份,请参阅将 App Identity 迁移到 OIDC ID 令牌

    如需查看在 App Engine 应用之间发送请求时如何使用请求标头来验证调用应用的身份的示例,请参阅 App Engine 到 App Engine 请求示例

  • 您可以使用 URL Fetch 设置所有请求的默认超时。大多数 Python 3 库(例如 requestsurllib)将默认超时设置为 None,因此您应该更新您的代码发出的每个请求以指定超时。

迁移过程概览

  1. 如果您的应用使用 URL Fetch API 发出请求,请更新您的代码以使用标准 Python 库。我们建议您为每个请求指定超时。

  2. 在本地开发服务器中测试出站请求。

  3. 将您的应用配置为在 App Engine 中运行时绕过 URL Fetch

  4. 部署应用。

使用 Python 库替换 URL Fetch API

  1. 如果您还没有使用标准 Python 库来发出传出请求,请选择库并将其添加到应用的依赖项中。

    例如,如需使用 Requests 库,请在 app.yaml 文件所在的文件夹中创建一个 requirements.txt 文件,并添加以下行:

    requests==2.24.0
    

    为了与 Python 2 兼容,我们建议您将 requests 库固定为 2.24.0 版。部署应用时,App Engine 将下载 requirements.txt 文件中定义的所有依赖项。

    对于本地开发,我们建议您在虚拟环境(例如 venv)中安装依赖项。

  2. 在代码中搜索使用 google.appengine.api.urlfetch 模块的地方,并更新代码以使用 Python 库。

发出简单 HTTPS 请求

以下示例展示了如何使用 requests 库发出标准 HTTPS 请求:

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from flask import Flask

import requests

app = Flask(__name__)

@app.route("/")
def index():
    url = "http://www.google.com/humans.txt"
    response = requests.get(url)
    response.raise_for_status()
    return response.text

@app.errorhandler(500)
def server_error(e):
    logging.exception("An error occurred during a request.")
    return (
        """
    An internal error occurred: <pre>{}</pre>
    See logs for full stacktrace.
    """.format(
            e
        ),
        500,
    )

if __name__ == "__main__":
    # This is used when running locally.
    app.run(host="127.0.0.1", port=8080, debug=True)

发出异步 HTTPS 请求

以下示例展示了如何使用 requests 库发出异步 HTTPS 请求:

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from flask import Flask, make_response

from requests_futures.sessions import FuturesSession
from time import sleep

TIMEOUT = 10  # Wait this many seconds for background calls to finish
app = Flask(__name__)

@app.route("/")  # Fetch and return remote page asynchronously
def get_async():
    session = FuturesSession()
    url = "http://www.google.com/humans.txt"

    rpc = session.get(url)

    # ... do other things ...

    resp = make_response(rpc.result().text)
    resp.headers["Content-type"] = "text/plain"
    return resp

@app.route("/callback")  # Fetch and return remote pages using callback
def get_callback():
    global response_text
    global counter

    response_text = ""
    counter = 0

    def cb(resp, *args, **kwargs):
        global response_text
        global counter

        if 300 <= resp.status_code < 400:
            return  # ignore intermediate redirection responses

        counter += 1
        response_text += "Response number {} is {} bytes from {}\n".format(
            counter, len(resp.text), resp.url
        )

    session = FuturesSession()
    urls = [
        "https://google.com/",
        "https://www.google.com/humans.txt",
        "https://www.github.com",
        "https://www.travis-ci.org",
    ]

    futures = [session.get(url, hooks={"response": cb}) for url in urls]

    # No wait functionality in requests_futures, so check every second to
    # see if all callbacks are done, up to TIMEOUT seconds
    for elapsed_time in range(TIMEOUT + 1):
        all_done = True
        for future in futures:
            if not future.done():
                all_done = False
                break
        if all_done:
            break
        sleep(1)

    resp = make_response(response_text)
    resp.headers["Content-type"] = "text/plain"
    return resp

@app.errorhandler(500)
def server_error(e):
    logging.exception("An error occurred during a request.")
    return (
        """
    An internal error occurred: <pre>{}</pre>
    See logs for full stacktrace.
    """.format(
            e
        ),
        500,
    )

在本地测试

如果您更新任何出站请求,请在本地开发服务器中运行应用并确认请求成功。

绕过 URL Fetch

在向 App Engine 部署应用时,如需阻止 URL Fetch 处理请求,请执行以下操作:

  1. app.yaml 文件中,将 GAE_USE_SOCKETS_HTTPLIB 环境变量设置为任意值。该值可以是任何值,包括空字符串。 例如:

    env_variables:
      GAE_USE_SOCKETS_HTTPLIB : ''
    
  2. 如果您启用了 URL Fetch 来处理从 requests 库发送的请求,您可以从应用中移除 Requests AppEngineAdapter

    例如,appengine_config.py 文件移除 requests_toolbelt.adapters.appengine,并从 Python 文件移除 requests_toolbelt.adapters.appengine.monkeypatch()

请注意,即使您按照前面的步骤绕过 URL Fetch,您的应用仍然可以直接使用 URL Fetch API。

部署应用

准备好部署应用后,您应该执行以下操作:

  1. 在 App Engine 上测试应用

    查看 Google Cloud Console 中的“App Engine 配额”页面,以确认应用没有进行 URL Fetch API 调用。

    查看 URL Fetch API 调用

  2. 如果应用运行正常(没有错误),请使用流量拆分功能为更新后的应用缓慢增加流量。请先仔细监控应用是否存在任何问题,然后再将更多流量路由到更新后的应用。