Spring Boot GCP数据扳手延迟问题

问题描述 投票:1回答:3

在Google Cloud Env中使用Spring Boot with Spanner。我们现在正在努力解决性能问题。为了演示我设置了一个小型演示案例,我们介绍了如何从扳手中检索数据的不同方法。

The first approach

使用来自Google的“原生”驱动程序来实例化dbClient并检索这样的数据。

@Repository
public class SpannerNativeDAO implements CustomerDAO {

  private final DatabaseClient dbClient;
  private final String SQL = "select * from customer where customer_id = ";

  public SpannerNativeDAO(
      @Value("${spring.cloud.gcp.spanner.instanceId}") String instanceId,
      @Value("${spring.cloud.gcp.spanner.database}") String dbId,
      @Value("${spring.cloud.gcp.spanner.project-id}") String projectId,
      @Value("${google.application.credentials}") String pathToCredentials)
      throws IOException {
    try (FileInputStream google_application_credentials = new FileInputStream(pathToCredentials)) {
      final SpannerOptions spannerOptions =
          SpannerOptions.newBuilder().setProjectId(projectId)
              .setCredentials(ServiceAccountCredentials.fromStream(google_application_credentials)).build();
      final Spanner spanner = spannerOptions.getService();
      final DatabaseId databaseId1 = DatabaseId.of(projectId, instanceId, dbId);
      dbClient = spanner.getDatabaseClient(databaseId1);
      // give it a first shot to speed up consecutive calls
      dbClient.singleUse().executeQuery(Statement.of("select 1 from customer"));
    }
  }

  private Customer readCustomerFromSpanner(Long customerId) {
    try {
      Statement statement = Statement.of(SQL + customerId);
      ResultSet resultSet = dbClient.singleUse().executeQuery(statement);
      while (resultSet.next()) {
        return Customer.builder()
            .customerId(resultSet.getLong("customer_id"))
            .customerStatus(CustomerStatus.valueOf(resultSet.getString("status")))
            .updateTimestamp(Timestamp.from(Instant.now())).build();
      }
    } catch (Exception ex) {
      //log
    }
    return null;
  }


....

}

The second approach

使用Spring Boot Data Starter(https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-starters/spring-cloud-gcp-starter-data-spanner

简直就是这样

@Repository
public interface SpannerCustomerRepository extends SpannerRepository<Customer, Long> {

  @Query("SELECT customer.customer_id, customer.status, customer.status_info, customer.update_timestamp "
      + "FROM customer customer WHERE customer.customer_id = @arg1")
  List<Customer> findByCustomerId(@Param("arg1") Long customerId);
}

现在,如果我采用第一种方法,建立与Spanner的初始gRPC连接需要> 5秒,所有连续呼叫大约需要1秒。第二种方法只需要约。初始呼叫后每次呼叫400ms。为了测试差异,我在一个Spring Boot项目中连接了两个解决方案,并将其与内存解决方案(~100ms)进行了比较。所有给定的时间都是指开发机器上的本地测试,但是回到调查云环境中的性能问题。

我测试了几个不同的SpannerOptions(SessionOptions)没有结果,并在项目上运行了一个分析器。我似乎96%的响应时间来自建立一个gRPC通道到扳手,而数据库本身在5ms内处理和响应。

我们真的不明白这种行为。我们只使用非常少的测试数据和几个小表。

  • DatabaseClient应该管理ConnectionPool,并且本身连接到Singleton-Scoped Repository-Bean。所以Sessions应该被重用,严格?
  • 为什么第一种方法比第二种方法花费的时间长得多。 Spring FW本身只是使用DatabaseClient作为SpannerOperations / SpannerTemplate中的成员。
  • 我们怎样才能减少延迟。每个数据库调用的平均响应超过200毫秒似乎是我们预期的四倍。 (我知道需要小心处理当地时间基准)
java spring-boot google-cloud-platform google-cloud-spanner grpc-java
3个回答
3
投票

跟踪使我们能够很好地了解客户,希望它可以帮助您诊断延迟。

运行TracingSample,我从stackdriver获得stackdriver trace。您可以使用不同的后端,或print it out as logs

上面的示例还会导出http://localhost:8080/rpczhttp://localhost:8080/tracez,您可以查看它们以检查延迟和痕迹。

有关设置的教程:Cloud Spanner, instrumented by OpenCensus and exported to Stackdriver


1
投票

这里的问题与Spring或DAO无关,但是你没有关闭查询返回的ResultSet。这会导致Spanner库认为用于执行查询的会话仍在使用中,并且每次执行查询时都会导致库创建新会话。客户端库会为您完成此会话创建,处理和池化,但它确实需要您在不再使用资源时关闭它们。

我用非常简单的例子对此进行了测试,并且我可以通过不关闭ResultSet来重现与您所看到的完全相同的行为。

请考虑以下示例:

/**
 * This method will execute the query quickly, as the ResultSet
 * is closed automatically by the try-with-resources block.
 */
private Long executeQueryFast() {
  Statement statement = Statement.of("SELECT * FROM T WHERE ID=1");
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      return resultSet.getLong("ID");
    }
  } catch (Exception ex) {
    // log
  }
  return null;
}

/**
 * This method will execute the query slowly, as the ResultSet is
 * not closed and the Spanner library thinks that the session is
 * still in use. Executing this method repeatedly will cause
 * the library to create a new session for each method call.
 * Closing the ResultSet will cause the session that was used
 * to be returned to the session pool, and the sessions will be
 * re-used.
 */
private Long executeQuerySlow() {
  Statement statement = Statement.of("SELECT * FROM T WHERE ID=1");
  try {
    ResultSet resultSet = dbClient.singleUse().executeQuery(statement);
    while (resultSet.next()) {
      return resultSet.getLong("ID");
    }
  } catch (Exception ex) {
    // log
  }
  return null;
}

您应该始终将ResultSets(和所有其他AutoCloseables)放在try-with-resources块中。

请注意,如果您使用完全由Spanner返回的ResultSet,即您调用ResultSet#next()直到它返回false,则ResultSet也会被隐式关闭,会话将返回到池中。然而,我建议不要仅仅依靠它,而是始终将ResultSet包装在资源试用中。


0
投票

如果两个方法之间的SQL字符串相同,您能否确认性能不会改变? (* vs单独拼写出来)。

此外,由于您在第一种方法中期望单个客户,我推断客户ID是关键列?如果是这样,您可以使用SpannerRepository中的read-by-key方法,这可能比SQL查询更快。

© www.soinside.com 2019 - 2024. All rights reserved.