使用 PHP 进行后台处理


许多应用都需要在 Web 请求的上下文之外进行后台处理。本教程会创建一个 Web 应用,允许用户输入要翻译的文本,然后显示先前译文的列表。系统会在后台进程中执行翻译,以避免阻塞用户的请求。

下图演示了翻译请求过程。

架构图。

以下事件序列演示了本教程应用的工作原理:

  1. 访问网页,查看存储在 Firestore 中的先前译文列表。
  2. 通过输入 HTML 表单请求翻译文本。
  3. 翻译请求被发布到 Pub/Sub。
  4. Cloud Run 应用接收 Pub/Sub 消息。
  5. Cloud Run 应用使用 Cloud Translation 翻译文本。
  6. Cloud Run 应用将结果存储到 Firestore 中。

本教程适合想要了解如何使用 Google Cloud 进行后台处理的用户,无需拥有 Pub/Sub、Firestore、App Engine 或 Cloud Run 相关经验。不过,具有一定程度的 PHP、JavaScript 和 HTML 相关经验可帮助您理解所有代码。

目标

  • 了解并部署 Cloud Run 服务。
  • 了解并部署 App Engine 应用。
  • 试用该应用。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Make sure that billing is enabled for your Google Cloud project.

  7. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  8. 在 Google Cloud 控制台中,在 Cloud Shell 中打开该应用。

    转到 Cloud Shell

    利用 Cloud Shell,您可以直接在浏览器中通过命令行访问云端资源。在浏览器中打开 Cloud Shell,然后点击继续下载示例代码并切换到应用目录。

  9. 在 Cloud Shell 中,配置 gcloud 工具以使用您的 Google Cloud 项目:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

了解 Cloud Run 后端

请定义一个 PHP 函数 translateString,并将 Cloud Run 服务配置为调用此函数来响应 Pub/Sub 消息。

use Google\Cloud\Firestore\FirestoreClient;
use Google\Cloud\Firestore\Transaction;
use Google\Cloud\Translate\TranslateClient;

/**
 * @param array $data {
 *     The PubSub message data containing text and target language.
 *
 *     @type string $text
 *           The full text to translate.
 *     @type string $language
 *           The target language for the translation.
 * }
 */
function translateString(array $data)
{
    if (empty($data['language']) || empty($data['text'])) {
        throw new Exception('Error parsing translation data');
    }

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();

    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);

    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }

            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

    echo "Done.";
}
  1. 该函数必须导入多个依赖项,以便与 Firestore 和 Translation 连接。

    use Google\Cloud\Firestore\FirestoreClient;
    use Google\Cloud\Firestore\Transaction;
    use Google\Cloud\Translate\TranslateClient;
    
  2. Cloud Run 首先初始化 Firestore 和 Pub/Sub 客户端,然后解析 Pub/Sub 消息数据,以获取要翻译的文本和所需的目标语言。

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();
    
    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];
  3. Translation API 用于将字符串翻译成所需的语言。

    $result = $translate->translate($translation['original'], [
        'target' => $translation['lang'],
    ]);
  4. 该函数为翻译请求提供一个独一无二的名称,以确保不会存储任何重复的译文。然后,该函数在 Firestore 事务中执行翻译,以确保并发执行不会意外地两次运行同一翻译。

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);
    
    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }
    
            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

构建和部署 Cloud Run 后端

  • backend 目录中构建 Cloud Run 应用:

    gcloud builds submit backend/ \
      --tag gcr.io/PROJECT_ID/background-function
  • 使用上一步中的映像标记部署 Cloud Run 应用:

    gcloud run deploy background-processing-function --platform managed \
      --image gcr.io/PROJECT_ID/background-function --region REGION

    其中,REGION 是一个 Google Cloud 区域

  • 部署完成后,您会在命令输出中看到已部署应用的网址。例如:

    Service [background-processing-function] revision [default-00002-vav] has been deployed and is serving 100 percent of traffic at https://default-c457u4v2ma-uc.a.run.app

    复制此网址以供下一步操作使用。

设置 Pub/Sub 订阅

每当有消息发布至 translate 主题时,您的 Cloud Run 应用就会收到 Pub/Sub 消息。

内置身份验证检查机制可确保 Pub/Sub 消息包含有权调用 Cloud Run 后端的服务账号的有效授权令牌。

接下来的步骤将引导您设置 Pub/Sub 主题、订阅和服务账号,以便对 Cloud Run 后端进行经过身份验证的调用。如需详细了解这项集成,请参阅进行服务间身份验证

  1. 创建用于发布新翻译请求的 translate 主题:

    gcloud pubsub topics create translate
    
  2. 为您的项目授予创建 Pub/Sub 身份验证令牌的权限:

    gcloud projects add-iam-policy-binding PROJECT_ID \
         --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
         --role=roles/iam.serviceAccountTokenCreator

    其中,PROJECT_NUMBER 是您的 Google Cloud 项目编号,可运行 gcloud projects describe PROJECT_ID | grep projectNumber 查找。

  3. 创建或选择一个服务账号,用于表示 Pub/Sub 订阅身份。

    gcloud iam service-accounts create cloud-run-pubsub-invoker \
         --display-name "Cloud Run Pub/Sub Invoker"

    注意:您可以使用 cloud-run-pubsub-invoker,也可以将其替换为在您的 Google Cloud 项目中唯一的名称。

  4. 为调用方服务账号授予调用您的 background-processing-function 服务的权限:

    gcloud run services add-iam-policy-binding background-processing-function \
       --member=serviceAccount:cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com \
       --role=roles/run.invoker  --platform managed --region REGION

    Identity and Access Management 更改可能需要几分钟时间才能完成传播。在此期间,您可能会在服务日志中看到 HTTP 403 错误。

  5. 使用该服务账号创建 Pub/Sub 订阅:

    gcloud pubsub subscriptions create run-translate-string --topic translate \
       --push-endpoint=CLOUD_RUN_URL \
       --push-auth-service-account=cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com

    其中,CLOUD_RUN_URL 是您在构建和部署后端之后复制的 HTTP 网址。

    --push-account-service-account 标志激活 Pub/Sub 推送功能,以进行身份验证和授权

    您的 Cloud Run 服务网域会自动注册以便与 Pub/Sub 订阅配合使用。

了解应用

Web 应用有两个主要组件:

  • 用于处理 Web 请求的 PHP HTTP 服务器。该服务器具有以下两个端点:
    • /:列出所有现有翻译,并显示一个用户可以提交以请求新翻译的表单。
    • /request-translation:表单提交会被发送到此端点,此端点会将请求发布到 Pub/Sub 进行异步翻译。
  • 由 PHP 服务器使用现有译文填充的 HTML 模板。

HTTP 服务器

  • app 目录中,index.php 首先设置 Lumen 应用并注册 HTTP 处理程序:

    $app = new Laravel\Lumen\Application(__DIR__);
    $app->router->group([
    ], function ($router) {
        require __DIR__ . '/routes/web.php';
    });
    $app->run();
  • 索引处理程序 (/) 从 Firestore 获取所有现有译文,并使用该列表呈现一个模板,如下所示:

    /**
     * Homepage listing all requested translations and their results.
     */
    $router->get('/', function (Request $request) use ($projectId) {
        $firestore = new FirestoreClient([
            'projectId' => $projectId,
        ]);
        $translations = $firestore->collection('translations')->documents();
        return view('home', ['translations' => $translations]);
    });
  • /request-translation 注册的请求译文处理程序解析提交的 HTML 表单、验证请求,并向 Pub/Sub 发布一条消息:

    /**
     * Endpoint which publishes a PubSub request for a new translation.
     */
    $router->post('/request-translation', function (Request $request) use ($projectId) {
        $acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw'];
        if (!in_array($lang = $request->get('lang'), $acceptableLanguages)) {
            throw new Exception('Unsupported Language: ' . $lang);
        }
        if (!$text = $request->get('v')) {
            throw new Exception('No text to translate');
        }
        $pubsub = new PubSubClient([
            'projectId' => $projectId,
        ]);
        $topic = $pubsub->topic('translate');
        $topic->publish(['data' => json_encode([
            'language' => $lang,
            'text' => $text,
        ])]);
    
        return '';
    });

HTML 模板

HTML 模板是显示给用户的 HTML 页面的基础,以便他们可以看到之前的翻译和请求新的翻译。该模板由 HTTP 服务器使用现有翻译列表填充。

  • HTML 模板的 <head> 元素包含该页面的元数据、样式表和 JavaScript:

    该页面会拉取 Material Design Lite (MDL) CSS 和 JavaScript 资产。借助 MDL,您可以向您的网站添加 Material Design 外观和风格。

    该页面使用 JQuery 等待文档完成加载并设置表单提交处理程序。 提交请求翻译表单后,该页面会进行最小的表单验证,以检查值是否不为空,然后将异步请求发送到 /request-translation 端点。

    最后,系统会显示一个 MDL 信息提示控件,指示请求是成功还是遇到了错误。

  • 网页的 HTML 正文使用 MDL 布局和多个 MDL 组件显示翻译列表和用于请求其他翻译的表单:
    <body>
      <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
        <header class="mdl-layout__header">
          <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">Translate with Background Processing</span>
          </div>
        </header>
        <main class="mdl-layout__content">
          <div class="page-content">
            <div class="mdl-grid">
              <div class="mdl-cell mdl-cell--1-col"></div>
              <div class="mdl-cell mdl-cell--3-col">
                <form id="translate-form" class="translate-form">
                  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                  </div>
                  <select class="mdl-textfield__input lang" name="lang">
                    <option value="de">de</option>
                    <option value="en">en</option>
                    <option value="es">es</option>
                    <option value="fr">fr</option>
                    <option value="ja">ja</option>
                    <option value="sw">sw</option>
                  </select>
                  <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                      name="submit">Submit</button>
                </form>
              </div>
              <div class="mdl-cell mdl-cell--8-col">
                <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                  <thead>
                    <tr>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                    </tr>
                  </thead>
                  <tbody>
                  <?php foreach ($translations as $translation): ?>
                    <tr>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--primary">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['originalLang'] ?></span>
                        </span>
                      <?= $translation['original'] ?>
                      </td>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--accent">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['lang'] ?></span>
                        </span>
                        <?= $translation['translated'] ?>
                      </td>
                    </tr>
                  <?php endforeach ?>
                  </tbody>
                </table>
                <br/>
                <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">Refresh</button>
              </div>
            </div>
          </div>
          <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
            <div class="mdl-snackbar__text mdl-color-text--black"></div>
            <button type="button" class="mdl-snackbar__action"></button>
          </div>
        </main>
      </div>
    </body>
    </html>
    

在 Cloud Shell 中运行应用

在尝试部署 Web 应用之前,请先安装依赖项并在本地运行该应用。

  1. 首先,使用 Composer 安装依赖项。必须安装适用于 PHP 的 gRPC 扩展程序,它已预安装在 Cloud Shell 中。

    composer install -d app
    
  2. 接下来,运行 PHP 内置 Web 服务器来提供您的应用:

    APP_DEBUG=true php -S localhost:8080 -t app
    

    APP_DEBUG=true 标志将显示出现的所有异常。

  3. 在 Cloud Shell 中,点击 Web 预览,然后选择在端口 8080 上预览。此时系统会打开一个新窗口,您的应用正在其中运行。

部署 Web 应用

通过 App Engine 标准环境,您可以构建和部署在繁重负载和大量数据的压力下仍能可靠运行的应用。

本教程使用 App Engine 标准环境来部署 HTTP 前端。

app.yaml 配置 App Engine 应用:

runtime: php73

env_variables:
  APP_DEBUG: true
  LOG_CHANNEL: stderr
  APP_STORAGE: /tmp
  • app.yaml 文件所在的目录中,将您的应用部署到 App Engine 标准环境:
    gcloud app deploy

测试应用

部署 Cloud Functions 函数和 App Engine 应用后,请尝试请求翻译。

  1. 若要在浏览器中查看应用,请输入以下网址:

    https://PROJECT_ID.REGION_ID.r.appspot.com

    替换以下内容:

    您将看到一个页面,其中包含一个空翻译列表和一个用于请求新翻译的表单。

  2. 要翻译的文本字段中,输入一些要翻译的文本,例如 Hello, World
  3. 从下拉列表中选择一种要将文本翻译成的语言。
  4. 点击提交
  5. 如需刷新页面,请点击刷新 。翻译列表中具有一个新行。如果您未看到翻译,请再等待几秒钟,然后重试。如果您仍未看到翻译,请参阅有关调试应用的下一部分。

调试应用

如果您无法连接到 App Engine 应用或未看到新的译文,请检查以下内容:

  1. 检查 gcloud 部署命令是否已成功完成,并且未输出任何错误。如果存在错误(例如 message=Build failed),请修正这些错误,然后尝试构建和部署 Cloud Run 应用并重新部署 App Engine 应用
  2. 在 Google Cloud 控制台中,转到“日志浏览器”页面。

    转到“日志浏览器”页面

    1. 最近选择的资源下拉列表中,点击 GAE 应用,然后点击所有 module_id。您将看到访问您的应用时的请求列表,如果您未发现请求列表,请确认您是否已从下拉列表中选择所有 module_id。如果您发现 Google Cloud 控制台出现错误消息,请检查应用代码是否与“了解 Web 应用”相关部分中的代码匹配。
    2. 最近选择的资源下拉列表中,点击 Cloud Run 修订版本,然后点击所有日志。您应该会看到系统向已部署应用的网址发送了一个 POST 请求。如果没有,请检查 Cloud Run 和 App Engine 应用是否使用同一 Pub/Sub 主题,并且是否已存在一个向 Cloud Run 端点推送内容的 Pub/Sub 订阅。

清理

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

删除 Google Cloud 项目

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

删除教程资源

  1. 删除您在本教程中创建的 App Engine 应用:

    1. In the Google Cloud console, go to the Versions page for App Engine.

      Go to Versions

    2. Select the checkbox for the non-default app version that you want to delete.
    3. 如需删除应用版本,请点击删除

  2. 删除您在本教程中部署的 Cloud Run 服务:

    gcloud run services delete background-processing-function

    您还可以从 Google Cloud 控制台中删除 Cloud Run 服务。

  3. 删除在本教程中创建的其他 Google Cloud 资源:

后续步骤