Pic-a-daily: Лабораторная работа 1 — Хранение и анализ изображений (родной код Java)

1. Обзор

В первой лаборатории кода вы будете хранить изображения в корзине. Это создаст событие создания файла, которое будет обрабатываться службой, развернутой в Cloud Run. Служба выполнит вызов Vision API для анализа изображения и сохранения результатов в хранилище данных.

c0650ee4a76db35e.png

Что вы узнаете

  • Облачное хранилище
  • Облачный бег
  • API облачного видения
  • Облачный пожарный магазин

2. Настройка и требования

Самостоятельная настройка среды

  1. Войдите в Google Cloud Console и создайте новый проект или повторно используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • Имя проекта — это отображаемое имя для участников этого проекта. Это строка символов, не используемая API Google. Вы всегда можете обновить его.
  • Идентификатор проекта уникален для всех проектов Google Cloud и является неизменяемым (невозможно изменить после его установки). Cloud Console автоматически генерирует уникальную строку; обычно тебя не волнует, что это такое. В большинстве лабораторий кода вам потребуется указать идентификатор проекта (обычно идентифицируемый как PROJECT_ID ). Если вам не нравится сгенерированный идентификатор, вы можете создать другой случайный идентификатор. Альтернативно, вы можете попробовать свой собственный и посмотреть, доступен ли он. Его нельзя изменить после этого шага и он сохраняется на протяжении всего проекта.
  • К вашему сведению, есть третье значение — номер проекта , которое используют некоторые API. Подробнее обо всех трех этих значениях читайте в документации .
  1. Затем вам необходимо включить выставление счетов в Cloud Console, чтобы использовать облачные ресурсы/API. Прохождение этой лаборатории кода не будет стоить много, если вообще что-то стоить. Чтобы отключить ресурсы и избежать выставления счетов за пределами этого руководства, вы можете удалить созданные вами ресурсы или удалить проект. Новые пользователи Google Cloud имеют право на участие в программе бесплатной пробной версии стоимостью 300 долларов США .

Запустить Cloud Shell

Хотя Google Cloud можно управлять удаленно с вашего ноутбука, в этой лаборатории вы будете использовать Google Cloud Shell , среду командной строки, работающую в облаке.

В Google Cloud Console щелкните значок Cloud Shell на верхней правой панели инструментов:

84688aa223b1c3a2.png

Подготовка и подключение к среде займет всего несколько минут. Когда все будет готово, вы должны увидеть что-то вроде этого:

320e18fedb7fbe0.png

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Он предлагает постоянный домашний каталог объемом 5 ГБ и работает в Google Cloud, что значительно повышает производительность сети и аутентификацию. Всю работу в этой лаборатории кода можно выполнять в браузере. Вам не нужно ничего устанавливать.

3. Включите API

В этом практическом занятии вы будете использовать Cloud Functions и Vision API, но сначала их необходимо включить либо в Cloud Console, либо с помощью gcloud .

Чтобы включить Vision API в Cloud Console, найдите Cloud Vision API в строке поиска:

8f3522d790bb026c.png

Вы попадете на страницу Cloud Vision API:

d785572fa14c87c2.png

Нажмите кнопку ENABLE .

Кроме того, вы также можете включить Cloud Shell с помощью инструмента командной строки gcloud.

Внутри 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 ».

d08ecb0ae29330a1.png

Назовите свое ведро

Нажмите кнопку CREATE BUCKET .

8951851554a430d2.png

Нажмите CONTINUE .

Выберите местоположение

24b24625157ab467.png

Создайте мультирегиональную корзину в выбранном вами регионе (здесь Europe ).

Нажмите CONTINUE .

Выберите класс хранилища по умолчанию

9e7bd365fa94a2e0.png

Выберите Standard класс хранения для ваших данных.

Нажмите CONTINUE .

Установить контроль доступа

1ff4a1f6e57045f5.png

Поскольку вы будете работать с общедоступными изображениями, вы хотите, чтобы все наши изображения, хранящиеся в этом сегменте, имели одинаковый единый контроль доступа.

Выберите вариант Uniform контроль доступа».

Нажмите CONTINUE .

Установить защиту/шифрование

2d469b076029d365.png

Оставьте значение по умолчанию ( Google-managed key) , так как вы не будете использовать собственные ключи шифрования.

Нажмите CREATE , чтобы окончательно завершить создание корзины.

Добавить allUsers в качестве средства просмотра хранилища

Перейдите на вкладку Permissions :

19564b3ad8688ae8.png

Добавьте в корзину член allUsers с ролью Storage > Storage Object Viewer , как показано ниже:

d655e760c76d62c1.png

Нажмите SAVE .

5. Создайте корзину (gsutil).

Вы также можете использовать инструмент командной строки gsutil в Cloud Shell для создания сегментов.

В Cloud Shell задайте переменную для уникального имени сегмента. В Cloud Shell уже указан уникальный идентификатор вашего проекта GOOGLE_CLOUD_PROJECT . Вы можете добавить это к имени корзины.

Например:

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

Убедитесь, что вы можете загружать изображения в корзину и что загруженные изображения общедоступны, как описано на предыдущем шаге.

6. Проверьте публичный доступ к корзине

Вернувшись в браузер хранилища, вы увидите свою корзину в списке с «Общественным» доступом (включая предупреждающий знак, напоминающий вам, что кто-либо имеет доступ к содержимому этой корзины).

e639a9ba625b71a6.png

Теперь ваше ведро готово к приему фотографий.

Если вы нажмете на имя корзины, вы увидите подробную информацию о корзине.

1f88a2290290aba8.png

Там вы можете попробовать кнопку Upload files , чтобы проверить, можете ли вы добавить изображение в корзину. Всплывающее окно выбора файла попросит вас выбрать файл. После выбора он будет загружен в вашу корзину, и вы снова увидите public доступ, который был автоматически присвоен этому новому файлу.

1209e7ebe1f63b10.png

Рядом с меткой Public доступ» вы также увидите небольшой значок ссылки. При нажатии на него ваш браузер перейдет на общедоступный URL-адрес этого изображения, который будет иметь вид:

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

BUCKET_NAME — это глобальное уникальное имя, которое вы выбрали для своего сегмента, а затем имя файла вашего изображения.

Установив флажок рядом с названием изображения, кнопка DELETE станет активной, и вы сможете удалить это первое изображение.

7. Подготовьте базу данных

Вы сохраните информацию об изображении, предоставленном Vision API, в базе данных Cloud Firestore — быстрой, полностью управляемой, бессерверной, облачной базе данных документов NoSQL. Подготовьте свою базу данных, перейдя в раздел Firestore облачной консоли:

e57a673537b5deca.png

Предлагаются два варианта: Native mode или Datastore mode . Используйте собственный режим, который предлагает дополнительные функции, такие как автономная поддержка и синхронизация в реальном времени.

Нажмите SELECT NATIVE MODE .

1a2e363fae5c7e96.png

Выберите мультирегион (здесь, в Европе, но в идеале по крайней мере тот же регион, в котором находятся ваша функция и сегмент хранилища).

Нажмите кнопку CREATE DATABASE .

После создания базы данных вы должны увидеть следующее:

7dcc82751ed483fb.png

Создайте новую коллекцию , нажав кнопку + START COLLECTION .

Назовите pictures из коллекции.

dce3d73884ac8c83.png

Вам не нужно создавать документ. Вы будете добавлять их программно по мере того, как новые изображения сохраняются в облачном хранилище и анализируются Vision API.

Нажмите Save .

Firestore создает первый документ по умолчанию во вновь созданной коллекции. Вы можете безопасно удалить этот документ, поскольку он не содержит никакой полезной информации:

63e95c844b3f79d3.png

Документы, которые будут созданы программно в нашей коллекции, будут содержать 4 поля:

  • имя (строка): имя файла загруженного изображения, которое также является ключом документа.
  • labels (массив строк): метки элементов, распознанных Vision API.
  • цвет (строка): шестнадцатеричный код доминирующего цвета (например, #ab12ef).
  • создано (дата): временная отметка, когда метаданные этого изображения были сохранены.
  • миниатюра (логическое значение): необязательное поле, которое будет присутствовать и иметь значение 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

Или вы также можете сделать это из облачной консоли, нажав Indexes в столбце навигации слева, а затем создав составной индекс, как показано ниже:

2236d3a024a59232.png

Нажмите Create . Создание индекса может занять несколько минут.

8. Клонируйте код

Клонируйте код, если вы еще этого не сделали в предыдущей лаборатории кода:

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

Затем вы можете перейти в каталог, содержащий сервис, чтобы начать создание лаборатории:

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

У вас будет следующий макет файла для сервиса:

4c2a18a2c8b69dc5.png

9. Изучите сервисный код

Вы начинаете с рассмотрения того, как клиентские библиотеки Java включаются в pom.xml с помощью спецификации:

Сначала откройте файл pom.xml , в котором перечислены зависимости нашего Java-приложения; основное внимание уделяется использованию API-интерфейсов Vision, Cloud Storage и Firestore.

<?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:

  • Обнаружение этикеток : чтобы понять, что на этих картинках
  • Свойства изображения : придать интересные атрибуты изображению (нас интересует доминирующий цвет изображения).
  • Безопасный поиск : узнать, безопасно ли показывать изображение (оно не должно содержать материалы для взрослых, медицинские, пикантные или жестокие материалы).

На этом этапе мы можем выполнить вызов 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

На этом необязательном этапе вы создадите JIT based app image , а затем Native Java app image , используя GraalVM.

Чтобы запустить сборку, вам необходимо убедиться, что у вас установлен и настроен соответствующий JDK и сборщик собственных образов. Доступно несколько вариантов.

To start загрузите GraalVM 22.3.x Community Edition и следуйте инструкциям на странице установки GraalVM .

Этот процесс можно значительно упростить с помощью SDKMAN!

Чтобы установить соответствующий дистрибутив JDK с помощью SDKman , начните с использования команды установки:

sdk install java 17.0.8-graal

Попросите SDKman использовать эту версию как для JIT, так и для AOT-сборок:

sdk use java 17.0.8-graal

В Cloudshell для вашего удобства вы можете установить GraalVM и утилиту собственного образа с помощью этих простых команд:

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

ls -lart

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

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

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

cd ../..

# validate the version with
java -version 

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

Сначала установите переменные среды проекта GCP:

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

Затем вы можете перейти в каталог, содержащий сервис, чтобы начать создание лаборатории:

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

Создайте образ JIT-приложения:

./mvnw package

Посмотрите журнал сборки в терминале:

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

Создайте образ Native (использует AOT):.

./mvnw native:compile -Pnative

Просмотрите журнал сборки в терминале, включая журналы сборки собственного образа:

Обратите внимание, что сборка занимает немного больше времени, в зависимости от машины, на которой вы тестируете.

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

11. Создание и публикация образов контейнеров

Давайте создадим образ контейнера в двух разных версиях: одну как JIT image , а другую — как Native Java image .

Сначала установите переменные среды проекта GCP:

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

Создайте JIT-образ:.

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

Посмотрите журнал сборки в терминале:

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

Создайте образ AOT (собственный):.

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

Просмотрите журнал сборки в терминале, включая журналы сборки собственного образа.

Примечание:

  • что сборка занимает немного больше времени, в зависимости от машины, на которой вы тестируете
  • изображения можно дополнительно сжать с помощью UPX, однако это оказывает небольшое негативное влияние на производительность при запуске, поэтому в этой сборке UPX не используется — это всегда небольшой компромисс.
...
[INFO]     [creator]     Saving docker.io/library/image-analysis-maven-native:latest...
[INFO]     [creator]     *** Images (13167702674e):
[INFO]     [creator]           docker.io/library/image-analysis-maven-native:latest
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:37 min
[INFO] Finished at: 2023-10-10T20:05:16Z
[INFO] ------------------------------------------------------------------------

Убедитесь, что изображения созданы:

docker images | grep image-analysis

Пометьте и отправьте два изображения в GCR:

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

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

12. Развертывание в Cloud Run

Пришло время развернуть службу.

Вы развернете службу дважды: один раз с использованием образа JIT, а второй раз с использованием образа AOT (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-образ и просмотрите журнал развертывания в консоли:

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'

Настройте триггеры Eventarc для образов JIT-сервиса и Native-сервиса для обработки образа:

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

На странице сведений о сегменте нажмите кнопку Upload files , чтобы загрузить изображение.

Например, изображение GeekHour.jpeg предоставляется вместе с вашей кодовой базой в /services/image-analysis/java . Выберите изображение и нажмите Open button :

d57529452f62bd32.png

Теперь вы можете проверить выполнение службы, начиная с image-analysis-jit , а затем image-analysis-native .

В меню «гамбургер» (☰) перейдите к Cloud Run > image-analysis-jit .

Нажмите «Журналы» и посмотрите результат:

ae1a4a94c7c7a166.png

И действительно, в списке журналов я вижу, что был вызван JIT-сервис image-analysis-jit .

В журналах указывается начало и окончание выполнения службы. А между ними мы можем видеть журналы, которые мы помещаем в нашу функцию, с операторами журнала на уровне INFO. Мы видим:

  • Подробности события, запускающего нашу функцию,
  • Необработанные результаты вызова Vision API,
  • Этикетки, которые были найдены на загруженной нами картинке,
  • Информация о доминирующих цветах,
  • Безопасно ли показывать изображение,
  • И в конечном итоге эти метаданные об изображении были сохранены в Firestore.

Вы повторите процесс для image-analysis-native .

В меню «гамбургер» (☰) перейдите к Cloud Run > image-analysis-native .

Нажмите «Журналы» и посмотрите результат:

4afe22833c1fd14c.png

Теперь вам нужно будет проверить, сохранены ли метаданные изображения в Fiorestore.

Снова из меню «гамбургер» (☰) перейдите в раздел Firestore . В подразделе Data (отображается по умолчанию) вы должны увидеть коллекцию pictures с добавленным новым документом, соответствующим только что загруженному изображению:

82d6c468956e7cfc.png

15. Очистка (необязательно)

Если вы не собираетесь продолжать другие лабораторные работы из этой серии, вы можете очистить ресурсы, чтобы сэкономить средства и стать в целом хорошим гражданином облака. Вы можете очистить ресурсы по отдельности следующим образом.

Удалить корзину:

gsutil rb gs://${BUCKET_PICTURES}

Удалить функцию:

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

Удалите коллекцию Firestore, выбрав Удалить коллекцию из коллекции:

6cc86a7b88fdb4d3.png

Альтернативно, вы можете удалить весь проект:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. Поздравляем!

Поздравляем! Вы успешно реализовали первый ключевой сервис проекта!

Что мы рассмотрели

  • Облачное хранилище
  • Облачный бег
  • API облачного видения
  • Облачный пожарный магазин
  • Собственные изображения Java

Следующие шаги