首页 > 解决方案 > Java 8 顺序流非常高地增加了 CPU 使用率

问题描述

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

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

products.stream()  
       .filter(Objects::nonNull)  
       .map(Product::getResource)  
       .filter(Objects::nonNull)  
       .filter(<SimplePredicate>)  
       .collect(Collectors.toList());  

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

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

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

编辑-1:

在使用负载测试进行进一步调查后,观察到:

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

CPU:1500m,线程:70

| TPS | 176  | 140 | 125 | 79 | 63 |
|----------------------------------|
| CPU | 1052 | 405 | 201 | 84 | 13 |  

CPU:1500m,线程:35

| TPS | 500 | 510 | 500 | 530 |
|-----------------------------|
| CPU | 1172| 1349| 1310| 1214|  

CPU:2500m,线程:70

| TPS |  20 |  20 |  25 |  28 | 26 |
|----------------------------------|
| CPU | 2063| 2429| 2303| 879 | 35 |  

CPU:2500m,线程:35

| TPS | 1193 | 1200 | 1200 | 1230 |
|---------------------------------|
| CPU | 600  | 1908 | 2044 | 1949 | 

使用的 Tomcat 配置:

server.tomcat.max-connections=100
server.tomcat.max-threads=100
server.tomcat.min-spare-threads=5

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

以下是 CPU 和 TPS 下降时的线程转储:

"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

标签: javatomcatwebsocketthreadpooldeadlock

解决方案


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

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

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

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

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

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

但是,作为一般规则:

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

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

我们不能说实际上是什么导致了您的问题,但如果我站在您的立场上,我会查看 Java GC 日志,并使用 OS 工具(如vmstatiostat)来寻找过度分页和过度 I/O 的迹象。


推荐阅读