Hintergrundverarbeitung mit .NET


Viele Anwendungen müssen eine Hintergrundverarbeitung außerhalb des Kontexts einer Webanfrage ausführen. In dieser Anleitung wird eine Webanwendung erstellt, mit der Nutzer Text eingeben können, der übersetzt werden soll. Anschließend wird eine Liste früherer Übersetzungen angezeigt. Die Übersetzung erfolgt in einem Hintergrundprozess, um die Anfrage des Nutzers nicht zu blockieren.

Das folgende Diagramm veranschaulicht den Ablauf der Übersetzungsanfrage.

Diagramm der Architektur

Wie die in dieser Anleitung verwendete Anwendung funktioniert, sehen Sie an der Abfolge der Ereignisse:

  1. Die Webseite wird aufgerufen, um eine Liste früherer Übersetzungen anzuzeigen, die in Firestore gespeichert sind.
  2. Die Übersetzung eines Textes wird angefordert. Dazu wird ein HTML-Formular ausgefüllt.
  3. Die Übersetzungsanfrage wird in Pub/Sub veröffentlicht.
  4. Ein Cloud Run-Dienst, der dieses Pub/Sub-Thema abonniert hat, wird ausgelöst.
  5. Der Cloud Run-Dienst übersetzt den Text mit Cloud Translation.
  6. Der Cloud Run-Dienst speichert das Ergebnis in Firestore.

Diese Anleitung richtet sich an alle, die mehr über die Hintergrundverarbeitung mit Google Cloud erfahren möchten. Es sind keine Vorkenntnisse in Pub/Sub, Firestore, App Engine oder Cloud Functions erforderlich. Etwas Erfahrung mit .NET, JavaScript und HTML ist allerdings hilfreich, um den gesamten Code zu verstehen.

Lernziele

  • Cloud Run-Dienste verstehen und bereitstellen
  • Anwendung testen

Kosten

In diesem Dokument verwenden Sie die folgenden kostenpflichtigen Komponenten von Google Cloud:

Mit dem Preisrechner können Sie eine Kostenschätzung für Ihre voraussichtliche Nutzung vornehmen. Neuen Google Cloud-Nutzern steht möglicherweise eine kostenlose Testversion zur Verfügung.

Nach Abschluss der in diesem Dokument beschriebenen Aufgaben können Sie weitere Kosten vermeiden, indem Sie die erstellten Ressourcen löschen. Weitere Informationen finden Sie unter Bereinigen.

Hinweis

  1. Melden Sie sich bei Ihrem Google Cloud-Konto an. Wenn Sie mit Google Cloud noch nicht vertraut sind, erstellen Sie ein Konto, um die Leistungsfähigkeit unserer Produkte in der Praxis sehen und bewerten zu können. Neukunden erhalten außerdem ein Guthaben von 300 $, um Arbeitslasten auszuführen, zu testen und bereitzustellen.
  2. Wählen Sie in der Google Cloud Console auf der Seite der Projektauswahl ein Google Cloud-Projekt aus oder erstellen Sie eines.

    Zur Projektauswahl

  3. Die Abrechnung für das Google Cloud-Projekt muss aktiviert sein.

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

    Aktivieren Sie die APIs

  5. Installieren Sie die Google Cloud CLI.
  6. Führen Sie folgenden Befehl aus, um die gcloud CLI zu initialisieren:

    gcloud init
  7. Wählen Sie in der Google Cloud Console auf der Seite der Projektauswahl ein Google Cloud-Projekt aus oder erstellen Sie eines.

    Zur Projektauswahl

  8. Die Abrechnung für das Google Cloud-Projekt muss aktiviert sein.

  9. Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs aktivieren.

    Aktivieren Sie die APIs

  10. Installieren Sie die Google Cloud CLI.
  11. Führen Sie folgenden Befehl aus, um die gcloud CLI zu initialisieren:

    gcloud init
  12. Aktualisieren Sie die gcloud-Komponenten:
    gcloud components update
  13. Bereiten Sie die Entwicklungsumgebung vor.

    .NET-Entwicklungsumgebung einrichten

Anwendung vorbereiten

  1. Klonen Sie das Beispiel-App-Repository in Ihrem Terminalfenster auf Ihren lokalen Computer:

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

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

  2. Wechseln Sie in das Verzeichnis, das den Beispielcode der Hintergrundaufgabe enthält:

    cd getting-started-dotnet/BackgroundProcessing

Grundlegendes zum TranslateWorker-Dienst

  • Der Dienst beginnt mit dem Import mehrerer Abhängigkeiten wie Firestore und Translation.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • Die Firestore- und Translation-Clients werden initialisiert, damit sie zwischen Handleraufrufen wiederverwendet werden können. Auf diese Weise müssen Sie nicht bei jedem Aufruf neue Clients initialisieren, was die Ausführung verlangsamen würde.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • Die Translation API übersetzt den String in die von Ihnen ausgewählte Sprache.

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • Der Konstruktor des Controllers empfängt die Firestore- und Pub/Sub-Clients.

    Die Methode Post parst die Pub/Sub-Nachricht, um den zu übersetzenden Text abzurufen. Die Nachrichten-ID wird als eindeutiger Name für die Übersetzungsanfrage verwendet, um sicherzustellen, dass keine doppelten Übersetzungen gespeichert werden.

    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-Dienst bereitstellen

  • Führen Sie im Verzeichnis BackgroundProcessing das PowerShell-Skript aus, um den Dienst zu erstellen und in Cloud Run bereitzustellen:

    PublishTo-CloudRun.ps1

Grundlegendes zum PublishTo-CloudRun.ps1-Skript

Das Skript PublishTo-CloudRun.ps1 veröffentlicht den Dienst in Cloud Run und schützt den TranslateWorker-Dienst vor Missbrauch. Wenn der Dienst alle eingehenden Verbindungen zulässt, könnte jeder beliebige Übersetzungsanfragen an den Controller senden und so Kosten verursachen. Daher richten Sie den Dienst so ein, dass er nur POST-Anfragen von Pub/Sub akzeptiert.

Das Skript tut Folgendes:

  1. Erstellt die Anwendung lokal mit dotnet publish.
  2. Erstellt einen Container, in dem die Anwendung mit Cloud Build ausgeführt wird.
  3. Stellt die Anwendung für Cloud Run bereit.
  4. Ermöglicht dem Projekt das Erstellen von Pub/Sub-Authentifizierungstokens.
  5. Erstellt ein Dienstkonto, das die Pub/Sub-Aboidentität darstellt.
  6. Gewährt dem Dienstkonto die Berechtigung, den Dienst TranslateWorker aufzurufen.
  7. Erstellt ein Pub/Sub-Thema und ein Abo.

    # 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
    }
    
    

Grundlegendes zum TranslateUI-Dienst

Der Dienst TranslateUI rendert eine Webseite, auf der aktuelle Übersetzungen angezeigt werden, und akzeptiert Anfragen für neue Übersetzungen.

  • Die Klasse StartUp konfiguriert eine ASP.NET-Anwendung und erstellt Pub/Sub- und Firestore-Clients.

    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?}");
                });
            }
    
        }
    }
    
  • Der Index-Handler Index ruft alle vorhandenen Übersetzungen aus Firestore ab und füllt eine ViewModel mit der Liste:

    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 });
            }
        }
    }
  • Neue Übersetzungen werden durch Senden eines HTML-Formulars angefordert. Der Übersetzungsanfrage-Handler validiert die Anfrage und veröffentlicht eine Nachricht an Pub/Sub:

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

TranslateUI-Dienst bereitstellen

  • Führen Sie im Verzeichnis BackgroundProcessing das PowerShell-Skript aus, um den Dienst zu erstellen und in Cloud Run bereitzustellen:

    ./PublishTo-CloudRun.ps1

Grundlegendes zum PublishTo-CloudRun.ps1-Skript

Das Skript PublishTo-CloudRun.ps1 veröffentlicht die Anwendung in Cloud Run.

Das Skript tut Folgendes:

  1. Erstellt die Anwendung lokal mit dotnet publish.
  2. Erstellt einen Container, in dem die Anwendung mit Cloud Build ausgeführt wird.
  3. Stellt die Anwendung für Cloud Run bereit.

    # 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
    
    

Anwendung testen

Fordern Sie nach erfolgreicher Ausführung des Skripts PublishTo-CloudRun.ps1 eine Übersetzung an.

  1. Der letzte Befehl im Skript PublishTo-CloudRun.ps1 gibt die URL für Ihren UI-Dienst an. Suchen Sie im Terminalfenster die URL für den Dienst TranslateUI:

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. Rufen Sie in Ihrem Browser die URL aus dem vorherigen Schritt auf.

    Sie sehen eine Seite mit einer leeren Liste von Übersetzungen und einem Formular zum Anfordern neuer Übersetzungen.

  3. Geben Sie im Feld Text to translate (Zu übersetzender Text) einen Text ein, der übersetzt werden soll, z. B. Hello, World..

  4. Klicken Sie auf Senden.

  5. Klicken Sie zum Aktualisieren der Seite auf Aktualisieren. Die Übersetzungsliste enthält jetzt eine neue Zeile. Wenn Sie keine Übersetzung sehen, warten Sie einige Sekunden und versuchen es dann noch einmal. Ist immer noch keine Übersetzung zu sehen, lesen Sie den nächsten Abschnitt zum Debugging der Anwendung.

Fehler in der Anwendung beheben

Wenn Sie keine Verbindung zu Ihrem Cloud Run-Dienst herstellen können oder keine neuen Übersetzungen sehen, prüfen Sie Folgendes:

  • Prüfen Sie, ob das Skript PublishTo-CloudRun.ps1 erfolgreich ausgeführt wurde und keine Fehler ausgegeben hat. Wenn Fehler aufgetreten sind (z. B. message=Build failed), beheben Sie diese und versuchen Sie es noch einmal.

  • Prüfen Sie die Logs auf Fehler:

    1. Rufen Sie in der Google Cloud Console die Seite „Cloud Run“ auf.

      Zur Seite „Cloud Run“

    2. Klicken Sie auf den Dienstnamen, translate-ui.

    3. Klicken Sie auf Logs.

Bereinigen

Damit Ihrem Google Cloud-Konto die in dieser Anleitung verwendeten Ressourcen nicht in Rechnung gestellt werden, löschen Sie entweder das Projekt, das die Ressourcen enthält, oder Sie behalten das Projekt und löschen die einzelnen Ressourcen.

Google Cloud-Projekt löschen

  1. Wechseln Sie in der Google Cloud Console zur Seite Ressourcen verwalten.

    Zur Seite „Ressourcen verwalten“

  2. Wählen Sie in der Projektliste das Projekt aus, das Sie löschen möchten, und klicken Sie dann auf Löschen.
  3. Geben Sie im Dialogfeld die Projekt-ID ein und klicken Sie auf Shut down (Beenden), um das Projekt zu löschen.

Cloud Run-Dienste löschen.

  • Löschen Sie die Cloud Run-Dienste, die Sie in dieser Anleitung erstellt haben:

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

Nächste Schritte