Closed takaaki7 closed 1 year ago
I forgot, this is FutureUtils.toCompletableFuture
code.
public class FutureUtils {
public static <T> CompletableFuture<T> toCompletableFuture(ApiFuture<T> apiFuture) {
CompletableFuture<T> completableFuture =
new CompletableFuture<T>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
boolean result = apiFuture.cancel(mayInterruptIfRunning);
super.cancel(mayInterruptIfRunning);
return result;
}
};
ApiFutureCallback<T> callback =
new ApiFutureCallback<T>() {
@Override
public void onFailure(Throwable throwable) {
completableFuture.completeExceptionally(throwable);
}
@Override
public void onSuccess(T t) {
completableFuture.complete(t);
}
};
ApiFutures.addCallback(apiFuture, callback, MoreExecutors.directExecutor());
return completableFuture;
}
}
Is this a bug? Or my benchmark code is wrong?
Looking into this.
You can provide your own ExecutorProvider to use with the async API using SpannerOptions.setAsyncExecutorProvider(executor)
.
The one you're using is the default that is automatically created, which has the following default settings:
Try passing in your own ExecutorProvider and benchmark again.
I think, generally non-blocking api throughput should not be limited by thread pool size. non-blocking api does exists for high concurrency with a few threads.
I'll be opening a Google Support Case through my company's (Hopper) account, but just for visibility we're running into the same issue using the async methods in Scala.
Here are 2 simple scripts that evaluate the time taken to execute 10k reads by primary key:
Sync:
package com.hopper.random
import com.google.cloud.spanner.DatabaseId
import com.google.cloud.spanner.Key
import com.google.cloud.spanner.SessionPoolOptions
import com.google.cloud.spanner.SpannerOptions
import com.google.cloud.spanner.Statement
import java.time.Instant
import scala.collection.compat.immutable.LazyList
import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.jdk.CollectionConverters._
import scala.util.Random
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
object SpannerScript extends App {
implicit val ec: ExecutionContext =
ExecutionContext.fromExecutor(
Executors.newFixedThreadPool(100)
)
val databaseId = DatabaseId.of(
"test-project",
"test-instance",
"test-table"
)
val options = SpannerOptions.newBuilder()
.setNumChannels(100)
.setAsyncExecutorProvider(
SpannerOptions.FixedCloseableExecutorProvider.create(
SpannerOptions.createAsyncExecutorProvider(100, 60L, TimeUnit.SECONDS).getExecutor
)
)
.setSessionPoolOption(
SessionPoolOptions.newBuilder()
.setMinSessions(10000)
.setMaxSessions(10000)
.build()
)
.build()
val client = options.getService().getDatabaseClient(databaseId)
val idSet = new Array[String](10000)
val rs = client.singleUse().executeQuery(
Statement.of("SELECT id FROM foo LIMIT 10000")
)
var i = 0
while (rs.next()) {
idSet.update(i, rs.getString("id"))
i += 1
}
rs.close()
def readRow(): Future[Unit] = Future {
client.singleUse().readRow(
"foo",
Key.newBuilder().append(idSet(Random.nextInt(10000))).build(),
List("id", "metadata", "state", "updated_at").asJava
)
}
val start = Instant.now.toEpochMilli()
Await.result(
Future.traverse(LazyList.continually(()).take(10000).toList)(_ => readRow()),
atMost = Duration.Inf
)
val total = Instant.now.toEpochMilli() - start
println(s"Finished 10000 queries in ${total} ms at rate ${10000.0 * 1000.0 / total} reads/second")
}
Sync output:
Finished 10000 queries in 5779 ms at rate 1730.4031839418585 reads/second
Async:
import com.google.cloud.spanner.DatabaseId
import com.google.cloud.spanner.Key
import com.google.cloud.spanner.SessionPoolOptions
import com.google.cloud.spanner.SpannerOptions
import com.google.cloud.spanner.Statement
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.time.Instant
import scala.collection.compat.immutable.LazyList
import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.jdk.CollectionConverters._
import scala.util.Random
object SpannerScript extends App {
implicit val ec: ExecutionContext =
ExecutionContext.fromExecutor(
Executors.newFixedThreadPool(100)
)
val databaseId = DatabaseId.of(
"test-project",
"test-instance",
"test-table"
)
val options = SpannerOptions.newBuilder()
.setNumChannels(100)
.setAsyncExecutorProvider(
SpannerOptions.FixedCloseableExecutorProvider.create(
SpannerOptions.createAsyncExecutorProvider(100, 60L, TimeUnit.SECONDS).getExecutor
)
)
.setSessionPoolOption(
SessionPoolOptions.newBuilder()
.setMinSessions(10000)
.setMaxSessions(10000)
.build()
)
.build()
val client = options.getService().getDatabaseClient(databaseId)
val idSet = new Array[String](10000)
val rs = client.singleUse().executeQuery(
Statement.of("SELECT id FROM foo LIMIT 10000")
)
var i = 0
while (rs.next()) {
idSet.update(i, rs.getString("id"))
i += 1
}
rs.close()
def readRow(): Future[Unit] = {
val apiFuture = client.singleUse().readRowAsync(
"foo",
Key.newBuilder().append(idSet(Random.nextInt(10000))).build(),
List("id", "metadata", "state", "updated_at").asJava
)
Future {
apiFuture.get()
}
}
val start = Instant.now.toEpochMilli()
Await.result(
Future.traverse(LazyList.continually(()).take(10000).toList)(_ => readRow()),
atMost = Duration.Inf
)
val total = Instant.now.toEpochMilli() - start
println(s"Finished 10000 queries in ${total} ms at rate ${10000.0 * 1000.0 / total} reads/second")
}
Outputs:
Finished 10000 queries in 59673 ms at rate 167.579977544283 reads/second
I think, generally non-blocking api throughput should not be limited by thread pool size. non-blocking api does exists for high concurrency with a few threads.
This is how Java Spanner concurrency has been implemented currently (limited to 8 threads by default). Feel free to use your custom implementation for any custom handlings.
On closer examination, #2698 is not related to this ticket. I've confirmed that the default AsyncExecutorProvider
has poor performance compared to the synchronous API, but by providing a larger thread pool, it is possible to achieve better performance than the synchronous API.
Environment details
Specify the API at the beginning of the title. For example, "BigQuery: ..."). General, Core, and Other are also allowed as types Spanner:readRows
OS type and version: osx 11.2.3 16cpu
Java version: openjdk 17.0.1 2021-10-19
version(s): 6.20.0
Overview
Nonblocking api like
readAsync()
,runAsync()
seems has low throughput. Using blocking read api with my own threads is much faster than nonblocking api.Especially,
readAsync().toListAsync()
concurrency seems to limitted to 8. The max value ofnum_sessions_in_pool(num_in_use_sessions)
is only 8. (num_sessions_in_pool(num_read_sessions)
is about 400).What is the best way to use nonblocking methods with good performance?
Benchmark code
result