Zdjęcia dzienne: przechowywanie i analizowanie zdjęć przy użyciu natywnych bibliotek klienta w Javie od Google

1. Omówienie

W pierwszym module do tworzenia kodu umieścisz zdjęcia w zasobniku. Spowoduje to wygenerowanie zdarzenia utworzenia pliku, które będzie obsługiwane przez usługę wdrożoną w Cloud Run. Usługa wywoła interfejs Vision API, aby przeprowadzić analizę obrazu i zapisać wyniki w magazynie danych.

427de3100de3a61e.png

Czego się nauczysz

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

2. Konfiguracja i wymagania

Samodzielne konfigurowanie środowiska

  1. Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub wykorzystaj już istniejący. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Nazwa projektu jest wyświetlaną nazwą uczestników tego projektu. To ciąg znaków, który nie jest używany przez interfejsy API Google. W każdej chwili możesz ją zmienić.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i nie można go zmienić (nie można go zmienić po ustawieniu). Cloud Console automatycznie wygeneruje unikalny ciąg znaków. zwykle nieważne, co ona jest. W większości ćwiczeń z programowania konieczne jest odwołanie się do identyfikatora projektu (zwykle nazywa się on PROJECT_ID). Jeśli nie podoba Ci się wygenerowany identyfikator, możesz wygenerować kolejny losowy. Możesz też spróbować własnych sił i sprawdzić, czy jest dostępna. Potem nie będzie można go zmienić. Pozostanie ono przez czas trwania projektu.
  • Dostępna jest trzecia wartość, numer projektu, z którego korzystają niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w Cloud Console, aby korzystać z zasobów Cloud/interfejsów API. Ukończenie tego ćwiczenia z programowania nie powinno kosztować zbyt wiele. Aby wyłączyć zasoby, aby nie naliczać opłat po zakończeniu tego samouczka, możesz usunąć utworzone zasoby lub cały projekt. Nowi użytkownicy Google Cloud mogą skorzystać z programu bezpłatnego okresu próbnego o wartości 300 USD.

Uruchamianie Cloud Shell

Google Cloud można obsługiwać zdalnie z laptopa, ale w ramach tego ćwiczenia z programowania wykorzystasz Google Cloud Shell – środowisko wiersza poleceń działające w Cloud.

W konsoli Google Cloud kliknij ikonę Cloud Shell na górnym pasku narzędzi:

55efc1aaa7a4d3ad.png

Uzyskanie dostępu do środowiska i połączenie się z nim powinno zająć tylko kilka chwil. Po zakończeniu powinno pojawić się coś takiego:

7ffe5cbb04455448.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, znacząco zwiększając wydajność sieci i uwierzytelnianie. Wszystkie zadania w ramach tego ćwiczenia z programowania można wykonywać w przeglądarce. Nie musisz niczego instalować.

3. Włącz interfejsy API

W tym module będziesz używać Cloud Functions i Vision API, ale najpierw musisz włączyć te funkcje w konsoli Cloud lub za pomocą usługi gcloud.

Aby włączyć Vision API w konsoli Cloud, wyszukaj Cloud Vision API na pasku wyszukiwania:

cf48b1747ba6a6fb.png

Wyświetli się strona Cloud Vision API:

ba4af419e6086fbb.png

Kliknij przycisk ENABLE.

Możesz też włączyć Cloud Shell za pomocą narzędzia wiersza poleceń gcloud.

W Cloud Shell uruchom to polecenie:

gcloud services enable vision.googleapis.com

Powinna pojawić się operacja dokończenia:

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

Włącz też Cloud Run i Cloud Build:

gcloud services enable cloudbuild.googleapis.com \
  run.googleapis.com

4. Tworzenie zasobnika (konsola)

Utwórz zasobnik na zdjęcia. Możesz to zrobić w konsoli Google Cloud Platform ( console.cloud.google.com) lub za pomocą narzędzia wiersza poleceń gsutil z Cloud Shell albo lokalnego środowiska programistycznego.

Z „hamburgera” (<!--) przejdź na stronę Storage.

1930e055d138150a.png

Nazwij zasobnik

Kliknij przycisk CREATE BUCKET.

34147939358517f8.png

Kliknij CONTINUE.

Wybierz lokalizację

197817f20be07678.png

Utwórz zasobnik z wieloma regionami w wybranym regionie (tutaj Europe).

Kliknij CONTINUE.

Wybierz domyślną klasę pamięci

53cd91441c8caf0e.png

Wybierz klasę pamięci Standard dla swoich danych.

Kliknij CONTINUE.

Ustaw kontrolę dostępu

8c2b3b459d934a51.png

Ponieważ pracujesz z obrazami dostępnymi publicznie, chcesz, aby wszystkie zdjęcia przechowywane w tym zasobniku miały taką samą jednolitą kontrolę dostępu.

Wybierz opcję kontroli dostępu w usłudze Uniform.

Kliknij CONTINUE.

Ustaw zabezpieczenia/szyfrowanie

d931c24c3e705a68.png

Zachowaj domyślne (Google-managed key), ponieważ nie będziesz używać własnych kluczy szyfrowania.

Kliknij CREATE, aby zakończyć tworzenie zasobnika.

Dodawanie użytkownika allUsers jako wyświetlającego miejsce na dane

Otwórz kartę Permissions:

d0ecfdcff730ea51.png

Dodaj do zasobnika użytkownika allUsers z rolą Storage > Storage Object Viewer w następujący sposób:

e9f25ec1ea0b6cc6.png

Kliknij SAVE.

5. Tworzenie zasobnika (gsutil)

Zasobniki możesz też tworzyć za pomocą narzędzia wiersza poleceń gsutil w Cloud Shell.

W Cloud Shell ustaw zmienną dla unikalnej nazwy zasobnika. W Cloud Shell wartość GOOGLE_CLOUD_PROJECT jest już ustawiona na Twój unikalny identyfikator projektu. Możesz to dodać do nazwy zasobnika.

Na przykład:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Tworzenie standardowej strefy obejmującej wiele regionów w Europie:

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

Sprawdź, czy jednolity dostęp na poziomie zasobnika:

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

Ustaw zasobnik jako publiczny:

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

Jeśli otworzysz sekcję Cloud Storage w konsoli, powinien być już publiczny zasobnik uploaded-pictures:

a98ed4ba17873e40.png

Sprawdź, czy możesz przesyłać zdjęcia do zasobnika i czy są one publicznie dostępne, jak wyjaśniliśmy w poprzednim kroku.

6. Testowanie dostępu publicznego do zasobnika

Gdy wrócisz do przeglądarki, zobaczysz zasobnik na liście z oznaczeniem „Publiczny”. dostępu (w tym znak ostrzegawczy przypominający o tym, że każdy ma dostęp do zawartości danego zasobnika).

89e7a4d2c80a0319.png

Twój zasobnik jest teraz gotowy na otrzymywanie zdjęć.

Po kliknięciu nazwy zasobnika zobaczysz jego szczegóły.

131387f12d3eb2d3.png

Tam możesz wypróbować przycisk Upload files, aby sprawdzić, czy możesz dodać zdjęcie do zasobnika. Pojawi się wyskakujące okienko wyboru plików z prośbą o wybranie pliku. Po wybraniu plik zostanie przesłany do zasobnika i ponownie zobaczysz uprawnienia dostępu na poziomie public, które zostały automatycznie przypisane do tego nowego pliku.

e87584471a6e9c6d.png

Na etykiecie dostępu Public zobaczysz też małą ikonę linku. Po kliknięciu obrazu przeglądarka otworzy jego publiczny adres URL, który będzie miał postać:

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

BUCKET_NAME to globalnie unikalna nazwa nadana przez Ciebie dla zasobnika oraz nazwa pliku zdjęcia.

Gdy klikniesz pole wyboru obok nazwy zdjęcia, przycisk DELETE stanie się aktywny i będzie można usunąć pierwszy obraz.

7. Przygotowywanie bazy danych

Informacje o zdjęciu wyświetlanym przez interfejs Vision API będziesz przechowywać w bazie danych Cloud Firestore – szybkiej, w pełni zarządzanej, bezserwerowej, chmurowej bazie dokumentów NoSQL. Przygotuj bazę danych w sekcji Firestore w konsoli Cloud:

9e4708d2257de058.png

Dostępne są 2 opcje: Native mode i Datastore mode. Korzystaj z trybu natywnego, który oferuje dodatkowe funkcje, takie jak obsługa offline i synchronizacja w czasie rzeczywistym.

Kliknij SELECT NATIVE MODE.

9449ace8cc84de43.png

Wybierz wiele regionów (w Europie, ale najlepiej co najmniej taki sam region, w jakim są Twoje funkcja i zasobnik na dane).

Kliknij przycisk CREATE DATABASE.

Po utworzeniu bazy danych powinien wyświetlić się ten komunikat:

56265949a124819e.png

Utwórz nową kolekcję, klikając przycisk + START COLLECTION.

Nazwij kolekcję pictures.

75806ee24c4e13a7.png

Nie musisz tworzyć dokumentu. Będziesz dodawać je automatycznie w miarę zapisywania nowych zdjęć w Cloud Storage i analizowania ich przez interfejs Vision API.

Kliknij Save.

Firestore tworzy w nowo utworzonej kolekcji pierwszy dokument domyślny. Dokument możesz bezpiecznie usunąć, ponieważ nie zawiera on żadnych przydatnych informacji.

5c2f1e17ea47f48f.png

Dokumenty, które zostaną utworzone automatycznie w naszej kolekcji, będą zawierać 4 pola:

  • name (ciąg znaków): nazwa pliku przesłanego zdjęcia, który jest również kluczem dokumentu.
  • labels (tablica ciągów znaków): etykiety rozpoznawane przez interfejs Vision API
  • color (ciąg znaków): szesnastkowy kod koloru dominującego (np. #ab12ef)
  • created (data): sygnatura czasowa przechowywania metadanych obrazu.
  • thumbnail (wartość logiczna): opcjonalne pole, które będzie obecne i będzie mieć wartość true (prawda), jeśli dla danego zdjęcia wygenerowano obraz miniatury

Będziemy szukać w Firestore zdjęć, które mają dostępne miniatury, i sortujemy według daty utworzenia, więc konieczne będzie utworzenie indeksu wyszukiwania.

Indeks możesz utworzyć za pomocą tego polecenia w Cloud Shell:

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

Możesz to też zrobić w konsoli Cloud. Aby to zrobić, w kolumnie nawigacji po lewej stronie kliknij Indexes, a potem utwórz indeks złożony w ten sposób:

ecb8b95e3c791272.png

Kliknij Create. Tworzenie indeksu może potrwać kilka minut.

8. Klonowanie kodu

Skopiuj kod, jeśli jeszcze nie udało Ci się tego zrobić w poprzednim module:

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

Następnie możesz przejść do katalogu zawierającego usługę, aby rozpocząć tworzenie modułu:

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

Masz do dyspozycji taki układ plików dla usługi:

f79613aff479d8ad.png

9. Zapoznaj się z kodem usługi

Najpierw sprawdź, jak biblioteki klienta Java są włączone w pom.xml przy użyciu BOM:

Najpierw edytuj plik pom.xml, który zawiera listę zależności naszej funkcji w Javie. Zaktualizuj kod, aby dodać zależność Maven interfejsu Cloud Vision API:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 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>

Funkcja jest wdrożona w klasie EventController. Za każdym razem, gdy do zasobnika jest przesyłany nowy obraz, usługa otrzymuje powiadomienie, które musi przetworzyć:

@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 {
...
}

Rozpocznie się kod sprawdzania nagłówków 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);
} 

Teraz można skompilować żądanie, a kod przygotuje jedno z nich, które zostanie wysłane do 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);

Prosimy o 3 kluczowe funkcje interfejsu Vision API:

  • Wykrywanie etykiet: sprawdzanie, co znajduje się na zdjęciach.
  • Właściwości obrazu: w celu nadania nam interesujących atrybutów zdjęcia (chcemy zwrócić uwagę na dominujący kolor zdjęcia).
  • Bezpieczne wyszukiwanie: pozwala określić, czy obraz jest bezpieczny do wyświetlenia (nie może zawierać treści dla dorosłych, medycyny, treści dla dorosłych ani przemocy).

W tym momencie możemy wywołać interfejs Vision API:

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

Oto, jak wygląda odpowiedź z interfejsu 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
}

Jeśli nie zostanie zwrócony żaden błąd, możemy przejść dalej, z tego powodu w przypadku blokady:

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);
}

Poznamy etykiety elementów, kategorii i tematów rozpoznawane na ilustracji:

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

Interesuje nas dominujący kolor zdjęcia:

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);
}

Sprawdźmy, czy zdjęcie można bezpiecznie wyświetlać:

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);
}

Sprawdzamy treści dla dorosłych / parodie / medycyna / przemoc / treści dla dorosłych, aby sprawdzić, czy są one prawdopodobne lub bardzo prawdopodobne.

Jeśli wynik bezpiecznego wyszukiwania jest prawidłowy, możemy przechowywać metadane w 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. Kompilowanie obrazów aplikacji przy użyciu GraalVM (opcjonalnie)

W tym opcjonalnym kroku utworzysz JIT(JVM) based app image, a następnie AOT(Native) Java app image, przy użyciu GraalVM.

Aby uruchomić kompilację, musisz mieć zainstalowany i skonfigurowany odpowiedni pakiet JDK oraz narzędzie do tworzenia obrazów natywnych. Dostępnych jest kilka opcji.

To start, pobierz GraalVM 22.2.x Community Edition i postępuj zgodnie z instrukcjami na stronie instalacji GraalVM.

Dzięki SDKMAN proces ten można znacznie uprościć.

Aby zainstalować odpowiednią dystrybucję JDK za pomocą dodatku SDKman, zacznij od polecenia instalacji:

sdk install java 22.2.r17-grl

Poproś SDKmana o używanie tej wersji zarówno w przypadku kompilacji JIT, jak i AOT:

sdk use java 22.2.0.r17-grl

Zainstaluj native-image utility dla GraalVM:

gu install native-image

W Cloudshell możesz dla Twojej wygody zainstalować GraalVM i narzędzie natywne do tworzenia obrazów za pomocą tych prostych poleceń:

# 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 ../..

Najpierw skonfiguruj zmienne środowiskowe projektu GCP:

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

Następnie możesz przejść do katalogu zawierającego usługę, aby rozpocząć tworzenie modułu:

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

Skompiluj obraz aplikacji JIT(JVM):

./mvnw package -Pjvm

Obserwuj log kompilacji w terminalu:

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

Utwórz obraz AOT(natywny).

./mvnw package -Pnative -DskipTests

Obserwuj log kompilacji w terminalu, w tym logi kompilacji obrazu natywnego:

Pamiętaj, że w zależności od maszyny, na której testujesz, kompilacja może zająć nieco więcej czasu.

...
[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. Kompilowanie i publikowanie obrazów kontenerów

Stwórzmy obraz kontenera w 2 różnych wersjach: jedną jako JIT(JVM) image, a drugą jako element AOT(Native) Java image.

Najpierw skonfiguruj zmienne środowiskowe projektu GCP:

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

Utwórz obraz JIT(JVM):

./mvnw package -Pjvm-image

Obserwuj log kompilacji w terminalu:

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

Utwórz obraz AOT(natywny).

./mvnw package -Pnative-image

Obserwuj log kompilacji w terminalu, w tym logi kompilacji obrazu natywnego oraz kompresję obrazu za pomocą UPX.

Pamiętaj, że kompilacja może zająć nieco więcej czasu, w zależności od maszyny, na której testujesz.

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

Sprawdź, czy obrazy zostały utworzone:

docker images | grep image-analysis

Oznacz oba obrazy tagami i przekaż je do 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. Wdrożenie w Cloud Run

Czas wdrożyć usługę.

Usługę wdrożysz dwukrotnie: raz z użyciem obrazu JIT(JVM) i drugi z obrazem AOT(natywnym). Oba wdrożenia usług będą równolegle przetwarzać ten sam obraz z zasobnika w celu porównania.

Najpierw skonfiguruj zmienne środowiskowe projektu 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

Wdróż obraz JIT(JVM) i obserwuj dziennik wdrożenia w konsoli:

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

Wdróż obraz AOT(Native) i obserwuj dziennik wdrożenia w konsoli:

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. Skonfiguruj aktywatory Eventarc

Eventarc oferuje ujednolicone rozwiązanie do zarządzania przepływem zmian stanu zwanych zdarzeniami między odłączonych mikroserwisami. Po uruchomieniu Eventarc kieruje te zdarzenia przez subskrypcje Pub/Sub do różnych miejsc docelowych (w tym dokumencie znajdziesz informacje o miejscach docelowych zdarzeń), jednocześnie zarządzając dostarczaniem, zabezpieczeniami, autoryzacją, dostrzegalnością i obsługą błędów.

Możesz utworzyć aktywator Eventarc, aby usługa Cloud Run otrzymywała powiadomienia o określonym zdarzeniu lub zbiorze zdarzeń. Określając filtry aktywatora, możesz skonfigurować routing zdarzenia, w tym do źródła zdarzeń i docelowej usługi Cloud Run.

Najpierw skonfiguruj zmienne środowiskowe projektu 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

Przyznaj uprawnienia pubsub.publisher do konta usługi 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'

Aby przetworzyć obraz, skonfiguruj aktywatory Eventarc dla obrazów usługi JVM(JIT) i AOT(natywne):

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    

Zwróć uwagę, że 2 aktywatory zostały utworzone:

gcloud eventarc triggers list --location=eu

14. Testuj wersje usługi

Po pomyślnym wdrożeniu usługi opublikujesz zdjęcie w Cloud Storage. Sprawdzisz, czy nasze usługi zostały wywołane, co zwraca interfejs Vision API i czy metadane są przechowywane w Firestore.

Wróć do Cloud Storage i kliknij zasobnik utworzony na początku modułu:

ff8a6567afc76235.png

Na stronie szczegółów zasobnika kliknij przycisk Upload files, aby przesłać zdjęcie.

Na przykład obraz GeekHour.jpeg jest udostępniany z bazą kodu w sekcji /services/image-analysis/java. Wybierz obraz i naciśnij Open button:

347b76e8b775f2f5.png

Możesz teraz sprawdzić wykonanie usługi, zaczynając od image-analysis-jvm, a następnie image-analysis-native.

Z „hamburgera” (<!--) przejdź do usługi Cloud Run > image-analysis-jvm.

Kliknij Logi i sprawdź dane wyjściowe:

810a8684414ceafa.png

Na liście logów widać, że usługa JIT(JVM) image-analysis-jvm została wywołana.

Logi wskazują początek i koniec wykonania usługi. Pomiędzy nimi widoczne są logi umieszczone w funkcji z instrukcją logu na poziomie INFO. Widzimy:

  • szczegóły zdarzenia aktywującego naszą funkcję,
  • Nieprzetworzone wyniki z wywołania interfejsu Vision API
  • Etykiety znalezione na przesłanym zdjęciu,
  • informacje o kolorach dominujących,
  • Whether the picture is safe to show,
  • Metadane obrazu zostały zapisane w Firestore.

Powtórz ten proces dla usługi image-analysis-native.

Z „hamburgera” (<!--) przejdź do usługi Cloud Run > image-analysis-native.

Kliknij Logi i sprawdź dane wyjściowe:

b80308c7d0f55a3.png

Teraz warto sprawdzić, czy metadane obrazu zostały zapisane w Fiorestore.

Ponownie od „hamburgera” (<!--) przejdź do sekcji Firestore. W podsekcji Data (wyświetlanej domyślnie) powinna być widoczna kolekcja pictures z nowym dokumentem odpowiadającym przesłanym właśnie zdjęciu:

933a20a9709cb006.png

15. Czyszczenie (opcjonalnie)

Jeśli nie chcesz przechodzić do innych modułów z tej serii, możesz zwolnić zasoby, aby zmniejszyć koszty i zachować dobre praktyki związane z chmurą. Zasoby możesz wyczyścić pojedynczo w ten sposób.

Usuń zasobnik:

gsutil rb gs://${BUCKET_PICTURES}

Usuń tę funkcję:

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

Usuń kolekcję Firestore, wybierając z niej Usuń kolekcję:

410b551c3264f70a.png

Możesz też usunąć cały projekt:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

16. Gratulacje!

Gratulacje! Udało Ci się wdrożyć pierwszą usługę kluczy w projekcie.

Omówione zagadnienia

  • Cloud Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • Natywne obrazy Java

Następne kroki