每日图片:实验 1 - 存储和分析图片(原生 Java)

1. 概览

在第一个 Codelab 中,您需要将图片存储在存储分区中。这将生成一个文件创建事件,该事件将由部署在 Cloud Run 中的服务处理。该服务将调用 Vision API 以执行图片分析,并将结果保存在数据存储区中。

c0650ee4a76db35e.png

学习内容

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

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

295004821bab6a87

37d264871000675d

96d86d3d5655cdbe.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用 PROJECT_ID 标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除项目。Google Cloud 新用户符合参与 300 美元免费试用计划的条件。

启动 Cloud Shell

虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

Google Cloud 控制台 中,点击右上角工具栏中的 Cloud Shell 图标:

84688aa223b1c3a2

预配和连接到环境应该只需要片刻时间。完成后,您应该会看到如下内容:

320e18fedb7fbe0

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5 GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。您在此 Codelab 中的所有工作都可以在浏览器中完成。您无需安装任何程序。

3. 启用 API

在本实验中,您将使用 Cloud Functions 和 Vision API,但首先需要在 Cloud 控制台中或通过 gcloud 启用它们。

如需在 Cloud 控制台中启用 Vision API,请在搜索栏中搜索 Cloud Vision API

8f3522d790bb026c

您将进入 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. 创建存储分区(控制台)

为图片创建存储分区。您可以通过 Google Cloud Platform 控制台 ( console.cloud.google.com) 执行此操作,也可以通过 Cloud Shell 中的 gsutil 命令行工具或本地开发环境执行此操作。

来自“汉堡”(☰) 菜单,前往 Storage 页面。

d08ecb0ae29330a1.png

为存储分区命名

点击 CREATE BUCKET 按钮。

8951851554a430d2

点击 CONTINUE

选择位置

24b24625157ab467

在您选择的区域中创建一个多区域存储分区(此处为 Europe)。

点击 CONTINUE

选择默认存储类别

9e7bd365fa94a2e0

为您的数据选择 Standard 存储类别。

点击 CONTINUE

设置访问权限控制

1ff4a1f6e57045f5

由于您将使用可公开访问的图片,因此您希望存储在此存储分区中的所有图片都拥有相同的统一访问权限控制。

选择 Uniform 访问权限控制选项。

点击 CONTINUE

设置保护/加密

2d469b076029d365

保留默认值(Google-managed key),因为您不会使用自己的加密密钥。

点击 CREATE,最终完成存储分区的创建。

将 allUsers 添加为存储空间查看者

转到 Permissions 标签页:

19564b3ad8688ae8

向存储分区添加一个角色为 Storage > Storage Object ViewerallUsers 成员,如下所示:

d655e760c76d62c1.png

点击 SAVE

5. 创建存储分区 (gsutil)

您还可以使用 Cloud Shell 中的 gsutil 命令行工具创建存储分区。

在 Cloud Shell 中,为唯一存储分区名称设置变量。Cloud Shell 已将 GOOGLE_CLOUD_PROJECT 设置为您的唯一项目 ID。您可以将其附加到存储分区名称后面。

例如:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

在欧洲创建标准多区域可用区:

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

确保统一存储分区级访问权限:

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

公开该存储桶:

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

如果您转到控制台的 Cloud Storage 部分,则应该有一个公开的 uploaded-pictures 存储分区:

65c63ef4a6eb30ad

如上一步中所述,测试您是否可以将照片上传到存储分区,以及上传的照片是否可公开访问。

6. 测试对存储分区的公开访问权限

返回 Storage 浏览器,您会在列表中看到您的存储分区,其中显示“公开”访问权限(包括一个警告标志,提醒您任何人都可以访问该存储分区的内容)。

e639a9ba625b71a6.png

您的存储分区现在已准备好接收图片。

如果您点击存储分区名称,会看到存储分区详细信息。

1f88a2290290aba8

在该页面上,您可以尝试使用 Upload files 按钮,测试是否可以向存储分区添加图片。系统会显示一个文件选择器弹出式窗口,要求您选择文件。选择后,系统会将其上传到您的存储分区,您可以再次看到自动归因于这个新文件的 public 访问权限。

1209e7ebe1f63b10

Public 访问权限标签旁边,您还会看到一个小链接图标。点击该图片后,浏览器会转到该图片的公共网址,该网址的格式为:

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

BUCKET_NAME 是您为存储分区选择的全局唯一名称,然后是您的照片的文件名。

点击图片名称旁的复选框,即可启用 DELETE 按钮,您可以删除第一张图片。

7. 准备数据库

将 Vision API 提供的图片信息存储在 Cloud Firestore 数据库中。Cloud Firestore 是一个快速、全代管式、无服务器、云原生的 NoSQL 文档数据库。转到 Cloud 控制台的 Firestore 部分,准备数据库:

e57a673537b5deca.png

系统提供两个选项:Native modeDatastore mode。使用原生模式,该模式可提供离线支持和实时同步等额外功能。

点击 SELECT NATIVE MODE

1a2e363fae5c7e96

选择一个多区域(这里位于欧洲,但最好至少与您的函数和存储分区位于同一区域)。

点击 CREATE DATABASE 按钮。

创建数据库后,您应该会看到以下内容:

7dcc82751ed483fb

点击 + START COLLECTION 按钮创建新集合

名称集合“pictures”。

dce3d73884ac8c83.png

您无需创建文档。当新图片存储在 Cloud Storage 中并由 Vision API 进行分析时,您将以编程方式添加这些图片。

点击 Save

Firestore 会在新创建的集合中创建第一个默认文档,您可以安全地删除该文档,因为它不包含任何实用信息:

63e95c844b3f79d3

在集合中以编程方式创建的文档将包含 4 个字段:

  • name(字符串):上传的图片的文件名,这也是文档的键
  • labels(字符串数组):Vision API 识别出的项目的标签
  • color(字符串):主色的十六进制颜色代码(即#ab12ef)
  • created(日期):存储此图片的元数据时的时间戳
  • thumbnail(布尔值):选填字段,如果为此照片生成了缩略图,此字段将是 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

点击 Create。 索引创建过程可能需要几分钟时间。

8. 克隆代码

如果之前的 Codelab 中尚未克隆该代码,请执行以下操作:

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

然后,您可以转到包含该服务的目录,开始构建实验:

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

该服务的文件布局如下:

4c2a18a2c8b69dc5

9. 探索服务代码

首先,使用 BoM 查看如何在 pom.xml 中启用 Java 客户端库:

首先,打开 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 image,然后构建 Native 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 build 使用此版本:

sdk use java 17.0.8-graal

为方便起见,在 Cloudshell 中,您可以通过以下简单的命令安装 GraalVM 和 native-image 实用程序:

# 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(原生)映像。两个服务部署将并行处理存储分区中的同一张图片,以便进行比较。

首先,设置 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,点击我们在实验开始时创建的存储分区:

33442485a1d76921

进入存储分区详情页面后,点击 Upload files 按钮以上传图片。

例如,GeekHour.jpeg 映像随代码库的 /services/image-analysis/java 一起提供。选择一张图片,然后按 Open button

d57529452f62bd32.png

现在,您可以检查服务的执行情况,从 image-analysis-jit 开始,然后是 image-analysis-native

来自“汉堡”(☰) 菜单中,导航到 Cloud Run > image-analysis-jit 服务。

点击“Logs”(日志)并观察输出结果:

ae1a4a94c7c7a166.png

事实上,在日志列表中,我可以看到 JIT 服务 image-analysis-jit 被调用了。

这些日志会指示服务执行的开始和结束。其中,我们可以看到包含 INFO 级别的日志语句放在函数中的日志。我们可以看到:

  • 触发函数的事件的详细信息
  • Vision API 调用的原始结果,
  • 在我们上传的照片中找到的标签
  • 主色信息
  • 照片是否可以安全显示
  • 最后,有关照片的元数据会存储在 Firestore 中。

您需要对 image-analysis-native 服务重复此过程。

来自“汉堡”(☰) 菜单中,导航到 Cloud Run > image-analysis-native 服务。

点击“Logs”(日志)并观察输出结果:

4afe22833c1fd14c

现在,您需要观察图像元数据是否已存储在 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 映像

后续步骤