תמונה יומית: שיעור 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

  • Project name הוא השם המוצג של המשתתפים בפרויקט. זו מחרוזת תווים שלא משמשת את Google APIs. תמיד אפשר לעדכן.
  • Project ID הוא ייחודי בכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר מחרוזת ייחודית באופן אוטומטי; בדרך כלל לא מעניין אותך מה זה. ברוב ה-codelabs תצטרכו להפנות למזהה הפרויקט שלכם (בדרך כלל מזוהה כ-PROJECT_ID). אם המזהה שנוצר לא מוצא חן בעיניכם, אתם יכולים ליצור מזהה אקראי אחר. לחלופין, אפשר לנסות שם משלך ולראות אם הוא זמין. לא ניתן לשנות אותו אחרי השלב הזה, והוא נשאר למשך הפרויקט.
  • לידיעתך, יש ערך שלישי, Project Number, שבו משתמשים בחלק מממשקי ה-API. מידע נוסף על כל שלושת הערכים האלה זמין במסמכי התיעוד.
  1. בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים או בממשקי API של Cloud. מעבר ב-Codelab הזה לא יעלה הרבה כסף, אם בכלל. כדי להשבית משאבים ולא לצבור חיובים מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים להשתתף בתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.

הפעלת Cloud Shell

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

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

84688aa223b1c3a2.png

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

320e18ifiedb7fbe0.png

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

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

בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם ב-Cloud Console או באמצעות gcloud.

כדי להפעיל את Vision API במסוף Cloud, מחפשים את Cloud Vision API בסרגל החיפוש:

8f3522d790bb026c.png

בשלב הזה מגיעים לדף Cloud Vision API:

d785572fa14c87c2.png

לוחצים על הלחצן ENABLE.

לחלופין, אפשר גם להפעיל אותו באמצעות Cloud Shell באמצעות כלי שורת הפקודה של Google Cloud.

בתוך 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 או מסביבת הפיתוח המקומית.

מתוך "המבורגר" (\t) בתפריט, צריך לעבור לדף Storage.

d08ecb0ae29330a1.png

מתן שם לקטגוריה

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

8951851554a430d2.png

לוחצים על CONTINUE.

בחירת מיקום

24b24625157ab467.png

יוצרים קטגוריה מרובת אזורים באזור הרצוי (כאן Europe).

לוחצים על CONTINUE.

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

9e7bd365fa94a2e0.png

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

לוחצים על CONTINUE.

הגדרה של בקרת גישה

1ff4a1f6e57045f5.png

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

צריך לבחור באפשרות Uniform של בקרת הגישה.

לוחצים על CONTINUE.

הגדרת הגנה/הצפנה

2d469b076029d365.png

אני רוצה להשאיר את ברירת המחדל (Google-managed key), כי לא ייעשה שימוש במפתחות ההצפנה שלכם).

לוחצים על CREATE כדי לסיים את יצירת הקטגוריה.

הוספת כל המשתמשים כמציג של נפח אחסון

מעבר לכרטיסייה Permissions:

19564b3ad8688ae8.png

מוסיפים לקטגוריה חבר allUsers עם התפקיד Storage > Storage Object Viewer:

d655e760c76d62c1.png

לוחצים על SAVE.

5. יצירת הקטגוריה (gsutil)

אפשר גם להשתמש בכלי שורת הפקודה gsutil ב-Cloud Shell כדי ליצור קטגוריות.

ב-Cloud Shell, מגדירים משתנה לשם הייחודי של הקטגוריה. השדה GOOGLE_CLOUD_PROJECT כבר מוגדר ב-Cloud Shell למזהה הפרויקט הייחודי שלכם. אפשר להוסיף אותו לשם הקטגוריה.

לדוגמה:

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. בדיקת הגישה הציבורית לקטגוריה

בחזרה לדפדפן האחסון, הקטגוריה שלך תופיע ברשימה עם הכיתוב 'Public' (ציבורי) גישה (כולל סימן אזהרה שמזכיר שלכל אחד יש גישה לתוכן של הקטגוריה).

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

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)
  • נוצר (תאריך): חותמת הזמן של התקופה שבה אוחסנו המטא-נתונים של התמונה
  • thumbnail (בוליאני): שדה אופציונלי שיוצג אם נוצרה תמונה ממוזערת לתמונה הזו

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

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

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

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

2236d3a024a59232.png

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

8. שכפול הקוד

משכפלים את הקוד, אם עדיין לא עשיתם זאת ב-Code Lab הקודם:

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 (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 ושהכלי ליצירת תמונות מקורי מותקן ומוגדר. יש כמה אפשרויות.

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

אפשר לפשט את התהליך הזה בעזרת SDKMAN!

כדי להתקין את הפצת ה-JDK המתאימה עם SDKman, יש להשתמש תחילה בפקודת ההתקנה:

sdk install java 17.0.8-graal

צריך להנחות את SDKman להשתמש בגרסה הזו, גם לגרסאות build של JIT וגם של AOT:

sdk use java 17.0.8-graal

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

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

ls -lart

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

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

echo JAVA HOME: $JAVA_HOME
echo PATH: $PATH

cd ../..

# validate the version with
java -version 

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

קודם כול, מגדירים את משתני הסביבה של הפרויקט ב-GCP:

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

עכשיו אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את שיעור ה-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] ------------------------------------------------------------------------

יוצרים את קובץ האימג' המקורי(באמצעות 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] ------------------------------------------------------------------------

יוצרים את קובץ האימג' (Native) ב-AOT:

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

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

הערה:

  • שה-build נמשך קצת יותר זמן, בהתאם למכונה שבודקים
  • ניתן לדחוס את התמונות עוד יותר באמצעות UPX, אבל יש להן השפעה שלילית קטנה על ביצועי ההפעלה, לכן ה-build הזה לא משתמש ב-UPX. תמיד מדובר בהבדלים קטנים
...
[INFO]     [creator]     Saving docker.io/library/image-analysis-maven-native:latest...
[INFO]     [creator]     *** Images (13167702674e):
[INFO]     [creator]           docker.io/library/image-analysis-maven-native:latest
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/bellsoft-liberica:native-image-svm'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/native-image:native-image'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/image-analysis-maven-native:latest'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:37 min
[INFO] Finished at: 2023-10-10T20:05:16Z
[INFO] ------------------------------------------------------------------------

בודקים שהתמונות נוצרו:

docker images | grep image-analysis

מתייגים את שתי התמונות ומעבירים אותן ל-GCR:

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

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

12. פריסה ב-Cloud Run

הגיע הזמן לפרוס את השירות.

את השירות תפרסו פעמיים, פעם אחת באמצעות תמונת JIT ופעם שנייה באמצעות קובץ אימג' של AOT(Native). שתי פריסות השירות יעבדו במקביל את אותה התמונה מהקטגוריה, למטרות השוואה.

קודם כול, מגדירים את משתני הסביבה של הפרויקט ב-GCP:

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

פורסים את קובץ האימג' של ה-JIT ומעיינים ביומן הפריסה במסוף:

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

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

פורסים את קובץ האימג' המקורי ומעיינים ביומן הפריסה במסוף:

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

13. הגדרת טריגרים של Eventarc

חברת Eventarc מציעה פתרון סטנדרטי לניהול זרימת השינויים במצב (State) שנקרא 'אירועים', בין מיקרו-שירותים (microservices) מופרדים. כשהאפשרות מופעלת, Eventarc מנתב את האירועים האלה דרך מינויי Pub/Sub ליעדים שונים (במסמך הזה מידע נוסף על יעדי אירועים) ומנהלת בשבילכם את המסירה, האבטחה, ההרשאות, הניראות (observability) והטיפול בשגיאות.

אתם יכולים ליצור טריגר של 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 ולוחצים על הקטגוריה שיצרנו בתחילת שיעור ה-Lab:

33442485a1d76921.png

בדף הפרטים של הקטגוריה, לוחצים על הלחצן Upload files כדי להעלות תמונה.

לדוגמה, ב-Codebase שלכם יש תמונה של GeekHour.jpeg במסגרת /services/image-analysis/java. בוחרים תמונה ומקישים על Open button:

d57529452f62bd32.png

עכשיו אפשר לבדוק את הפעלת השירות, החל מ-image-analysis-jit ולאחר מכן image-analysis-native.

מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-jit.

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

ae1a4a94c7c7a166.png

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

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

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

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

מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-native.

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

4afe22833c1fd14c.png

חשוב לבדוק עכשיו אם המטא-נתונים של התמונה אוחסנו ב-Fiorestore.

שוב מהמבורגר (\t) בתפריט, עוברים לקטע 'Firestore'. בקטע המשנה Data (מוצג כברירת מחדל), אמור להופיע האוסף pictures עם מסמך חדש שנוסף לתמונה שהעלית:

82d6c468956e7cfc.png

15. הסרת המשאבים (אופציונלי)

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

מוחקים את הקטגוריה:

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 מקוריות

השלבים הבאים