【问题标题】:Java 8 sequential streams increase CPU usage very highJava 8 顺序流非常高地增加了 CPU 使用率
【发布时间】:2021-02-04 02:38:07
【问题描述】:

在我的 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:
线程转储分析表明: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

【问题讨论】:

  • 您的 “更高的 cpu 使用率” 与什么比较?一个 for 循环?
  • @ernest_k 我说的是其他没有流的服务。该服务与其他服务没有什么不同,我唯一使用的是流和可选项而不是 for-loop 和 if 条件。
  • 你怎么知道这种行为是由流引起的?数据库和查询、连接池、消耗的外部服务等呢?
  • 假设 TPS 的意思是“每秒事务数”,那么如果您看到高 TPS 速率,那么 CPU 使用率也很高。关于内存使用,它真的很大程度上取决于你的服务做什么。如果在增加线程数时 TPS 急剧下降,这可能表明这些线程正在“争夺资源”,例如锁、池化数据库连接、文件 I/O 资源或类似资源。也许您正在遇到(临时)死锁、活锁或其他类型的资源匮乏。没有MCVE,我不能说更具体的了。
  • 好吧,除非 OP 澄清后来 cmets 中提出的各种观点,否则他不太可能得到除笼统之外的任何答案。好像他在浪费他的赏金点:-)

标签: java tomcat websocket threadpool deadlock


【解决方案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 过多。

【讨论】:

    【解决方案2】:

    是因为 Streams 消耗更多的 CPU 吗?

    我假设您的意思是:消耗流比循环更多的 CPU?
    如果循环和流在做同样的事情,似乎没有太大区别。
    根据具体情况,可能会有细微差别。以下是关于这个问题的另外 2 篇文章(有这个结果):
    https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html https://dzone.com/articles/java-performance-for-looping-vs-streaming

    还是因为每次 Streams 迭代后内部内存都可能被垃圾回收,所以垃圾回收率很高?

    根据您的代码 sn -p 这个问题无法回答。看不出来是不是有些对象不再被引用了,所以垃圾回收有事可做。
    在这个问题中解释了触发垃圾收集的原因:
    What triggers garbage collection
    但是您的问题不包含有关内存使用情况的信息。

    如果您想调整代码,可以选择使用并行流:

    products.stream().parallel()  
       .filter(Objects::nonNull)
       ...
    

    您会发现几篇文章(例如,请参阅我的第一个链接),这些文章得出的结论是,在某些情况下,并行流确实更快。所以你可以试试这个来提高性能。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-25
      • 1970-01-01
      • 2011-09-20
      • 1970-01-01
      相关资源
      最近更新 更多