查看 gateway进程状态发现
7301 XNIO-1 I/O-2 95.3% 7298 XNIO-1 I/O-1 94.4% 两个线程占用大
-
XNIO 是 Undertow 的底层 NIO 网络库,用于非阻塞网络 IO。
-
如果出现高 CPU,可能是因为这两个线程在处理某些 阻塞任务、超高频请求 或者 网络连接卡死但未关闭。
盲猜是Undertow导致的问题
在pox里面果真找到了Undertow
,这时候我们看看这两个线程在干嘛
要在 jstack 里找到它们,需要把这两个十进制的 TID 转成十六进制:
printf "7301 → 0x%x\\n7298 → 0x%x\\n" 7301 7298
使用jdk的 jstack(先保证你有,你可以点进bin里面看有没有jstack)导出dump.txt
/home/java/openJDk/jdk-17.0.15/bin/jstack 4603 > dump.txt
它会在你当前所在的目录导出一个 dump.txt
然后你就打开这个txt搜索0x1c85和0x1c82
这里是栈信息,可以分析这两个线程在干什么
"XNIO-1 I/O-2" #71 prio=5 os_prio=0 cpu=342102189.69ms elapsed=357373.87s tid=0x00007f9afd09c250 nid=0x1c85 runnable [0x00007f9a846fd000] java.lang.Thread.State: RUNNABLE at org.xnio.nio.WorkerThread.run(WorkerThread.java:480)
"XNIO-1 I/O-1" #70 prio=5 os_prio=0 cpu=341952471.37ms elapsed=357373.88s tid=0x00007f9afd09b570 nid=0x1c82 runnable [0x00007f9a847fe000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPoll.wait(java.base@17.0.15/Native Method) at sun.nio.ch.EPollSelectorImpl.doSelect(java.base@17.0.15/EPollSelectorImpl.java:118) at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@17.0.15/SelectorImpl.java:129) – locked <0x00000000e18de358> (a sun.nio.ch.Util$2) – locked <0x00000000e18de260> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(java.base@17.0.15/SelectorImpl.java:146) at org.xnio.nio.WorkerThread.run(WorkerThread.java:532)
可以看到
-
线程来源是 XNIO(Undertow 的 NIO 底层) 线程名 XNIO-1 I/O-* 明确说明你在跑的是 Undertow 的 I/O 线程池,不是 Gateway 默认的 Reactor Netty。
-
为什么会高 CPU?
-
XNIO-1 I/O-2 直接在 WorkerThread.run 主循环里,一直 RUNNABLE,说明它不停地在做 select + 事件分发,即便在空闲时也可能使用“忙轮询”(busy-spin)模式。
-
XNIO-1 I/O-1 虽然卡在了 EPoll.wait,但因为 XNIO 默认可能把 epoll 调用 timeout 设置为 0 或非常短,也会非常频繁地唤醒再 select,从而消耗大量 CPU。
-
用Undertow理论上是没有问题的
Undertow是一个开源的、灵活的、高性能的非阻塞性应用服务器,由JBoss提供。它可以用作嵌入式服务器,也可以用作大型项目的全功能应用服务器。Undertow的设计以提供最高的性能和最大的灵活性为主要目标,支持非阻塞性和阻塞性处理方式,可以处理十万级的并发连接。
问题可能出在你的依赖引入了spring-boot-starter-web
为什么不能引入 spring-boot-starter-web
-
spring-boot-starter-web 是基于 Servlet + Spring MVC 的栈,默认内嵌 Tomcat(或者你显式改成 Undertow)。
-
一旦你的应用里有了 Servlet 容器(Tomcat/Undertow),Spring Boot 就会把 WebFlux(reactive)模式关闭,走 Servlet 模式。
-
而 Spring Cloud Gateway 是基于 Reactor Netty(纯异步非阻塞)的,底层依赖 Reactor Netty 的 reactor-http-* 线程池,路由转发、过滤器链、限流等都走的异步逻辑。
-
加了 spring-boot-starter-web 以后,网关会被迫跑到一个阻塞的 Servlet 容器里,反而引出你现在看到的 XNIO(Undertow 的底层 NIO)线程,并且所有的转发、过滤都成了阻塞调用,不仅不高效,还容易死锁、无限重试。
但是不巧的是,我的gateway的pom没有见到spring-boot-starter-web的身影,说明不是这个问题。
那没办法了,只能回归 Gateway 标配 — Reactor Netty
移除
-
spring-boot-starter-web(如果有)
-
spring-boot-starter-undertow(如果有)
application.yml 加上 web-application-type
在你的 application.yml 头部,加上这几行:
spring: main: web-application-type: reactive
然后重新打包重新上传jar包运行
终于清爽了。
总结
当你把 Spring Cloud Gateway 部署到 Undertow(即 Servlet 容器)上,而不是走它内置的 Reactor Netty,底层就不再用“阻塞式 epoll 等待”,而是变成了 XNIO 的 忙轮询(busy-spin)模型——几乎无间断地调用 Selector.select(0) 或直接在 WorkerThread.run() 的主循环里打转。
从你贴的线程栈看:
css
"XNIO-1 I/O-2" … at org.xnio.nio.WorkerThread.run(WorkerThread.java:480) "XNIO-1 I/O-1" … at sun.nio.ch.EPoll.wait(Native Method) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:118) at org.xnio.nio.WorkerThread.run(WorkerThread.java:532)
-
XNIO-1 I/O-2 一直在 WorkerThread.run() 的主循环里做 “收消息 → 分发 → 再收” 的工作,即便空闲也在不停循环;
-
XNIO-1 I/O-1 卡在了 EPoll.wait,但 XNIO 默认给 epoll 的 超时时间为 0(或者非常短),于是它根本不阻塞,立刻返回再下一次 select——这就相当于在“死循环”中不断地唤醒和轮询。
两个线程都处于 RUNNABLE 状态、CPU 全速运转,结果就是 100% 的占用率。
为什么 Reactor Netty 没有这个问题?
-
Reactor Netty 的 epoll/kqueue 等待是 阻塞式 的:当没有事件时,内核会把线程挂起,直到有网络可读写或超时才唤醒——因此空闲时几乎不消耗 CPU。
-
XNIO 为了极限的低延迟,默认使用 busy-spin idle strategy,牺牲 CPU 周期去换取亚微秒级的唤醒时间,但在 Gateway 这种并发不极端、事件量不算天量的场景下就显得“咄咄逼人”——不停地在空中打转。
小结
-
Undertow/XNIO:忙轮询 ⇒ 高实时性 ⇒ CPU 飙升
-
Reactor Netty:阻塞式 epoll 等待 ⇒ 低空闲消耗 ⇒ Gateway 推荐
这也是为什么官方强烈建议 Spring Cloud Gateway 一定要跑在 WebFlux + Reactor Netty 模式下:它的所有调度、限流、熔断、路由机制都是围绕 Reactor 事件循环设计的,放到 XNIO 上就会出现这种 100% CPU 的“空转”情况。
评论前必须登录!
注册