每日一指令:運用 Google 原生 Java 用戶端程式庫儲存及分析圖片

1. 總覽

在第一個程式碼研究室中,您會將圖片儲存在 bucket 中。這會產生檔案建立事件,並由部署在 Cloud Run 中的服務處理。這項服務會呼叫 Vision API 進行圖片分析,並將結果儲存在資料存放區中。

427de3100de3a61e.png

課程內容

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore

2. 設定和需求

自修實驗室環境設定

  1. 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 專案名稱是這個專案參與者的顯示名稱。這是 Google API 未使用的字元字串。你隨時可以更新該位置資訊。
  • 專案 ID 在所有 Google Cloud 專案中不得重複,且設定後即無法變更。Cloud 控制台會自動產生不重複的字串,通常您不需要在意這個字串。在大多數程式碼研究室中,您需要參照專案 ID (通常會標示為 PROJECT_ID)。如果您不喜歡產生的 ID,可以產生另一個隨機 ID。你也可以嘗試自訂名稱,看看是否可用。完成這個步驟後就無法變更,且專案期間都會維持這個設定。
  • 請注意,部分 API 會使用第三個值,也就是「專案編號」。如要進一步瞭解這三種值,請參閱說明文件
  1. 接著,您需要在 Cloud 控制台中啟用帳單,才能使用 Cloud 資源/API。完成本程式碼研究室的費用應該不高,甚至完全免費。如要關閉資源,避免產生本教學課程以外的費用,您可以刪除自己建立的資源,或刪除整個專案。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。

啟動 Cloud Shell

雖然可以透過筆電遠端操作 Google Cloud,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是可在雲端執行的指令列環境。

Google Cloud 控制台中,點選右上工具列的 Cloud Shell 圖示:

55efc1aaa7a4d3ad.png

佈建並連線至環境的作業需要一些時間才能完成。完成後,您應該會看到如下的內容:

7ffe5cbb04455448.png

這部虛擬機器搭載各種您需要的開發工具,並提供永久的 5GB 主目錄,而且可在 Google Cloud 運作,大幅提升網路效能並強化驗證功能。您可以在瀏覽器中完成本程式碼研究室的所有作業。您不需要安裝任何軟體。

3. 啟用 API

在本實驗室中,您將使用 Cloud Functions 和 Vision API,但首先必須在 Cloud 控制台或使用 gcloud 啟用這些服務。

如要在 Cloud 控制台中啟用 Vision API,請在搜尋列中搜尋 Cloud Vision API

cf48b1747ba6a6fb.png

系統會將您導向 Cloud Vision API 頁面:

ba4af419e6086fbb.png

按一下 ENABLE 按鈕。

或者,您也可以使用 gcloud 指令列工具,透過 Cloud Shell 啟用。

在 Cloud Shell 中執行下列指令:

gcloud services enable vision.googleapis.com

您應該會看到作業順利完成:

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

同時啟用 Cloud Run 和 Cloud Build:

gcloud services enable cloudbuild.googleapis.com \
  run.googleapis.com

4. 建立 bucket (主控台)

建立圖片的儲存空間值區。您可以透過 Google Cloud Platform 主控台 ( console.cloud.google.com) 或 Cloud Shell 或本機開發環境的 gsutil 指令列工具執行這項操作。

在「漢堡」選單 (☰) 中,前往 Storage 頁面。

1930e055d138150a.png

為 bucket 命名

按一下 CREATE BUCKET 按鈕。

34147939358517f8.png

按一下「CONTINUE」。

選擇位置

197817f20be07678.png

在所選區域 (此處為 Europe) 建立多區域 bucket。

按一下「CONTINUE」。

選擇預設儲存空間級別

53cd91441c8caf0e.png

為資料選擇 Standard 儲存空間級別。

按一下「CONTINUE」。

設定存取權控管

8c2b3b459d934a51.png

由於您將使用可公開存取的圖片,因此希望儲存在這個 bucket 中的所有圖片都具有相同的統一存取權控管機制。

選擇 Uniform 存取權控管選項。

按一下「CONTINUE」。

設定保護/加密

d931c24c3e705a68.png

保留預設值 (Google-managed key)),因為您不會使用自己的加密金鑰。

按一下 CREATE,最終完成值區建立程序。

將 allUsers 新增為儲存空間檢視者

前往 Permissions 分頁:

d0ecfdcff730ea51.png

allUsers 成員新增至 bucket,並指派 Storage > Storage Object Viewer 角色,如下所示:

e9f25ec1ea0b6cc6.png

按一下「SAVE」。

5. 建立 bucket (gsutil)

您也可以使用 Cloud Shell 中的 gsutil 指令列工具建立 bucket。

在 Cloud Shell 中,為不重複的值區名稱設定變數。Cloud Shell 已將 GOOGLE_CLOUD_PROJECT 設為專屬專案 ID。你可以將該值附加至 bucket 名稱。

例如:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

在歐洲建立標準多區域:

gsutil mb -l EU gs://${BUCKET_PICTURES}

確認統一值區層級存取權:

gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}

將 bucket 設為公開:

gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}

前往控制台的 Cloud Storage 部分,您應該會看到公開 uploaded-pictures bucket:

a98ed4ba17873e40.png

如上一個步驟所述,測試您是否可以將圖片上傳至值區,並確認上傳的圖片可公開存取。

6. 測試 bucket 的公開存取權

返回儲存空間瀏覽器,您會在清單中看到自己的 bucket,並顯示「公開」存取權 (包括提醒您任何人都能存取該 bucket 內容的警告符號)。

89e7a4d2c80a0319.png

現在值區已可接收圖片。

按一下 bucket 名稱,即可查看 bucket 詳細資料。

131387f12d3eb2d3.png

您可以在該處嘗試 Upload files 按鈕,測試是否能將圖片新增至值區。檔案選擇器彈出式視窗會要求你選取檔案。選取後,系統會將檔案上傳至儲存空間,並再次顯示自動指派給這個新檔案的public存取權。

e87584471a6e9c6d.png

Public 存取權標籤旁邊也會顯示小小的連結圖示。點選後,瀏覽器會前往該圖片的公開網址,格式如下:

https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png

其中 BUCKET_NAME 是您為 bucket 選擇的全域專屬名稱,後面則是圖片的檔案名稱。

按一下圖片名稱旁的核取方塊,即可啟用 DELETE 按鈕,並刪除第一張圖片。

7. 準備資料庫

您會將 Vision API 提供的圖片資訊儲存到 Cloud Firestore 資料庫。這項服務是快速、全代管、無伺服器且雲端原生的 NoSQL 文件資料庫。前往 Cloud 控制台的「Firestore」部分,準備資料庫:

9e4708d2257de058.png

系統會提供兩個選項:Native modeDatastore mode。使用原生模式,即可享有離線支援和即時同步等額外功能。

按一下 SELECT NATIVE MODE

9449ace8cc84de43.png

選擇多地區 (這裡選擇歐洲,但最好與函式和儲存空間值區位於相同地區)。

按一下 CREATE DATABASE 按鈕。

資料庫建立完成後,您應該會看到下列內容:

56265949a124819e.png

按一下 + START COLLECTION 按鈕,建立新的集合

為集合命名 pictures

75806ee24c4e13a7.png

您不需要建立文件,當新圖片儲存在 Cloud Storage 中,並由 Vision API 分析時,您會以程式輔助方式新增圖片。

按一下「Save」。

Firestore 會在新建立的集合中建立第一個預設文件,您可以安全地刪除該文件,因為其中不含任何實用資訊:

5c2f1e17ea47f48f.png

我們將在集合中以程式輔助方式建立的文件會包含 4 個欄位:

  • name (字串):上傳圖片的檔案名稱,也是文件的鍵
  • 標籤 (字串陣列):Vision API 辨識項目的標籤
  • color (字串):主色的十六進位顏色代碼 (即 #ab12ef)
  • created (日期):儲存這張圖片中繼資料的時間戳記
  • 縮圖 (布林值):選用欄位,如果系統已為這張相片產生縮圖,這個欄位就會存在並設為 true

由於我們會在 Firestore 中搜尋有縮圖的圖片,並依建立日期排序,因此需要建立搜尋索引。

您可以在 Cloud Shell 中使用下列指令建立索引:

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

您也可以在 Cloud 控制台中執行這項操作,方法是點選左側導覽欄中的 Indexes,然後建立複合索引,如下所示:

ecb8b95e3c791272.png

按一下 Create,建立索引可能需要幾分鐘的時間。

8. 複製程式碼

複製程式碼 (如果您在上一個程式碼研究室中尚未複製):

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

接著,您可以前往包含服務的目錄,開始建構實驗室:

cd serverless-photosharing-workshop/services/image-analysis/java

服務的檔案版面配置如下:

f79613aff479d8ad.png

9. 探索服務代碼

首先,您會瞭解如何透過 BOM 啟用 Java 用戶端程式庫:pom.xml

首先,請編輯 pom.xml 檔案,其中列出 Java 函式的依附元件。更新程式碼,新增 Cloud Vision API Maven 依附元件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cloudfunctions</groupId>
  <artifactId>gcs-function</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud.functions</groupId>
      <artifactId>functions-framework-api</artifactId>
      <version>1.0.4</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
  </dependencies>

這項功能是在 EventController 類別中實作。每次將新圖片上傳至值區時,服務都會收到通知以進行處理:

@RestController
public class EventController {
  private static final Logger logger = Logger.getLogger(EventController.class.getName());
    
  private static final List<String> requiredFields = Arrays.asList("ce-id", "ce-source", "ce-type", "ce-specversion");

  @RequestMapping(value = "/", method = RequestMethod.POST)
  public ResponseEntity<String> receiveMessage(
    @RequestBody Map<String, Object> body, @RequestHeader Map<String, String> headers) throws IOException, InterruptedException, ExecutionException {
...
}

程式碼會繼續驗證 Cloud Events 標頭:

System.out.println("Header elements");
for (String field : requiredFields) {
    if (headers.get(field) == null) {
    String msg = String.format("Missing expected header: %s.", field);
    System.out.println(msg);
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
    } else {
    System.out.println(field + " : " + headers.get(field));
    }
}

System.out.println("Body elements");
for (String bodyField : body.keySet()) {
    System.out.println(bodyField + " : " + body.get(bodyField));
}

if (headers.get("ce-subject") == null) {
    String msg = "Missing expected header: ce-subject.";
    System.out.println(msg);
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
} 

現在可以建構要求,程式碼會準備這類要求,以便傳送至 Vision API

try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
    List<AnnotateImageRequest> requests = new ArrayList<>();
    
    ImageSource imageSource = ImageSource.newBuilder()
        .setGcsImageUri("gs://" + bucketName + "/" + fileName)
        .build();

    Image image = Image.newBuilder()
        .setSource(imageSource)
        .build();

    Feature featureLabel = Feature.newBuilder()
        .setType(Type.LABEL_DETECTION)
        .build();
    Feature featureImageProps = Feature.newBuilder()
        .setType(Type.IMAGE_PROPERTIES)
        .build();
    Feature featureSafeSearch = Feature.newBuilder()
        .setType(Type.SAFE_SEARCH_DETECTION)
        .build();
        
    AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
        .addFeatures(featureLabel)
        .addFeatures(featureImageProps)
        .addFeatures(featureSafeSearch)
        .setImage(image)
        .build();
    
    requests.add(request);

我們要求 Vision API 提供 3 項主要功能:

  • 標籤偵測:瞭解圖片內容
  • 圖片屬性:提供圖片的有趣屬性 (我們感興趣的是圖片的主色)
  • 安全搜尋:判斷圖片是否適合顯示 (不應含有成人 / 醫療 / 煽情 / 暴力內容)

此時,我們可以呼叫 Vision API:

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

以下是 Vision API 的回應範例:

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
     - - - 
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
         - - - 
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

如果沒有傳回任何錯誤,我們就可以繼續,這就是我們有這個 if 區塊的原因:

if (responses.size() == 0) {
    logger.info("No response received from Vision API.");
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
    logger.info("Error: " + response.getError().getMessage());
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

我們要取得圖片中辨識到的事物、類別或主題的標籤:

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

我們想知道圖片的主色:

String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
    DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
    ColorInfo colorInfo = colorsAnn.getColors(0);

    mainColor = rgbHex(
        colorInfo.getColor().getRed(), 
        colorInfo.getColor().getGreen(), 
        colorInfo.getColor().getBlue());

    logger.info("Color: " + mainColor);
}

讓我們檢查圖片是否安全無虞:

boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
    SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();

    isSafe = Stream.of(
        safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
        safeSearch.getSpoof(), safeSearch.getViolence())
    .allMatch( likelihood -> 
        likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
    );

    logger.info("Safe? " + isSafe);
}

我們會檢查成人 / 惡搞 / 醫療 / 暴力 / 煽情特徵,判斷這些特徵不太可能非常可能存在。

如果安全搜尋結果沒問題,我們可以在 Firestore 中儲存中繼資料:

// Saving result to Firestore
if (isSafe) {
    FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
    Firestore pictureStore = firestoreOptions.getService();

    DocumentReference doc = pictureStore.collection("pictures").document(fileName);

    Map<String, Object> data = new HashMap<>();
    data.put("labels", labels);
    data.put("color", mainColor);
    data.put("created", new Date());

    ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

    logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}

10. 使用 GraalVM 建構應用程式映像檔 (選用)

在這個選用步驟中,您將使用 GraalVM 建構 JIT(JVM) based app imageAOT(Native) Java app image

如要執行建構作業,請務必安裝並設定適當的 JDK 和 native-image 建構工具。你可以選擇以下做法:

To start,下載 GraalVM 22.2.x Community Edition,然後按照 GraalVM 安裝頁面的操作說明進行。

SDKMAN! 可大幅簡化這個程序。

如要使用 SDKman 安裝適當的 JDK 發行版本,請先使用安裝指令:

sdk install java 22.2.r17-grl

指示 SDKman 使用這個版本,進行 JIT 和 AOT 建構:

sdk use java 22.2.0.r17-grl

安裝 GraalVM 適用的 native-image utility

gu install native-image

Cloudshell 中,您可以執行下列簡單指令,輕鬆安裝 GraalVM 和原生映像檔公用程式:

# install GraalVM in your home directory
cd ~

# download GraalVM
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.2.0/graalvm-ce-java17-linux-amd64-22.2.0.tar.gz
ls
tar -xzvf graalvm-ce-java17-linux-amd64-22.2.0.tar.gz

# configure Java 17 and GraalVM 22.2
echo Existing JVM: $JAVA_HOME
cd graalvm-ce-java17-22.2.0
export JAVA_HOME=$PWD
cd bin
export PATH=$PWD:$PATH

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

# install the native image utility
java -version
gu install native-image

cd ../..

首先,設定 GCP 專案環境變數:

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)

接著,您可以前往包含服務的目錄,開始建構實驗室:

cd serverless-photosharing-workshop/services/image-analysis/java

建構 JIT(JVM) 應用程式映像檔:

./mvnw package -Pjvm

在終端機中觀察建構記錄:

...
[INFO] --- spring-boot-maven-plugin:2.7.3:repackage (repackage) @ image-analysis ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  24.009 s
[INFO] Finished at: 2022-09-26T22:17:32-04:00
[INFO] ------------------------------------------------------------------------

建構 AOT(原生) 映像檔:

./mvnw package -Pnative -DskipTests

在終端機中觀察建構記錄,包括原生映像檔建構記錄:

請注意,建構作業需要較長的時間,視您測試的機器而定。

...
[2/7] Performing analysis...  [**********]                                                              (95.4s @ 3.57GB)
  23,346 (94.42%) of 24,725 classes reachable
  44,625 (68.71%) of 64,945 fields reachable
 163,759 (70.79%) of 231,322 methods reachable
     989 classes, 1,402 fields, and 11,032 methods registered for reflection
      63 classes,    69 fields, and    55 methods registered for JNI access
       5 native libraries: -framework CoreServices, -framework Foundation, dl, pthread, z
[3/7] Building universe...                                                                              (10.0s @ 5.35GB)
[4/7] Parsing methods...      [***]                                                                      (9.7s @ 3.13GB)
[5/7] Inlining methods...     [***]                                                                      (4.5s @ 3.29GB)
[6/7] Compiling methods...    [[6/7] Compiling methods...    [********]                                                                (67.6s @ 5.72GB)
[7/7] Creating image...                                                                                  (8.7s @ 4.59GB)
  62.21MB (54.80%) for code area:   100,371 compilation units
  50.98MB (44.91%) for image heap:  465,035 objects and 365 resources
 337.09KB ( 0.29%) for other data
 113.52MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
   2.36MB com.google.protobuf                                 12.70MB byte[] for code metadata
   1.90MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3    6.66MB java.lang.Class
   1.73MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3     6.47MB byte[] for embedded resources
   1.67MB sun.security.ssl                                     4.61MB byte[] for java.lang.String
   1.54MB com.google.cloud.vision.v1                           4.37MB java.lang.String
   1.46MB com.google.firestore.v1                              3.38MB byte[] for general heap data
   1.37MB io.grpc.xds.shaded.io.envoyproxy.envoy.api.v2.core   1.96MB com.oracle.svm.core.hub.DynamicHubCompanion
   1.32MB i.g.xds.shaded.io.envoyproxy.envoy.api.v2.route      1.80MB byte[] for reflection metadata
   1.09MB java.util                                          911.80KB java.lang.String[]
   1.08MB com.google.re2j                                    826.48KB c.o.svm.core.hub.DynamicHub$ReflectionMetadata
  45.91MB for 772 more packages                                6.45MB for 3913 more object types
------------------------------------------------------------------------------------------------------------------------
                        15.1s (6.8% of total time) in 56 GCs | Peak RSS: 7.72GB | CPU load: 4.37
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /Users/ddobrin/work/dan/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable)
 /Users/ddobrin/work/dan/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt)
========================================================================================================================
Finished generating 'image-analysis' in 3m 41s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:56 min
[INFO] Finished at: 2022-09-26T22:22:29-04:00
[INFO] ------------------------------------------------------------------------

11. 建構及發布容器映像檔

現在讓我們建構兩個不同版本的容器映像檔:一個是 JIT(JVM) image,另一個是 AOT(Native) Java image

首先,設定 GCP 專案環境變數:

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)

建構 JIT(JVM) 映像檔:

./mvnw package -Pjvm-image

在終端機中觀察建構記錄:

[INFO]     [creator]     Adding layer 'process-types'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     Setting default process type 'web'
[INFO]     [creator]     Saving docker.io/library/image-analysis-jvm:r17...
[INFO]     [creator]     *** Images (03a44112456e):
[INFO]     [creator]           docker.io/library/image-analysis-jvm:r17
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-jvm:r17'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:11 min
[INFO] Finished at: 2022-09-26T13:09:34-04:00
[INFO] ------------------------------------------------------------------------

建構 AOT(原生) 映像檔:

./mvnw package -Pnative-image

在終端機中觀察建構記錄,包括原生映像檔建構記錄,以及使用 UPX 壓縮映像檔。

請注意,建構時間會因測試的機器而異,可能需要較長的時間

...
[INFO]     [creator]     [2/7] Performing analysis...  [***********]                    (147.6s @ 3.10GB)
[INFO]     [creator]       23,362 (94.34%) of 24,763 classes reachable
[INFO]     [creator]       44,657 (68.67%) of 65,029 fields reachable
[INFO]     [creator]      163,926 (70.76%) of 231,656 methods reachable
[INFO]     [creator]          981 classes, 1,402 fields, and 11,026 methods registered for reflection
[INFO]     [creator]           63 classes,    68 fields, and    55 methods registered for JNI access
[INFO]     [creator]            4 native libraries: dl, pthread, rt, z
[INFO]     [creator]     [3/7] Building universe...                                      (21.1s @ 2.66GB)
[INFO]     [creator]     [4/7] Parsing methods...      [****]                            (13.7s @ 4.16GB)
[INFO]     [creator]     [5/7] Inlining methods...     [***]                              (9.6s @ 4.20GB)
[INFO]     [creator]     [6/7] Compiling methods...    [**********]                     (107.6s @ 3.36GB)
[INFO]     [creator]     [7/7] Creating image...                                         (14.7s @ 4.87GB)
[INFO]     [creator]       62.24MB (51.35%) for code area:   100,499 compilation units
[INFO]     [creator]       51.99MB (42.89%) for image heap:  473,948 objects and 473 resources
[INFO]     [creator]        6.98MB ( 5.76%) for other data
[INFO]     [creator]      121.21MB in total
[INFO]     [creator]     --------------------------------------------------------------------------------
[INFO]     [creator]     Top 10 packages in code area:           Top 10 object types in image heap:
[INFO]     [creator]        2.36MB com.google.protobuf             12.71MB byte[] for code metadata
[INFO]     [creator]        1.90MB i.g.x.s.i.e.e.config.core.v3     7.59MB byte[] for embedded resources
[INFO]     [creator]        1.73MB i.g.x.s.i.e.e.config.route.v3    6.66MB java.lang.Class
[INFO]     [creator]        1.67MB sun.security.ssl                 4.62MB byte[] for java.lang.String
[INFO]     [creator]        1.54MB com.google.cloud.vision.v1       4.39MB java.lang.String
[INFO]     [creator]        1.46MB com.google.firestore.v1          3.66MB byte[] for general heap data
[INFO]     [creator]        1.37MB i.g.x.s.i.e.envoy.api.v2.core    1.96MB c.o.s.c.h.DynamicHubCompanion
[INFO]     [creator]        1.32MB i.g.x.s.i.e.e.api.v2.route       1.80MB byte[] for reflection metadata
[INFO]     [creator]        1.09MB java.util                      910.41KB java.lang.String[]
[INFO]     [creator]        1.08MB com.google.re2j                826.95KB c.o.s.c.h.DynamicHu~onMetadata
[INFO]     [creator]       45.94MB for 776 more packages            6.69MB for 3916 more object types
[INFO]     [creator]     --------------------------------------------------------------------------------
[INFO]     [creator]         20.4s (5.6% of total time) in 81 GCs | Peak RSS: 6.75GB | CPU load: 4.53
[INFO]     [creator]     --------------------------------------------------------------------------------
[INFO]     [creator]     Produced artifacts:
[INFO]     [creator]      /layers/paketo-buildpacks_native-image/native-image/services.ImageAnalysisApplication (executable)
[INFO]     [creator]      /layers/paketo-buildpacks_native-image/native-image/services.ImageAnalysisApplication.build_artifacts.txt (txt)
[INFO]     [creator]     ================================================================================
[INFO]     [creator]     Finished generating '/layers/paketo-buildpacks_native-image/native-image/services.ImageAnalysisApplication' in 5m 59s.
[INFO]     [creator]         Executing upx to compress native image
[INFO]     [creator]                            Ultimate Packer for eXecutables
[INFO]     [creator]                               Copyright (C) 1996 - 2020
[INFO]     [creator]     UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020
[INFO]     [creator]     
[INFO]     [creator]             File size         Ratio      Format      Name
[INFO]     [creator]        --------------------   ------   -----------   -----------
 127099880 ->  32416676   25.50%   linux/amd64   services.ImageAnalysisApplication
...
[INFO]     [creator]     ===> EXPORTING
...
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-native:r17'
------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  05:28 min
[INFO] Finished at: 2022-09-26T13:19:53-04:00
[INFO] ------------------------------------------------------------------------

驗證映像檔是否已建構完成:

docker images | grep image-analysis

將這兩個映像檔加上標記並推送至 GCR:

# JIT(JVM) image
docker tag image-analysis-jvm:r17 gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-jvm:r17
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-jvm:r17

# AOT(Native) image
docker tag image-analysis-native:r17 gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-native:r17
docker push  gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-native:r17

12. 部署至 Cloud Run

部署服務。

您將部署服務兩次,一次使用 JIT(JVM) 映像檔,另一次使用 AOT(原生) 映像檔。這兩項服務部署作業會平行處理來自 Bucket 的相同圖片,以供比較。

首先,設定 GCP 專案環境變數:

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region 
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1

部署 JIT(JVM) 映像檔,並在控制台中觀察部署記錄:

gcloud run deploy image-analysis-jvm \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-jvm:r17 \
     --region europe-west1 \
     --memory 2Gi --allow-unauthenticated

...
Deploying container to Cloud Run service [image-analysis-jvm] in project [...] region [europe-west1]
✓ Deploying... Done.                                                                                                                                                               
  ✓ Creating Revision...                                                                                                                                                           
  ✓ Routing traffic...                                                                                                                                                             
  ✓ Setting IAM Policy...                                                                                                                                                          
Done.                                                                                                                                                                              
Service [image-analysis-jvm] revision [image-analysis-jvm-00009-huc] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-jvm-...-ew.a.run.app

部署 AOT(原生) 映像檔,並觀察控制台中的部署記錄:

gcloud run deploy image-analysis-native \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-native:r17 \
     --region europe-west1 \
     --memory 2Gi --allow-unauthenticated 
...
Deploying container to Cloud Run service [image-analysis-native] in project [...] region [europe-west1]
✓ Deploying... Done.                                                                                                                                                               
  ✓ Creating Revision...                                                                                                                                                           
  ✓ Routing traffic...                                                                                                                                                             
  ✓ Setting IAM Policy...                                                                                                                                                          
Done.                                                                                                                                                                              
Service [image-analysis-native] revision [image-analysis-native-00005-ben] has been deployed and is serving 100 percent of traffic.
Service URL: https://image-analysis-native-...-ew.a.run.app

13. 設定 Eventarc 觸發條件

Eventarc 提供標準化解決方案,可管理已分離微服務之間的狀態變更流程 (稱為「事件」)。觸發之後,Eventarc 會透過 Pub/Sub 訂閱項目將這些事件轉送至不同的目的地 (請參閱本文的「事件目的地」一節),同時為您管理傳送、安全防護、授權、觀測和錯誤處理工作。

您可以建立 Eventarc 觸發條件,讓 Cloud Run 服務接收特定事件或事件集的通知。指定觸發條件的篩選器,即可設定事件的轉送路徑,包括事件來源和目標 Cloud Run 服務。

首先,設定 GCP 專案環境變數:

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
gcloud config set project ${GOOGLE_CLOUD_PROJECT}
gcloud config set run/region 
gcloud config set run/platform managed
gcloud config set eventarc/location europe-west1

pubsub.publisher 授予 Cloud Storage 服務帳戶:

SERVICE_ACCOUNT="$(gsutil kms serviceaccount -p ${GOOGLE_CLOUD_PROJECT})"

gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role='roles/pubsub.publisher'

為 JVM(JIT) 和 AOT(原生) 服務映像檔設定 Eventarc 觸發條件,以處理圖片:

gcloud eventarc triggers list --location=eu

gcloud eventarc triggers create image-analysis-jvm-trigger \
     --destination-run-service=image-analysis-jvm \
     --destination-run-region=europe-west1 \
     --location=eu \
     --event-filters="type=google.cloud.storage.object.v1.finalized" \
     --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
     --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com

gcloud eventarc triggers create image-analysis-native-trigger \
     --destination-run-service=image-analysis-native \
     --destination-run-region=europe-west1 \
     --location=eu \
     --event-filters="type=google.cloud.storage.object.v1.finalized" \
     --event-filters="bucket=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}" \
     --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com    

您會發現已建立兩個觸發條件:

gcloud eventarc triggers list --location=eu

14. 測試服務版本

服務部署成功後,請將圖片發布至 Cloud Storage,確認服務是否已叫用、Vision API 傳回的內容,以及中繼資料是否儲存在 Firestore 中。

返回 Cloud Storage,然後按一下我們在實驗室開始時建立的 bucket:

ff8a6567afc76235.png

進入值區詳細資料頁面後,按一下 Upload files 按鈕即可上傳圖片。

舉例來說,程式碼基底的 /services/image-analysis/java 下提供 GeekHour.jpeg 圖片。選取圖片並按下 Open button

347b76e8b775f2f5.png

現在可以檢查服務的執行情況,先輸入 image-analysis-jvm,再輸入 image-analysis-native

在「漢堡」選單 (☰) 中,前往 Cloud Run > image-analysis-jvm 服務。

按一下「記錄」,觀察輸出內容:

810a8684414ceafa.png

確實,在記錄清單中,我可以看到 JIT(JVM) 服務 image-analysis-jvm 已遭叫用。

記錄會顯示服務執行的開始和結束時間。中間則會顯示我們在函式中加入的記錄,記錄陳述式位於 INFO 層級。我們看到:

  • 觸發函式的事件詳細資料,
  • Vision API 呼叫的原始結果,
  • 我們在您上傳的圖片中找到的標籤,
  • 主要顏色資訊,
  • 圖片是否適合顯示
  • 最終,這些圖片中繼資料會儲存在 Firestore 中。

請為 image-analysis-native 服務重複執行這個程序。

在「漢堡」選單 (☰) 中,前往 Cloud Run > image-analysis-native 服務。

按一下「記錄」,觀察輸出內容:

b80308c7d0f55a3.png

現在請觀察圖片中繼資料是否已儲存在 Fiorestore 中。

再次從「漢堡」選單 (☰) 前往 Firestore 部分。在 Data 子專區 (預設顯示),您應該會看到 pictures 集合新增的文件,對應您剛上傳的圖片:

933a20a9709cb006.png

15. 清除 (選用)

如果您不打算繼續進行本系列的其他實驗室,可以清除資源來節省費用,並成為優質的雲端使用者。您可以按照下列方式個別清除資源。

刪除值區:

gsutil rb gs://${BUCKET_PICTURES}

刪除函式:

gcloud functions delete picture-uploaded --region europe-west1 -q

從集合中選取「刪除集合」,即可刪除 Firestore 集合:

410b551c3264f70a.png

或者,您也可以刪除整個專案:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. 恭喜!

恭喜!您已成功實作專案的第一項重要服務!

涵蓋內容

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • 原生 Java 圖片

後續步驟