带有光纤的可扩展,健壮和标准的Java Web服务
這篇博客文章討論了負載下的基準Web服務性能。 要了解有關Web服務性能理論的更多信息,請閱讀利特爾定律,可伸縮性和容錯 。
使用阻塞和異步IO對Web服務進行基準測試
Web應用程序(或Web服務)如何在負載下,面對各種故障時以及在兩種情況的組合下表現如何,這是我們代碼最重要的特性-當然是正確的。 由于Web服務通常執行非常常見的操作-詢問緩存,數據庫或其他Web服務以收集數據,將其組合并返回給調用方-因此,這種行為主要取決于Web框架/服務器及其架構的選擇。 在先前的博客文章中 ,我們討論了利特爾定律,并將其應用于分析Web服務器采用的不同體系結構方法的理論限制。 這篇文章(對該文章的補充)重新討論了同一主題,只是這次我們將在實踐中衡量績效。
Web框架(我用這個術語來指代任何通過運行用戶代碼來響應HTTP請求的軟件環境,無論是被稱為框架,應用程序服務器,Web容器,還是該語言標準庫的一部分),都選擇以下一種兩種架構。 首先是分配一個OS線程,該線程將運行我們的所有代碼,直到請求完成。 這是標準Java servlet , Ruby , PHP和其他環境所采用的方法。 這些服務器中的某些服務器在單個線程中運行所有用戶代碼,因此它們一次只能處理一個請求。 其他人在不同的并發線程上運行并發請求。 這種稱為“每個請求線程”的方法需要非常簡單的代碼。
另一種方法是對一個或多個OS線程(盡可能使用比并發請求數更少的OS線程)使用異步IO并盡可能多地將請求處理代碼調度到多個并發請求。 這是Node.js ,Java 異步servlet和JVM框架(如Vert.x和Play)采用的方法 。 據推測,這種方法的優點是(這正是我們要衡量的)更好的可伸縮性和魯棒性(面對使用率高峰,失敗等),但是為此類異步服務器編寫代碼比為線程編寫代碼更復雜。每個請求的。 代碼的復雜程度取決于使用各種“回調地獄緩解”技術(例如promise和/或其他通常涉及monad的功能編程方法)的使用。
其他環境則試圖將兩種方法的優點結合起來。 在幕后,他們使用異步IO,但是他們沒有讓程序員使用回調或monad,而是為程序員提供了光纖 (又名輕量級線程或用戶級線程),這些光纖消耗很少的RAM并且阻塞開銷可以忽略不計。 這樣,這些環境在保持同步(阻塞)代碼的簡單性和熟悉性的同時,具有與異步方法相同的可伸縮性/性能/魯棒性優點。 這樣的環境包括Erlang , Go和Quasar (將纖維添加到JVM)。
基準測試
- 完整的基準測試項目可以在這里找到。
為了測試兩種方法的相對性能,我們將使用一個簡單的Web服務,該Web服務是使用JAX-RS API用Java編寫的。 測試代碼將模擬微服務的一種常見的現代體系結構,但結果絕不限于微服務的使用。 在微服務架構中,客戶端(Web瀏覽器,手機,機頂盒)將請求發送到單個HTTP端點。 然后,該請求由服務器分解為幾個(通常是很多)其他子請求,這些子請求被發送到各種內部HTTP服務,每個子服務負責提供一種類型的數據或執行一種操作(例如,一個微服務可以負責返回用戶個人資料,另一個微服務負責返回他們的朋友圈)。
我們將對單個主服務進行基準測試,該主服務將發出對一個或兩個其他微服務的調用,并檢查當微服務正常運行或發生故障時主服務的行為。
將通過安裝在http://ourserver:8080/internal/foo的此簡單服務來模擬微服務:
@Singleton @Path("/foo") public class SimulatedMicroservice {@GET@Produces("text/plain")public String get(@QueryParam("sleep") Integer sleep) throws IOException, SuspendExecution, InterruptedException {if (sleep == null || sleep == 0)sleep = 10;Strand.sleep(sleep); // <-- Why we use Strand.sleep rather than Thread.sleep will be made clear laterreturn "slept for " + sleep + ": " + new Date().getTime();} }它所做的就是使用一個sleep查詢參數,該參數指定服務在完成之前應休眠的時間(以毫秒為單位)(最少10 ms)。 這可以模擬可能需要很長時間(或很短時間)才能完成的遠程微服務。
為了模擬負載,我們使用了Photon , Photon是一種非常簡單的負載生成工具,使用Quasar光纖以相對較少的協調遺漏的方式發出大量并發請求并測量其延遲:每個請求都是由新產生的請求發送的纖維,然后依次以恒定速率生成纖維。
我們在三種不同的嵌入式Java Web服務器上測試了該服務: Jetty , Tomcat (嵌入式)和Undertow (為JBoss Wildfly應用程序服務器提供動力的Web服務器)。 現在,由于所有三個服務器均符合Java標準,因此我們為所有三個服務器重用了相同的服務代碼。 不幸的是,沒有用于以編程方式配置Web服務器的標準API,因此,基準測試項目中的大多數代碼都簡單地抽象出了三臺服務器的不同配置API(在JettyServer , TomcatServer和UndertowServer類中)。 Main類僅解析命令行參數,配置嵌入式服務器,并將Jersey設置為JAX-RS容器。
我們已經在c3.8xlarge EC2實例上運行了Load Generator和服務器,分別運行了Ubunto Server 14.04 64位和JDK8。如果您想自己使用基準測試,請按照此處的說明進行操作。
此處顯示的結果是在Jetty上運行測試時獲得的結果。 Tomcat對普通阻止代碼的響應類似,但是使用光纖時,其響應性比Jetty差(這需要進一步研究)。 Undertow的行為與之相反:使用光纖時,其性能與Jetty相似,但是當線程阻塞代碼面臨高負載時,崩潰很快。
配置操作系統
因為我們將在高負載下測試我們的服務,所以需要一些配置才能在操作系統級別上支持它。
我們的/etc/sysctl.conf將包含
net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 1 net.ipv4.tcp_timestamps = 1 net.ipv4.tcp_syncookies = 0 net.ipv4.ip_local_port_range = 1024 65535并因此被加載:
sudo sysctl -p /etc/sysctl.conf/etc/security/limits.conf將包含
* hard nofile 200000 * soft nofile 200000配置垃圾收集
大多數Java垃圾收集器都是基于生成假設的 ,該假設假設大多數對象的壽命很短。 但是,當我們開始使用(模擬的)失敗的微服務測試系統時,它會生成持續數秒的開放連接,然后才斷開。 這種“中等壽命”(即不是很短,但也不能太長)是最糟糕的一種垃圾。 看到默認的GC導致了令人無法接受的暫停,并且不想浪費太多時間來微調GC之后,我們選擇嘗試使用HotSpot的新(ish)G1垃圾收集器。 我們要做的就是選擇一個最大的暫停時間目標(我們選擇了200ms)。 G1表現出色(1),因此我們沒有花更多時間調整收集器。
基準同步方法
這是我們的被測服務的代碼,從同步方法開始,該代碼安裝在/api/service 。 (完整的類,其中還包括HTTP客戶端的配置,可以在此處找到):
@Singleton @Path("/service") public class Service extends HttpServlet {private final CloseableHttpClient httpClient;private static final BasicResponseHandler basicResponseHandler = new BasicResponseHandler();public Service() {httpClient = HttpClientBuilder.create()... // configure.build();}@GET@Produces("text/plain")public String get(@QueryParam("sleep") int sleep) throws IOException {// simulate a call to a service that always completes in 10 ms - service AString res1 = httpClient.execute(new HttpGet(Main.SERVICE_URL + 10), basicResponseHandler);// simulate a call to a service that might fail and cause a delay - service BString res2 = sleep > 0 ? httpClient.execute(new HttpGet(Main.SERVICE_URL + sleep), basicResponseHandler) : "skipped";return "call response res1: " + res1 + " res2: " + res2;} }然后,我們的服務會調用一個或兩個其他微服務,我們可以將其命名為A和B(當然,兩者都是由SimulatedMicroservice )。 雖然服務A總是需要10毫秒才能完成,但是可以模擬服務B以顯示不同的延遲。
假設服務B正常運行,并在工作10毫秒后返回其結果。 這是我們的服務隨時間推移每秒響應1000個請求的方式(服務器使用2000個線程池)。 紅線是同時需要兩種微服務的請求的延遲,綠線是僅觸發對微服務A的調用的請求的延遲:
我們甚至可以將速率提高到3000Hz:
超過3000Hz,服務器會遇到嚴重困難。
現在,我們假設在某個時候,服務B發生故障,導致B以更大的延遲進行響應。 比方說5000毫秒 如果我們每秒通過300個觸發服務A和B的請求以及另外10個僅觸發A(這是控制組)的請求到達服務器,則該服務將按應有的方式執行:觸發B的那些請求會增加延遲,但是繞過它的人不受影響。
但是,如果我們隨后將請求速率提高到400Hz,則會發生一些不良情況:
這里發生了什么? 當服務B失敗時,觸發主服務的對主服務的請求將長時間阻塞,它們中的每一個都持有一個線程,直到請求完成,該線程才能返回到服務器的線程池。 線程開始堆積,直到耗盡服務器的線程池為止,此時,沒有請求-甚至沒有嘗試使用失敗的服務的請求-都無法通過,服務器實質上崩潰了。 這被稱為級聯故障 。 單個失敗的微服務可以關閉整個應用程序。 我們怎樣做才能減輕這種故障?
我們可以嘗試進一步增加最大線程池大小,但最大限制為(相當低)。 OS線程給系統帶來了兩種負擔:第一,它們的堆棧消耗相對大量的RAM;第二,它們的堆棧占用大量RAM。 使用該RAM來存儲數據緩存的響應式應用程序要好得多。 其次,將多個線程調度到相對較少的CPU內核上會增加不可忽略的開銷。 如果服務器僅執行很少的CPU密集型計算(通常是這種情況;服務器通常只是從其他來源收集數據),則調度開銷可能會變得很大。
當我們將線程池大小增加到5000時,我們的服務器性能會更好。 在500Hz的頻率下,它仍然運行良好:
在700 Hz時,它搖搖欲墜:
…并在我們增加費率時崩潰。 但是,一旦我們將線程池大小增加到6000,其他線程便無濟于事。 這是在1100Hz下具有6000個線程的服務器:
這里有7000個線程,處理相同的負載:
我們可以嘗試在微服務調用上設置超時。 超時始終是一個好主意,但是選擇什么超時值? 太低了,我們可能使應用程序的可用性降低了。 太高,我們還沒有真正解決問題。
我們還可以安裝一個斷路器,例如Netfilx的Hystrix ,它將嘗試快速發現問題并隔離發生故障的微服務。 像超時一樣,斷路器始終是個好主意,但是如果我們可以顯著提高電路的容量,我們可能應該這樣做(并且為了安全起見,仍然要安裝斷路器)。
現在,讓我們看看異步方法的發展。
對異步方法進行基準測試
異步方法不為每個連接分配線程,而是使用少量線程來處理大量IO事件。 Servlet標準現在除了阻塞API之外還支持異步API,但是由于沒有人喜歡回調(特別是在具有共享可變狀態的多線程環境中),因此很少有人使用它。 Play框架還具有異步API,為了減輕與異步代碼始終相關的某些麻煩,Play用功能性編程的Monadic組合替換了簡單的回調。 Play API不僅是非標準的,對于Java開發人員來說也感覺很陌生。 這也無助于減少與無法避免競爭條件的環境中運行異步代碼相關的問題。 簡而言之,異步代碼是一團糟。
但是,我們仍然可以使用光纖測試這種方法的行為,同時保持我們的代碼美觀,簡單和阻塞。 我們仍將使用異步IO,但是丑陋對我們完全隱藏了。
對
Comsat是一個開源項目,將標準或流行的Web相關API與Quasar光纖集成在一起。 這是我們的服務,現在使用Comsat( 此處為全班制):
@Singleton @Path("/service") public class Service extends HttpServlet {private final CloseableHttpClient httpClient;private static final BasicResponseHandler basicResponseHandler = new BasicResponseHandler();public Service() {httpClient = FiberHttpClientBuilder.create() // <---------- FIBER....build();}@GET@Produces("text/plain")@Suspendable // <------------- FIBERpublic String get(@QueryParam("sleep") int sleep) throws IOException {// simulate a call to a service that always completes in 10 ms - service AString res1 = httpClient.execute(new HttpGet(Main.SERVICE_URL + 10), basicResponseHandler);// simulate a call to a service that might fail and cause a delay - service BString res2 = sleep > 0 ? httpClient.execute(new HttpGet(Main.SERVICE_URL + sleep), basicResponseHandler) : "skipped";return "call response res1: " + res1 + " res2: " + res2;} }該代碼與我們的線程阻止服務相同,除了幾行(用箭頭標記)和Main類中的一行。
當B正確執行時,一切都很好(當服務器處理前幾個請求時,您會在控制臺上看到一些警告,提示光纖占用了太多的CPU時間。沒關系。這只是執行的初始化代碼):
事不宜遲,以下是我們的光纖服務(使用40個OS線程,這是Jetty的最小線程池大小),頻率為3000Hz:
在5000Hz時:
在6000Hz頻率下需要一些時間才能完全預熱,但隨后會收斂:
現在,讓我們踢出問題的微服務,即我們親愛的服務B,以使其經歷5秒的延遲。 這是我們的服務器,頻率為1000Hz:
在2000Hz時:
使用故障服務B響應請求時,除了偶爾出現尖峰以外,航行仍然平穩,但是僅撞到A的人什么也沒有。 在4000Hz時,它開始顯示出一些明顯的但不是災難性的抖動:
每秒需要處理5000個請求(在失敗條件下!),以使服務器無響應。 糟糕的是,服務B可能會導致20秒的延遲,但是我們的服務器仍然可以每秒處理1500次觸發失敗服務的請求,而那些未達到錯誤服務的請求甚至都不會注意到:
那么,這是怎么回事? 當服務B開始顯示非常高的延遲時,服務于調用B的請求的光纖會堆積一段時間,但是由于我們可以擁有這么多的光纖,并且由于它們的開銷如此之低,系統很快就達到了一個新的穩態-數以萬計的阻塞光纖,但這完全可以!
進一步擴大我們的能力
因為我們的Web服務向微服務發出傳出請求,并且因為我們現在可以處理很多并發請求,所以我們的服務最終可能會遇到另一個操作系統限制。 每個傳出的TCP套接字都捕獲一個臨時端口 。 我們已經將net.ipv4.ip_local_port_range設置為1024 65535 ,總共65535 – 1024 = 64511傳出連接,但是我們的服務可以處理更多內容。 不幸的是,我們不能再提高此限制,但是由于此限制是針對每個網絡接口的,因此我們只能定義虛擬接口 ,并讓傳出請求隨機或基于某種邏輯選擇一個接口。
結論
光纖使用戶能夠享受異步IO,同時保持簡單和標準的代碼。 因此,我們通過異步IO獲得的好處不是減少延遲(我們尚未進行基準測試,但是沒有理由相信它比純線程阻塞IO更好),但是容量顯著增加。 系統的穩定狀態支持更高的負載。 異步IO可以更好地利用硬件資源。
當然,這種方法也有缺點。 其中最主要的(實際上,我認為這是唯一的)是庫集成。 我們在光纖上調用的每個阻塞API都必須專門支持光纖。 順便說一下,這并非僅是輕量級線程方法獨有:要使用異步方法,所有使用的IO庫也必須是異步的。 實際上,如果庫具有異步API,則可以輕松地將其轉換為光纖阻塞的API。 Comsat項目是一組將標準或流行的IO API與Quasar光纖集成在一起的模塊。 Comsat的最新版本支持servlet,JAX-RS服務器和客戶端以及JDBC。 即將發布的版本(以及基準中使用的版本)將增加對Apache HTTP客戶端,Dropwizard,JDBI,Retrofit以及可能的jOOQ的支持。
翻譯自: https://www.javacodegeeks.com/2015/04/scalable-robust-and-standard-java-web-services-with-fibers.html
總結
以上是生活随笔為你收集整理的带有光纤的可扩展,健壮和标准的Java Web服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用随机数发生器射击自己的脚
- 下一篇: 鞋柜的设置(鞋柜的设置弱电箱)