פתרון בעיות בספריות לקוח של Cloud ל-Java

במסמך הזה מוסבר על רישום ביומנים ועל פתרון בעיות נפוצות בספריות לקוח של Cloud ל-Java.

רישום ביומן

יש שתי דרכים להפעיל רישום ביומן עבור ספריות הלקוח: באמצעות java.util.logging או באמצעות רישום ביומן של ניפוי באגים בספריית הלקוח עם SLF4J.

שימוש ב-java.util.logging

ספריות הלקוח של Cloud ל-Java משתמשות ב-Java logging API (java.util.logging) package. הגדרת רמת הרישום ביומן חושפת מידע שיעזור לכם לפתור בעיות, כולל:

  • התזמון של התקשורת הבסיסית בין הלקוח לשרת.
  • כותרות של הודעות בקשה ותגובה.
  • הודעות מפורטות בספריות הבסיסיות של קשרי התלות.

כדי להפעיל במהירות רישום מפורט ביומן של ספריות לקוח של Cloud ל-Java, יוצרים קובץ logging.properties עם התוכן הבא:

# run java program pointing to this properties file with the java arg
#   -Djava.util.logging.config.file=path/to/logging.properties
handlers=java.util.logging.ConsoleHandler
java.util.logging.SimpleFormatter.format=%1$tF %1$tT,%1$tL %4$-8s %3$-50s - %5$s %6$s%n

# --- ConsoleHandler ---
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
.level=INFO

# --- Specify logging level for certain packages ---
# com.google.api is for HTTP 1.1 layer
com.google.api.level=ALL
# io.grpc is for gRPC + Netty layer
io.grpc.level=FINE
# com.google.auth is for authentication
com.google.auth.level=FINE

# Example when you specify the storage library's level. This works when
# the target Cloud library uses the logging API.
com.google.cloud.storage.level=INFO

לאחר מכן, מריצים את האפליקציה עם -Djava.util.logging.config.file=path/to/logging.properties בתור VM argument, ולא בתור Program argument.

אם משתמשים ב-IntelliJ, מציינים את הארגומנט של המכונה הווירטואלית בRun/Debug Configuration (הגדרות הרצה/ניפוי באגים):

הגדרת הרצה/ניפוי באגים ב-IntelliJ שבה מצוין איפה צריך לציין את הארגומנטים של ה-VM

אם ה-JVM של התוכנית פועל עם ההגדרה בצורה תקינה, יומן הרישום ברמה FINE יופיע במסוף.

פלט לדוגמה:

2023-04-05 13:03:01,761 FINE     com.google.auth.oauth2.DefaultCredentialsProvider  - Attempting to load credentials from well known file: /usr/local/google/home/suztomo/.config/gcloud/application_default_credentials.json
2023-04-05 13:03:01,847 FINE     io.grpc.ManagedChannelRegistry                     - Unable to find OkHttpChannelProvider
java.lang.ClassNotFoundException: io.grpc.okhttp.OkHttpChannelProvider
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:315)
...

זוהי אחת הדרכים להגדיר את רמת הרישום ביומן. מידע נוסף על השימוש ב-Java Logging API זמין במאמר סקירה כללית על Java Logging.

רישום ביומן לניפוי באגים בספריית לקוח (SLF4J)

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

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

דרישות מוקדמות

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

ספריות לקוח ו-Libraries BOM לא כוללות תלות ב-slf4j-api. לפני שמפעילים את רישום הבאגים בספריית הלקוח, צריך להגדיר תלות ברישום, כולל SLF4J והטמעות והגדרות רישום תואמות.

NOTE: כדי להשתמש בתכונת רישום הבאגים בספריית הלקוח, צריך שיהיו לכם SLF4J וספקי רישום מתאימים, למשל logback,‏ log4j2 וכו', בנתיב המחלקות. אחרת, לא יתבצע רישום באגים גם אם תפעילו אותו באמצעות משתנה סביבה.

הפעלת רישום ביומן באמצעות משתנה סביבה

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

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

דוגמאות להגדרות של רישום ביומן

בתיעוד מובאת דוגמה לרישום ביומן של ניפוי באגים באמצעות Logback.

מוסיפים יחסי תלות של Logback לאפליקציה. כך יתווסף יחס התלות slf4j באופן טרנזיטיבי. המשתמשים צריכים לעיין בהגדרת Logback כדי לראות את הגרסאות העדכניות. אפשר לדלג על השלב הזה אם לאפליקציה שלכם כבר יש יחסי תלות ב-logback בנתיב המחלקות.

אם אתם משתמשים ב-Maven:

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>LATEST_VERSION</version>
</dependency>

כדי לראות יומנים ברמת DEBUG, צריך להוסיף את התוכן לקובץ ההגדרות של logback. אפשר לדלג על השלב הזה אם אתם צריכים רק יומנים ברמת INFO. מידע נוסף זמין במאמר בנושא הגדרת Logback.

<!-- set Client Library log level to DEBUG -->
<logger name="com.google.api"  level="DEBUG"/>
<!-- set Auth Library log level to DEBUG -->
<logger name="com.google.auth" level="DEBUG"/>

הפרוטוקול ALPN לא מוגדר כמו שצריך

אם מופיעים חריגים שקשורים ל-ALPN is not configured properly, כמו:

Caused by: java.lang.IllegalArgumentException: ALPN is not configured properly. See https://github.com/grpc/grpc-java/blob/master/SECURITY.md#troubleshooting for more information.

כדי לבדוק אם הסביבה שלכם תואמת ללקוחות מבוססי-gRPC, אפשר להשתמש בכלי לבדיקת תאימות.

אי-תאימות יכולה להיות אחת מהסיבות הבאות:

פתרון בעיות שקשורות ל-ClassNotFoundException, ל-NoSuchMethodError ול-NoClassDefFoundError

לרוב, השגיאות האלה נגרמות כשיש כמה גרסאות של אותה תלות בנתיב המחלקה, או גרסאות סותרות. סכסוכי התלות האלה מתרחשים לעיתים קרובות עם guava או protobuf-java.

יכול להיות שמספר מקורות יגרמו להתנגשויות בנתיב המחלקה:

  • יש כמה גרסאות של אותה תלות טרנזיטיבית בעץ התלות.
  • ב-classpath של זמן הריצה יש גרסאות שונות של יחסי תלות מאלה שציינתם ב-build.

לדוגמה, אם יש לכם תלות ישירה או טרנזיטיבית ב-Guava מגרסה 19.0, ו-google-cloud-java משתמש ב-Guava מגרסה 30.0, יכול להיות ש-google-cloud-java משתמש בשיטות של Guava שלא קיימות ב-Guava 19.0, וגורם ל-NoSuchMethodError.

באופן דומה, אם בנתיב המחלקות (classpath) יש גרסה ישנה יותר של protobuf-java, אבל google-cloud-java דורש גרסה חדשה יותר, יכול להיות שתופיע שגיאה NoClassDefFoundError שלא מאפשרת לאתחל מחלקות של google-cloud-java.

לדוגמה:

java.lang.NoClassDefFoundError: Could not initialize class com.google.pubsub.v1.PubsubMessage$AttributesDefaultEntryHolder

אימות ההתנגשות

בודקים את עץ התלות כדי לראות אם יש לכם כמה גרסאות של אותן תלויות:

$ mvn dependency:tree

מחפשים גרסאות של יחסי תלות שיכול להיות שמתנגשים, כמו guava או protobuf-java.

אם השגיאה מופיעה רק במהלך זמן הריצה, יכול להיות שסביבת זמן הריצה מוסיפה קובצי JAR שמתנגשים עם נתיב המחלקה של זמן הריצה. בדרך כלל, הבעיה מתרחשת אם בנתיב המחלקה יש גרסאות סותרות של קובצי JAR של Hadoop, ‏ Spark או תוכנות שרת אחרות שהאפליקציה פועלת עליהן, כמו netty, ‏ guava או protobuf-java.

זיהוי התנגשויות במהלך הבנייה

כדי לזהות שגיאות של קישור תלות בזמן ההידור, מוסיפים את הכלל Linkage Checker Enforcer בקובץ pom.xml:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <version>3.0.0-M3</version>
        <dependencies>
          <dependency>
            <groupId>com.google.cloud.tools</groupId>
            <artifactId>linkage-checker-enforcer-rules</artifactId>
            <version>1.5.7</version>
          </dependency>
        </dependencies>
        <executions>
          <execution>
            <id>enforce-linkage-checker</id>
            <!-- Important! Should run after compile -->
            <phase>verify</phase>
            <goals>
              <goal>enforce</goal>
            </goals>
            <configuration>
              <rules>
                <LinkageCheckerRule
                    implementation="com.google.cloud.tools.dependencies.enforcer.LinkageCheckerRule"/>
              </rules>
            </configuration>
          </execution>
        </executions>
      </plugin>

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

פתרון ההתנגשות

יש אסטרטגיות שונות לפתרון קונפליקטים, אבל חשוב להבין את הסיבה הבסיסית לקונפליקטים:

  • אם יש לכם שליטה בעץ התלות, משדרגים את יחסי התלות שיוצרים את הקונפליקט (לדוגמה, שדרוג של Guava). הגישה הזו היא הכי חזקה, אבל היא עשויה לדרוש מאמץ משמעותי וכמה גרסאות של הספרייה כדי להבטיח תאימות.
  • אם אין לכם אפשרות לשנות ולדחוף גרסאות חדשות של התלויות, אתם יכולים לייבא את com.google.cloud:libraries-bom:25.1.0 (או גרסה עדכנית יותר) כדי לבחור גרסאות תלויות עקביות. הגישה הזו מפשטת את ניהול התלות. לדוגמה, כך אפשר להסתמך על גרסאות עקביות של Guava ושל com.google.cloud:google-cloud-storage בלי להגדיר במפורש את הגרסה של כל אחת מהן:
  ...
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>25.1.0</version>
        <type>pom</type>
        <scope>import</scope>
       </dependency>
     </dependencies>
  </dependencyManagement>
  ...
  <dependencies>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
    </dependency>
    ...
  </dependencies>
  ...
  • בהערות הגרסה של libraries-bom מפורטות ספריות התלות התואמות. לדוגמה, בכתובת https://github.com/googleapis/java-cloud-bom/releases/tag/v26.31.0 מופיע:

    These client libraries are built with the following Java libraries:
    
    - Guava: 32.1.3-jre
    - Protobuf Java: 3.25.2
    - Google Auth Library: 1.22.0
    - Google API Client: 2.2.0
    - gRPC: 1.61.0
    - GAX: 2.41.0
    - Google Cloud Core: 2.31.0
    

    בדיקה של תרשים התלות של הפרויקט (mvn dependency:tree -Dverbose,‏ gradle dependencies או sbt dependencyTree) עשויה להראות שחלק מהתלויות הן בגרסאות לא צפויות, מה שעלול לגרום להתנגשויות בין תלויות.

  • אם שינוי גרסאות של יחסי תלות גורם לכשלים אחרים, כדאי להסתיר יחסי תלות שמתנגשים עם Google Cloud ספריות Java.

    לדוגמה, כדי להוסיף הצללה ל-guava ול-protobuf-java:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>...</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <keepDependenciesWithProvidedScope>false</keepDependenciesWithProvidedScope>
              <relocations>
                <!-- move protobuf to a shaded package -->
                <relocation>
                  <pattern>com.google.protobuf</pattern>
                  <shadedPattern>myapp.shaded.com.google.protobuf</shadedPattern>
                </relocation>
                <!-- move Guava to a shaded package -->
                <relocation>
                  <pattern>com.google.common</pattern>
                  <shadedPattern>myapp.shaded.com.google.common</shadedPattern>
                </relocation>
              </relocations>
            </configuration>
          </execution>
        </executions>
      </plugin>