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

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

    לדוגמה, כדי להוסיף הצללה ל-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>