关于springboot:Spring-Cloud-升级之路-20200x-2-使用-Undertow-作为我们的-Web-服务容器

5次阅读

共计 8306 个字符,预计需要花费 21 分钟才能阅读完成。

本我的项目代码地址:https://github.com/HashZhang/…

在咱们的我的项目中,咱们没有采纳默认的 Tomcat 容器,而是应用了 UnderTow 作为咱们的容器。其实性能上的差别并没有那么显著,然而应用 UnderTow 咱们能够利用间接内存作为网络传输的 buffer,缩小业务的 GC,优化业务的体现。

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

然而,Undertow 有一些 令人担忧 的中央:

  1. NIO 框架采纳的是 XNIO,在官网 3.0 roadmap 申明中提到了将会在 3.0 版本开始,从 XNIO 迁徙到 netty,参考:Undertow 3.0 Announcement。然而,目前曾经过了快两年了,3.0 还是没有公布,并且 github 上 3.0 的分支曾经一年多没有更新了。目前,还是在用 2.x 版本的 Undertow。不晓得是 3.0 目前没必要开发,还是胎死腹中了呢?目前国内的环境对于 netty 应用更加宽泛并且大部分人对于 netty 更加相熟一些,XNIO 利用并不是很多。不过,XNIO 的设计与 netty 大同小异。
  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 ByteBufferBuffer 是一个具备状态的数组,用来承载数据,能够追踪记录曾经写入或者曾经读取的内容。次要属性包含:capacity(Buffer 的容量),position(下一个要读取或者写入的地位下标),limit(以后能够写入或者读取的极限地位)。程序必须通过将数据放入 Buffer,能力从 Channel 读取或者写入数据 ByteBuffer 是更加非凡的 Buffer,它能够以间接内存调配,这样 JVM 能够间接利用这个 Bytebuffer 进行 IO 操作,省了一步复制(具体能够参考我的一篇文章:Java 堆外内存、零拷贝、间接内存以及针对于 NIO 中的 FileChannel 的思考)。也能够通过文件映射内存间接调配,即 Java MMAP(具体能够参考我的一篇文章:JDK 外围 JAVA 源码解析(5)– JAVA File MMAP 原理解析)。所以,个别的 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 剖析),然而简化了很多。咱们只须要配置 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

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 结构器:

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 这个类。目前 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

正文完
 0