undertow,jetty和tomcat能夠說是javaweb項目當下最火的三款服務器,tomcat是apache下的一款重量級的服務器,不用多說歷史悠久,經得起實踐的考驗。然而:當下微服務興起,spring boot ,spring cloud 愈來愈熱的狀況下,選擇一款輕量級而性能優越的服務器是必要的選擇。spring boot 完美集成了tomcat,jetty和undertow,本文將經過對jetty和undertow服務器的分析以及測試,來比較兩款服務器的性能如何。java
值得一提的是jetty和undertow都是基於NIO實現的高併發輕量級的服務器,支持servlet3.1和websocket。因此,有必要先了解下什麼是NIO。web
NIO(非阻塞式輸入輸出)spring
Client和Server只向Buffer讀寫數據不關注數據的流向,數據經過Channel通道進行流轉。而Selector是存在與服務端的,用於Channel的註冊以此實現數據I/O操做。Acceptor負責接受因此的鏈接通道而且註冊到Channel中。而整個過程客戶端與服務端是非阻塞的也就是異步操做。apache
Jetty和Undertow主要配置tomcat
對於服務器端而言咱們關心的重點不是鏈接超時時間,socket超時時間以及任務的超時時間等的配置,重點是線程池設置,包括工做線程,I/O線程的分配。Jetty在這方面彷佛有點太隨意,全局使用一個線程池QueuedThreadPool,而最小線程數8最大200,Acceptor線程默認1個,Selector線程數默認2個。而Undertow就比較合理點,Acceptor經過遞歸循環註冊,而用於I/O的線程默認是cpu的線程數,而工做線程是cpu線程數*8。服務器
對於服務器而言,如何分配線程能夠提升服務器的併發性能。因此,下面將分析兩款服務器的詳細配置。websocket
服務器如何實現通道的註冊網絡
Jetty能夠設置acceptors的線程數默認是1個。詳細實現以下:架構
protected void doStart() throws Exception { if(this._defaultProtocol == null) { throw new IllegalStateException("No default protocol for " + this); } else { this._defaultConnectionFactory = this.getConnectionFactory(this._defaultProtocol); if(this._defaultConnectionFactory == null) { throw new IllegalStateException("No protocol factory for default protocol \'" + this._defaultProtocol + "\' in " + this); } else { SslConnectionFactory ssl = (SslConnectionFactory)this.getConnectionFactory(SslConnectionFactory.class); if(ssl != null) { String i = ssl.getNextProtocol(); ConnectionFactory a = this.getConnectionFactory(i); if(a == null) { throw new IllegalStateException("No protocol factory for SSL next protocol: \'" + i + "\' in " + this); } } super.doStart(); this._stopping = new CountDownLatch(this._acceptors.length); for(int var4 = 0; var4 < this._acceptors.length; ++var4) { AbstractConnector.Acceptor var5 = new AbstractConnector.Acceptor(var4, null); this.addBean(var5); this.getExecutor().execute(var5); } this.LOG.info("Started {}", new Object[]{this}); } } }
加黑地方就是啓動全部的acceptors線程,如下是線程詳細執行過程。併發
public void run() { Thread thread = Thread.currentThread(); String name = thread.getName(); this._name = String.format("%s-acceptor-%d@%x-%s", new Object[]{name, Integer.valueOf(this._id), Integer.valueOf(this.hashCode()), AbstractConnector.this.toString()}); thread.setName(this._name); int priority = thread.getPriority(); if(AbstractConnector.this._acceptorPriorityDelta != 0) { thread.setPriority(Math.max(1, Math.min(10, priority + AbstractConnector.this._acceptorPriorityDelta))); } AbstractConnector stopping = AbstractConnector.this; synchronized(AbstractConnector.this) { AbstractConnector.this._acceptors[this._id] = thread; } while(true) { boolean var24 = false; try { var24 = true; if(!AbstractConnector.this.isRunning()) { var24 = false; break; } try { Lock stopping2 = AbstractConnector.this._locker.lock(); Throwable var5 = null; try { if(!AbstractConnector.this._accepting && AbstractConnector.this.isRunning()) { AbstractConnector.this._setAccepting.await(); continue; } } catch (Throwable var41) { var5 = var41; throw var41; } finally { if(stopping2 != null) { if(var5 != null) { try { stopping2.close(); } catch (Throwable var38) { var5.addSuppressed(var38); } } else { stopping2.close(); } } } } catch (InterruptedException var43) { continue; } try { AbstractConnector.this.accept(this._id); } catch (Throwable var40) { if(!AbstractConnector.this.handleAcceptFailure(var40)) { var24 = false; break; } } } finally { if(var24) { thread.setName(name); if(AbstractConnector.this._acceptorPriorityDelta != 0) { thread.setPriority(priority); } AbstractConnector stopping1 = AbstractConnector.this; synchronized(AbstractConnector.this) { AbstractConnector.this._acceptors[this._id] = null; } CountDownLatch stopping4 = AbstractConnector.this._stopping; if(stopping4 != null) { stopping4.countDown(); } } } } thread.setName(name); if(AbstractConnector.this._acceptorPriorityDelta != 0) { thread.setPriority(priority); } stopping = AbstractConnector.this; synchronized(AbstractConnector.this) { AbstractConnector.this._acceptors[this._id] = null; } CountDownLatch stopping3 = AbstractConnector.this._stopping; if(stopping3 != null) { stopping3.countDown(); } }
能夠看到經過while循環監聽全部創建的鏈接通道,而後在將通道submit到SelectorManager中。
Undertow就沒有這方面的處理,經過向通道中註冊Selector,使用ChannelListener API進行事件通知。在建立Channel時,就賦予I/O線程,用於執行全部的ChannelListener回調方法。
SelectionKey registerChannel(AbstractSelectableChannel channel) throws ClosedChannelException { if(currentThread() == this) { return channel.register(this.selector, 0); } else if(THREAD_SAFE_SELECTION_KEYS) { SelectionKey task1; try { task1 = channel.register(this.selector, 0); } finally { if(this.polling) { this.selector.wakeup(); } } return task1; } else { WorkerThread.SynchTask task = new WorkerThread.SynchTask(); this.queueTask(task); SelectionKey var3; try { this.selector.wakeup(); var3 = channel.register(this.selector, 0); } finally { task.done(); } return var3; }
因此:不管設計架構如何能夠看到兩個服務器都是基於NIO實現的,並且都有經過Selector來執行全部的I/O操做,經過IP的hash來將Channel放入不一樣的WorkThread或SelectorManager中,而後具體的處理工做有線程池來完成。全部,我我的認爲Jetty的selectors數和Undertow的IOThreads數都是用於Selector或說是作I/O操做的線程數。不一樣的是Jetty全局線程池。而對於兩個服務器的承載能力以及讀寫效率,包括LifeCycle過程的管理等,決定了兩個服務器性能的好壞。畢竟用於工做的線程全部的開銷在於業務,全部我的以爲:I/O操做,管理與監聽,決定了兩個服務器的優劣。
Jetty和Undertow壓測分析
準備工具:
項目準備:
Jetty:acceptors=1,selectors=2, min and max threads=200
Undertow: work_threads=200,io_threads=2
壓測梯度:
siege -c 50 -r 2000 -t 2 --log=/Users/maybo/joinit_200.log http://127.0.0.1:8080/test
siege -c 80 -r 2000 -t 2 --log=/Users/maybo/joinit_200.log http://127.0.0.1:8080/test
siege -c 100 -r 2000 -t 2 --log=/Users/maybo/joinit_200.log http://127.0.0.1:8080/test
測試結果:
服務器 | 命中 | 成功率 | 吞吐量 | 平均耗時 |
Jetty | 11488 | 100% | 96.25 trans/sec | 0.00sec |
18393 | 100% | 153.92 trans/sec | 0.01sec | |
21484 | 99.99% | 179.51 trans/sec | 0.01sec | |
Undertow | 11280 | 100% | 94.02 trans/sec | 0.00sec |
19442 | 100% | 163.35 trans/sec | 0.01sec | |
23277 | 100% | 195.54 tran/sec | 0.01sec | |
Tomcat | 10845 | 100% | 90.95 trans/sec | 0.02sec |
21673 | 99.98% | 181 trans/sec | 0.01sec | |
25084 | 99.98% | 209.10 trans/sec | 0.01sec |
從中能夠看出在高負載下Undertow的吞吐量高於Jetty並且隨着壓力增大Jetty和Undertow成功率差距會拉大。而在負載不是太大狀況下服務器處理能力差很少,jetty還略微高於Undertow。而tomcat的負載能力彷佛和Undertow很接近。
對比三個服務器發如今Undertow在負載太重狀況下比Jetty和Tocmat更加頑強,實踐證實在負載繼續加大狀況下Undertow的成功率高於其它二者,可是在併發不是太大狀況下三款服務器總體來看差異不大。這次測試網絡傳輸數據量過小,因此沒有經過不斷加大數據傳輸量來觀察負載狀況,我的決定測試一款服務器的I/O狀況,還要經過改變數據傳輸量來看看在大數據文本高負載下三款服務器的性能。
大數據量測試
使用1892byte回覆數據測試三款服務器性能,下面是開啓線程執行狀況圖。
Undertow:
Jetty:
Tomcat:
實驗過程發現Undertow和Tomcat的負載能力很接近可是Undertow比較好點,而Jetty遠遠不足。經過觀察以上三張圖不難發現,Undertow的I/O線程執行100% , Tomcat的執行也是100%二者不一樣的是Undertow用於I/O的線程數是能夠調整的,而Tomcat不能夠,起碼經過spring boot 沒法調整,這樣就制約了它的負載能力。而Jetty因爲全局共享線程池因此,會存在Selector和Acceptor阻塞狀況,這樣就制約了I/O操做。可是有個好處就是在負載不是過重的狀況下可使工做線程有更多佔用資源來處理程序,提升了吞吐量。可是,整體而言這種差距是很小的。
結論:
本篇不在分析三款服務器孰好孰壞,意在經過分析源碼理解服務器實現原理,而後搞清楚服務器配置參數的意義,更好的爲項目服務。