跳到主要内容

Gateway采用undertow带来的问题

问题背景

gateway模块引入了一些公共依赖,公共依赖主要是Spring容器等,但是移除了tomcat,引入了undertow容器,所以Gateway就引入了undertow依赖,导致出现了问题

这会导致 Spring-Cloud-Gateway 本身的 Netty 的 Reactive 的 web 容器被替换成了 Undertow 的 Reactive 的 web 容器,从而导致了一系列的 Spring-Cloud-Gateway 不兼容的问题。

问题复现

Spring Cloud Gateway使用了undertow,当undertow容器运行一段时间后,此时CPU来到了将近300:

image-20240826152416415

image-20240826152446457

占用较高的几个:

7    79 39.7
7 80 12.5
7 81 38.4
7 82 32.1

保存堆栈信息:jstack 7 > undertow-cpu.log

查看其中的信息

"XNIO-1 I/O-1" #70 prio=5 os_prio=0 tid=0x00007f5c22e5e800 nid=0x4f runnable [0x00007f5b8a4b6000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
- locked <0x00000006ca7462b0> (a sun.nio.ch.Util$3)
- locked <0x00000006ca7462a0> (a java.util.Collections$UnmodifiableSet)
- locked <0x00000006ca7450a0> (a sun.nio.ch.EPollSelectorImpl)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:101)
at org.xnio.nio.WorkerThread.run(WorkerThread.java:532)

线程正在使用 EPoll 进行 I/O 事件的监听和处理,具体细节:

  • sun.nio.ch.EPollArrayWrapper.epollWait(Native Method):线程正在调用本地方法 epollWait,这是 Linux 系统中的 epoll 调用,用于等待 I/O 事件的发生。
  • sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)poll 方法会调用 epollWait 来等待事件,并返回发生的事件列表。
  • sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)doSelect 是基于 EPollSelectorImpl 实现的 Selector,用于处理选择就绪的 I/O 事件。
  • sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)SelectorImpl 会加锁并执行 doSelect,确保线程安全。
    • 锁住了三个对象,分别是:
      • sun.nio.ch.Util$3:一个与 NIO 操作相关的锁。
      • java.util.Collections$UnmodifiableSet:与通道集有关的锁。
      • sun.nio.ch.EPollSelectorImpl:选择器对象的锁。
  • sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)SelectorImpl.select(SelectorImpl.java:101):这些是 Java NIO 中的 select() 调用,用于等待 I/O 事件。

源码分析

入口:

public static void main(String[] args) {
SpringApplication.run(CasllGatewayApplication.class, args);
}

这是我们启动SpringBoot程序的入口,也是启动网关的入口,我们进入源码

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

发现会通过构造函数创建SpringApplication对象,在这里会指定webApplicationType的类型,我们接着进入this.webApplicationType = WebApplicationType.deduceFromClasspath();

public enum WebApplicationType {

/**
* The application should not run as a web application and should not start an
* embedded web server.
*/
NONE,

/**
* The application should run as a servlet-based web application and should start an
* embedded servlet web server.
*/
SERVLET,

/**
* The application should run as a reactive web application and should start an
* embedded reactive web server.
*/
REACTIVE;

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}

}

接着就得到了这个类,这个 WebApplicationType 枚举类用于确定 Spring Boot 应用程序应该以哪种方式启动。它通过检查类路径中的特定类来推断应用程序是应该作为传统的基于 Servlet 的 Web 应用启动(即使用嵌入式 Servlet 容器),还是作为响应式(Reactive)Web 应用启动,或者不启动嵌入式 Web 服务器(NONE 模式)。接下来分析关键部分:

枚举类型:

  1. NONE: 应用程序不应作为 Web 应用程序运行,不启动嵌入式 Web 服务器。
  2. SERVLET: 应用程序应该作为基于 Servlet 的 Web 应用程序运行,并启动嵌入式 Servlet Web 服务器。
  3. REACTIVE: 应用程序应该作为响应式 Web 应用程序运行,并启动嵌入式响应式 Web 服务器。

WebApplicationType 主要通过类路径中的特定类来推断应用程序类型:

  • 如果类路径中有 WebFlux 的核心类(org.springframework.web.reactive.DispatcherHandler),则推断为响应式应用。
  • 如果类路径中没有 WebFlux 但有 Servlet 的相关类,则推断为传统的基于 Servlet 的 Web 应用。
  • 如果既没有 WebFlux 也没有 Servlet 相关的类,则推断为 NONE,即不启动嵌入式 Web 服务器。

如果是Reactive环境,会使用org/springframework/boot/web/reactive/server/ReactiveWebServerFactory.java实现的Bean创建Web容器,这些都是响应式 web 容器 Factory

image-20240826150032048

那么这些容器的加载顺序是什么?

@Import({BeanPostProcessorsRegistrar.class,
ReactiveWebServerFactoryConfiguration.EmbeddedTomcat.class,
ReactiveWebServerFactoryConfiguration.EmbeddedJetty.class,
ReactiveWebServerFactoryConfiguration.EmbeddedUndertow.class,
ReactiveWebServerFactoryConfiguration.EmbeddedNetty.class})
public class ReactiveWebServerFactoryAutoConfiguration {

可以看到Netty的顺序是最后的,只要你的依赖中加入了任何 Web 容器(例如 Undertow),那么最后创建的就是基于那个 web 容器的异步容器,而不是基于 netty 的

为什么不是Netty就会有问题?官方文档有着相应的解答:

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter

image-20240826150527251

就是 Spring Cloud Gateway 只能在 Netty 的环境中运行,在设计的时候,就假定了容器只能是 Netty,后续开发各种 Spring Cloud Gateway 的内置 Filter 以及 Filter 插件的时候,有很多假设当前就是 Netty 的代码,例如缓存 Body 的 Filter 使用的工具类ServerWebExchangeUtils

private static <T> Mono<T> cacheRequestBody(ServerWebExchange exchange, boolean cacheDecoratedRequest,
Function<ServerHttpRequest, Mono<T>> function) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory factory = response.bufferFactory();
// Join all the DataBuffers so we have a single DataBuffer for the body
return DataBufferUtils.join(exchange.getRequest().getBody()).defaultIfEmpty(factory.wrap(EMPTY_BYTES))
.map(dataBuffer -> decorate(exchange, dataBuffer, cacheDecoratedRequest))
.switchIfEmpty(Mono.just(exchange.getRequest())).flatMap(function);
}

image-20240826152234768

到目前为止,仍然没有Netty之外的其他容器:

容器对比

Undertow的NIO框架采用的是X-NIO,并且在3.0版本会从XNIO迁移到Netty,参考链接

Undertow 目前(2.x) 还是基于 Java XNIO,Java XNIO 是一个对于 JDK NIO 类的扩展,和 netty 的基本功能是一样的,但是 netty 更像是对于 Java NIO 的封装,Java XNIO 更像是扩展封装。主要是 netty 中基本传输承载数据的并不是 Java NIO 中的 ByteBuffer,而是自己封装的 ByteBuf,而 Java XNIO 各个接口设计还是基于 ByteBuffer 为传输处理单元。设计上也很相似,都是 Reactor 模型的设计。

XNIO 是 Java 中的一个高性能网络 I/O 框架,核心概念包括以下几个方面:

  1. Java NIO ByteBufferByteBuffer 是 NIO 中用于处理数据的缓冲区。它不仅保存数据,还会跟踪其状态,主要包括以下属性:

    • capacity:缓冲区的容量,即可以容纳的最大数据量。

    • position:当前可以写入或读取数据的位置。

    • limit:可以进行写入或读取的最大位置。 要从 Channel 读取或向 Channel 写入数据,必须通过 ByteBufferByteBuffer 还支持直接内存分配,使 JVM 可以直接使用它进行 I/O 操作,减少数据复制,提高性能。

  2. Java NIO ChannelChannel 是 Java 中对 I/O 操作的抽象,它代表了与硬件设备、文件、网络连接等外部实体的连接。所有的 I/O 操作(读写数据)都需要通过 Channel 进行。NIO 中的 Channel 通常与 Selector 配合使用,以非阻塞的方式通知 I/O 事件(如读就绪或写就绪)。数据传输通过 ByteBuffer 完成。

  3. XNIO Worker:Worker 是 XNIO 框架中的核心网络处理单元,它负责管理与处理 I/O 事件。Worker 包含两个主要线程池:

    • IO 线程池:专门处理 I/O 事件的回调,包含读线程和写线程。每个 CPU 通常分配一个 I/O 线程(通过 WORKER_IO_THREADS 设置),这些线程不能处理阻塞任务,以免影响其他连接的处理。

      • 读线程:处理读事件。
      • 写线程:处理写事件。
    • Worker 线程池:处理阻塞任务,比如调用 servlet 或其他耗时任务的执行。通过 WORKER_TASK_CORE_THREADS 设置线程池大小。

  4. XNIO ChannelListenerChannelListener 是用于监听和处理 Channel 上各种事件的接口。典型的事件包括:

    • channel readable:可读事件。

    • channel writable:可写事件。

    • channel opened:通道打开。

    • channel closed:通道关闭。

    • channel bound:通道绑定。

    • channel unbound:通道解绑。

参考资料