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

标签 java tomcat websocket threadpool deadlock

在我的 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 的每次迭代之后,内部内存可能会被垃圾收集?
EDIT-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 |  
    
    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:
    线程转储分析显示:http-nio 的 80%线程在 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
    

    最佳答案

    Is it because Streams consume more CPU? Or is it because of high Garbage Collection because after every iteration of Streams the internal memory might be garbage collected?


    显然流确实会消耗 CPU。一般来说,使用非并行流实现的代码确实比使用老式循环实现的代码运行得慢一些。但是,性能上的差异并不大。 (可能是 5% 或 10%?)
    通常,流不会比执行相同计算的老式循环产生更多的垃圾。例如,如果我们将您的示例与执行相同操作的循环(即生成一个新列表)进行比较,那么我希望两个版本的内存分配之间存在一对一的对应关系。
    简而言之,我认为流与此无关。显然,如果您的服务正在为每个请求处理大量列表(使用流或循环),那么这将影响 TPS。如果列表实际上是从您的后端数据库中获取的,则更是如此。但这也很正常。这可以通过诸如请求缓存之类的事情来解决,并调整 API 请求的粒度以计算调用者实际上并不需要的昂贵结果。
    (我不建议在您的场景中将 parallel() 添加到您的流中。由于您的服务已经受计算(或交换)限制,因此没有“备用”周期来并行运行流。使用 parallel() 可能降低你的 TPS。)
    您问题的第二部分是关于性能(TPS)与线程数与(我们认为)VCPU 的关系。无法解释您给出的结果,因为您没有解释测量单位,并且....因为我怀疑还有其他因素在起作用。
    但是,作为一般规则:
  • 当应用程序是计算密集型时添加更多线程无济于事。
  • 更多线程意味着更多的内存利用率(线程堆栈+只能从线程堆栈访问的对象)。
  • 更多的内存利用率意味着 GC 将不太符合人体工程学。
  • 如果您的 JVM 使用的虚拟内存比物理内存多,那么操作系统通常必须将页面从 RAM 交换到磁盘并返回。这会影响性能,尤其是在垃圾收集期间。

  • 也可能有一些影响可以归因于您的云平台。例如,如果您在具有大量虚拟服务器的计算节点上的虚拟服务器中运行,您可能无法获得每个 VCPU 的完整 CPU 值(value)。如果您的虚拟服务器正在产生大量交换流量,那很可能会进一步减少您的服务器在 CPU 资源中的份额。
    我们不能说到底是什么导致了你的问题,但如果我站在你的立场上,我会查看 Java GC 日志,并使用像 vmstat 这样的 OS 工具。和 iostat一般来说,寻找过度分页和过度 I/O 的迹象。

    关于Java 8 顺序流非常高地增加了 CPU 使用率,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64459192/

    相关文章:

    java - 在运行时更改 Spring 任务的预定时间?

    apache - Tomcat 8 URL 重写问题

    java - Apache tomcat安装报错

    apache - JSESSIONID cookie 域

    node.js - 仅使用 websockets 构建整个站点(通过 socket.io 和 node.js,没有 Ajax)?

    java - 如何在 Mac 上使用 SceneBuilder 和 IntelliJ

    java - 向 Web 应用程序添加搜索功能和页面支持

    java - LDAP 过滤器查询如何工作,特别是在 Java LDAP/spring LDAP 中?

    azure - Azure 应用服务上的 Flask Socket.io 400 错误代码

    python - 使用 python 从网站上抓取 socket.io 数据