undertow,jetty和tomcat可以說是javaweb項目當下最火的三款服務器,tomcat是apache下的一款重量級的服務器,不用多說歷史悠久,經得起實踐的考驗。然而:當下微服務興起,spring boot ,spring cloud 越來越熱的情況下,選擇一款輕量級而性能優越的服務器是必要的選擇。spring boot 完美集成了tomcat,jetty和undertow,本文將通過對jetty和undertow服務器的分析以及測試,來比較兩款服務器的性能如何。
值得一提的是jetty和undertow都是基於NIO實現的高並發輕量級的服務器,支持servlet3.1和websocket。所以,有必要先了解下什么是NIO。
NIO(非阻塞式輸入輸出)
- Channel
- Selector
- Buffer
- Acceptor
Client和Server只向Buffer讀寫數據不關注數據的流向,數據通過Channel通道進行流轉。而Selector是存在與服務端的,用於Channel的注冊以此實現數據I/O操作。Acceptor負責接受所以的連接通道並且注冊到Channel中。而整個過程客戶端與服務端是非阻塞的也就是異步操作。
Jetty和Undertow主要配置
對於服務器端而言我們關心的重點不是連接超時時間,socket超時時間以及任務的超時時間等的配置,重點是線程池設置,包括工作線程,I/O線程的分配。Jetty在這方面似乎有點太隨意,全局使用一個線程池QueuedThreadPool,而最小線程數8最大200,Acceptor線程默認1個,Selector線程數默認2個。而Undertow就比較合理點,Acceptor通過遞歸循環注冊,而用於I/O的線程默認是cpu的線程數,而工作線程是cpu線程數*8。
對於服務器而言,如何分配線程可以提高服務器的並發性能。所以,下面將分析兩款服務器的詳細配置。
服務器如何實現通道的注冊
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壓測分析
准備工具:
- siege用於壓測
- VisualVm用於監測
項目准備:
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操作。但是有個好處就是在負載不是太重的情況下可以使工作線程有更多占用資源來處理程序,提高了吞吐量。但是,總體而言這種差距是很小的。
結論:
本篇不在分析三款服務器孰好孰壞,意在通過分析源碼理解服務器實現原理,然后搞清楚服務器配置參數的意義,更好的為項目服務。