Spring Cloud 升级之路 - 2020.0.x - 2. 使用 Undertow 做为咱们的 Web 服务容器

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

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

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

可是,Undertow 有一些使人担心的地方:github

  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 要注意的问题web

  1. 须要开启 NIO DirectBuffer 的特性,理解并配置好相关的参数。
  2. access.log 中要包括必要的一些时间,调用链等信息,而且默认配置下,有些只配置 access.log 参数仍是显示不出来咱们想看的信息,官网对于 access.log 中的参数的一些细节并无详细说明。

使用 Undertow 做为咱们的 Web 服务容器

对于 Servlet 容器,依赖以下:spring

<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 主要包括以下几个概念:tomcat

  • 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 的基础上,增长:bash

  • Undertow BufferPool: 若是每次须要 ByteBuffer 的时候都去申请,对于堆内存的 ByteBuffer 须要走 JVM 内存分配流程(TLAB -> 堆),对于直接内存则须要走系统调用,这样效率是很低下的。因此,通常都会引入内存池。在这里就是 BufferPool。目前,UnderTow 中只有一种 DefaultByteBufferPool,其余的实现目前没有用。这个 DefaultByteBufferPool 相对于 netty 的 ByteBufArena 来讲,很是简单,相似于 JVM TLAB 的机制(能够参考个人另外一系列:全网最硬核 JVM TLAB 分析),可是简化了不少。咱们只须要配置 buffer size ,并开启使用直接内存便可
  • Undertow Listener: 默认内置有 3 种 Listener ,分别是 HTTP/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

相关文章
相关标签/搜索