Pic-a-daily:使用 Google 原生 Java 客户端库存储和分析图片

1. 概览

在第一个代码实验中,您将图片存储在存储分区中。这会生成一个文件创建事件,该事件将由部署在 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 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用 PROJECT_ID 标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

启动 Cloud Shell

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

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

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

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 中启用该 API。

在 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) 或使用 gsutil 命令行工具从 Cloud Shell 或本地开发环境中执行此操作。

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

1930e055d138150a.png

指定存储桶的名称

点击 CREATE BUCKET 按钮。

34147939358517f8.png

点击 CONTINUE

选择位置

197817f20be07678.png

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

点击 CONTINUE

选择默认存储类别

53cd91441c8caf0e.png

为数据选择 Standard 存储类别。

点击 CONTINUE

设置访问权限控制

8c2b3b459d934a51.png

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

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

点击 CONTINUE

设置保护/加密

d931c24c3e705a68.png

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

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

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

前往 Permissions 标签页:

d0ecfdcff730ea51.png

向存储分区添加 allUsers 成员,并为其分配 Storage > Storage Object Viewer 角色,如下所示:

e9f25ec1ea0b6cc6.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 存储分区:

a98ed4ba17873e40.png

测试您是否可以向存储分区上传图片,以及上传的图片是否公开可用,如上一步中所述。

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

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

89e7a4d2c80a0319.png

您的存储分区现在可以接收图片了。

点击相应存储分区名称后,您会看到该存储分区的详细信息。

131387f12d3eb2d3.png

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

e87584471a6e9c6d.png

Public 访问权限标签旁边,您还会看到一个小链接图标。点击该链接后,浏览器会前往相应图片的公开网址,该网址的格式如下:

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

其中 BUCKET_NAME 是您为存储分区选择的全局唯一名称,后面是图片的相应文件名。

点击图片名称旁边的复选框后,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(日期):相应图片元数据的存储时间戳
  • 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,然后创建复合索引,如下所示:

ecb8b95e3c791272.png

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

8. 克隆代码

如果您在上一个 Codelab 中尚未克隆代码,请克隆代码:

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

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

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

该服务的布局文件如下:

f79613aff479d8ad.png

9. 探索服务代码

首先,我们来看看如何在 pom.xml 中使用 BOM 启用 Java 客户端库:

首先,修改列出 Java 函数依赖项的 pom.xml 文件。更新代码以添加 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 image,然后构建 AOT(Native) Java app image

如需运行 build,您需要确保已安装并配置合适的 JDK 和 native-image 构建器。有多种方式可供选择。

To start,请下载 GraalVM 22.2.x 社区版,然后按照 GraalVM 安装页面上的说明操作。

借助 SDKMAN! 可以大大简化此流程。

如需安装包含 SDKman 的相应 JDK 分发版本,请先使用以下安装命令:

sdk install java 22.2.r17-grl

指示 SDKman 将此版本用于 JIT 和 AOT build:

sdk use java 22.2.0.r17-grl

为 GraalVM 安装 native-image utility

gu install native-image

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

# 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

在终端中查看 build 日志:

...
[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

在终端中查看 build 日志,包括原生映像 build 日志:

请注意,根据您测试所用的机器,构建所需的时间会相当长。

...
[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

在终端中查看 build 日志:

[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

在终端中观察 build 日志,包括原生映像 build 日志和使用 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(Native) 映像。这两个服务部署将并行处理来自相应存储分区的同一张图片,以进行比较。

首先,设置 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

向 Cloud Storage 服务账号授予 pubsub.publisher 权限:

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(Native) 服务映像设置 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,然后点击我们在实验开始时创建的存储分区:

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

现在,您需要观察图片元数据是否已存储在 Firestore 中。

再次从“汉堡”菜单 (☰) 中前往 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 映像

后续步骤