> 本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford

在我们的项目中,我们没有采用默认的 Tomcat 容器,而是使用了 UnderTow 作为我们的容器。其实性能上的差异并没有那么明显,但是使用 UnderTow 我们可以利用直接内存作为网络传输的 buffer,减少业务的 GC,优化业务的表现。

**Undertow 的官网**:https://undertow.io/

但是,Undertow 有一些**令人担忧**的地方:

  1. NIO 框架采用的是 [XNIO](http://xnio.jboss.org/),在官网 3.0  roadmap 声明中提到了将会在 3.0 版本开始,从 XNIO 迁移到 netty, 参考:[Undertow 3.0 Announcement](https://undertow.io/blog/2019/04/15/Undertow-3.html)。但是,目前已经过了快两年了,**3.0 还是没有发布,并且 github 上 3.0 的分支已经一年多没有更新了**。目前,还是在用 2.x 版本的 Undertow。不知道是 3.0 目前没必要开发,还是胎死腹中了呢?目前国内的环境对于 netty 使用更加广泛并且大部分人对于 netty 更加熟悉一些, XNIO 应用并不是很多。不过,XNIO 的设计与 netty 大同小异。2. **官方文档的更新比较慢,可能会慢 1~2 个小版本**,导致 Spring Boot 粘合 Undertow 的时候,配置显得不会那么优雅。**参考官方文档的同时,最好还是看一下源码,至少看一下配置类,才能搞懂究竟是怎么设置的**。

  2. **官方文档的更新比较慢,可能会慢 1~2 个小版本**,导致 Spring Boot 粘合 Undertow 的时候,配置显得不会那么优雅。**参考官方文档的同时,最好还是看一下源码,至少看一下配置类,才能搞懂究竟是怎么设置的**。

  3. 仔细看 Undertow 的源码,会发现有很多防御性编程的设计或者功能性设计 Undertow 的作者想到了,但是就是没实现,**有很多没有实现的半成品代码**。这也令人担心 Underow 是否开发动力不足,哪一天会突然死掉?

**使用 Undertow 要注意的问题**:

1. 需要开启 NIO DirectBuffer 的特性,理解并配置好相关的参数。

2. access.log 中要包括必要的一些时间,调用链等信息,并且默认配置下,有些只配置 access.log 参数还是显示不出来我们想看的信息,官网对于 access.log 中的参数的一些细节并没有详细说明。

# 使用 Undertow 作为我们的 Web 服务容器

对于 Servlet 容器,依赖如下:

```
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
```

对于 Weflux 容器,依赖如下:

```
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
```

# Undertow 基本结构

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

Java XNIO 主要包括如下几个概念:

     - Java NIO `ByteBuffer`:`Buffer` 是一个具有状态的数组,用来承载数据,可以追踪记录已经写入或者已经读取的内容。主要属性包括:capacity(Buffer 的容量),position(下一个要读取或者写入的位置下标),limit(当前可以写入或者读取的极限位置)。**程序必须通过将数据放入 Buffer,才能从 Channel 读取或者写入数据**。`ByteBuffer`是更加特殊的 Buffer,它可以以直接内存分配,这样 JVM 可以直接利用这个 Bytebuffer 进行 IO 操作,省了一步复制(具体可以参考我的一篇文章:[Java 堆外内存、零拷贝、直接内存以及针对于NIO中的FileChannel的思考](https://zhuanlan.zhihu.com/p/161939673))。也可以通过文件映射内存直接分配,即 Java MMAP(具体可以参考我的一篇文章:[JDK核心JAVA源码解析(5) - JAVA File MMAP原理解析](https://zhuanlan.zhihu.com/p/258934554))。所以,一般的 IO 操作都是通过 ByteBuffer 进行的。

     - Java NIO `Channel`:Channel 是 Java 中对于打开和某一外部实体(例如硬件设备,文件,网络连接 socket 或者可以执行 IO 操作的某些组件)连接的抽象。Channel 主要是 IO 事件源,所有写入或者读取的数据都必须经过 Channel。对于 NIO 的 Channel,会通过 `Selector` 来通知事件的就绪(例如读就绪和写就绪),之后通过 Buffer 进行读取或者写入。

      - XNIO `Worker`: Worker 是 Java XNIO 框架中的基本网络处理单元,一个 Worker 包含两个不同的线程池类型,分别是:

      - **IO 线程池**,主要调用`Selector.start()`处理对应事件的各种回调,原则上不能处理任何阻塞的任务,因为这样会导致其他连接无法处理。IO 线程池包括两种线程(在 XNIO 框架中,通过设置 WORKER_IO_THREADS 来设置这个线程池大小,默认是一个 CPU 一个 IO 线程):

       - **读线程**:处理读事件的回调

       - **写线程**:处理写事件的回调

       - **Worker 线程池**,处理阻塞的任务,在 Web 服务器的设计中,一般将调用 servlet 任务放到这个线程池执行(在 XNIO 框架中,通过设置 WORKER_TASK_CORE_THREADS 来设置这个线程池大小)

        - XNIO `ChannelListener`:ChannelListener 是用来监听处理 Channel 事件的抽象,包括:`channel readable`, `channel writable`, `channel opened`, `channel closed`, `channel bound`, `channel unbound`

Undertow 是基于 XNIO 的 Web 服务容器。在 XNIO 的基础上,增加:

       - Undertow `BufferPool`: 如果每次需要 ByteBuffer 的时候都去申请,对于堆内存的 ByteBuffer 需要走 JVM 内存分配流程(TLAB -> 堆),对于直接内存则需要走系统调用,这样效率是很低下的。所以,一般都会引入内存池。在这里就是 `BufferPool`。目前,UnderTow 中只有一种 `DefaultByteBufferPool`,其他的实现目前没有用。这个 DefaultByteBufferPool 相对于 netty 的 ByteBufArena 来说,非常简单,类似于 JVM TLAB 的机制(可以参考我的另一系列:[全网最硬核 JVM TLAB 分析](https://juejin.cn/post/6925217498723778568)),但是简化了很多。**我们只需要配置 buffer size ,并开启使用直接内存即可**。

       - Undertow `Listener`: 默认内置有 3 种 Listener ,分别是 HTTP/1.1、AJP 和 HTTP/2 分别对应的 Listener(HTTPS 通过对应的 HTTP Listner 开启 SSL 实现),负责所有请求的解析,将请求解析后包装成为 `HttpServerExchange` 并交给后续的 `Handler` 处理。

       - Undertow `Handler`: 通过 Handler 处理响应的业务,这样组成一个完整的 Web 服务器。

# Undertow 的一些默认配置

Undertow 的 Builder 设置了一些默认的参数,参考源码:

[`Undertow`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/Undertow.java)

```
private Builder() {
    ioThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2);
    workerThreads = ioThreads * 8;
    long maxMemory = Runtime.getRuntime().maxMemory();
    //smaller than 64mb of ram we use 512b buffers
    if (maxMemory < 64 * 1024 * 1024) {
        //use 512b buffers
        directBuffers = false;
        bufferSize = 512;
    } else if (maxMemory < 128 * 1024 * 1024) {
        //use 1k buffers
        directBuffers = true;
        bufferSize = 1024;
    } else {
        //use 16k buffers for best performance
        //as 16k is generally the max amount of data that can be sent in a single write() call
        directBuffers = true;
        bufferSize = 1024 * 16 - 20; //the 20 is to allow some space for protocol headers, see UNDERTOW-1209
    }
}
```

 - ioThreads 大小为可用 CPU 数量 * 2,即 Undertow 的 XNIO 的读线程个数为可用 CPU 数量,写线程个数也为可用 CPU 数量。

 - workerThreads 大小为 ioThreads 数量 * 8.

 - 如果内存大小小于 64 MB,则不使用直接内存,bufferSize 为 512 字节

 - 如果内存大小大于 64 MB 小于 128 MB,则使用直接内存,bufferSize 为 1024 字节

 - 如果内存大小大于 128 MB,则使用直接内存,bufferSize 为 16 KB 减去 20 字节,这 20 字节用于协议头。

# Undertow Buffer Pool 配置

[`DefaultByteBufferPool`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/server/DefaultByteBufferPool.java) 构造器:

```
public DefaultByteBufferPool(boolean direct, int bufferSize, int maximumPoolSize, int threadLocalCacheSize, int leakDecetionPercent) {
    this.direct = direct;
    this.bufferSize = bufferSize;
    this.maximumPoolSize = maximumPoolSize;
    this.threadLocalCacheSize = threadLocalCacheSize;
    this.leakDectionPercent = leakDecetionPercent;
    if(direct) {
        arrayBackedPool = new DefaultByteBufferPool(false, bufferSize, maximumPoolSize, 0, leakDecetionPercent);
    } else {
        arrayBackedPool = this;
    }
}
```

其中:

 - direct:是否使用直接内存,我们需要设置为 true,来使用直接内存。

 - bufferSize:每次申请的 buffer 大小,我们主要要考虑这个大小

 - maximumPoolSize:buffer 池最大大小,一般不用修改

 - threadLocalCacheSize:线程本地 buffer 池大小,一般不用修改

 - leakDecetionPercent:内存泄漏检查百分比,目前没啥卵用

对于 bufferSize,最好和你系统的 TCP Socket Buffer 配置一样。在我们的容器中,我们将微服务实例的容器内的 TCP Socket Buffer 的读写 buffer 大小成一模一样的配置(因为微服务之间调用,发送的请求也是另一个微服务接受,所以调整所有微服务容器的读写 buffer 大小一致,来优化性能,默认是根据系统内存来自动计算出来的)。

查看 Linux 系统 TCP Socket Buffer 的大小:

 - `/proc/sys/net/ipv4/tcp_rmem` (对于读取)

 - `/proc/sys/net/ipv4/tcp_wmem` (对于写入)

在我们的容器中,分别是:

```
bash-4.2# cat /proc/sys/net/ipv4/tcp_rmem
4096    16384   4194304 
bash-4.2# cat /proc/sys/net/ipv4/tcp_wmem
4096    16384   4194304 
```

从左到右三个值分别为:每个 TCP Socket 的读 Buffer 与写 Buffer 的大小的 最小值,默认值和最大值,单位是字节。

我们设置我们 Undertow 的 buffer size 为 TCP Socket Buffer 的默认值,**即 16 KB**。Undertow 的 Builder 里面,如果内存大于 128 MB,buffer size 为 16 KB 减去 20 字节(为协议头预留)。所以,**我们使用默认的即可**。

`application.yml` 配置:

```
server.undertow:
    # 是否分配的直接内存(NIO直接分配的堆外内存),这里开启,所以java启动参数需要配置下直接内存大小,减少不必要的GC
    # 在内存大于 128 MB 时,默认就是使用直接内存的
    directBuffers: true
    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作
    # 如果每次需要 ByteBuffer 的时候都去申请,对于堆内存的 ByteBuffer 需要走 JVM 内存分配流程(TLAB -> 堆),对于直接内存则需要走系统调用,这样效率是很低下的。
    # 所以,一般都会引入内存池。在这里就是 `BufferPool`。
    # 目前,UnderTow 中只有一种 `DefaultByteBufferPool`,其他的实现目前没有用。
    # 这个 DefaultByteBufferPool 相对于 netty 的 ByteBufArena 来说,非常简单,类似于 JVM TLAB 的机制
    # 对于 bufferSize,最好和你系统的 TCP Socket Buffer 配置一样
    # `/proc/sys/net/ipv4/tcp_rmem` (对于读取)
    # `/proc/sys/net/ipv4/tcp_wmem` (对于写入)
    # 在内存大于 128 MB 时,bufferSize 为 16 KB 减去 20 字节,这 20 字节用于协议头
    buffer-size: 16384 - 20
```

# Undertow Worker 配置

Worker 配置其实就是 XNIO 的核心配置,主要需要配置的即 io 线程池以及 worker 线程池大小。

默认情况下,io 线程大小为可用 CPU 数量 * 2,即读线程个数为可用 CPU 数量,写线程个数也为可用 CPU 数量。worker 线程池大小为 io 线程大小 * 8.

微服务应用由于涉及的阻塞操作比较多,所以可以将 worker 线程池大小调大一些。我们的应用设置为 io 线程大小 * 32.

`application.yml` 配置:

```
server.undertow.threads:
    # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个读线程和一个写线程
    io: 16
    # 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
    # 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
    worker: 128
```

# Spring Boot 中的 Undertow 配置

Spring Boot 中对于 Undertow 相关配置的抽象是 [`ServerProperties`](https://github.com/spring-projects/spring-boot/blob/2.4.x/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java) 这个类。目前 Undertow 涉及的所有配置以及说明如下(不包括 accesslog 相关的,accesslog 会在下一节详细分析):

```
server:
  undertow:
    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作
    # 如果每次需要 ByteBuffer 的时候都去申请,对于堆内存的 ByteBuffer 需要走 JVM 内存分配流程(TLAB -> 堆),对于直接内存则需要走系统调用,这样效率是很低下的。
    # 所以,一般都会引入内存池。在这里就是 `BufferPool`。
    # 目前,UnderTow 中只有一种 `DefaultByteBufferPool`,其他的实现目前没有用。
    # 这个 DefaultByteBufferPool 相对于 netty 的 ByteBufArena 来说,非常简单,类似于 JVM TLAB 的机制
    # 对于 bufferSize,最好和你系统的 TCP Socket Buffer 配置一样
    # `/proc/sys/net/ipv4/tcp_rmem` (对于读取)
    # `/proc/sys/net/ipv4/tcp_wmem` (对于写入)
    # 在内存大于 128 MB 时,bufferSize 为 16 KB 减去 20 字节,这 20 字节用于协议头
    buffer-size: 16364
    # 是否分配的直接内存(NIO直接分配的堆外内存),这里开启,所以java启动参数需要配置下直接内存大小,减少不必要的GC
    # 在内存大于 128 MB 时,默认就是使用直接内存的
    directBuffers: true
    threads:
      # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个读线程和一个写线程
      io: 4
      # 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
      # 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
      worker: 128
    # http post body 大小,默认为 -1B ,即不限制
    max-http-post-size: -1B
    # 是否在启动时创建 filter,默认为 true,不用修改
    eager-filter-init: true
    # 限制路径参数数量,默认为 1000
    max-parameters: 1000
    # 限制 http header 数量,默认为 200
    max-headers: 200
    # 限制 http header 中 cookies 的键值对数量,默认为 200
    max-cookies: 200
    # 是否允许 / 与 %2F 转义。/ 是 URL 保留字,除非你的应用明确需要,否则不要开启这个转义,默认为 false
    allow-encoded-slash: false
    # 是否允许 URL 解码,默认为 true,除了 %2F 其他的都会处理
    decode-url: true
    # url 字符编码集,默认是 utf-8
    url-charset: utf-8
    # 响应的 http header 是否会加上 'Connection: keep-alive',默认为 true
    always-set-keep-alive: true
    # 请求超时,默认是不超时,我们的微服务因为可能有长时间的定时任务,所以不做服务端超时,都用客户端超时,所以我们保持这个默认配置
    no-request-timeout: -1
    # 是否在跳转的时候保持 path,默认是关闭的,一般不用配置
    preserve-path-on-forward: false
    options:
      # spring boot 没有抽象的 xnio 相关配置在这里配置,对应 org.xnio.Options 类
      socket:
        SSL_ENABLED: false
      # spring boot 没有抽象的 undertow 相关配置在这里配置,对应 io.undertow.UndertowOptions 类
      server:
        ALLOW_UNKNOWN_PROTOCOLS: false
```

Spring Boot 并没有将所有的 Undertow 与 XNIO 配置进行抽象,如果你想自定义一些相关配置,可以通过上面配置最后的 `server.undertow.options` 进行配置。`server.undertow.options.socket` 对应 XNIO 的相关配置,配置类是 `org.xnio.Options`;`server.undertow.options.server` 对应 Undertow 的相关配置,配置类是 `io.undertow.UndertowOptions`。