使用 .NET 进行后台处理

许多应用都需要在网络请求的具体情境之外进行后台处理。本教程创建了一个 Web 应用,让用户输入要翻译的文本,然后显示之前的翻译的列表。翻译在后台进程中完成,避免阻止用户的请求。

下图说明了翻译请求过程。

架构图。

教程应用的工作原理的事件序列如下:

  1. 访问网页,查看存储在 Firestore 中的之前的翻译的列表。
  2. 通过输入 HTML 表单请求翻译文本。
  3. 翻译请求被发布到 Pub/Sub。
  4. 触发已订阅该 Pub/Sub 主题的 Cloud Run 服务。
  5. Cloud Run 服务使用 Cloud Translation 翻译文本。
  6. Cloud Run 服务将结果存储到 Firestore 中。

本教程适用于有兴趣了解如何使用 Google Cloud 进行后台处理的用户。无需任何有关 Pub/Sub、Firestore、App Engine 或 Cloud Functions 的先前经验。不过,具有一定程度的 .NET、JavaScript 和 HTML 相关经验会有助于您了解所有代码。

目标

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

费用

本教程使用 Google Cloud 的以下收费组件:

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

完成本教程后,您可以删除所创建的资源以避免继续计费。如需了解详情,请参阅清理

准备工作

  1. 登录您的 Google 帐号。

    如果您还没有 Google 帐号,请注册一个新帐号

  2. 在 Cloud Console 的项目选择器页面上,选择或创建 Cloud 项目。

    转到项目选择器页面

  3. 确保您的 Google Cloud 项目已启用结算功能。 了解如何确认您的项目已启用结算功能

  4. 启用 Firestore, Cloud Run, Pub/Sub, and Cloud Translation API。

    启用 API

  5. 安装并初始化 Cloud SDK
  6. 更新 gcloud 组件:
    gcloud components update
  7. 准备开发环境。

    设置 .NET 开发环境

准备应用

  1. 在终端窗口中,将示例应用代码库克隆到本地机器:

    git clone https://github.com/GoogleCloudPlatform/getting-started-dotnet.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

  2. 切换到包含后台任务示例代码的目录:

    cd getting-started-dotnet/BackgroundProcessing

了解 TranslateWorker 服务

  • 该服务首先导入多个依赖项,例如 Firestore 和 Translation。

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • 系统初始化 Firestore 和 Translation 客户端,以便可以在各处理程序调用之间重复使用它们。这样,您便无需针对每次调用初始化新的客户端(此操作会降低执行速度)。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • Translation API 会将字符串翻译为您选择的语言。

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • 控制器的构造函数接收 Firestore 和 Pub/Sub 客户端。

    Post 方法解析 Pub/Sub 消息以获取要翻译的文本。它使用消息 ID 作为翻译请求的唯一名称,以确保它不存储任何重复的翻译。

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace TranslateWorker.Controllers
    {
        /// <summary>
        /// The message Pubsub posts to our controller.
        /// </summary>
        public class PostMessage
        {
            public PubsubMessage message { get; set; }
            public string subscription { get; set; }
        }
    
        /// <summary>
        /// Pubsub's inner message.
        /// </summary>
        public class PubsubMessage
        {
            public string data { get; set; }
            public string messageId { get; set; }
            public Dictionary<string, string> attributes { get; set; }
        }
    
        [Route("api/[controller]")]
        [ApiController]
        public class TranslateController : ControllerBase
        {
            private readonly ILogger<TranslateController> _logger;
            private readonly FirestoreDb _firestore;
            private readonly TranslationClient _translator;
            // The Firestore collection where we store translations.
            private readonly CollectionReference _translations;
    
            public TranslateController(ILogger<TranslateController> logger,
                FirestoreDb firestore,
                TranslationClient translator)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
                _firestore = firestore ?? throw new ArgumentNullException(
                    nameof(firestore));
                _translator = translator ?? throw new ArgumentNullException(
                    nameof(translator));
                _translations = _firestore.Collection("Translations");
            }
    
            /// <summary>
            /// Handle a posted message from Pubsub.
            /// </summary>
            /// <param name="request">The message Pubsub posts to this process.</param>
            /// <returns>NoContent on success.</returns>
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] PostMessage request)
            {
                // Unpack the message from Pubsub.
                string sourceText;
                try
                {
                    byte[] data = Convert.FromBase64String(request.message.data);
                    sourceText = Encoding.UTF8.GetString(data);
                }
                catch (Exception e)
                {
                    _logger.LogError(1, e, "Bad request");
                    return BadRequest();
                }
                // Translate the source text.
                _logger.LogDebug(2, "Translating {0} to Spanish.", sourceText);
                var result = await _translator.TranslateTextAsync(sourceText, "es");
                // Store the result in Firestore.
                Translation translation = new Translation()
                {
                    TimeStamp = DateTime.UtcNow,
                    SourceText = sourceText,
                    TranslatedText = result.TranslatedText
                };
                _logger.LogDebug(3, "Saving translation {0} to {1}.",
                    translation.TranslatedText, _translations.Path);
                await _translations.Document(request.message.messageId)
                    .SetAsync(translation);
                // Return a success code.
                return NoContent();
            }
    
            /// <summary>
            /// Serve a root page so Cloud Run knows this process is healthy.
            /// </summary>
            [Route("/")]
            public IActionResult Index()
            {
                return Content("Serving translate requests...");
            }
        }
    }
    

部署 TranslateWorker 服务

  • BackgroundProcessing 目录中,运行 PowerShell 脚本以构建服务并将其部署到 Cloud Run:

    PublishTo-CloudRun.ps1

了解 PublishTo-CloudRun.ps1 脚本

PublishTo-CloudRun.ps1 脚本将服务发布到 Cloud Run,并保护 TranslateWorker 服务不被滥用。如果该服务允许所有传入连接,则任何人都可以将翻译请求发布到控制器,从而产生成本。因此,您将服务设置为仅接受来自 Pub/Sub 的 POST 请求。

该脚本将执行以下操作:

  1. 使用 dotnet publish 在本地构建应用。
  2. 使用 Cloud Build 构建运行应用的容器。
  3. 将应用部署到 Cloud Run。
  4. 让项目能够创建 Pub/Sub 身份验证令牌。
  5. 创建一个服务帐号来表示 Pub/Sub 订阅身份。
  6. 授予服务帐号调用 TranslateWorker 服务的权限。
  7. 创建 Pub/Sub 主题和订阅。

    # 1. Build the application locally.
    dotnet publish -c Release
    
    # Collect some details about the project that we'll need later.
    $projectId = gcloud config get-value project
    $projectNumber = gcloud projects describe $projectId --format="get(projectNumber)"
    $region = "us-central1"
    
    # 2. Use Google Cloud Build to build the worker's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-worker `
        TranslateWorker/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-worker --region $region --platform managed `
        --image gcr.io/$projectId/translate-worker --no-allow-unauthenticated
    $url = gcloud beta run services describe translate-worker --platform managed `
        --region $region --format="get(status.address.hostname)"
    
    # 4. Enable the project to create pubsub authentication tokens.
    gcloud projects add-iam-policy-binding $projectId `
         --member=serviceAccount:service-$projectNumber@gcp-sa-pubsub.iam.gserviceaccount.com `
         --role=roles/iam.serviceAccountTokenCreator
    
    # 5. Create a service account to represent the Cloud Pub/Sub subscription identity.
    $serviceAccountExists = gcloud iam service-accounts describe `
        cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com 2> $null
    if (-not $serviceAccountExists) {
        gcloud iam service-accounts create cloud-run-pubsub-invoker `
            --display-name "Cloud Run Pub/Sub Invoker"
    }
    
    # 6. For Cloud Run, give this service account permission to invoke
    # translate-worker service.
    gcloud beta run services add-iam-policy-binding translate-worker `
         --member=serviceAccount:cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com `
         --role=roles/run.invoker --region=$region
    
    # 7. Create a pubsub topic and subscription, if they don't already exist.
    $topicExists = gcloud pubsub topics describe translate-requests 2> $null
    if (-not $topicExists) {
        gcloud pubsub topics create translate-requests
    }
    $subscriptionExists = gcloud pubsub subscriptions describe translate-requests 2> $null
    if ($subscriptionExists) {
        gcloud beta pubsub subscriptions modify-push-config translate-requests `
            --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    } else {
        gcloud beta pubsub subscriptions create translate-requests `
            --topic translate-requests --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    }
    
    

了解 TranslateUI 服务

TranslateUI 服务将呈现一个显示最近翻译的网页,并接受新翻译请求。

  • StartUp 类将配置 ASP.NET 应用并创建 Pub/Sub 和 Firestore 客户端。

    using Google.Apis.Auth.OAuth2;
    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Net.Http;
    
    namespace TranslateUI
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSingleton<FirestoreDb>(
                    provider => FirestoreDb.Create(GetFirestoreProjectId()));
                services.AddSingleton<PublisherClient>(
                    provider => PublisherClient.CreateAsync(new TopicName(
                        GetProjectId(), GetTopicName())).Result);
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
                app.UseHttpsRedirection();
                app.UseStaticFiles();
                app.UseCookiePolicy();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
    
        }
    }
    
  • 索引处理程序 Index 从 Firestore 获取所有现有翻译,并用以下列表填充 ViewModel

    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Google.Protobuf;
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading.Tasks;
    using TranslateUI.Models;
    
    namespace TranslateUI.Controllers
    {
        public class HomeController : Controller
        {
            private readonly FirestoreDb _firestore;
            private readonly PublisherClient _publisher;
            private CollectionReference _translations;
    
            public HomeController(FirestoreDb firestore, PublisherClient publisher)
            {
                _firestore = firestore;
                _publisher = publisher;
                _translations = _firestore.Collection("Translations");
            }
    
            [HttpPost]
            [HttpGet]
            public async Task<IActionResult> Index(string SourceText)
            {
                // Look up the most recent 20 translations.
                var query = _translations.OrderByDescending("TimeStamp")
                    .Limit(20);
                var snapshotTask = query.GetSnapshotAsync();
    
                if (!string.IsNullOrWhiteSpace(SourceText))
                {
                    // Submit a new translation request.
                    await _publisher.PublishAsync(new PubsubMessage()
                    {
                        Data = ByteString.CopyFromUtf8(SourceText)
                    });
                }
    
                // Render the page.
                var model = new HomeViewModel()
                {
                    Translations = (await snapshotTask).Documents.Select(
                        doc => doc.ConvertTo<Translation>()).ToList(),
                    SourceText = SourceText
                };
                return View(model);
            }
    
            [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult Error()
            {
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }
        }
    }
  • 通过提交 HTML 表单来请求新的翻译。请求翻译处理程序将验证请求,并将消息发布到 Pub/Sub:

    // Submit a new translation request.
    await _publisher.PublishAsync(new PubsubMessage()
    {
        Data = ByteString.CopyFromUtf8(SourceText)
    });
    

部署 TranslateUI 服务

  • BackgroundProcessing 目录中,运行 PowerShell 脚本以构建服务并将其部署到 Cloud Run:

    ./PublishTo-CloudRun.ps1

了解 PublishTo-CloudRun.ps1 脚本

PublishTo-CloudRun.ps1 脚本将应用发布到 Cloud Run。

该脚本将执行以下操作:

  1. 使用 dotnet publish 在本地构建应用。
  2. 使用 Cloud Build 构建运行应用的容器。
  3. 将应用部署到 Cloud Run。

    # 1. Build the application locally.
    dotnet publish -c Release
    # 2. Use Google Cloud Build to build the UI's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-ui `
        TranslateUI/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-ui --region $region --platform managed `
        --image gcr.io/$projectId/translate-ui --allow-unauthenticated
    
    

测试应用

成功运行 PublishTo-CloudRun.ps1 脚本后,尝试请求翻译。

  1. PublishTo-CloudRun.ps1 脚本中的最终命令告诉您 UI 服务的 网址。在您的终端窗口中,找到 TranslateUI 服务的网址:

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. 在浏览器中,转到上一步获得的网址。

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

  3. 要翻译的文本 (Text to translate) 字段中,输入一些要翻译的文本,例如 Hello, World.

  4. 点击提交

  5. 如需刷新页面,请点击刷新 。翻译列表中有一个新行。如果您未看到翻译,请再等待几秒钟,然后重试。如果您仍未看到翻译,请参阅下一部分,了解如何调试应用。

调试应用

如果您无法连接到 Cloud Run 服务或看不到新的翻译,请检查以下内容:

  • 检查 PublishTo-CloudRun.ps1 脚本是否已成功完成,并且未输出任何错误。如果有错误(如 message=Build failed),请修正这些错误,然后重试运行。

  • 查看日志以了解错误:

    1. 在 Google Cloud Console 中,转到 Cloud Run 页面。

      转到 Cloud Run 页面

    2. 点击服务名称 translate-ui

    3. 点击日志

清理

为避免因本教程中使用的资源导致您的 Google Cloud Platform 帐号产生费用,请执行以下操作:

删除 Cloud 项目

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”页面

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除 Cloud Run 服务。

  • 删除您在本教程中创建的 Cloud Run 服务:

    gcloud beta run services delete --region=$region translate-ui
    gcloud beta run services delete --region=$region translate-worker

后续步骤