Spring Boot GCP数据扳手延迟问题

时间:2018-06-14 15:56:56

标签: java spring-boot google-cloud-platform google-cloud-spanner grpc-java

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

第一种方法

使用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;
  }


....

}

第二种方法

使用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连接&gt; 5秒,所有连续通话都在 1秒附近。第二种方法只需要约。初次通话后每次通话 400ms 。 为了测试差异,我在一个Spring Boot项目中连接了两个解决方案,并将其与内存解决方案( ~100ms )进行了比较。 所有给定的时间都是指dev机器上的本地测试,但是回到调查云环境中的性能问题。

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

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

  • DatabaseClient应该管理ConnectionPool,并且本身连接到Singleton-Scoped Repository-Bean。那么Sessions应该重复使用吗?
  • 为什么第一种方法比第二种方法花费的时间长得多。 Spring FW本身只是使用DatabaseClient作为SpannerOperations / SpannerTemplate中的成员。
  • 我们如何通常减少延迟。每个数据库调用的平均响应超过200毫秒似乎是我们预期的四倍。 (我知道需要谨慎对待当地的时间基准)

3 个答案:

答案 0 :(得分:3)

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

运行TracingSample,我从堆栈驱动程序获得stackdriver trace。您可以使用不同的后端,也可以使用print it out as logs

上面的示例还导出了http://localhost:8080/rpczhttp://localhost:8080/tracez,您可以在其中检查延迟和跟踪。

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

答案 1 :(得分: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;
}

您应该始终将ResultSet(以及所有其他AutoCloseable)放置在try-with-resources块中。

请注意,如果您完全消耗了Spanner返回的ResultSet,即您调用ResultSet#next()直到返回false,则ResultSet也将隐式关闭,并将会话返回到游泳池。但是,我建议不要仅依赖于此,而应始终将ResultSet包装在try-with-resources中。

答案 2 :(得分:0)

如果使两种方法之间的SQL字符串相同,是否可以确定性能不会改变? (*与单独拼写)。

此外,由于您期望第一种方法中只有一个客户,所以我推断客户ID是关键列吗?如果是这样,您可以使用SpannerRepository中的按键读取方法,这可能比SQL查询要快。