在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内处理和响应。
我们真的不明白这种行为。我们只使用非常少的测试数据和几个小表。
答案 0 :(得分:3)
跟踪使我们可以更好地了解客户,希望它可以帮助您诊断延迟。
运行TracingSample,我从堆栈驱动程序获得。您可以使用不同的后端,也可以使用print it out as logs。
上面的示例还导出了http://localhost:8080/rpcz和http://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查询要快。