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

1. Обзор

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

c0650ee4a76db35e.png

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

  • Облачное хранилище
  • Cloud Run
  • API Cloud Vision
  • Облачный Firestore

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

Настройка среды для самостоятельного обучения

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

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

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

Запустить Cloud Shell

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

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

84688aa223b1c3a2.png

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

320e18fedb7fbe0.png

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

3. Включите API.

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

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

8f3522d790bb026c.png

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

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)

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

В 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 в консоли Cloud Console:

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 поля:

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

Или же вы можете сделать это из консоли Cloud Console, щелкнув 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. Изучите код сервиса.

Для начала нужно разобраться, как в файле pom.xml включаются клиентские библиотеки Java с помощью BOM:

Сначала откройте файл 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);

Мы запрашиваем 3 ключевые возможности Vision API:

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

На этом этапе мы можем обратиться к API Vision:

...
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 и native-image builder. Доступно несколько вариантов.

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 и утилиту native-image с помощью следующих простых команд:

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

ls -lart

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

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

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

cd ../..

# validate the version with
java -version 

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

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

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

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

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

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

./mvnw package

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

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

Создайте нативный образ (использующий 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. Развертывание в облаке

Пора развернуть сервис.

Вы развернете сервис дважды: первый раз, используя JIT-образ, а второй раз — AOT-образ (нативный). Оба развертывания сервиса будут обрабатывать один и тот же образ из хранилища параллельно для целей сравнения.

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

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

Разверните JIT-образ и просмотрите журнал развертывания в консоли:

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

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

Разверните образ Native и просмотрите журнал развертывания в консоли:

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

Предоставьте учетной записи службы 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'

Настройте триггеры Eventarc для обработки образов как JIT-компилятора, так и нативных сервисов:

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

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

ae1a4a94c7c7a166.png

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

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

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

Вам потребуется повторить этот процесс для image-analysis-native .

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

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

4afe22833c1fd14c.png

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

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

82d6c468956e7cfc.png

15. Уборка (необязательно)

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

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

gsutil rb gs://${BUCKET_PICTURES}

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

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

Удалите коллекцию Firestore, выбрав пункт «Удалить коллекцию» в списке коллекций:

6cc86a7b88fdb4d3.png

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

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

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

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

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

  • Облачное хранилище
  • Cloud Run
  • API Cloud Vision
  • Облачный Firestore
  • Исходные изображения Java

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