תמונה יומית: שיעור Lab 1 – אחסון וניתוח של תמונות (Native Java)

1. סקירה כללית

בשיעור הראשון של ה-Lab הזה, תאחסנו תמונות בדלי. כך ייצור אירוע של יצירת קובץ שיטופל על ידי שירות שנפרס ב-Cloud Run. השירות יבצע קריאה ל-Vision API כדי לבצע ניתוח תמונות ולשמור את התוצאות במאגר נתונים.

c0650ee4a76db35e.png

מה תלמדו

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

‫2. הגדרה ודרישות

הגדרת סביבה בקצב אישי

  1. נכנסים ל-מסוף Google Cloud ויוצרים פרויקט חדש או משתמשים בפרויקט קיים. אם עדיין אין לכם חשבון Gmail או Google Workspace, אתם צריכים ליצור חשבון.

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • שם הפרויקט הוא השם המוצג של הפרויקט הזה למשתתפים. זו מחרוזת תווים שלא נמצאת בשימוש ב-Google APIs. תמיד אפשר לעדכן את המיקום.
  • מזהה הפרויקט הוא ייחודי לכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו אחרי שהוא מוגדר. מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית, ובדרך כלל לא צריך לדעת מה היא. ברוב ה-Codelabs, תצטרכו להפנות למזהה הפרויקט (בדרך כלל מסומן כ-PROJECT_ID). אם אתם לא אוהבים את המזהה שנוצר, אתם יכולים ליצור מזהה אקראי אחר. אפשר גם לנסות שם משתמש משלכם ולבדוק אם הוא זמין. אי אפשר לשנות את ההגדרה הזו אחרי השלב הזה, והיא תישאר לאורך הפרויקט.
  • לידיעתכם, יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. במאמרי העזרה מפורט מידע נוסף על שלושת הערכים האלה.
  1. בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API של Cloud. השלמת ה-codelab הזה לא תעלה לכם הרבה, אם בכלל. כדי להשבית את המשאבים ולמנוע חיובים נוספים אחרי שתסיימו את המדריך הזה, תוכלו למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים לתוכנית תקופת ניסיון בחינם בשווי 300$.

מפעילים את Cloud Shell

אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-codelab הזה תשתמשו ב-Google Cloud Shell, סביבת שורת פקודה שפועלת בענן.

ב-מסוף Google Cloud, לוחצים על סמל Cloud Shell בסרגל הכלים שבפינה הימנית העליונה:

84688aa223b1c3a2.png

יחלפו כמה רגעים עד שההקצאה והחיבור לסביבת העבודה יושלמו. בסיום התהליך, אמור להופיע משהו כזה:

320e18fedb7fbe0.png

המכונה הווירטואלית הזו כוללת את כל הכלים שדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. אפשר לבצע את כל העבודה ב-codelab הזה בדפדפן. לא צריך להתקין שום דבר.

‫3. הפעלת ממשקי ה-API

בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם במסוף Cloud או באמצעות 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

Name your bucket (שם הקטגוריה)

לוחצים על הלחצן CREATE BUCKET.

8951851554a430d2.png

לוחצים על CONTINUE.

בחירת מיקום

24b24625157ab467.png

יוצרים קטגוריה במספר אזורים באזור הרצוי (בדוגמה הזו Europe).

לוחצים על CONTINUE.

בחירת סוג האחסון (storage class) שמוגדר כברירת מחדל

9e7bd365fa94a2e0.png

בוחרים את Standard סוג האחסון (storage class) של הנתונים.

לוחצים על 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, שהוא מסד נתונים מהיר, מנוהל, מבוסס-ענן, בלי שרת (serverless) ולא יחסי (NoSQL). כדי להכין את מסד הנתונים, עוברים לקטע Firestore ב-Cloud Console:

e57a673537b5deca.png

יש שתי אפשרויות: Native mode או Datastore mode. משתמשים במצב המקורי, שמציע תכונות נוספות כמו תמיכה באופליין וסנכרון בזמן אמת.

לוחצים על SELECT NATIVE MODE.

1a2e363fae5c7e96.png

בוחרים אזור רב-אזורי (כאן באירופה, אבל עדיף לפחות באותו אזור שבו נמצאים הפונקציה ודלי האחסון).

לוחצים על הלחצן CREATE DATABASE.

אחרי שיוצרים את מסד הנתונים, אמור להופיע המסך הבא:

7dcc82751ed483fb.png

כדי ליצור אוסף חדש, לוחצים על הלחצן + START COLLECTION.

אוסף שנקרא pictures.

dce3d73884ac8c83.png

לא צריך ליצור מסמך. התמונות יתווספו באופן אוטומטי כשתמונות חדשות יאוחסנו ב-Cloud Storage וינותחו על ידי Vision API.

לוחצים על Save.

‫Firestore יוצר מסמך ברירת מחדל ראשון באוסף החדש שנוצר. אפשר למחוק את המסמך הזה בבטחה כי הוא לא מכיל מידע שימושי:

63e95c844b3f79d3.png

המסמכים שייווצרו באופן אוטומטי באוסף שלנו יכללו 4 שדות:

  • name (מחרוזת): שם הקובץ של התמונה שהועלתה, שהוא גם המפתח של המסמך
  • labels (מערך של מחרוזות): התוויות של פריטים שזוהו על ידי Vision API
  • color (מחרוזת): קוד הצבע ההקסדצימלי של הצבע הדומיננטי (למשל #ab12ef)
  • created (תאריך): חותמת הזמן של מועד שמירת המטא-נתונים של התמונה
  • thumbnail (בוליאני): שדה אופציונלי שיופיע ויקבל את הערך true אם נוצרה תמונה ממוזערת לתמונה הזו

כדי שנוכל לחפש ב-Firestore תמונות שיש להן תמונות ממוזערות ולמיין אותן לפי תאריך היצירה, נצטרך ליצור אינדקס חיפוש.

אפשר ליצור את האינדקס באמצעות הפקודה הבאה ב-Cloud Shell:

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

אפשר גם לעשות את זה מ-Cloud Console. לשם כך, לוחצים על Indexes בעמודת הניווט בצד ימין, ואז יוצרים אינדקס מורכב כמו שמוצג בהמשך:

2236d3a024a59232.png

לוחצים על Create. יצירת האינדקס יכולה להימשך כמה דקות.

8. שיבוט הקוד

משכפלים את הקוד, אם עדיין לא עשיתם את זה בסדנת הקוד הקודמת:

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

אחר כך אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את ה-Lab:

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

הפריסה של הקובץ בשירות תהיה כזו:

4c2a18a2c8b69dc5.png

9. בדיקת קוד השירות

מתחילים בבדיקה איך ספריות הלקוח של Java מופעלות ב-pom.xml באמצעות 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:

  • זיהוי תוויות: כדי להבין מה יש בתמונות
  • מאפייני התמונה: כדי לתת מאפיינים מעניינים של התמונה (אנחנו מתעניינים בצבע הדומיננטי של התמונה)
  • חיפוש בטוח: כדי לדעת אם התמונה בטוחה להצגה (אסור שהיא תכיל תוכן למבוגרים בלבד, תוכן רפואי, תוכן נועז או תוכן אלים)

בשלב הזה אפשר לבצע את הקריאה ל-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.

כדי להריץ את ה-build, צריך לוודא שיש לכם JDK מתאים ושהתקנתם והגדרתם את כלי ה-build של native-image. יש כמה אפשרויות:

To start, מורידים את GraalVM 22.3.x Community Edition ופועלים לפי ההוראות בדף GraalVM installation.

אפשר לפשט מאוד את התהליך הזה בעזרת 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)

אחר כך אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את ה-Lab:

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

יוצרים את קובץ האימג' של אפליקציית JIT:

./mvnw package

בודקים את יומן ה-build בטרמינל:

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

מעיינים ביומן ה-build במסוף, כולל יומני ה-build של התמונה המקורית:

שימו לב: תהליך ה-build יימשך זמן רב יותר, בהתאם למכונה שבה אתם מבצעים את הבדיקה.

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

בודקים את יומן ה-build בטרמינל:

[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(Native):

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

בודקים את יומן ה-build בטרמינל, כולל יומני ה-build של התמונה המקורית.

הערה:

  • הגרסה תיצור build קצת יותר זמן, בהתאם למכונה שעליה אתם בודקים
  • אפשר לדחוס את התמונות עוד יותר באמצעות 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

מבצעים פריסה של תמונת 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

נותנים את ההרשאה 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.

לוחצים על Logs (יומנים) ומתבוננים בפלט:

ae1a4a94c7c7a166.png

ואכן, ברשימת היומנים אפשר לראות שהופעל שירות ה-JIT‏ image-analysis-jit.

היומנים מציינים את ההתחלה והסיום של הפעלת השירות. ובאמצע, אפשר לראות את היומנים שהכנסנו לפונקציה עם הצהרות היומן ברמת INFO. הנתונים שמוצגים הם:

  • פרטי האירוע שמפעיל את הפונקציה,
  • התוצאות הגולמיות מקריאה ל-Vision API,
  • התוויות שנמצאו בתמונה שהעלינו,
  • מידע על הצבעים הדומיננטיים,
  • אם התמונה בטוחה לצפייה,
  • בסופו של דבר, המטא-נתונים האלה לגבי התמונה מאוחסנים ב-Firestore.

תחזרו על התהליך עבור שירות image-analysis-native.

בתפריט ההמבורגר (☰), עוברים לשירות Cloud Run > image-analysis-native.

לוחצים על Logs (יומנים) ומתבוננים בפלט:

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 Storage
  • Cloud Run
  • Cloud Vision API
  • Cloud Firestore
  • תמונות ב-Java Native

השלבים הבאים