【问题标题】:Spring Boot GCP Data Spanner Latency IssuesSpring Boot GCP Data Spanner 延迟问题
【发布时间】:2018-06-14 15:56:56
【问题描述】:

在 Google Cloud Env 中使用 Spring Boot 和 Spanner。我们现在正在努力解决性能问题。 为了演示我设置了一个小型演示案例,以了解我们如何从 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 数据启动器 (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 秒左右。第二种方法只需要大约。 400 毫秒 为初始呼叫后的每个呼叫。 为了测试差异,我在一个 Spring Boot 项目中连接了两种解决方案,并将其与内存解决方案 (~100ms) 进行了比较。 所有给定的时间都指的是开发机器上的本地测试,但会返回到调查云环境中的性能问题。

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

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

  • DatabaseClient 应该管理 ConnectionPool,它本身被连接到 Singleton-Scoped Repository-Bean。所以会话应该被重用,对吗?
  • 为什么第一种方法比第二种方法花费的时间长得多。 Spring FW 本身只是使用 DatabaseClient 作为 SpannerOperations / SpannerTemplate 中的成员。
  • 我们一般如何才能减少延迟。每个 db 调用的简单响应超过 200 毫秒似乎是我们预期的四倍。 (我知道本地时间基准需要谨慎对待)

【问题讨论】:

  • gRPC 连接延迟如何测量?
  • 我正在运行一个 bash 脚本 > 在循环中抛出 curl 语句并测量从接收调用到发送响应的平均响应时间。

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


【解决方案1】:

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

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

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

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

【讨论】:

  • 感谢您为我指明了那个方向,之前没有注意到 opencensus 跟踪的高级跟踪功能,
【解决方案2】:

这里的问题与 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 包装在 try-with-resources 中。

【讨论】:

    【解决方案3】:

    如果在两种方法之间使 SQL 字符串相同,您能否确认性能不会改变? (* vs 单独拼出它们)。

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

    【讨论】:

    • 如果我只选择一列或所有列,它不会改变查询时间。同样使用一个键选择器。在这个非常小的测试设置中没有可测量的影响。
    猜你喜欢
    • 2020-07-05
    • 2022-07-06
    • 2021-02-01
    • 2018-03-01
    • 2023-03-03
    • 2015-05-17
    • 1970-01-01
    • 1970-01-01
    • 2017-09-14
    相关资源
    最近更新 更多