分析 Ratpack:ExecControllerBindingThreadFactory 高 CPU 使用率和大量线程
Profiling Ratpack: ExecControllerBindingThreadFactory high CPU usage and lots of threads
我们有一个用 Ratpack 1.5.1 编写的移动应用程序 API 服务器即将上线,我们目前正在分析该应用程序以发现任何性能瓶颈。该应用程序由 SQL 数据库支持,我们始终小心 运行 使用 Blocking
class 进行查询。代码是用 Kotlin 编写的,我们编写了一些协程粘合代码来强制在 Ratpack 的阻塞线程上执行阻塞操作。
由于 Ratpack 的线程模型是独一无二的,我们想确保这种情况是正常的:我们模拟了应用程序的 2500 个并发用户,我们的线程数增加到 400(甚至一度达到 600),大部分这些是 ratpack-blocking-x-yyy
个线程。
对 CPU 进行采样,我们在 ratpack.exec.internal.DefaultExecController$ExecControllerBindingThreadFactory.lambda$newThread[=13=]
方法中花费了 92% 的时间,但这可能是采样的产物。
所以,问具体的问题:给定 Ratpack 的线程模型,高阻塞线程数是否正常,我们是否应该担心在上述方法中花费的高 CPU 时间?
Ratpack 为阻塞操作创建无限(*) thread-pool。它在 DefaultExecController
:
中创建
public DefaultExecController(int numThreads) {
this.numThreads = numThreads;
this.eventLoopGroup = ChannelImplDetector.eventLoopGroup(numThreads, new ExecControllerBindingThreadFactory(true, "ratpack-compute", Thread.MAX_PRIORITY));
this.blockingExecutor = Executors.newCachedThreadPool(new ExecControllerBindingThreadFactory(false, "ratpack-blocking", Thread.NORM_PRIORITY));
}
在此池中创建的线程不会在阻塞操作完成后立即终止 - 它们在池中空闲并等待下一个作业执行。其背后的主要原因是保持线程处于空闲状态比在需要时产生新线程更便宜。这就是为什么当您模拟 2500 个并发用户调用和执行阻塞操作的端点时,您将在该池中看到 2500 个线程。创建的缓存 thread-pool 使用以下 ThreadPoolExecutor
对象:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory);
}
其中 2147483647
是最大池大小,60L
是以秒表示的 TTL。这意味着执行程序服务将保留这些线程 60 秒,当它们在 60 秒后没有得到 re-used 时,它将清理它们。
高 CPU 在这种情况下实际上是预期的。 2500 个线程正在使用 CPU 的几个核心。这也很重要 - 您的 SQL 数据库在哪里 运行ning?如果你 运行 它在同一台机器上,那么你的 CPU 就更难完成了。如果您 运行 阻塞 thread-pool 的操作消耗了大量 CPU 时间,那么您必须优化这些阻塞操作。 Ratpack 的强大功能来自异步和 non-blocking 架构 - 处理程序使用 ratpack-compute
thread-pool 并将所有阻塞操作委托给 ratpack-blocking
因此您的应用程序不会被阻塞并且可以处理大量请求。
(*) unlimited 在这种情况下意味着受可用内存限制,或者如果您有足够的内存,则受 2147483647
线程限制(此值用于 ExecutorService.newCachedThreadPool(factory)
)。
仅以 Szymon 的回答为基础……
Ratpack 本身并不限制任何操作。这实际上取决于你。您的一种选择是使用 https://ratpack.io/manual/current/api/ratpack/exec/Throttle.html 来限制和排队对资源的访问。
我们有一个用 Ratpack 1.5.1 编写的移动应用程序 API 服务器即将上线,我们目前正在分析该应用程序以发现任何性能瓶颈。该应用程序由 SQL 数据库支持,我们始终小心 运行 使用 Blocking
class 进行查询。代码是用 Kotlin 编写的,我们编写了一些协程粘合代码来强制在 Ratpack 的阻塞线程上执行阻塞操作。
由于 Ratpack 的线程模型是独一无二的,我们想确保这种情况是正常的:我们模拟了应用程序的 2500 个并发用户,我们的线程数增加到 400(甚至一度达到 600),大部分这些是 ratpack-blocking-x-yyy
个线程。
对 CPU 进行采样,我们在 ratpack.exec.internal.DefaultExecController$ExecControllerBindingThreadFactory.lambda$newThread[=13=]
方法中花费了 92% 的时间,但这可能是采样的产物。
所以,问具体的问题:给定 Ratpack 的线程模型,高阻塞线程数是否正常,我们是否应该担心在上述方法中花费的高 CPU 时间?
Ratpack 为阻塞操作创建无限(*) thread-pool。它在 DefaultExecController
:
public DefaultExecController(int numThreads) {
this.numThreads = numThreads;
this.eventLoopGroup = ChannelImplDetector.eventLoopGroup(numThreads, new ExecControllerBindingThreadFactory(true, "ratpack-compute", Thread.MAX_PRIORITY));
this.blockingExecutor = Executors.newCachedThreadPool(new ExecControllerBindingThreadFactory(false, "ratpack-blocking", Thread.NORM_PRIORITY));
}
在此池中创建的线程不会在阻塞操作完成后立即终止 - 它们在池中空闲并等待下一个作业执行。其背后的主要原因是保持线程处于空闲状态比在需要时产生新线程更便宜。这就是为什么当您模拟 2500 个并发用户调用和执行阻塞操作的端点时,您将在该池中看到 2500 个线程。创建的缓存 thread-pool 使用以下 ThreadPoolExecutor
对象:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory);
}
其中 2147483647
是最大池大小,60L
是以秒表示的 TTL。这意味着执行程序服务将保留这些线程 60 秒,当它们在 60 秒后没有得到 re-used 时,它将清理它们。
高 CPU 在这种情况下实际上是预期的。 2500 个线程正在使用 CPU 的几个核心。这也很重要 - 您的 SQL 数据库在哪里 运行ning?如果你 运行 它在同一台机器上,那么你的 CPU 就更难完成了。如果您 运行 阻塞 thread-pool 的操作消耗了大量 CPU 时间,那么您必须优化这些阻塞操作。 Ratpack 的强大功能来自异步和 non-blocking 架构 - 处理程序使用 ratpack-compute
thread-pool 并将所有阻塞操作委托给 ratpack-blocking
因此您的应用程序不会被阻塞并且可以处理大量请求。
(*) unlimited 在这种情况下意味着受可用内存限制,或者如果您有足够的内存,则受 2147483647
线程限制(此值用于 ExecutorService.newCachedThreadPool(factory)
)。
仅以 Szymon 的回答为基础……
Ratpack 本身并不限制任何操作。这实际上取决于你。您的一种选择是使用 https://ratpack.io/manual/current/api/ratpack/exec/Throttle.html 来限制和排队对资源的访问。