排查 Java 版 Cloud 客户端库的问题

本文档概述了 Java 版 Cloud 客户端库的日志记录和常见问题排查。

日志记录

您可以通过两种方式为客户端库启用日志记录:使用 java.util.logging 或使用 SLF4J 进行客户端库调试日志记录。

使用 java.util.logging

Java 版 Cloud 客户端库使用 Java 日志记录 API (java.util.logging) 软件包。配置日志记录级别可显示有助于您排查问题的信息,包括:

  • 底层客户端-服务器通信的时序。
  • 请求和响应消息标头。
  • 底层依赖库中的详细消息。

如需快速为 Java 版 Cloud 客户端库启用详细日志记录,请创建包含以下内容的 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 运行/调试配置,显示了指定虚拟机实参的位置

如果程序的 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 接口进行调试日志记录。

客户端库和库 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 的客户端兼容。

不兼容可能意味着以下某种情况:

ClassNotFoundExceptionNoSuchMethodErrorNoClassDefFoundError 问题排查

这些错误通常是由类路径中存在同一依赖项的多个版本或冲突版本引起的。这些依赖项冲突通常发生在 guavaprotobuf-java 中。

多个来源可能会导致类路径冲突:

  • 依赖树中同一传递依赖项的多个版本。
  • 您的运行时类路径中的依赖项版本与您在 build 中指定的版本不同。

例如,如果您对 Guava 19.0 版有直接或传递依赖项,而 google-cloud-java 使用的是 Guava 30.0 版,那么 google-cloud-java 可能会使用 Guava 19.0 版中不存在的 Guava 方法,从而导致 NoSuchMethodError

同样,如果您的类路径包含旧版 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

查找可能存在冲突的依赖项的版本,例如 guavaprotobuf-java

如果您仅在运行时遇到此错误,则可能是您的运行时环境将冲突的 JAR 引入了运行时类路径。一种典型情况是,您的应用所运行的 Hadoop、Spark 或其他服务器软件在类路径中具有冲突的版本 nettyguavaprotobuf-java JAR。

在 build 期间检测冲突

如需在编译时检测依赖项关联错误,请在 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 -Dverbosegradle dependenciessbt dependencyTree),您可能会发现某些依赖项的版本出乎意料,这可能会导致依赖项冲突。

  • 如果更改依赖项版本会导致其他故障,请考虑对与 Google Cloud Java 库冲突的依赖项进行着色

    例如,为 guavaprotobuf-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>