Spring Cloud 升級之路 - 2020.0.x - 2. 使用 Undertow 做爲咱們

> 本項目代碼地址: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](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 的時候,配置顯得不會那麼優雅。**參考官方文檔的同時,最好仍是看一下源碼,至少看一下配置類,才能搞懂到底是怎麼設置的**。web

  2. **官方文檔的更新比較慢,可能會慢 1~2 個小版本**,致使 Spring Boot 粘合 Undertow 的時候,配置顯得不會那麼優雅。**參考官方文檔的同時,最好仍是看一下源碼,至少看一下配置類,才能搞懂到底是怎麼設置的**。spring

  3. 仔細看 Undertow 的源碼,會發現有不少防護性編程的設計或者功能性設計 Undertow 的做者想到了,可是就是沒實現,**有不少沒有實現的半成品代碼**。這也使人擔憂 Underow 是否開發動力不足,哪一天會忽然死掉?編程

**使用 Undertow 要注意的問題**:數組

1. 須要開啓 NIO DirectBuffer 的特性,理解並配置好相關的參數。tomcat

2. access.log 中要包括必要的一些時間,調用鏈等信息,而且默認配置下,有些只配置 access.log 參數仍是顯示不出來咱們想看的信息,官網對於 access.log 中的參數的一些細節並無詳細說明。bash

# 使用 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.一、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`。

相關文章
相關標籤/搜索