Java 8 顺序流增加 CPU 使用率非常高

muk*_*und 8 java tomcat deadlock threadpool websocket

在我的 Spring Boot 服务中,我根据订单详细信息和客户详细信息验证收到的订单。

在客户详细信息中,我有不同的对象列表,例如服务、属性、产品等,对于每个列表,我正在执行以下操作:

products.stream()  
       .filter(Objects::nonNull)  
       .map(Product::getResource)  
       .filter(Objects::nonNull)  
       .filter(<SimplePredicate>)  
       .collect(Collectors.toList());  
Run Code Online (Sandbox Code Playgroud)

我多次将这样的流用于产品、服务和属性。我们观察到,在性能方面,它提供了非常高的 TPS,并且内存使用率也非常理想。但这非常消耗CPU。我们在 Kubernetes pod 中运行该服务,它占用了所提供 CPU 的 90%。

一个更有趣的观察是,我们提供的 CPU 越多,实现的 TPS 就越高,CPU 使用率也达到 90%。

是因为 Streams 消耗更多 CPU 吗?或者是因为高垃圾收集,因为在每次迭代 Streams 之后,内部内存可能会被垃圾收集?

编辑-1:

使用负载测试进行进一步调查后,发现:

  • 每当我们增加并发线程时,由于 CPU 使用率高,服务开始无响应,随后 CPU 突然减少,从而导致 TPS 低。
  • 每当我们减少并发线程时,CPU 使用率仍然很高,但服务正在以最佳方式执行,即高 TPS。

以下是不同CPU/线程配置下TPS vs. CPU的统计。

CPU:1500m,线程:70

| TPS | 176  | 140 | 125 | 79 | 63 |
|----------------------------------|
| CPU | 1052 | 405 | 201 | 84 | 13 |  
Run Code Online (Sandbox Code Playgroud)

CPU:1500m,线程:35

| TPS | 500 | 510 | 500 | 530 |
|-----------------------------|
| CPU | 1172| 1349| 1310| 1214|  
Run Code Online (Sandbox Code Playgroud)

CPU:2500m,线程:70

| TPS |  20 |  20 |  25 |  28 | 26 |
|----------------------------------|
| CPU | 2063| 2429| 2303| 879 | 35 |  
Run Code Online (Sandbox Code Playgroud)

CPU:2500m,线程:35

| TPS | 1193 | 1200 | 1200 | 1230 |
|---------------------------------|
| CPU | 600  | 1908 | 2044 | 1949 | 
Run Code Online (Sandbox Code Playgroud)

使用的Tomcat配置:

server.tomcat.max-connections=100
server.tomcat.max-threads=100
server.tomcat.min-spare-threads=5
Run Code Online (Sandbox Code Playgroud)

EDIT-2:
线程转储分析说:80% 的http-nio线程处于Waiting on condition状态。这意味着所有线程都在等待某事,并且没有人消耗任何解释低 CPU 使用率的 CPU。但是什么可能导致线程等待?我也没有在服务中使用任何异步调用。即使我没有使用任何并行流,只使用上面提到的顺序流。

以下是 CPU 和 TPS 下降时的 Thread dump:

"http-nio-8090-exec-72" #125 daemon prio=5 os_prio=0 tid=0x00007f014001e800 nid=0x8f waiting on condition [0x00007f0158ae1000]
   java.lang.Thread.State: **TIMED_WAITING** (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000d7470b10> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
    at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
    at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:89)
    at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:33)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
    - None
Run Code Online (Sandbox Code Playgroud)

Ste*_*n C 1

是因为Streams消耗更多的CPU吗?或者是因为高垃圾收集,因为在 Streams 的每次迭代之后,内部内存都可能被垃圾收集?

显然,流确实会消耗 CPU。一般来说,使用非并行流实现的代码确实比使用老式循环实现的代码运行得慢一些。然而,性能差异并不大。(也许 5% 或 10%?)

一般来说,流不会比执行相同计算的老式循环产生更多的垃圾。例如,如果我们将您的示例与执行相同操作(即生成新列表)的循环进行比较,那么我预计两个版本的内存分配之间存在一一对应关系。

简而言之,我认为流与此没有直接关系。显然,如果您的服务正在为每个请求处理大量列表(使用流或循环),那么这将影响 TPS。如果列表实际上是从后端数据库获取的,则更是如此。但这也很正常。这可以通过执行请求缓存等操作来解决,并调整 API 请求的粒度以计算调用者实际上并不需要的昂贵结果。

(我不建议parallel()在您的场景中添加流。由于您的服务已经受到计算(或交换)限制,因此没有“备用”周期来并行运行流。在parallel()这里使用可能会降低您的 TPS。)

您问题的第二部分是关于性能 (TPS) 与线程数与(我们认为)VCPU 的关系。无法解释您给出的结果,因为您没有解释测量单位,并且......因为我怀疑还有其他因素在起作用。

然而,作为一般规则:

  • 当应用程序是计算密集型时添加更多线程没有帮助。
  • 更多线程意味着更多的内存利用率(线程堆栈+只能从线程堆栈访问的对象)。
  • 更多的内存利用率意味着 GC 将不太符合人体工程学。
  • 如果 JVM 使用的虚拟内存多于物理内存,那么操作系统通常必须在 RAM 和磁盘之间交换页面。这会影响性能,尤其是在垃圾收集期间。

您的云平台也可能会产生一些影响。例如,如果您在具有大量虚拟服务器的计算节点上的虚拟服务器中运行,则您可能无法获得每个 VCPU 的完整 CPU 价值。如果您的虚拟服务器生成大量交换流量,则很可能会进一步减少服务器的 CPU 资源份额。

我们不能说到底是什么导致了您的问题,但如果我处于您的位置,我会查看 Java GC 日志,并使用 和 等操作系统工具vmstatiostat查找过度分页和过多 I/O 的迹象。