SpringCloud升級之路2020.0.x版-12.UnderTow 簡介與內部原理

本系列代碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford

image

在我們的項目中,我們沒有采用默認的 Tomcat 容器,而是使用了 UnderTow 作爲我們的容器。其實性能上的差異並沒有那麼明顯,但是使用 UnderTow 我們可以利用直接內存作爲網絡傳輸的 buffer,減少業務的 GC,優化業務的表現。其實 Tomcat 也有使用直接內存作爲網絡傳輸的 buffer 的配置,即 Connector 使用 NIO 或者 NIO2,還有 APR 這種基於 JNI 的優化文件與請求傳輸的方式,但是 tomcat 隨着不斷迭代與發展,功能越來越完善以及組件化的同時,架構也越來越複雜,這也帶來了代碼設計與質量上的一些降低。對比 Tomcat Connector 那裏的源代碼與設計,我最終選擇了更爲輕量設計的 Undertow。至於不選 Jetty 的原因和 Tomcat 類似,不選 reactor-netty 的主要原因是項目還是比較新並且不太成熟,並且基於異步回調,很多時候異常處理不全面,導致最後詭異的響應並且異常定位成本比較高

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

  • 紅帽開源產品,目前是 WildFly(JBoss AS)的默認 Web 容器。
  • 在不需要非常複雜的配置情況下,可以展現出很高的性能以及穩定性。
  • 非常輕量,只需通過 API 即可快速搭建 Web 服務器。
  • 底層基於 XNIO,和 Netty 設計類似,使用 NIO 作爲網絡交互的方式,並且使用直接內存作爲網絡傳輸的 buffer,減少業務的 GC。
  • 由於基於這種異步框架,所以配置也是交由鏈式Handler配置和處理請求,可以最小化按需加載模塊,無須加載多餘功能
  • 支持 Servlet 4.0 以及向下兼容,支持 WebSocket

但是,Undertow 有一些令人擔憂的地方:

  1. 官網一股貧窮的氣息,背靠紅帽這個不太靠譜的爹
  2. 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 大同小異。
  3. 官方文檔的更新比較慢,可能會慢 1~2 個小版本,導致 Spring Boot 粘合 Undertow 的時候,配置顯得不會那麼優雅。參考官方文檔的同時,最好還是看一下源碼,至少看一下配置類,才能搞懂究竟是怎麼設置的
  4. 仔細看 Undertow 的源碼,會發現有很多防禦性編程的設計或者功能性設計 Undertow 的作者想到了,但是就是沒實現,有很多沒有實現的半成品代碼。這也令人擔心 Underow 是否開發動力不足,哪一天會突然死掉?

不過,幸好有 spring-boot,在 spring-boot 項目中,切換容器的成本不大,修改依賴即可。同時要注意,不同 web 容器的配置。

image

Undertow 目前(2.x) 還是基於 Java XNIO,Java XNIO 是一個對於 JDK NIO 類的擴展,和 netty 的基本功能是一樣的,但是 netty 更像是對於 Java NIO 的封裝,Java XNIO 更像是擴展封裝。主要是 netty 中基本傳輸承載數據的並不是 Java NIO 中的 ByteBuffer,而是自己封裝的 ByteBuf,而 Java XNIO 各個接口設計還是基於 ByteBuffer 爲傳輸處理單元。設計上也很相似,都是 Reactor 模型的設計。其結構如下所示:

image

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 的優點以及讓我們比較擔心的地方,並且與其他的 Web 容器做了對比。下一節我們將會分析 Undertow 的詳細配置。

微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章