每日一指令:研究室 1—儲存與分析圖片 (原生 Java)

1. 總覽

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

c0650ee4a76db35e.png

課程內容

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

2. 設定和需求

自修實驗室環境設定

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

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.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 圖示:

84688aa223b1c3a2.png

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

320e18fedb7fbe0.png

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

3. 啟用 API

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

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

8f3522d790bb026c.png

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

d785572fa14c87c2.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 頁面。

d08ecb0ae29330a1.png

為 bucket 命名

按一下 CREATE BUCKET 按鈕。

8951851554a430d2.png

按一下「CONTINUE」。

選擇位置

24b24625157ab467.png

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

按一下「CONTINUE」。

選擇預設儲存空間級別

9e7bd365fa94a2e0.png

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

按一下「CONTINUE」。

設定存取權控管

1ff4a1f6e57045f5.png

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

選擇 Uniform 存取權控管選項。

按一下「CONTINUE」。

設定保護/加密

2d469b076029d365.png

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

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

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

前往 Permissions 分頁:

19564b3ad8688ae8.png

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

d655e760c76d62c1.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:

65c63ef4a6eb30ad.png

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

6. 測試 bucket 的公開存取權

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

e639a9ba625b71a6.png

現在值區已可接收圖片。

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

1f88a2290290aba8.png

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

1209e7ebe1f63b10.png

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

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

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

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

7. 準備資料庫

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

e57a673537b5deca.png

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

按一下 SELECT NATIVE MODE

1a2e363fae5c7e96.png

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

按一下 CREATE DATABASE 按鈕。

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

7dcc82751ed483fb.png

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

為集合命名 pictures

dce3d73884ac8c83.png

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

按一下「Save」。

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

63e95c844b3f79d3.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,然後建立複合索引,如下所示:

2236d3a024a59232.png

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

8. 複製程式碼

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

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

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

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

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

4c2a18a2c8b69dc5.png

9. 探索服務代碼

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

首先,請開啟 pom.xml 檔案,其中列出 Java 應用程式的依附元件;重點是 Vision、Cloud Storage 和 Firestore API 的使用方式

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0-M3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>services</groupId>
        <artifactId>image-analysis</artifactId>
        <version>0.0.1</version>
        <name>image-analysis</name>
        <description>Spring App for Image Analysis</description>
    <properties>
        <java.version>17</java.version>
        <maven.compiler.target>17</maven.compiler.target>
        <maven.compiler.source>17</maven.compiler.source>        
        <spring-cloud.version>2023.0.0-M2</spring-cloud.version>
        <testcontainers.version>1.19.1</testcontainers.version>
    </properties>
...
  <dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>libraries-bom</artifactId>
            <version>26.24.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
  </dependencyManagement>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-function-web</artifactId>
                </dependency>
        <dependency>
            <groupId>com.google.cloud.functions</groupId>
            <artifactId>functions-framework-api</artifactId>
            <version>1.1.0</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>        

這項功能是在 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) {
          ApiFuture<WriteResult> writeResult = 
               eventService.storeImage(fileName, labels,
                                       mainColor);
          logger.info("Picture metadata saved in Firestore at " + 
               writeResult.get().getUpdateTime());
}
...
  public ApiFuture<WriteResult> storeImage(String fileName, 
                                           List<String> labels, 
                                           String mainColor) {
    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());

    return doc.set(data, SetOptions.merge());
  }

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

在這個選用步驟中,您將使用 GraalVM 建構 JIT based app imageNative Java app image

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

To start,下載 GraalVM 22.3.x Community Edition,然後按照 GraalVM 安裝頁面的指示操作。

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

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

sdk install java 17.0.8-graal

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

sdk use java 17.0.8-graal

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

# download GraalVM
wget https://download.oracle.com/graalvm/17/latest/graalvm-jdk-17_linux-x64_bin.tar.gz 
tar -xzf graalvm-jdk-17_linux-x64_bin.tar.gz

ls -lart

# configure Java 17 and GraalVM for Java 17
# note the name of the latest GraalVM version, as unpacked by the tar command
echo Existing JVM: $JAVA_HOME
cd graalvm-jdk-17.0.8+9.1

export JAVA_HOME=$PWD
cd bin
export PATH=$PWD:$PATH

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

cd ../..

# validate the version with
java -version 

# observe
Java(TM) SE Runtime Environment Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.8+9.1 (build 17.0.8+9-LTS-jvmci-23.0-b14, mixed mode, sharing)

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

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

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

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

建構 JIT 應用程式映像檔:

./mvnw package

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

...
[INFO] Results:
[INFO] 
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) @ image-analysis ---
[INFO] Building jar: /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:3.2.0-M3:repackage (repackage) @ image-analysis ---
[INFO] Replacing main artifact /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis-0.0.1.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  15.335 s
[INFO] Finished at: 2023-10-10T19:33:25Z
[INFO] ------------------------------------------------------------------------

建構 Native(使用 AOT) 映像檔:

./mvnw native:compile -Pnative

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

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

...
[2/7] Performing analysis...  [*********]                                                              (124.5s @ 4.53GB)
  29,732 (93.19%) of 31,905 classes reachable
  60,161 (70.30%) of 85,577 fields reachable
 261,973 (67.29%) of 389,319 methods reachable
   2,940 classes, 2,297 fields, and 97,421 methods registered for reflection
      81 classes,    90 fields, and    62 methods registered for JNI access
       4 native libraries: dl, pthread, rt, z
[3/7] Building universe...                                                                              (11.7s @ 4.67GB)
[4/7] Parsing methods...      [***]                                                                      (6.1s @ 5.91GB)
[5/7] Inlining methods...     [****]                                                                     (4.5s @ 4.39GB)
[6/7] Compiling methods...    [******]                                                                  (35.3s @ 4.60GB)
[7/7] Creating image...                                                                                 (12.9s @ 4.61GB)
  80.08MB (47.43%) for code area:   190,483 compilation units
  73.81MB (43.72%) for image heap:  660,125 objects and 189 resources
  14.95MB ( 8.86%) for other data
 168.84MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
   2.66MB com.google.cloud.vision.v1p4beta1                   18.51MB byte[] for code metadata
   2.60MB com.google.cloud.vision.v1                           9.27MB java.lang.Class
   2.49MB com.google.protobuf                                  7.34MB byte[] for reflection metadata
   2.40MB com.google.cloud.vision.v1p3beta1                    6.35MB byte[] for java.lang.String
   2.17MB com.google.storage.v2                                5.72MB java.lang.String
   2.12MB com.google.firestore.v1                              4.46MB byte[] for embedded resources
   1.64MB sun.security.ssl                                     4.30MB c.oracle.svm.core.reflect.SubstrateMethodAccessor
   1.51MB i.g.xds.shaded.io.envoyproxy.envoy.config.core.v3    4.27MB byte[] for general heap data
   1.47MB com.google.cloud.vision.v1p2beta1                    2.50MB com.oracle.svm.core.hub.DynamicHubCompanion
   1.34MB i.g.x.shaded.io.envoyproxy.envoy.config.route.v3     1.17MB java.lang.Object[]
  58.34MB for 977 more packages                                9.19MB for 4667 more object types
------------------------------------------------------------------------------------------------------------------------
                        13.5s (5.7% of total time) in 75 GCs | Peak RSS: 9.44GB | CPU load: 6.13
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis (executable)
 /home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis.build_artifacts.txt (txt)
========================================================================================================================
Finished generating '/home/user/serverless-photosharing-workshop/services/image-analysis/java/target/image-analysis' in 3m 57s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:28 min
[INFO] Finished at: 2023-10-10T19:53:30Z
[INFO] ------------------------------------------------------------------------

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

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

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

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

建構 JIT 映像檔:

./mvnw spring-boot:build-image -Pji

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

[INFO]     [creator]     Timer: Saving docker.io/library/image-analysis-maven-jit:latest... started at 2023-10-10T20:00:31Z
[INFO]     [creator]     *** Images (4c84122a1826):
[INFO]     [creator]           docker.io/library/image-analysis-maven-jit:latest
[INFO]     [creator]     Timer: Saving docker.io/library/image-analysis-maven-jit:latest... ran for 6.975913605s and ended at 2023-10-10T20:00:38Z
[INFO]     [creator]     Timer: Exporter ran for 8.068588001s and ended at 2023-10-10T20:00:38Z
[INFO]     [creator]     Timer: Cache started at 2023-10-10T20:00:38Z
[INFO]     [creator]     Reusing cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO]     [creator]     Timer: Cache ran for 200.449002ms and ended at 2023-10-10T20:00:38Z
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-jit:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  43.887 s
[INFO] Finished at: 2023-10-10T20:00:39Z
[INFO] ------------------------------------------------------------------------

建構 AOT(原生) 映像檔:

./mvnw spring-boot:build-image -Pnative

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

注意:

  • 建構時間會較長,視您測試的機器而定
  • 圖片可使用 UPX 進一步壓縮,但會對啟動效能造成些微負面影響,因此這個建構版本不會使用 UPX,這始終是微小的取捨
...
[INFO]     [creator]     Saving docker.io/library/image-analysis-maven-native:latest...
[INFO]     [creator]     *** Images (13167702674e):
[INFO]     [creator]           docker.io/library/image-analysis-maven-native:latest
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:37 min
[INFO] Finished at: 2023-10-10T20:05:16Z
[INFO] ------------------------------------------------------------------------

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

docker images | grep image-analysis

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

# JIT image
docker tag image-analysis-maven-jit gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit
docker push gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-jit

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

12. 部署至 Cloud Run

部署服務。

您將部署服務兩次,一次使用 JIT 映像檔,另一次使用 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 映像檔,並在控制台中觀察部署記錄:

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

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

部署原生映像檔,並觀察主控台中的部署記錄:

gcloud run deploy image-analysis-native \
     --image gcr.io/${GOOGLE_CLOUD_PROJECT}/image-analysis-maven-native \
     --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'

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

gcloud eventarc triggers list --location=eu

gcloud eventarc triggers create image-analysis-jit-trigger \
     --destination-run-service=image-analysis-jit \
     --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:

33442485a1d76921.png

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

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

d57529452f62bd32.png

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

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

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

ae1a4a94c7c7a166.png

在記錄清單中,我確實看到 JIT 服務 image-analysis-jit 已遭叫用。

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

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

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

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

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

4afe22833c1fd14c.png

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

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

82d6c468956e7cfc.png

15. 清除 (選用)

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

刪除值區:

gsutil rb gs://${BUCKET_PICTURES}

刪除函式:

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

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

6cc86a7b88fdb4d3.png

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

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. 恭喜!

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

涵蓋內容

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

後續步驟