本項目代碼地址: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
使用 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 目前(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
ByteBuffer
:Buffer
是一個具備狀態的數組,用來承載數據,能夠追蹤記錄已經寫入或者已經讀取的內容。主要屬性包括:capacity(Buffer 的容量),position(下一個要讀取或者寫入的位置下標),limit(當前能夠寫入或者讀取的極限位置)。程序必須經過將數據放入 Buffer,才能從 Channel 讀取或者寫入數據。ByteBuffer
是更加特殊的 Buffer,它能夠以直接內存分配,這樣 JVM 能夠直接利用這個 Bytebuffer 進行 IO 操做,省了一步複製(具體能夠參考個人一篇文章:Java 堆外內存、零拷貝、直接內存以及針對於NIO中的FileChannel的思考)。也能夠經過文件映射內存直接分配,即 Java MMAP(具體能夠參考個人一篇文章:JDK核心JAVA源碼解析(5) - JAVA File MMAP原理解析)。因此,通常的 IO 操做都是經過 ByteBuffer 進行的。Channel
:Channel 是 Java 中對於打開和某一外部實體(例如硬件設備,文件,網絡鏈接 socket 或者能夠執行 IO 操做的某些組件)鏈接的抽象。Channel 主要是 IO 事件源,全部寫入或者讀取的數據都必須通過 Channel。對於 NIO 的 Channel,會經過 Selector
來通知事件的就緒(例如讀就緒和寫就緒),以後經過 Buffer 進行讀取或者寫入。Worker
: Worker 是 Java XNIO 框架中的基本網絡處理單元,一個 Worker 包含兩個不一樣的線程池類型,分別是:
Selector.start()
處理對應事件的各類回調,原則上不能處理任何阻塞的任務,由於這樣會致使其餘鏈接沒法處理。IO 線程池包括兩種線程(在 XNIO 框架中,經過設置 WORKER_IO_THREADS 來設置這個線程池大小,默認是一個 CPU 一個 IO 線程):
ChannelListener
:ChannelListener 是用來監聽處理 Channel 事件的抽象,包括:channel readable
, channel writable
, channel opened
, channel closed
, channel bound
, channel unbound
Undertow 是基於 XNIO 的 Web 服務容器。在 XNIO 的基礎上,增長:bash
BufferPool
: 若是每次須要 ByteBuffer 的時候都去申請,對於堆內存的 ByteBuffer 須要走 JVM 內存分配流程(TLAB -> 堆),對於直接內存則須要走系統調用,這樣效率是很低下的。因此,通常都會引入內存池。在這裏就是 BufferPool
。目前,UnderTow 中只有一種 DefaultByteBufferPool
,其餘的實現目前沒有用。這個 DefaultByteBufferPool 相對於 netty 的 ByteBufArena 來講,很是簡單,相似於 JVM TLAB 的機制(能夠參考個人另外一系列:全網最硬核 JVM TLAB 分析),可是簡化了不少。咱們只須要配置 buffer size ,並開啓使用直接內存便可。Listener
: 默認內置有 3 種 Listener ,分別是 HTTP/1.一、AJP 和 HTTP/2 分別對應的 Listener(HTTPS 經過對應的 HTTP Listner 開啓 SSL 實現),負責全部請求的解析,將請求解析後包裝成爲 HttpServerExchange
並交給後續的 Handler
處理。Handler
: 經過 Handler 處理響應的業務,這樣組成一個完整的 Web 服務器。Undertow 的 Builder 設置了一些默認的參數,參考源碼:
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 } }
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; } }
其中:
對於 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
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 相關配置的抽象是 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
。