李思莉,楊井榮,茍 強(qiáng)
(成都理工大學(xué) 工程技術(shù)學(xué)院 電子信息與計(jì)算機(jī)工程系,四川 樂(lè)山 614000)
大量用戶在同一個(gè)時(shí)間點(diǎn)同時(shí)訪問(wèn)某個(gè)相同的站點(diǎn)稱為高并發(fā)。高并發(fā)現(xiàn)象在如今的互聯(lián)網(wǎng)行業(yè)應(yīng)用中非常普遍,如12306鐵路購(gòu)票網(wǎng)站,雙11時(shí)阿里巴巴、京東、唯品會(huì)等電子商務(wù)網(wǎng)站要處理的并發(fā)數(shù)通常都高達(dá)每秒百萬(wàn)級(jí)。但如何處理高并發(fā)卻是一個(gè)非常難的技術(shù)瓶頸。該文研究的是在單機(jī)無(wú)集群的情況[1],以NIO為基礎(chǔ)的同步非阻塞IO,而非傳統(tǒng)的IO方式,結(jié)合Vert.x[2]的事件驅(qū)動(dòng)完成同步通信與異步事件處理的并行運(yùn)算,是數(shù)據(jù)通信部分百萬(wàn)級(jí)別的并發(fā)。并在此研究基礎(chǔ)上利用Java Spring線程池,完成了課外學(xué)分管理系統(tǒng)。通過(guò)大量的實(shí)驗(yàn)數(shù)據(jù),與傳統(tǒng)Web應(yīng)用的IO方式進(jìn)行對(duì)比,得出論文研究并實(shí)現(xiàn)的MVC層的擴(kuò)展、數(shù)據(jù)安全優(yōu)化、同步非阻塞模式與NIO在Web的應(yīng)用中完全能勝任百萬(wàn)級(jí)甚至更高的并發(fā)量的結(jié)論。同時(shí),由于這種異步事件處理方式是基于Spring管理的線程池,在系統(tǒng)擴(kuò)展上,很容易實(shí)現(xiàn)分布式系統(tǒng)完成更多的并發(fā)與集群架設(shè)。
客戶/服務(wù)器模式(C/S)不能應(yīng)對(duì)多平臺(tái)帶來(lái)的開(kāi)發(fā)時(shí)間、開(kāi)發(fā)效率、開(kāi)發(fā)投入等多方面要求,加之各PC之間操作系統(tǒng)不同,為了兼顧過(guò)時(shí)的Windows XP系統(tǒng),在開(kāi)發(fā)PC端系統(tǒng)時(shí)通常出現(xiàn)兩種情況:(1)開(kāi)發(fā)多個(gè)版本;(2)兼顧XP不使用高版本W(wǎng)indows系統(tǒng)的特性和高效率API。這兩種情況都不好,因此在開(kāi)發(fā)學(xué)分管理系統(tǒng)時(shí),放棄C/S架構(gòu),使用B/S[3]架構(gòu)。這套架構(gòu),在客戶端上只需要前端網(wǎng)頁(yè)和可運(yùn)行在系統(tǒng)上的瀏覽器就可滿足用戶對(duì)于多平臺(tái),不同系統(tǒng)設(shè)備的需求,節(jié)約開(kāi)發(fā)時(shí)間和開(kāi)發(fā)成本。在具體的程序內(nèi)部架構(gòu)設(shè)計(jì)上,傳統(tǒng)的三層架構(gòu)已經(jīng)無(wú)法滿足系統(tǒng)高并發(fā)需求,數(shù)據(jù)傳輸中傳統(tǒng)的I/O設(shè)計(jì)模式和傳統(tǒng)的I/O傳輸必將面臨性能瓶頸甚至?xí)?dǎo)致整個(gè)課外學(xué)分管理系統(tǒng)的崩潰。因此在實(shí)際的開(kāi)發(fā)過(guò)程中,將系統(tǒng)設(shè)計(jì)成5層模式,由外向內(nèi)展開(kāi)依次是:
(1)負(fù)責(zé)與前端信息交互的restfulApi層;
(2)負(fù)責(zé)管理處理邏輯的中央組件管理層;
(3)負(fù)責(zé)管理并發(fā)線程的調(diào)度和管理的并發(fā)層;
(4)負(fù)責(zé)處理信息的邏輯層;
(5)負(fù)責(zé)持久化信息的ORM層。
通過(guò)實(shí)驗(yàn)證明,該架構(gòu)在技術(shù)上是可行的,在并發(fā)請(qǐng)求每秒10萬(wàn)數(shù)量級(jí)上依然保持穩(wěn)定。
要完成十萬(wàn)級(jí),百萬(wàn)級(jí)的并發(fā)請(qǐng)求,普通的IO會(huì)導(dǎo)致系統(tǒng)性能急速下降,這將導(dǎo)致系統(tǒng)無(wú)法正常運(yùn)行。因此,在具體的開(kāi)發(fā)實(shí)現(xiàn)中,使用了非阻塞式IO。非阻塞式IO分為異步非阻塞IO和同步非阻塞IO。通過(guò)對(duì)學(xué)分管理系統(tǒng)的需求分析,得出整個(gè)流程不需要消耗很多的等待時(shí)間,因此,采用同步非阻塞IO模式。加之非阻塞IO、零拷貝、事件驅(qū)動(dòng)等特性,在開(kāi)發(fā)生態(tài)圈里有很多經(jīng)驗(yàn)可取,在框架的設(shè)計(jì)上也能利用現(xiàn)有的同步非阻塞IO框架,不必重頭開(kāi)發(fā)底層。
Server層的高并發(fā),著重體現(xiàn)在線程安全上,在數(shù)據(jù)處理上,不能出現(xiàn)很多線程去同時(shí)操作運(yùn)算數(shù)據(jù)的情況。對(duì)于線程安全,在整個(gè)Server層實(shí)現(xiàn)上完全使用了線程安全的數(shù)據(jù)結(jié)構(gòu),如:ConcurrentHashMap,SynchronizeList等,需要注意的是要避免使用過(guò)時(shí)的線程安全的數(shù)據(jù)結(jié)構(gòu),如:vector,HashTable等,這會(huì)降低整體的效率。
除了線程安全的數(shù)據(jù)結(jié)構(gòu),很多方法的邏輯也不允許多線程同時(shí)操作,一般的解決方案是使用Synchronize關(guān)鍵字對(duì)需要加鎖的方法或者代碼塊進(jìn)行修飾,但這是一種悲觀鎖,如果發(fā)生異常,會(huì)出現(xiàn)阻塞,這對(duì)系統(tǒng)是致命的,不僅會(huì)導(dǎo)致后續(xù)的操作掛起,還會(huì)導(dǎo)致程序崩潰。要避免發(fā)生這種情況,在Server層實(shí)現(xiàn)上采用了非阻塞的并發(fā)算法CountDownLatch[4],它是Java提供的原生非阻塞并發(fā)算法,可以有效實(shí)現(xiàn)學(xué)分管理系統(tǒng)的線程同步。
利用數(shù)據(jù)庫(kù)的隔離機(jī)制完成數(shù)據(jù)安全是一種低效的做法。在學(xué)分管理系統(tǒng)持久化層的設(shè)計(jì)中,將數(shù)據(jù)安全因素放到調(diào)用持久化層的Server層里面去實(shí)現(xiàn)[5]。持久化層事務(wù)的傳播機(jī)制統(tǒng)一采用Spring的傳播機(jī)制,并利用緩存技術(shù),減少系統(tǒng)響應(yīng)時(shí)間。在初始化時(shí),采用快速數(shù)據(jù)庫(kù)連接池初始化一個(gè)足夠大的數(shù)據(jù)庫(kù)連接池交給持久化層使用[6]。
并行數(shù)據(jù)接收是并發(fā)的開(kāi)始,這里采用了成熟的模式設(shè)計(jì),即一個(gè)接收的總線Boss線,多個(gè)負(fù)責(zé)傳輸轉(zhuǎn)發(fā)到相應(yīng)處理的Server邏輯的Worker線程,將多個(gè)Worker線程初始化為一個(gè)線程池由Spring統(tǒng)一管理,它存在于整個(gè)Springboot程序中。這個(gè)模式實(shí)現(xiàn)了代碼復(fù)用,減少了初始化、調(diào)用等冗余代碼,也能更好地融合在主框架里。
Restful層的設(shè)計(jì)采用Vert.x Web框架而放棄了低性能的SpringMvc[7],Vert.x是事件驅(qū)動(dòng)的,整個(gè)處理過(guò)程基于事件總線而非單獨(dú)的控制器。
整個(gè)系統(tǒng)采用YAML配置模板,Server配置了Http訪問(wèn)端口,訪問(wèn)根路徑和內(nèi)嵌的Tomcat編碼[8]、響應(yīng)時(shí)間等配置,其關(guān)鍵參數(shù)如表1所示。
表1 關(guān)鍵配置參數(shù)
Restful層的設(shè)計(jì)采用Vert.x Web框架,它采用異步模式,通過(guò)事件循環(huán)調(diào)用存儲(chǔ)在異步任務(wù)隊(duì)列中的任務(wù),大大降低了傳統(tǒng)阻塞模型中線程對(duì)于操作系統(tǒng)的開(kāi)銷[9]。
整個(gè)學(xué)分管理系統(tǒng)實(shí)現(xiàn)高并發(fā)通信高效率的核心步驟是創(chuàng)建多路復(fù)用的通信通道。為了減少冗余代碼,主框架采用Springboot。將復(fù)用通道交由Spring統(tǒng)一管理,在此之前要?jiǎng)?chuàng)建由Spring管理的Worker線程池,部分代碼如下:
@Component
public class SpringVertxFactory implements VerticleFactory, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public String prefix() {
return "credit"; }
@Override
public boolean blockingCreate() {
return true;
}
@Override
public Verticle createVerticle(String s, ClassLoader classLoader) throws Exception {
String clazz=VerticleFactory.removePrefix(s);
return (Verticle) applicationContext.getBean(Class.forName(clazz));
}
@Override
public voidsetApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext; }}
上述代碼完成了三個(gè)目標(biāo):
(1)實(shí)現(xiàn)了VerticleFactory和Application ContextAware接口,VerticleFactory接口能產(chǎn)生Vert.x工作線程,ApplicationContextAware接口是當(dāng)SpringContext初始化完成后,用于獲取SpringContext的接口,其目的是將產(chǎn)生的Vert.x工作線程Verticle加入到SpringContext中,達(dá)到由Spring容器統(tǒng)一管理Verticle線程池的目的[10]。
(2)初始化通道總線和事件總線,注冊(cè)RestfulApi到Vert.x,并設(shè)置相關(guān)聯(lián)的屬性。使用了線程同步的方式保證初始化順序執(zhí)行。
(3)初始化Vert.x核心容器Vertx,并設(shè)置最大線程量和最大連接響應(yīng)時(shí)間。注冊(cè)Vert.x工作線程到Vert.x容器,檢查初始化過(guò)程中是否超時(shí),初始化過(guò)程中是否有錯(cuò)誤,以及是否全部線程都已經(jīng)初始化完成。
由于Vert.x的工作線程由Spring容器統(tǒng)一管理[11],只有當(dāng)Spring容器初始化完畢后才能使用Spring容器里的Vert.x工作線程,故需要監(jiān)聽(tīng)Springboot的啟動(dòng)消息事件。Vert.x中的RestfulApi沒(méi)有一套現(xiàn)成的能直接完成映射注冊(cè)的開(kāi)發(fā)注解或模板類,因此Vert.x的RestfulApi需要自己去實(shí)現(xiàn)。
該文定義的RestfulApi必須繼承AbstractVerticle這個(gè)抽象類,才能被Vert.x核心容器接收作為通信處理鏈上的一部分。并且會(huì)去執(zhí)行該對(duì)象類里面的start方法,所以一定要重寫(xiě)這個(gè)方法。這個(gè)方法里面首先要?jiǎng)?chuàng)建處理邏輯的代理接口,而且這個(gè)接口也必須要能被Vert.x核心容器接受,然后注冊(cè)訪問(wèn)地址[12]到Vert.x的核心路由上面,由于整個(gè)過(guò)程是事件驅(qū)動(dòng)的,所以要設(shè)立監(jiān)聽(tīng)端口。將事件處理的邏輯結(jié)果寫(xiě)入到Router的routingContext中,這樣才可到前端解析[13]。
Java原生的NIO API在開(kāi)發(fā)中顯得過(guò)于繁瑣,也未封裝成一個(gè)高并發(fā)的架構(gòu)。為了減少開(kāi)發(fā)帶來(lái)的時(shí)間消耗和框架封裝的性能消耗,采用現(xiàn)有的Vert.x框架。現(xiàn)有主流的Web開(kāi)發(fā)中Spring是必不可少的,將兩者結(jié)合由Spring管理Vert.x的部分組件能用工程化的開(kāi)發(fā)流程去簡(jiǎn)化異步Web程序的開(kāi)發(fā)。
將部分ajax請(qǐng)求接口更改為Vert.x開(kāi)發(fā),應(yīng)用更多Spring帶來(lái)的方便且規(guī)范的服務(wù),減少在后續(xù)服務(wù)帶來(lái)的開(kāi)發(fā)難度和性能消耗。
整合Web其余所有部分通過(guò)Spring與Vert.x協(xié)同工作,并借此管理Vert.x的異步線程池,動(dòng)態(tài)地申請(qǐng)資源,減少性能浪費(fèi)。
為了保證實(shí)驗(yàn)的可行度和可信度,采用由Apache基金會(huì)開(kāi)發(fā)的JMeter壓力測(cè)試工具[14-15],對(duì)該項(xiàng)目進(jìn)行測(cè)試,并且實(shí)驗(yàn)是基于課外學(xué)分管理系統(tǒng)設(shè)計(jì)的,這兩個(gè)不同接口會(huì)運(yùn)行在同一個(gè)Java虛擬機(jī)中,最大程度地保證了在運(yùn)行環(huán)境、參數(shù)、性能等各方面的一致性,得出的實(shí)驗(yàn)結(jié)果對(duì)比也更有說(shuō)服力。
設(shè)定為百萬(wàn)級(jí)并發(fā)請(qǐng)求:讓程序能模擬一百萬(wàn)個(gè)用戶對(duì)同一個(gè)接口模塊請(qǐng)求。
設(shè)定圖形結(jié)果計(jì)算包括吞吐量和響應(yīng)時(shí)間。
固定時(shí)間為一分鐘或者一分鐘又幾秒鐘(結(jié)束上百個(gè)線程會(huì)消耗幾秒時(shí)間)。
接口調(diào)用的邏輯和功能完全一致。
設(shè)定線程請(qǐng)求無(wú)延遲,即延遲0 ms。
在實(shí)驗(yàn)過(guò)程中,為了保證發(fā)送的數(shù)量是一樣的,應(yīng)當(dāng)同時(shí)啟動(dòng)兩個(gè)線程組,且設(shè)置完全一模一樣,設(shè)置在同一個(gè)測(cè)試組中,啟動(dòng)整個(gè)測(cè)試組。
在此期間密切關(guān)注線程數(shù)量變化,記錄線程非滿載的情況下的測(cè)試數(shù)據(jù),在后期處理數(shù)據(jù)時(shí)需要除去這一部分不合格的啟動(dòng)數(shù)據(jù)。觀察后臺(tái)是否已經(jīng)崩潰,因?yàn)樵诎偃f(wàn)級(jí)的并發(fā)下SpringMvc大概率會(huì)假死,如果已經(jīng)崩潰或者假死則數(shù)據(jù)上沒(méi)有對(duì)比的必要性。
在實(shí)驗(yàn)數(shù)據(jù)監(jiān)聽(tīng)器中取得相應(yīng)數(shù)據(jù)和統(tǒng)計(jì)圖形,首先在SpringMvc組里面取得吞吐量和響應(yīng)時(shí)間結(jié)果,如圖1和圖2所示。
圖1 學(xué)分管理系統(tǒng)SpringMvc吞吐量
圖2 學(xué)分管理系統(tǒng)SpringMvc響應(yīng)時(shí)間
Vert.x的響應(yīng)時(shí)間和吞吐量如圖3和圖4所示。
圖3 學(xué)分管理系統(tǒng)Vert.x吞吐量
圖4 學(xué)分管理系統(tǒng)Vert.x響應(yīng)時(shí)間
由上面四幅圖片可以獲得的信息,仍然需要比對(duì)SpringMvc和Vert.x,需要排除不合格的測(cè)試量。首先排除前10秒鐘的線程啟動(dòng)時(shí)測(cè)試的數(shù)據(jù),再減去20秒后衰減的線程量這樣的響應(yīng)時(shí)間才是合格的比對(duì)樣本,其結(jié)果如圖5所示。
相同時(shí)間發(fā)出的數(shù)量能保證在誤差范圍內(nèi),故可以記錄所有的量一次吞吐代表完成一次請(qǐng)求,結(jié)果如圖6所示。
圖6 吞吐量對(duì)比
在實(shí)驗(yàn)數(shù)據(jù)的對(duì)比下能發(fā)現(xiàn),在響應(yīng)時(shí)間是萬(wàn)倍的差距,在吞吐量上是數(shù)十倍的差距,在同一JVM,同一功能,執(zhí)行同一邏輯,同一線程組中排除不合格數(shù)據(jù)得出的數(shù)據(jù)對(duì)比中可以得到如下結(jié)論:
(1)相較于傳統(tǒng)且主流的SpringMvc的IO模式,NIO更能勝任高并發(fā)環(huán)境,而且這個(gè)性能是提升巨大的,能在主要的兩方面中體現(xiàn)出指數(shù)倍的差距;
(2)能在相同邏輯下大幅度減少通信時(shí)間;
(3)相同條件下,NIO通信的程序能處理更多的請(qǐng)求。