עיבוד ברקע באמצעות PHP

אפליקציות רבות צריכות לבצע עיבוד ברקע מחוץ להקשר של בקשה לאחזור מהרשת. במדריך הזה ניצור אפליקציית אינטרנט שמאפשרת למשתמשים להזין טקסט לתרגום, ואז מציגה רשימה של תרגומים קודמים. התרגום מתבצע בתהליך ברקע כדי למנוע חסימה של בקשת המשתמש.

התרשים הבא מדגים את תהליך בקשת התרגום.

דיאגרמה של הארכיטקטורה.

כך פועלת אפליקציית המדריך:

  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. נכנסים לחשבון Google Cloud . אם אתם משתמשים חדשים ב- Google Cloud, צרו חשבון כדי שתוכלו להעריך את הביצועים של המוצרים שלנו בתרחישים מהעולם האמיתי. לקוחות חדשים מקבלים בחינם גם קרדיט בשווי 300$ להרצה, לבדיקה ולפריסה של עומסי העבודה.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

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

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

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

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  6. Verify that billing is enabled for your Google Cloud project.

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

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  8. במסוף Google Cloud , פותחים את האפליקציה ב-Cloud Shell.

    כניסה ל-Cloud Shell

    ‫Cloud Shell נותנת גישה למשאבים בענן בממשק שורת פקודה ישירות מהדפדפן. פותחים את Cloud Shell בדפדפן ולוחצים על Proceed כדי להוריד את קוד הדוגמה ולעבור לספריית האפליקציה.

  9. ב-Cloud Shell, מגדירים את הכלי gcloud כך שישתמש בפרויקט Google Cloud :
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

הסבר על ה-Backend של 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 ולתרגום.

    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

  • מבצעים Build לאפליקציית Cloud Run בספרייה backend:

    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 אזור.

  • בסיום הפריסה, כתובת ה-URL של האפליקציה הפרוסה תוצג בפלט פקודה. לדוגמה:

    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

    מעתיקים את כתובת ה-URL הזו לשלב הבא.

הגדרת המינוי ל-Pub/Sub

אפליקציית Cloud Run תקבל הודעות מ-Pub/Sub בכל פעם שהודעה מתפרסמת בנושא translate.

בדיקת אימות מובנית מוודאת שההודעה ב-Pub/Sub מכילה אסימון הרשאה תקין מחשבון שירות שיש לו הרשאה להפעיל את העורף של Cloud Run.

בשלבים הבאים מוסבר איך להגדיר את נושא ה-Pub/Sub, את המינוי ואת חשבון השירות כדי לבצע קריאות מאומתות אל ה-Backend של 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 הוא מספר הפרויקט שלכם, שאפשר למצוא אותו על ידי הפעלת הפקודה gcloud projects describe PROJECT_ID | grep projectNumber. Google Cloud

  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

    יכולות לחלוף כמה דקות עד שהשינויים בניהול הזהויות והרשאות הגישה יופצו. בינתיים, יכול להיות שתראו שגיאות 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 היא כתובת ה-URL של ה-HTTPS שהעתקתם אחרי יצירה ופריסה של ה-Backend.

    הדגל --push-account-service-account מפעיל את הפונקציונליות של Pub/Sub push עבור אימות והרשאה.

    הדומיין של שירות Cloud Run נרשם אוטומטית לשימוש במינויים ל-Pub/Sub.

הסבר על האפליקציה

יש שני רכיבים עיקריים לאפליקציית האינטרנט:

  • שרת HTTP של PHP לטיפול בבקשות אינטרנט. לשרת יש את שתי נקודות הקצה הבאות:
    • /: מציג רשימה של כל התרגומים הקיימים וטופס שמשתמשים יכולים לשלוח כדי לבקש תרגומים חדשים.
    • /request-translation: שליחות של טפסים נשלחות לנקודת הקצה הזו, שמפרסמת את הבקשה ב-Pub/Sub כדי לתרגם אותה באופן אסינכרוני.
  • תבנית HTML שממולאת בתרגומים הקיימים על ידי שרת PHP.

שרת ה-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]);
    });
  • ה-handler של בקשת התרגום, שרשום ב-/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 עם רשימת התרגומים הקיימים.

  • האלמנט <head> בתבנית ה-HTML כולל מטא-נתונים, גיליונות סגנונות ו-JavaScript לדף:

    הדף מאחזר נכסי CSS ו-JavaScript של Material Design Lite (MDL). ‫MDL מאפשר לכם להוסיף לאתרים שלכם מראה ותחושה של Material Design.

    הדף משתמש ב-JQuery כדי להמתין לסיום הטעינה של המסמך ולהגדיר handler לשליחת טופס. בכל פעם ששולחים את טופס בקשת התרגום, הדף מבצע אימות מינימלי של הטופס כדי לוודא שהערך לא ריק, ואז שולח בקשה אסינכרונית לנקודת הקצה /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

לפני שמנסים לפרוס את אפליקציית האינטרנט, צריך להתקין את התלות ולהפעיל אותה באופן מקומי.

  1. קודם כול, מתקינים את יחסי התלות באמצעות Composer. נדרש התוסף gRPC ל-PHP, והוא מותקן מראש ב-Cloud Shell.

    composer install -d app
    
  2. לאחר מכן, מריצים את שרת האינטרנט המובנה של PHP כדי להציג את האפליקציה:

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

    אם יתרחשו חריגים, הם יופיעו בסימון APP_DEBUG=true.

  3. ב-Cloud Shell, לוחצים על תצוגה מקדימה באינטרנט ובוחרים באפשרות תצוגה מקדימה ביציאה 8080. ייפתח חלון חדש עם האפליקציה הפועלת.

פריסת אפליקציית האינטרנט

אתם יכולים להשתמש בסביבה רגילה של 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 Run ואת אפליקציית App Engine, מנסים לשלוח בקשה לתרגום.

  1. כדי להציג את האפליקציה בדפדפן,מזינים את כתובת ה-URL הבאה:

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

    מחליפים את מה שכתוב בשדות הבאים:

    מוצג דף עם רשימה ריקה של תרגומים וטופס לבקשת תרגומים חדשים.

  2. בשדה Text to translate (טקסט לתרגום), מזינים טקסט לתרגום, לדוגמה, Hello, World.
  3. בוחרים שפה מהרשימה הנפתחת שאליה רוצים לתרגם את הטקסט.
  4. לוחצים על שליחה.
  5. כדי לרענן את הדף, לוחצים על רענון . נוספה שורה חדשה לרשימת התרגומים. אם לא רואים תרגום, מחכים עוד כמה שניות ומנסים שוב. אם עדיין לא מופיע תרגום, אפשר לעבור לקטע הבא בנושא ניפוי באגים באפליקציה.

ניפוי באגים באפליקציה

אם אתם לא מצליחים להתחבר לאפליקציית App Engine או שלא רואים תרגומים חדשים, כדאי לבדוק את הדברים הבאים:

  1. בודקים שהפקודות gcloud deploy הושלמו בהצלחה ולא הוחזרו שגיאות. אם היו שגיאות (לדוגמה, message=Build failed), צריך לתקן אותן ולנסות שוב לבנות ולפרוס את אפליקציית Cloud Run ולפרוס את אפליקציית App Engine.
  2. נכנסים לדף Logs Explorer במסוף Google Cloud .

    כניסה לדף Logs Explorer

    1. ברשימה הנפתחת Recently selected resources (משאבים שנבחרו לאחרונה), לוחצים על GAE Application (אפליקציית GAE) ואז על All module_id (כל מזהי המודולים). מוצגת רשימה של בקשות מהביקור שלכם באפליקציה. אם לא מוצגת רשימה של בקשות, צריך לוודא שבחרתם באפשרות All module_id מהרשימה הנפתחת. אם הודעות השגיאה מודפסות ב Google Cloud מסוף, צריך לוודא שהקוד של האפליקציה זהה לקוד שבקטע על הבנת אפליקציית האינטרנט.
    2. ברשימה הנפתחת Recently selected resources, לוחצים על Cloud Run Revision ואז על All logs. אמורה להופיע בקשת POST שנשלחת לכתובת ה-URL של האפליקציה שפרסתם. אם לא, צריך לוודא שאפליקציית Cloud Run ואפליקציית App Engine משתמשות באותו נושא Pub/Sub, ושיש מינוי Pub/Sub לשליחה לנקודת הקצה של Cloud Run.

הסרת המשאבים

כדי להימנע מחיובים בחשבון Google Cloud בגלל השימוש במשאבים שנעשה במסגרת המדריך הזה, אפשר למחוק את הפרויקט שמכיל את המשאבים, או להשאיר את הפרויקט ולמחוק את המשאבים בנפרד.

מחיקת הפרויקט Google Cloud

  1. במסוף Google Cloud , נכנסים לדף Manage resources.

    כניסה לדף Manage resources

  2. ברשימת הפרויקטים, בוחרים את הפרויקט שרוצים למחוק ולוחצים על Delete.
  3. כדי למחוק את הפרויקט, כותבים את מזהה הפרויקט בתיבת הדו-שיח ולוחצים על Shut down.

מחיקת משאבי המדריך

  1. מוחקים את אפליקציית App Engine שיצרתם במדריך הזה:

    1. במסוף Google Cloud , נכנסים לדף Versions של App Engine.

      כניסה לדף Versions

    2. מסמנים את התיבה שלצד גרסת האפליקציה שלא מוגדרת כברירת מחדל ושרוצים למחוק.
    3. כדי למחוק את גרסת האפליקציה, לוחצים על Delete.

  2. מוחקים את שירות Cloud Run שפרסתם במדריך הזה:

    gcloud run services delete background-processing-function

    אפשר גם למחוק שירותים של Cloud Run מGoogle Cloud המסוף.

  3. מחיקת משאבים אחרים Google Cloud שנוצרו במדריך הזה:

המאמרים הבאים