在JVM上对高并发HTTP服务器进行基准测试
在第一篇有關HTTP客戶端的文章 (我將您重定向到JVM上的高效HTTP的介紹)之后,現在讓我們討論HTTP 服務器 。
有一些關于HTTP服務器的基準測試,但是它們經常受到諸如以下缺點的阻礙:
- 沒有有效地執行高并發方案,并且更普遍地考慮了不現實和不具有代表性的方案,例如:
- 純開銷方案,其中請求處理基本上為零(實際上,總要進行一些處理)。
- 實際不連接,隔離和/或確定負載生成和負載目標過程的尺寸。
- 沒有分配可比的系統資源來加載目標。
- 不包括足夠廣泛的方法(例如,僅專注于“同步”或“異步”服務器)。
- 不分析數據以產生結果 。
我們將分析新的基準,這些基準試圖解決上述問題,并且產生了非常有趣的,有時甚至是意外的結果:
- 如果請求的壽命不是很長,同步的線程阻塞Jetty和Undertow服務器也可以有效地用于高并發情況。
- 當請求需要保持很長時間(例如,長輪詢,服務器推送)時,異步服務器和使用Quasar光纖的服務器可以支持更多并發活動請求。
- 異步API相當復雜,而光纖則很簡單(與傳統的線程阻塞一樣多),并且不僅在高并發情況下而且在各種情況下都表現出色。
基準和負載生成器
高并發方案已成為重要的使用和基準案例。 它們可能是由于特定功能(例如聊天)和/或有時是不希望的技術狀況(例如“冥想”數據層)導致的長期請求所導致的。
和以前一樣 ,使用的負載生成器是Pinterest的jbender ,后者又基于Quasar和Comsat的HTTP客戶端 。 多虧Quasar光纖, jbender甚至支持來自單個節點的大量并發請求,一個不錯的同步API,并且作為負載測試框架而不是單個工具,它提供了很多靈活性和便利性(包括例如響應驗證)。
在基準測試特定的等待之后,加載目標提供最小的“ hello” HTTP響應1 ,這是測試并發的一種非常簡單的方法:等待時間越長,請求持續時間和加載目標必須支持的并發級別越高。避免請求隊列。
對于負載情況,只有第一個基準是最大并發基準,它的唯一目的是測量可以同時處理的實際最大請求數。 為此,它會啟動盡可能多的請求,并讓它們等待很長時間。 其余所有負載情況都是目標速率 ,這意味著它們衡量負載目標在某個目標請求頻率下的行為方式,而不管它們是否可以(或不能)以足夠快的速度分派請求2 。 更詳細的描述如下:
由于我們正在測試JVM服務器,并且HotSpot JVM包含JIT配置文件引導的優化編譯器 ,因此在上述基準測試2-4之前,我始終以固定的并發級別1000個請求運行100k請求的預熱。 每個圖都是10次運行的最佳結果,在這些運行中,既未停止裝入目標也未停止裝入生成器,以便為JVM提供最佳機會來優化代碼路徑。
基于comsat-httpclient (基于Apache的異步HTTP客戶端 4.1 )的JBender負載生成器已用于基準測試1、2和3以及預熱階段,而comsat-okhttp (基于OkHttp 2.6異步) ,它對于短命的請求往往表現更好,已用于基準測試4。二者的設置如下:
- 沒有重試。
- 1h讀/寫超時。
- 最大連接池。
- 工作線程的數量等于內核的數量。
- 禁用Cookie,以便每個請求都屬于一個新創建的會話3 。
系統篇
已采取一些系統預防措施:
- 我們不希望負載生成器和服務器進程相互竊取資源,因此必須將它們分開到足以實現資源隔離的程度。
- 我們不想讓負載生成器成為瓶頸,因此讓它使用大量資源(相對于服務器)和最佳JVM性能設置(當然,我們也希望服務器使用)是最安全的。
- 我們希望網絡實際上在那兒,以便我們模擬一個現實的場景,但我們也希望它盡可能快,以免它也不會成為瓶頸。
考慮到上述注意事項,已設置以下基準測試AWS環境:
- 加載目標 :
- AWS EC2 Linux m4.large(8 GB,2 vcpus,具有增強的網絡性能,網絡性能中等)
- 負載生成器 :
- AWS EC2 Linux m4.xlarge(16 GB,4 vcpus,具有增強網絡功能的高性能網絡)
與“ t”等其他類型相比,AWS EC2“ m”個虛擬實例旨在提供更可預測的性能。
AWS內部網絡承擔基準負載,并且實例位于相同的區域和可用的區域中,以實現最佳連接。
有關JVM設置的一些注意事項:
- 負載生成器使用了12GB的堆內存。 G1垃圾收集器試圖最大程度地減少暫停并保持高吞吐量,已成為6GB堆以上的可行選擇,并且已被用來在負載生成期間最大程度地減少抖動。
- 加載目標使用了4GB的堆內存; 這是一個適中的數量,但不足以利用G1,因此已改用默認的吞吐量優化收集器。 基本原理是代表服務器環境,在該環境中內存可用性足夠但仍然受到一定限制(例如,出于成本原因,例如基于云的服務器機群)。
基于JBender的建議略有不同,已在負載生成器和服務器系統上執行了Linux OS調整。
負載目標和負載生成器代碼
這些基準測試的代碼最初是由nqzero的jempower派生的 ,該功能在最近的基準測試文章中 jempower介紹,該文章又來自TechEmpower的。 使用Capsule (而不是腳本)將其轉換為完整的JVM,多模塊Gradle項目。
為了使處理程序與服務器技術和負載目標分離,代碼也進行了實質性的重構,每個處理程序都將處理程序與支持其API的技術集成在一起。 還對其進行了重構,以共享盡可能多的邏輯和設置。
我還為線程阻塞和Comsat(光纖阻塞)同步API以及有趣的異步變量添加了更多的加載目標,并且由于庫似乎沒有維護,我刪除了Kilim目標。
匹配的API和服務器技術:加載目標
基準測試包含基于多種API和服務器技術的多個負載目標:
- 以下服務器技術上的標準同步JEE Servlet API:
- Undertow 1.3.15.Final
- 標準異步JEE Servlet API( startAsync &friends,3.0 +),具有容器提供的執行器( dispatch )和用戶提供的執行器( complete ),它們都與上述服務器技術相同。
- 非標準化的Comsat Web Actors API 0.7.0-SNAPSHOT ( 0.6.0 ,進一步修復和改進了Web actor),它將傳入(請求)和出站(響應)隊列附加到接收傳入請求的真正的輕量級順序進程(光纖)并通過簡單,同步和有效的(特別是光纖阻塞而不是線程阻塞) receive和send操作發送響應。 這些過程是成熟的Erlang風格的參與者 4 。 目前,Web Actor可以在Servlet容器上運行,既可以作為Undertow處理程序 ,也可以作為本地Netty處理程序運行 ; 本機Netty和Undertow部署已包含在基準測試中。 Netty版本是4.0.34.Final和Undertow與上面相同。
- 與上述相同的Jetty上的非標準化Jetty嵌入式API (同步和異步5) 。
- 與上述相同的Undertow上的非標準化的Undertow處理程序API (同步和異步)。
- 使用Jetty 9.3.2.v20150730的非標準化Spark服務器/處理程序API 2.3 。
同步處理程序是最簡單的處理程序:它們將在啟動該請求的同一OS線程(或使用Comsat時使用fibre )中執行整個請求處理。 通過直接的線程(或光纖)睡眠來實現響應之前的等待。
異步處理程序更加復雜,因為它們推遲了請求的完成,并且需要執行其他簿記和調度工作。 所有這些都將通過立即將待處理的請求存儲在靜態數組中開始,之后每隔10毫秒安排一次的TimerTask從中將它們提取以進行處理,這時策略會根據處理程序而有所不同:
- 使用dispatch異步處理程序會將請求處理作業調度到服務器提供的執行程序。 當等待時間不為0時,將通過直接線程休眠來實現。
- 其他異步處理程序不依賴服務器提供的執行程序,而是使用以下不同策略開始處理請求。 但是,如果等待時間不為0,則它??們將全部將完成作業進一步調度到ScheduledExecutorService :這模擬了一個完全非阻塞的實現,其中通過異步API執行外部(例如,DB,微服務等)調用也一樣 ScheduledExecutor的最大線程數將與服務器提供的執行器的最大線程數相同。
- FJP :使用默認設置將請求處理作業分派到fork-join池。
“每個會話” Web Actor的目標是每個會話產生一個actor,并且由于禁用了cookie,這意味著每個請求都由由其自己的光纖6支持的不同actor處理。
HTTP服務器資源設置偏向于基于線程的同步技術,該技術可以使用比異步/光纖線程更多的OS線程:這是因為實際上,如果要以高并發性使用它們,將被迫使用場景。 除此之外,相同的HTTP服務器設置已盡可能統一地使用:
- 同步服務器以及使用dispatch異步服務器在進行區分的Undertow上最多使用了5k I / O線程以及5k工作者線程,在Tomcat,Jetty和Spark上最多使用了10k通用處理線程。
- 在Tomcat,Jetty和Netty上運行的其他異步服務器最多使用100個處理線程。
- 在Undertow上運行的其他異步服務器最多可以使用50個I / O線程和50個輔助線程。
- 套接字接受隊列(又稱積壓)最多可以保持10k個連接。
- 會話有效期為1分鐘。
- 對于Tomcat,Jetty,Netty和Undertow TCP_NODELAY明確設置為true 。
- 對于Jetty,Netty和Undertow SO_REUSEADDR顯式設置為true 。
數據
您可以直接訪問基準的電子表格,這是統計信息:
| 加載目標 | 最高 | 錯誤編號 | 時間平均(毫秒) | 最長時間(毫秒) | 錯誤(#) | 時間平均(毫秒) | 最長時間(毫秒) | 誤差(%) | 時間平均(毫秒) | 最長時間(毫秒) | 錯誤(#) |
| Comsat Jetty Servlet同步 | 54001 | 0 | 1000.777 | 1088.422 | 0 | 110.509 | 1103.102 | 0 | 189.742 | 3015.705 | 0 |
| Jetty Servlet同步 | 9997 | 0 | 1000.643 | 1044.382 | 0 | 112.641 | 1114.636 | 0 | 222.452 | 2936.013 | 0 |
| Jetty Servlet異步(調度) | 9997 | 0 | 1005.828 | 1083.179 | 0 | 121.719 | 1173.357 | 0 | 289.229 | 3066.036 | 0 |
| Jetty Servlet Aync(FJP /隊列) | 45601 | 4435 | 1005.769 | 1041.236 | 0 | 119.819 | 1120.928 | 0 | 281.602 | 5700.059 | 0 |
| 碼頭同步 | 9997 | 54 | 1000.645 | 1043.857 | 0 | 113.508 | 1143.996 | 0 | 193.487 | 1779.433 | 0 |
| 碼頭異步(FJP /完整) | 47970 | 1909年 | 1005.754 | 1041.76 | 0 | 109.067 | 1120.928 | 0 | 266.918 | 4408.214 | 0 |
| 碼頭異步(調度) | 9997 | 0 | 1005.773 | 1045.43 | 0 | 127.65 | 1385.169 | 0 | 397.948 | 4626.317 | 0 |
| Spark(碼頭)Spark Handler | 9997 | 58 | 1000.718 | 1245.708 | 0 | 134.482 | 3118.465 | 0 | 391.374 | 7021.265 | 0 |
| Comsat Tomcat Servlet同步 | 26682 | 13533 | 1000.636 | 1039.139 | 0 | 不適用 | 不適用 | 不適用 | 307.903 | 5523.898 | 0 |
| Tomcat Servlet同步 | 9999 | 0 | 1000.625 | 1087.373 | 0 | 不適用 | 不適用 | 不適用 | 329.06 | 7239.369 | 0 |
| Tomcat Servlet異步(調度) | 9999 | 0 | 1005.986 | 1108.345 | 0 | 不適用 | 不適用 | 不適用 | 289.703 | 4886.364 | 0 |
| Tomcat Servlet異步(FJP /完整) | 9999 | 29965 | 1005.891 | 1041.76 | 0 | 不適用 | 不適用 | 不適用 | 159.501 | 4483.711 | 0 |
| Comsat Undertow Servlet同步 | 53351 | 0 | 1000.648 | 1060.635 | 0 | 107.757 | 1309.671 | 0 | 204.795 | 4273.996 | 0 |
| Undertow Servlet同步 | 4999 | 7758 | 1000.723 | 1089.47 | 0 | 110.599 | 1319.109 | 0 | 193.436 | 4307.55 | 0 |
| Undertow Servlet異步(調度) | 4999 | 576 | 1006.011 | 1123.025 | 0 | 1756.198 | 15183.38 | 83 | 697.811 | 6996.099 | 0 |
| Undertow Servlet異步(FJP /完整) | 52312 | 1688 | 1005.81 | 1071.645 | 0 | 108.324 | 1113.588 | 0 | 214.423 | 4408.214 | 0 |
| 同步下 | 4999 | 0 | 1000.644 | 1049.625 | 0 | 108.843 | 3114.271 | 0 | 316.991 | 4789.895 | 0 |
| Undertow異步(調度) | 49499 | 4501 | 1005.742 | 1162.871 | 0 | 121.554 | 3116.368 | 0 | 318.306 | 5486.15 | 0 |
| Undertow異步(FJP /隊列) | 33720 | 0 | 1005.656 | 1040.712 | 0 | 109.899 | 1113.588 | 0 | 236.558 | 3632.267 | 0 |
| Comsat Netty網絡演員 | 53448 | 0 | 1000.701 | 1085.276 | 0 | 107.697 | 1106.248 | 0 | 320.986 | 2917.138 | 0 |
| Comsat Undertow網絡演員 | 53436 | 0 | 1000.674 | 1037.042 | 0 | 123.791 | 3118.465 | 0 | 358.97 | 7046.431 | 0 |
這是圖形:
結果
錯誤主要是“連接重置”(可能是由于接受時變慢),盡管在極端情況下,處理變慢導致并發超過了網絡接口可用的端口數。
一些特定于基準的注意事項:
請求完成時間的分布也有一些考慮因素:該基準測試的負載生成器使用了由JBender提供的基于Gil Tene的HDRHistogram的事件記錄器。 您可以直接訪問直方圖數據。
關于“慢速請求”基準的最短最長時間的直方圖顯示,Comsat Tomcat Servlet(次優)在絕對最小的1秒(睡眠時間)的1毫秒內完成了100000個請求中的98147個請求,而其余請求的完成時間分布在1001.39毫秒和1039.139毫秒之間(最大值):
Comsat Undertow Servlet的最大延遲最短,但平均延遲稍差,因為它在1001毫秒內完成了約96%的請求,而其余的則均勻地分布到1037.042毫秒(最大):
另一方面,Spark(最差的)的分布不太均勻:它在1001ms(99221)內完成的工作更多,但很少有其他請求可以占用1245.708ms(最大):
在“ Realistic High Concurrency”中,Comsat Jetty Servlet產生了最短的最大延遲,但是線程阻塞Jetty Servlet的目標緊隨其后:它在101ms內完成了78152個請求(最小等于100ms的睡眠時間)并完成了其余的分布在兩個不同的群集中,一個從100ms到367ms有規律地分布,另一個大約1100ms到1114.636ms的最大值分布:
Comsat Jetty Servlet的目標行為非常相似:75303個請求在101ms內完成,幾乎所有其余請求在328.466ms內完成,只有48個在1097ms左右(最大1103.102ms)內完成:
有趣的是,從主群集到“尾巴”的距離大致對應于該運行的最大GC暫停時間(576毫秒)。
建立在稍舊的9.3 Jetty上的Spark表現出類似的行為,但是第一個集群的時間分布更多(超過一半,或者請求在101ms和391ms之間完成),另外還有大約1300ms和3118ms(其尾部)集群距離太粗略地對應于該運行的最大GC時間,即1774ms):
Comsat Netty Web Actor的分布(每次會話)是不同的:大約66%的分布在101毫秒內完成,而85%的分布在103.5毫秒內,然后直到c??a為止幾乎都是對數分布。 260毫秒,此時有一個中斷,一個群集發生在334毫秒,最后一個群集發生在1098毫秒至1106毫秒之間。 在這種情況下,似乎與GC活動無關,后者與預期的要高得多,并且最大GC時間超過4s無關:
相反,Undertow的GC開銷非常低,包括與Quasar光纖集成時(在后一種情況下,運行6個GC時最長為407ms)。 具體來說,Comsat Undertow Servlet在101毫秒內完成了超過92.5%的請求,一個長達341毫秒的主集群(包括超過99.5%的請求)以及另外兩個似乎與GC活動沒有嚴格關聯的集群:
Undertow Sync的圖形非常相似,主群集更加緊密,在101毫秒內完成了90%以上的請求,從而獲得了非常好的平均值,但附加的尾部群集使最大值進一步超過了3秒。
最終,使用dispatch調用的Undertow異步Servlet的性能最差,并且其多集群分布在15秒內非常緩慢地上升! 群集距離似乎與最大GC運行時間沒有特別的關系:
在“實際開銷”基準中,此負載目標的性能也很差,這表明Undertow可能不太理想地實現了dispatch servlet異步調用。
這些觀察結果認為,在中高并發情況下,高延遲似乎與底層網絡/ HTTP技術的關聯更多,而不是與請求處理技術或API的關聯,在某些情況下,更具體地,與敏感性相關。由GC活動引起的抖動。 它還表明分布的主要集群也與基礎網絡/ HTTP技術相關。
除了使用dispatch Undertow Servlet Async之外,“ Realistic Overhead”直方圖顯示了具有2或3個不同趨勢的所有目標所共有的均勻分布的結構:一個關于快速完成的請求,直到一個目標特定的數量,另一個關于包含已完成的剩余請求的請求更慢。
例如,Jetty Sync Handler目標(最佳)在31.457毫秒內完成了75%的請求,而其他請求似乎平均分配到最大1779.433毫秒:
它的GC活性也非常有限(3次運行,最長113ms)。
Tomcat Servlet最糟糕,其中65%的請求在32.621ms內完成,99219個請求在2227ms內完成,并且進一步的趨勢是僅在ca的完成時間增加了5s。 80個請求。 在這種情況下,GC干預也很低(盡管高于Jetty的干預):
經驗教訓
結果導致一些重要的考慮:
- 如果您不處理高并發情??況,則無需考慮異步庫,因為基于光纖和線程的服務器將完美運行,并且同樣重要的是,它們將允許您編寫高度可讀,可維護且面向未來的同步代碼。
- 即使在高并發情況下,也確實無需跳入異步陷阱,因為基于光纖的服務器具有廣泛的適用性:借助Quasar光纖,您可以在一個單一的服務器中獲得非常高的并發性,非常好的通用性能和面向未來的代碼包。
- 必須說,即使在高并發情況下,某些同步的線程阻塞服務器也設法獲得良好的性能,并且確切地了解這絕對是一個有趣的研究。 它們的實際最大并發性比異步或Quasar的低得多,因此,如果您希望盡早開始處理盡可能多的請求,那么使用異步/光纖技術仍然會更好。
- 在零請求處理時間的情況下,即使是同步單線程服務器也可以很好地工作:當請求處理時間增加并且并發效應開始時,麻煩就開始了。
同樣,在運行基準測試時(甚至在分析結果之前)所觀察到的(和錯誤)也強調了充分應對某些JVM特有特征的重要性:
- JVM在使用運行時信息優化代碼方面做得非常出色:如果您不相信我嘗試使用-Xcomp標志(不帶-Xcomp標志)來運行您的應用程序,它將執行預運行JIT,并親自了解如何獲得最佳結果(提示: -Xcomp可能會產生明顯較差的性能)。 另一方面,這意味著逐漸進行JVM預熱是將HTTP服務器暴露于傳入請求之前必須執行的重要步驟,因為未優化的代碼路徑很容易無法跟上突然的高并發和/或高速率負載會導致或多或少的嚴重故障。
- 抖動/打cup是一個嚴重的問題,尤其是對于最大延遲而言,但如果它發生在“糟糕”的時刻(例如,大量傳入請求),它甚至可能使系統屈服。 GC暫停是造成抖動的一個重要原因,因此通常最好仔細考慮一下您的JVM內存設置和將要使用的GC。 特別是,基準測試中的最大延遲似乎受到影響,甚至在某些情況下甚至與GC運行有關。 朝著這個方向的另一個提示是,即使在低并發情況下,由于即使在較簡單的服務器上,GC壓力也會增加,因此使用較小的1GB堆運行的基準測試更喜歡更復雜的技術(異步和光纖)。 這意味著減少GC的數量和持續時間是值得的,但是我們該怎么做呢? 一種方法是準確選擇JVM內存設置,并在可能的情況下使用較低延遲的GC(例如G1或商用JVM Azul Zing) 。 另一種方法是再次選擇最簡單的工具來完成這項工作:如果您不處于高并發情況下,則只需使用最簡單的技術,因為與更復雜的技術相比,它們往往會產生更少的垃圾。
- 出于類似的原因,如果您需要會話,則每個會話的Web Actor很棒,因為它們基本上也像Erlang一樣啟用“每個用戶的Web服務器”范例 。 另一方面,如果您不需要會話或那種可靠性,那么您將獲得GC開銷,因為可能需要為每個請求實例化(然后稍后進行垃圾收集)新的參與者(及其對象圖) 。 這反映在“現實開銷”結果中。
進一步的工作
雖然此基準可以作為您評估的一個很好的起點,但它絕不是詳盡無遺的,并且可以通過許多方式加以改進,例如:
- 添加更多的負載目標。
- 添加基準案例。
- 在其他系統(例如硬件,其他云,其他AWS實例)上進行基準測試。
- 在非Oracle JVM上進行基準測試。
- 使用不同的JVM設置進行基準測試。
- 進一步分析系統數據。
- 研究奇怪的行為,包括令人驚訝的好行為(例如,高并發情況下的Jetty線程阻塞同步服務器)和出奇的壞行為(例如,Undertow的基于dispatch的處理程序和Tomcat Servlet)。
- 更好地分析相關性,例如GC引起的抖動和統計數據之間的相關性。
盡管這是一項昂貴的工作,但我認為通常仍需要更多的基準測試,因為它確實可以更好地理解,改進和評估軟件系統。
結論
此處的主要目標是查看不同的HTTP服務器API和技術在更接近實際的情況下的性能,在這種情況下,具有預定系統資源的單獨的客戶端和服務器JVM進程通過真實網絡進行通信,并且請求處理為非零時間。
事實證明,Quasar光纖可用于構建承受高并發負載的多功能執行器,并且至少同樣重要的是,與異步API相比,它們是更好的軟件編寫工具。 事實再次證明,沒有靈丹妙藥:不同的情況需要不同的解決方案,甚至有時被認為是過時的技術(例如線程阻塞服務器(甚至單線程服務器))也可以勝任。
除了性能以外,API的選擇還應在您的決定中起主要作用,因為這將決定服務器代碼的未來。 根據情況,根據項目的要求和開發環境,非標準API(及其相關風險,采用和退出成本)可能是可行的,也可能不是可行的選擇。 要考慮的另一件事是,異步API比同步API難用得多,并且傾向于通過異步7感染整個代碼庫,這意味著使用異步API可能會妨礙代碼的可維護性并縮短其未來。
就是說,我完全意識到以下事實:性能基準會(盡力而為)對有限的工具和知識不斷變化的格局進行部分盡力而為的努力,并且設計,運行和發布基準是一項艱苦的工作,也是一項重大的投資。 。
我希望這一輪對許多人有用,我將熱烈歡迎和贊賞并鼓勵任何建議,改進和進一步的努力。
翻譯自: https://www.javacodegeeks.com/2016/05/benchmarking-high-concurrency-http-servers-jvm.html
總結
以上是生活随笔為你收集整理的在JVM上对高并发HTTP服务器进行基准测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蓝戟锐炫 A750 / A770 亚运联
- 下一篇: 曝vivo T2 Pro 5G下周推出