1. סקירה כללית
בשיעור ה-Lab הראשון של כתיבת קוד, תאחסנו תמונות בקטגוריה. ייווצר אירוע של יצירת קובץ, שיטופל על ידי שירות שנפרס ב-Cloud Run. השירות יקרא ל-Vision API כדי לבצע ניתוח תמונות ולשמור את התוצאות במאגר נתונים.
מה תלמדו
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
2. הגדרה ודרישות
הגדרת סביבה בקצב עצמאי
- נכנסים למסוף Google Cloud ויוצרים פרויקט חדש או עושים שימוש חוזר בפרויקט קיים. אם אין לכם עדיין חשבון Gmail או חשבון Google Workspace, עליכם ליצור חשבון.
- Project name הוא השם המוצג של המשתתפים בפרויקט. זו מחרוזת תווים שלא משמשת את Google APIs. אפשר לעדכן אותו בכל שלב.
- Project ID חייב להיות ייחודי בכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר מחרוזת ייחודית באופן אוטומטי; בדרך כלל לא מעניין אותך מה זה. ברוב ה-Codelabs תצטרכו להפנות אל מזהה הפרויקט (בדרך כלל הוא מזוהה כ-
PROJECT_ID
). אם המזהה שנוצר לא מוצא חן בעיניך, יש לך אפשרות ליצור מזהה אקראי אחר. לחלופין, אפשר לנסות תבנית משלך ולבדוק אם היא זמינה. לא ניתן לשנות אותו אחרי השלב הזה, והוא יישאר למשך הפרויקט. - לידיעתך, יש ערך שלישי – Project Number (מספר פרויקט), שחלק מממשקי ה-API משתמשים בו. מידע נוסף על כל שלושת הערכים האלה זמין במסמכי התיעוד.
- בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים או בממשקי API של Cloud. מעבר ב-Codelab הזה לא אמור לעלות הרבה, אם בכלל. כדי להשבית את המשאבים ולא לצבור חיובים מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את הפרויקט כולו. משתמשים חדשים ב-Google Cloud זכאים להצטרף לתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.
הפעלת Cloud Shell
אומנם אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-Codelab הזה משתמשים ב-Google Cloud Shell, סביבת שורת הפקודה שפועלת ב-Cloud.
במסוף Google Cloud, לוחצים על הסמל של Cloud Shell בסרגל הכלים שבפינה השמאלית העליונה:
נדרשים רק כמה דקות כדי להקצות את הסביבה ולהתחבר אליה. בסיום התהליך, אתם אמורים לראות משהו כזה:
למכונה הווירטואלית הזו נטען כל כלי הפיתוח הדרושים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. כל העבודה ב-Codelab הזה יכולה להתבצע בתוך דפדפן. אתה לא צריך להתקין שום דבר.
3. הפעלת ממשקי API
בשיעור ה-Lab הזה תשתמשו ב-Cloud Functions וב-Vision API, אבל קודם צריך להפעיל אותם ב-Cloud Console או באמצעות gcloud
.
כדי להפעיל את Vision API במסוף Cloud, מחפשים את Cloud Vision API
בסרגל החיפוש:
בשלב הזה מגיעים לדף Cloud Vision API:
לוחצים על הלחצן 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
.
מתן שם לקטגוריה
לוחצים על הלחצן CREATE BUCKET
.
לוחצים על CONTINUE
.
בחירת מיקום
יוצרים קטגוריה מרובת אזורים באזור הרצוי (כאן Europe
).
לוחצים על CONTINUE
.
בחירת סוג האחסון (storage class) שמוגדר כברירת מחדל
צריך לבחור את סוג האחסון (storage class) Standard
לנתונים.
לוחצים על CONTINUE
.
הגדרה של בקרת גישה
מאחר שעובדים עם תמונות הנגישות לציבור, רצוי שלכל התמונות שלנו המאוחסנות בקטגוריה הזו תהיה אותה בקרת גישה אחידה.
צריך לבחור באפשרות Uniform
של בקרת הגישה.
לוחצים על CONTINUE
.
הגדרת הגנה/הצפנה
אני רוצה להשאיר את ברירת המחדל (Google-managed key)
, כי לא ייעשה שימוש במפתחות ההצפנה שלכם).
לוחצים על CREATE
כדי לסיים את יצירת הקטגוריה.
הוספת כל המשתמשים כמציג של נפח אחסון
מעבר לכרטיסייה Permissions
:
מוסיפים לקטגוריה חבר allUsers
עם התפקיד Storage > Storage Object Viewer
:
לוחצים על 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
:
בודקים שניתן להעלות תמונות לקטגוריה ושהתמונות שהועלו יהיו זמינות לציבור, כפי שהוסבר בשלב הקודם.
6. בדיקת הגישה הציבורית לקטגוריה
בחזרה לדפדפן האחסון, הקטגוריה שלך תופיע ברשימה עם הכיתוב 'Public' (ציבורי) גישה (כולל סימן אזהרה שמזכיר שלכל אחד יש גישה לתוכן של הקטגוריה).
הקטגוריה שלכם מוכנה עכשיו לקבל תמונות.
בלחיצה על שם הקטגוריה יוצגו פרטי הקטגוריה.
שם תוכלו לנסות את הלחצן Upload files
, כדי לבדוק אם אפשר להוסיף תמונה לקטגוריה. חלון קופץ של הכלי לבחירת קבצים יבקש מכם לבחור קובץ. אחרי הבחירה, הקובץ יועלה לקטגוריה, ואז תוצג שוב הגישה מסוג public
ששויכו באופן אוטומטי לקובץ החדש.
לצד תווית הגישה Public
, יופיע גם סמל קישור קטן. כשתלחצו עליו, הדפדפן שלכם ינווט לכתובת ה-URL הציבורית של אותה תמונה, שתופיע בתבנית:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
כאשר BUCKET_NAME
הוא השם הייחודי הגלובלי שבחרתם לקטגוריה, ואז שם הקובץ של התמונה שלכם.
על ידי לחיצה על תיבת הסימון לצד שם התמונה, הלחצן DELETE
יופעל, ותהיה לך אפשרות למחוק את התמונה הראשונה.
7. הכנת מסד הנתונים
תוכלו לאחסן מידע על התמונה שסופקה על ידי Vision API במסד הנתונים של Cloud Firestore – מסד נתונים מהיר, מנוהל וללא שרת (serverless), שתומך בענן. כדי להכין את מסד הנתונים, עוברים לקטע Firestore
במסוף Cloud:
מוצעות שתי אפשרויות: Native mode
או Datastore mode
. להשתמש במצב המקורי, שמציע תכונות נוספות כמו תמיכה במצב אופליין וסנכרון בזמן אמת.
לוחצים על SELECT NATIVE MODE
.
בוחרים מספר אזורים (כאן באירופה, אבל עדיף לפחות אותו אזור שבו הפונקציה וקטגוריית האחסון שלכם מוגדרות.
לוחצים על הלחצן CREATE DATABASE
.
לאחר שמסד הנתונים נוצר, אתם אמורים לראות:
כדי ליצור אוסף חדש, לוחצים על הלחצן + START COLLECTION
.
שם האוסף pictures
.
לא צריך ליצור מסמך. תוכלו להוסיף אותן באופן פרוגרמטי כי תמונות חדשות יאוחסנו ב-Cloud Storage וינותחו באמצעות Vision API.
לוחצים על Save
.
Firestore יוצר מסמך ברירת מחדל ראשון באוסף החדש שנוצר. ניתן למחוק את המסמך בבטחה כי הוא לא מכיל מידע שימושי:
המסמכים שייווצרו באופן פרוגרמטי באוסף שלנו יכילו 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
, בעמודת הניווט שמימין, ולאחר מכן יצירת אינדקס מורכב כמו שמוצג כאן:
לוחצים על Create
. יצירת האינדקס עשויה להימשך מספר דקות.
8. שכפול הקוד
משכפלים את הקוד, אם עדיין לא עשיתם זאת ב-Code Lab הקודם:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
עכשיו אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את שיעור ה-Lab:
cd serverless-photosharing-workshop/services/image-analysis/java
תהיה לכם פריסת קבצים הבאה עבור השירות:
9. עיון בקוד השירות
כדי להתחיל, בודקים איך ספריות הלקוח של Java מופעלות ב-pom.xml
באמצעות BOM:
קודם כול, עורכים את הקובץ pom.xml
שבו מפורטים יחסי התלות של פונקציית Java שלנו. מעדכנים את הקוד כדי להוסיף את התלות של Maven ב-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>
הפונקציונליות מוטמעת במחלקה 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) {
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. יצירת תמונות של אפליקציות באמצעות GraalVM (אופציונלי)
בשלב האופציונלי הזה, תיצרו JIT(JVM) based app image
, ואז AOT(Native) Java app image
, באמצעות GraalVM.
כדי להריץ את ה-build, צריך לוודא שמותקנים ב-JDK ושהכלי ליצירת תמונות מקורי מותקן ומוגדר. יש כמה אפשרויות.
To start
, מורידים את GraalVM 22.2.x Community Edition ופועלים לפי ההוראות בדף התקנה של GraalVM.
אפשר לפשט את התהליך הזה בעזרת SDKMAN!
כדי להתקין את הפצת ה-JDK המתאימה עם SDKman
, יש להשתמש תחילה בפקודת ההתקנה:
sdk install java 22.2.r17-grl
צריך להנחות את SDKman להשתמש בגרסה הזו, גם לגרסאות build של JIT וגם של AOT:
sdk use java 22.2.0.r17-grl
מתקינים את native-image utility
של GraalVM:
gu install native-image
ב-Cloudshell
, לנוחותך, אפשר להתקין את GraalVM ואת כלי העזר לתמונה מותאמת באמצעות הפקודות הפשוטות הבאות:
# 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 ../..
קודם כול, מגדירים את משתני הסביבה של הפרויקט ב-GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
עכשיו אפשר לעבור לספרייה שמכילה את השירות כדי להתחיל לבנות את שיעור ה-Lab:
cd serverless-photosharing-workshop/services/image-analysis/java
יוצרים את קובץ האימג' של האפליקציה JIT(JVM):
./mvnw package -Pjvm
בודקים את יומן ה-build בטרמינל:
... [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] ------------------------------------------------------------------------
יוצרים את קובץ האימג' (Native) ב-AOT:
./mvnw package -Pnative -DskipTests
בודקים את יומן ה-build בטרמינל, כולל יומני ה-build של קובצי האימג' המקוריים:
שימו לב שה-build נמשך קצת יותר זמן, בהתאם למכונה שבה בודקים.
... [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. יצירה ופרסום של תמונות קונטיינרים
ניצור קובץ אימג' בקונטיינר בשתי גרסאות שונות: אחת בפורמט JIT(JVM) image
והשנייה בפורמט AOT(Native) Java image
.
קודם כול, מגדירים את משתני הסביבה של הפרויקט ב-GCP:
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
יוצרים את קובץ האימג' של JIT(JVM):
./mvnw package -Pjvm-image
בודקים את יומן ה-build בטרמינל:
[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] ------------------------------------------------------------------------
יוצרים את קובץ האימג' (Native) ב-AOT:
./mvnw package -Pnative-image
בודקים את יומן ה-build בטרמינל, כולל יומני build מקורי של תמונות ודחיסת תמונות באמצעות UPX.
חשוב לשים לב שה-build נמשך קצת יותר זמן, בהתאם למכונה שבה בודקים
... [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] ------------------------------------------------------------------------
בודקים שהתמונות נוצרו:
docker images | grep image-analysis
מתייגים את שתי התמונות ומעבירים אותן ל-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. פריסה ב-Cloud Run
הגיע הזמן לפרוס את השירות.
המערכת תפרס את השירות פעמיים, פעם אחת באמצעות תמונת JIT(JVM) ופעם השנייה באמצעות קובץ האימג' של 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(JVM) ובודקים את יומן הפריסה במסוף:
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
פורסים את קובץ האימג' AOT(Native) ומעיינים ביומן הפריסה במסוף:
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. הגדרת טריגרים של 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 לתמונות של שירות JVM(JIT) וגם AOT(Native):
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
חשוב לבדוק ששני הטריגרים נוצרו:
gcloud eventarc triggers list --location=eu
14. גרסאות של שירות בדיקה
אחרי שפריסות השירות יבוצעו בהצלחה, מפרסמים תמונה ב-Cloud Storage, בדקו אם השירותים שלנו הופעלו, מה מחזיר Vision API ואם המטא-נתונים מאוחסנים ב-Firestore.
חוזרים חזרה אל Cloud Storage
ולוחצים על הקטגוריה שיצרנו בתחילת שיעור ה-Lab:
בדף הפרטים של הקטגוריה, לוחצים על הלחצן Upload files
כדי להעלות תמונה.
לדוגמה, ב-Codebase שלכם יש תמונה של GeekHour.jpeg
במסגרת /services/image-analysis/java
. בוחרים תמונה ומקישים על Open button
:
עכשיו אפשר לבדוק את הפעלת השירות, החל מ-image-analysis-jvm
ולאחר מכן image-analysis-native
.
מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-jvm
.
לוחצים על Logs ומעיינים בפלט:
ואכן, ברשימת היומנים, אני רואה שהופעל שירות ה-JIT(JVM) image-analysis-jvm
.
היומנים מציינים את ההתחלה והסיום של הפעלת השירות. בין היתר, נוכל לראות את היומנים שהכנסנו לפונקציה שלנו עם הצהרות היומן ברמת INFO. אנחנו רואים:
- פרטי האירוע שהפעיל את הפונקציה שלנו,
- התוצאות הגולמיות מהקריאה ל-Vision API
- התוויות שנמצאו בתמונה שהעלינו,
- את המידע על הצבעים הדומיננטיים
- אם בטוח להציג את התמונה,
- בסופו של דבר, המטא-נתונים לגבי התמונה אוחסנו ב-Firestore.
חוזרים על התהליך בשביל השירות image-analysis-native
.
מתוך "המבורגר" (\t) בתפריט, צריך לעבור לשירות Cloud Run > image-analysis-native
.
לוחצים על Logs ומעיינים בפלט:
חשוב לבדוק עכשיו אם המטא-נתונים של התמונה אוחסנו ב-Fiorestore.
שוב מהמבורגר (\t) בתפריט, עוברים לקטע 'Firestore
'. בקטע המשנה Data
(מוצג כברירת מחדל), אמור להופיע האוסף pictures
עם מסמך חדש שנוסף לתמונה שהעלית:
15. הסרת המשאבים (אופציונלי)
אם אתם לא מתכוונים להמשיך עם שיעורי ה-Lab האחרים בסדרה, תוכלו לפנות משאבים כדי לחסוך בעלויות ולהיות אזרחי הענן באופן כללי. אפשר למחוק משאבים בנפרד באופן הבא.
מוחקים את הקטגוריה:
gsutil rb gs://${BUCKET_PICTURES}
מוחקים את הפונקציה:
gcloud functions delete picture-uploaded --region europe-west1 -q
מוחקים את האוסף של Firestore על ידי בחירה באפשרות 'מחיקת אוסף' מהאוסף:
לחלופין, אפשר למחוק את הפרויקט כולו:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
16. מעולה!
מעולה! הטמעת בהצלחה את השירות למפתחות הצפנה הראשון של הפרויקט!
אילו נושאים דיברנו?
- Cloud Storage
- Cloud Run
- Cloud Vision API
- Cloud Firestore
- תמונות Java מקוריות