שליחת בקשות באצווה

במאמר הזה נסביר איך לקבץ באצווה קריאות ל-API בפורמט JSON כדי לצמצם את מספר חיבורי ה-HTTP שהלקוח צריך לבצע בשביל גישה ל-Cloud Storage.

סקירה כללית

כל חיבור HTTP שהלקוח יוצר מגדיל במידה מסוימת את התקורה. ה-API בפורמט JSON של Cloud Storage תומך בקיבוץ באצווה של קריאות כדי לאפשר ללקוח לבצע מספר קריאות ל-API בבקשת HTTP אחת.

דוגמאות למצבים שבהם כדאי להשתמש בקיבוץ באצווה של קריאות:

  • עדכון מטא-נתונים, כמו הרשאות, באובייקטים רבים.
  • מחיקת אובייקטים רבים.

במקרים כאלה, במקום לשלוח כל קריאה בנפרד, אפשר לקבץ את כל הקריאות בבקשת HTTP אחת. כל הבקשות הפנימיות חייבות לעבור ל-API בפורמט JSON של Cloud Storage.

אסור לכלול יותר מ-100 קריאות בבקשה באצווה אחת. אם צריך לבצע יותר קריאות, תוכלו להשתמש במספר בקשות באצווה. המטען הייעודי (payload) הכולל של בקשות באצווה צריך להיות קטן מ-10MB.

פירוט לגבי בקשות באצווה

בקשת Batch מורכבת מכמה קריאות ל-API שמאוגדות לבקשת HTTP אחת, שאותה אפשר לשלוח אל נקודת הקצה של Cloud Storage לאצווה, שהיא https://storage.googleapis.com/batch/storage/v1. בקטע הזה נתאר בפירוט את התחביר של קריאות באצווה; ובהמשך נציג דוגמה.

הפורמט של בקשה באצווה

בקשה באצווה היא בקשת HTTP רגילה אחת שמכילה מספר קריאות ל-API בפורמט JSON של Cloud Storage. הבקשה הראשית הזו משתמשת בסוג התוכן multipart/mixed. בבקשת ה-HTTP הראשית יש מספר חלקים, שכל אחד מהם מכיל בתוכו בקשת HTTP פנימית.

כל חלק מתחיל בכותרת HTTP ‏Content-Type: application/http משלו. לחלק יכולה להיות גם כותרת Content-ID אופציונלית. הכותרות האלה מציינות את תחילת החלק, אבל הן נפרדות מבקשת ה-HTTP הפנימית. המשמעות היא שאחרי שהשרת מפרק את הבקשה באצווה לבקשות נפרדות, המערכת מתעלמת מכותרות החלקים.

הגוף של כל חלק הוא בעצמו בקשת HTTP מלאה עם פועל, כתובת URL, כותרות וגוף. בקשות ה-HTTP צריכות להכיל רק את החלק של הנתיב שבכתובת ה-URL; ההתנהגות של כתובות URL מלאות עלולה להיות לא צפויה.

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

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

כש-Cloud Storage מקבל את הבקשה באצווה, המערכת מחילה את הפרמטרים והכותרות של השאילתה החיצונית (לפי הכללים) על כל חלק, ולאחר מכן מתייחסת לכל חלק כאילו היה בקשת HTTP נפרדת.

תשובה לבקשה באצווה

התשובה ב-Cloud Storage היא תשובת HTTP רגילה יחידה עם סוג תוכן multipart/mixed. כל חלק בתגובה הראשית הזו הוא תשובה לאחת מהבקשות שבבקשה באצווה. סדר התשובות זהה לסדר הבקשות.

כמו החלקים בבקשה, כל חלק בתשובה מכיל תשובת HTTP מלאה הכוללת קוד סטטוס, כותרות וגוף. בדומה לחלקים שבבקשה, לפני כל חלק של תשובה מופיעה כותרת Content-Type שמסמנת את תחילת החלק. מידע נוסף על קודי סטטוס אפשר למצוא במאמר בנושא קודי סטטוס ושגיאה של HTTP ב-API בפורמט JSON של Cloud Storage.

אם לחלק מסוים בבקשה יש כותרת Content-ID, לחלק המתאים בתשובה יש כותרת Content-ID תואמת. הכותרת Content-ID של התשובה מתחילה ב-response-, ואחריו הערך Content-ID שנמצא בשימוש בבקשה, כפי שמוצג בדוגמה.

דוגמה

בדוגמה הבאה הקריאה באצווה מעדכנת את המטא-נתונים המותאמים אישית של שלושה אובייקטים ב-example-bucket.

דוגמה לבקשת HTTP באצווה

HTTP

POST /batch/storage/v1 HTTP/1.1
Host: storage.googleapis.com
Content-Length: 960
Content-Type: multipart/mixed; boundary="===============7330845974216740156=="
Authorization: Bearer ya29.AHES6ZRVmB7fkLtd1XTmq6mo0S1wqZZi3-Lh_s-6Uw7p8vtgSwg

--===============7330845974216740156==
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: <b29c5de2-0db4-490b-b421-6a51b598bd22+1>

PATCH /storage/v1/b/example-bucket/o/obj1 HTTP/1.1
Content-Type: application/json
accept: application/json
content-length: 31

{"metadata": {"type": "tabby"}}
--===============7330845974216740156==
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: <b29c5de2-0db4-490b-b421-6a51b598bd22+2>

PATCH /storage/v1/b/example-bucket/o/obj2 HTTP/1.1
Content-Type: application/json
accept: application/json
content-length: 32

{"metadata": {"type": "tuxedo"}}
--===============7330845974216740156==
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: <b29c5de2-0db4-490b-b421-6a51b598bd22+3>

PATCH /storage/v1/b/example-bucket/o/obj3 HTTP/1.1
Content-Type: application/json
accept: application/json
content-length: 32

{"metadata": {"type": "calico"}}
--===============7330845974216740156==--

ספריות לקוח

C++‎

ספריית הלקוח C++‎ לא תומכת בבקשות באצווה.

C#‎

ספריית הלקוח C#‎ לא תומכת בבקשות באצווה.

Go

ספריית הלקוח Go לא תומכת בבקשות באצווה.

Java

מידע נוסף מופיע במאמרי העזרה של Cloud Storage Java API.

import com.google.api.gax.paging.Page;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageBatch;
import com.google.cloud.storage.StorageBatchResult;
import com.google.cloud.storage.StorageException;
import com.google.cloud.storage.StorageOptions;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class BatchSetObjectMetadata {
  public static void batchSetObjectMetadata(
      String projectId, String bucketName, String pathPrefix) {
    // The ID of your GCP project
    // String projectId = "your-project-id";

    // The ID of your GCS bucket
    // String bucketName = "your-unique-bucket-name";

    // The directory prefix. All objects in the bucket with this prefix will have their metadata
    // updated
    // String pathPrefix = "yourPath/";

    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
    Map<String, String> newMetadata = new HashMap<>();
    newMetadata.put("keyToAddOrUpdate", "value");
    Page<Blob> blobs =
        storage.list(
            bucketName,
            Storage.BlobListOption.prefix(pathPrefix),
            Storage.BlobListOption.delimiter("/"));
    StorageBatch batchRequest = storage.batch();

    // Add all blobs with the given prefix to the batch request
    List<StorageBatchResult<Blob>> batchResults =
        blobs
            .streamAll()
            .map(blob -> batchRequest.update(blob.toBuilder().setMetadata(newMetadata).build()))
            .collect(Collectors.toList());

    // Execute the batch request
    batchRequest.submit();
    List<StorageException> failures =
        batchResults.stream()
            .map(
                r -> {
                  try {
                    BlobInfo blob = r.get();
                    return null;
                  } catch (StorageException e) {
                    return e;
                  }
                })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

    System.out.println(
        (batchResults.size() - failures.size())
            + " blobs in bucket "
            + bucketName
            + " with prefix '"
            + pathPrefix
            + "' had their metadata updated successfully.");

    if (!failures.isEmpty()) {
      System.out.println("While processing, there were " + failures.size() + " failures");

      for (StorageException failure : failures) {
        failure.printStackTrace(System.out);
      }
    }
  }
}

Node.js

ספריית הלקוח Node.js לא תומכת בבקשות באצווה.

PHP

ספריית הלקוח PHP לא תומכת בבקשות באצווה.

Python

מידע נוסף מופיע במאמרי העזרה של Cloud Storage Python API.


from google.cloud import storage


def batch_request(bucket_name, prefix=None):
    """
    Use a batch request to patch a list of objects with the given prefix in a bucket.

    Note that Cloud Storage does not support batch operations for uploading or downloading.
    Additionally, the current batch design does not support library methods whose return values
    depend on the response payload.
    See https://cloud.google.com/python/docs/reference/storage/latest/google.cloud.storage.batch
    """
    # The ID of your GCS bucket
    # bucket_name = "my-bucket"
    # The prefix of the object paths
    # prefix = "directory-prefix/"

    client = storage.Client()
    bucket = client.bucket(bucket_name)

    # Accumulate in a list the objects with a given prefix.
    blobs_to_patch = [blob for blob in bucket.list_blobs(prefix=prefix)]

    # Use a batch context manager to edit metadata in the list of blobs.
    # The batch request is sent out when the context manager closes.
    # No more than 100 calls should be included in a single batch request.
    with client.batch():
        for blob in blobs_to_patch:
            metadata = {"your-metadata-key": "your-metadata-value"}
            blob.metadata = metadata
            blob.patch()

    print(
        f"Batch request edited metadata for all objects with the given prefix in {bucket.name}."
    )

Ruby

מידע נוסף על שליחת בקשה באצווה באמצעות Ruby מופיע במאמרי העזרה של Cloud Storage Ruby API.

דוגמה לתשובת HTTP באצווה

זוהי התשובה לבקשת ה-HTTP מהדוגמה שבקטע הקודם.

HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_pK7JBAk73-E=_AA5eFwv4m2Q=
Date: Mon, 22 Jan 2018 18:56:00 GMT
Expires: Mon, 22 Jan 2018 18:56:00 GMT
Cache-Control: private, max-age=0
Content-Length: 3767

--batch_pK7JBAk73-E=_AA5eFwv4m2Q=
Content-Type: application/http
Content-ID: <response-b29c5de2-0db4-490b-b421-6a51b598bd22+1>

HTTP/1.1 200 OK
ETag: "lGaP-E0memYDumK16YuUDM_6Gf0/V43j6azD55CPRGb9b6uytDYl61Y"
Content-Type: application/json; charset=UTF-8
Date: Mon, 22 Jan 2018 18:56:00 GMT
Expires: Mon, 22 Jan 2018 18:56:00 GMT
Cache-Control: private, max-age=0
Content-Length: 846

{
 "kind": "storage#object",
 "id": "example-bucket/obj1/1495822576643790",
 .
 .
 .
 "metadata": {
  "type": "tabby"
  },
  .
  .
  .
}

--batch_pK7JBAk73-E=_AA5eFwv4m2Q=
Content-Type: application/http
Content-ID: <response-b29c5de2-0db4-490b-b421-6a51b598bd22+2>

HTTP/1.1 200 OK
ETag: "lGaP-E0memYDumK16YuUDM_6Gf0/91POdd-sxSAkJnS8Dm7wMxBSDKk"
Content-Type: application/json; charset=UTF-8
Date: Mon, 22 Jan 2018 18:56:00 GMT
Expires: Mon, 22 Jan 2018 18:56:00 GMT
Cache-Control: private, max-age=0
Content-Length: 846

{
 "kind": "storage#object",
 "id": "example-bucket/obj2/1495822576643790",
 .
 .
 .
 "metadata": {
  "type": "tuxedo"
  },
  .
  .
  .
}

--batch_pK7JBAk73-E=_AA5eFwv4m2Q=
Content-Type: application/http
Content-ID: <response-b29c5de2-0db4-490b-b421-6a51b598bd22+3>

HTTP/1.1 200 OK
ETag: "lGaP-E0memYDumK16YuUDM_6Gf0/d2Z1F1_ZVbB1dC0YKM9rX5VAgIQ"
Content-Type: application/json; charset=UTF-8
Date: Mon, 22 Jan 2018 18:56:00 GMT
Expires: Mon, 22 Jan 2018 18:56:00 GMT
Cache-Control: private, max-age=0
Content-Length: 846

{
 "kind": "storage#object",
 "id": "example-bucket/obj3/1495822576643790",
 .
 .
 .
 "metadata": {
  "type": "calico"
  },
  .
  .
  .
}

--batch_pK7JBAk73-E=_AA5eFwv4m2Q=--

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

כשהבקשה הכוללת מוחזרת עם קוד הסטטוס 200, התשובה כוללת תוצאות לכל בקשת משנה, כולל קוד סטטוס לכל אחת, שמציין אם בקשת המשנה הצליחה או נכשלה. לדוגמה, כשמוחקים אובייקטים בקבוצה, כל בקשת משנה שמושלמת בהצלחה מכילה קוד סטטוס 204 No Content.