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

1. 概览

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

427de3100de3a61e

学习内容

  • 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 中将其启用。

在 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 页面。

1930e055d138150a

指定存储桶的名称

点击 CREATE BUCKET 按钮。

34147939358517f8

点击 CONTINUE

选择位置

197817f20be07678

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

点击 CONTINUE

选择默认存储类别

53cd91441c8caf0e

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

点击 CONTINUE

设置访问权限控制

8c2b3b459d934a51

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

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

点击 CONTINUE

设置保护/加密

d931c24c3e705a68.png

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

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

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

转到 Permissions 标签页:

d0ecfdcff730ea51.png

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

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. 测试对存储分区的公开访问权限

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

89e7a4d2c80a0319

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

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

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 数据库中。Cloud Firestore 是一个快速、全代管式、无服务器、云原生的 NoSQL 文档数据库。转到 Cloud 控制台的 Firestore 部分,准备数据库:

9e4708d2257de058

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

点击 SELECT NATIVE MODE

9449ace8cc84de43.png

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

点击 CREATE DATABASE 按钮。

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

56265949a124819e

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

名称集合“pictures”。

75806ee24c4e13a7

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

点击 Save

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

5c2f1e17ea47f48f

在集合中以编程方式创建的文档将包含 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,然后创建复合索引,如下所示:

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. 探索服务代码

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

首先,修改 pom.xml 文件,其中列出了 Java 函数的依赖项。更新代码以添加 Cloud Vision API Maven 依赖项:

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

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

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

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

该功能在 EventController 类中实现。每次有新图片上传到存储分区时,该服务都会收到处理以下通知的通知:

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

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

代码将继续验证 Cloud Events 标头:

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

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

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

现在,可以构建一个请求,代码将准备一个要发送到 Vision API 的请求:

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

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

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

我们要求 Vision API 的 3 项主要功能如下:

  • 标签检测:了解照片中的内容
  • 图片属性:提供图片的一些有趣属性(我们关注的是图片的主色)
  • 安全搜索:了解图片是否可以安全显示(其中不得包含成人 / 医疗 / 少儿不宜 / 暴力内容)

此时,我们可以调用 Vision API:

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

以下是 Vision API 的响应如下所示,供您参考:

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

如果没有返回错误,我们可以继续,这就是为什么出现以下 if 代码块:

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

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

我们将获取图片中识别出的事物、类别或主题的标签:

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

我们想要了解图片的主色:

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

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

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

我们来检查一下图片是否可以安全显示:

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

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

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

我们正在检查成人 / 仿冒 / 医疗 / 暴力 / 少儿不宜的特征,以了解相应特征是“不太可能”还是“极有可能”

如果安全搜索的结果没问题,我们就可以将元数据存储在 Firestore 中:

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

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

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

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

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

10. 使用 GraalVM 构建应用映像(可选)

在此可选步骤中,您将使用 GraalVM 构建 JIT(JVM) based app image,然后构建 AOT(Native) Java app image

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

To start,请下载 GraalVM 22.2.x Community Edition 并按照 GraalVM 安装页面中的说明操作。

借助 SDKMAN!

如需使用 SDKman 安装相应的 JDK 发行版,请先使用安装命令:

sdk install java 22.2.r17-grl

指示 SDKman 针对 JIT 和 AOT 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

查看终端中的构建日志:

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

构建 AOT(原生)映像:

./mvnw package -Pnative -DskipTests

查看终端中的构建日志,包括原生映像构建日志:

请注意,构建时间会稍长一些,具体取决于您要测试的机器。

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

11. 构建和发布容器映像

让我们构建两个不同版本的容器映像:一个作为 JIT(JVM) image,另一个作为 AOT(Native) Java image

首先,设置 GCP 项目环境变量:

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

构建 JIT(JVM) 映像:

./mvnw package -Pjvm-image

查看终端中的构建日志:

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

构建 AOT(原生)映像:

./mvnw package -Pnative-image

查看终端中的构建日志,包括原生映像构建日志和使用 UPX 压缩映像。

请注意,构建时间会稍长一些,具体取决于您测试所用的机器

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

验证映像是否已构建:

docker images | grep image-analysis

为两个映像添加标记并将其推送到 GCR:

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

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

12. 部署到 Cloud Run

现在该部署服务了。

您将部署服务两次,一次使用 JIT(JVM) 映像,第二次使用 AOT(原生)映像。两个服务部署将并行处理存储分区中的同一张图片,以便进行比较。

首先,设置 GCP 项目环境变量:

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

部署 JIT(JVM) 映像并观察控制台中的部署日志:

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

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

部署 AOT(原生)映像并观察控制台中的部署日志:

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

13. 设置 Eventarc 触发器

Eventarc 提供了一个标准化解决方案,用于管理分离微服务之间的状态更改流(称为事件)。触发后,Eventarc 通过 Pub/Sub 订阅将这些事件路由到各个目的地(本文档中请参阅“事件”目的地),同时为您管理传送、安全性、授权、可观测性和错误处理。

您可以创建 Eventarc 触发器,以便 Cloud Run 服务接收指定事件或一组事件的通知。通过为触发器指定过滤条件,您可以配置事件的路由,包括事件来源和目标 Cloud Run 服务。

首先,设置 GCP 项目环境变量:

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

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

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

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

为 JVM(JIT) 和 AOT(原生)服务映像设置 Eventarc 触发器来处理映像:

gcloud eventarc triggers list --location=eu

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

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

您会发现这两个触发器已经创建:

gcloud eventarc triggers list --location=eu

14. 测试服务版本

服务部署成功后,您要将图片发布到 Cloud Storage,看看我们的服务是否被调用、Vision API 会返回什么,以及元数据是否存储在 Firestore 中。

返回 Cloud Storage,点击我们在实验开始时创建的存储分区:

ff8a6567afc76235.png

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

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

347b76e8b775f2f5

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

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

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

810a8684414ceafa.png

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

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

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

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

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

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

b80308c7d0f55a3.png

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

同样是(☰) 菜单,前往“Firestore”部分。在 Data 子部分(默认显示)中,您应该会看到 pictures 集合,其中添加了一个新文档,与您刚刚上传的图片相对应:

933a20a9709cb006

15. 清理(可选)

如果您不打算继续完成本系列中的其他实验,可以清理资源以节省成本,并成为一个整体优秀的云公民。您可以按以下步骤逐个清理资源。

删除存储分区:

gsutil rb gs://${BUCKET_PICTURES}

删除函数:

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

选择“从集合中删除集合”,以删除 Firestore 集合:

410b551c3264f70a

或者,您也可以删除整个项目:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. 恭喜!

恭喜!您已成功实现该项目的第一个密钥服务!

所学内容

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • 原生 Java 映像

后续步骤