Undertow,Tomcat和Jetty伺服器配置詳解與效能測試
轉載:https://www.cnblogs.com/maybo/p/7784687.html
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.loghttp://127.0.0.1:8080/test
siege -c 80 -r 2000 -t 2 --log=/Users/maybo/joinit_200.loghttp://127.0.0.1:8080/test
siege -c 100 -r 2000 -t 2 --log=/Users/maybo/joinit_200.loghttp://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操作。但是有個好處就是在負載不是太重的情況下可以使工作執行緒有更多佔用資源來處理程式,提高了吞吐量。但是,總體而言這種差距是很小的。
結論:
本篇不在分析三款伺服器孰好孰壞,意在通過分析原始碼理解伺服器實現原理,然後搞清楚伺服器配置引數的意義,更好的為專案服務