? ? ? 筆者剛?cè)肼毿鹿绢I(lǐng)導(dǎo)讓針對(duì)api項(xiàng)目進(jìn)行重構(gòu)缚甩,由于當(dāng)前系統(tǒng)用play框架寫的加上歷史遺留原因薇搁,造成當(dāng)前的api項(xiàng)目難以維護(hù)以及部署粤铭。重構(gòu)便成了迫在眉睫的事畏铆。由于公司的業(yè)務(wù)性質(zhì)双泪,要求單臺(tái)機(jī)器api的吞吐量很高持搜,大家都知道springboot的好處,可以快速搭建起web服務(wù)焙矛。所以在選型時(shí)筆者只是寫了個(gè)簡(jiǎn)單的接口然后用ab命令對(duì)這個(gè)接口進(jìn)行了性能壓測(cè)葫盼。因?yàn)楣P者認(rèn)為吞吐量問(wèn)題springboot可以完全勝任。沒(méi)有過(guò)多的考慮性能不達(dá)標(biāo)的問(wèn)題村斟。
? ? ? 于是筆者便開開心心的按照老系統(tǒng)的邏輯進(jìn)行重構(gòu)贫导。根據(jù)需求接口返回類型需要根據(jù)請(qǐng)求后綴是json還是xml提供相應(yīng)的返回?cái)?shù)據(jù)格式。其他后綴結(jié)尾的或者沒(méi)有后綴的返回錯(cuò)誤碼蟆盹。筆者當(dāng)時(shí)想到兩種方案脱盲。一種是直接在@RequestMapping注解中通過(guò)value設(shè)置支持的后綴格式。如:@RequestMapping(value = {"/ping.json", "/ping.xml"}, method = RequestMethod.GET)日缨。另一種是在@RequestMapping中不設(shè)置后綴如圖一钱反。通過(guò)實(shí)現(xiàn)WebMvcConfigurer配置類。實(shí)現(xiàn)configurePathMatch方法開啟后綴匹配匣距。實(shí)現(xiàn)configureContentNegotiation方法根據(jù)后綴進(jìn)行返回格式設(shè)置如圖二面哥。然后再寫個(gè)攔截器對(duì)非json和xml結(jié)尾的請(qǐng)求進(jìn)行攔截如圖三。為了簡(jiǎn)單少寫代碼毅待。筆者選擇了第二種方式實(shí)現(xiàn)尚卫。然后就開啟了擼代碼的模式。在完成所有開發(fā)任務(wù)尸红,進(jìn)入測(cè)試階段時(shí)吱涉。測(cè)試小朋友跑過(guò)來(lái)跟我說(shuō):少年你重構(gòu)的api性能不達(dá)標(biāo)。現(xiàn)有的2核4G單機(jī)QPS能達(dá)到2000外里。你重構(gòu)的只能達(dá)到七八百怎爵。當(dāng)時(shí)內(nèi)心數(shù)萬(wàn)個(gè)草泥馬在奔騰。
? ? ? 沒(méi)辦法各種百度尋找優(yōu)化方案盅蝗。試過(guò)換各種web容器鳖链。由tomcat換到j(luò)etty再到undertow。試過(guò)配置各種參數(shù)墩莫。然而并沒(méi)有什么提升芙委。看到一篇文章說(shuō)可以使用異步請(qǐng)求如圖四狂秦。先釋放容器分配給請(qǐng)求的線程與相關(guān)資源灌侣,減輕系統(tǒng)負(fù)擔(dān),釋放了容器所分配線程的請(qǐng)求裂问,其響應(yīng)將被延后侧啼,可以在耗時(shí)處理完成時(shí)再對(duì)客戶端進(jìn)行響應(yīng)玖姑。頓時(shí)喜出望外,以為找到了解決的辦法慨菱。然而并沒(méi)有什么卵用焰络。一度懷疑最初的選型是錯(cuò)誤的。但是我想springboot的性能應(yīng)該不能這么不堪吧符喝。于是便開始查找自己的代碼闪彼。跟蹤線程耗時(shí)方法。
? ? ? 有過(guò)性能調(diào)優(yōu)的同學(xué)應(yīng)該都熟悉 jvisualvm协饲,jdk自帶監(jiān)控程序畏腕。可以監(jiān)控本地或遠(yuǎn)端cpu茉稠、內(nèi)存描馅、線程等實(shí)時(shí)動(dòng)態(tài)信息。以及對(duì)線程進(jìn)行快照而线。對(duì)線程內(nèi)方法調(diào)用耗時(shí)統(tǒng)計(jì)等功能铭污。非常強(qiáng)大。筆者用的是undertow做為web容器膀篮∴谀可以看到圖五、圖六它有跟netty類似的IO模型誓竿,IO線程負(fù)責(zé)接收請(qǐng)求磅网,然后把請(qǐng)求放到任務(wù)池中,由后面的任務(wù)線程進(jìn)行處理筷屡。這也解釋了為什么我之前用異步請(qǐng)求沒(méi)有提升性能的原因涧偷。因?yàn)楸旧韚ndertow已經(jīng)是異步的了。自己再進(jìn)行異步操作毫無(wú)意義毙死。tomcat也是同樣的道理燎潮。tomcat7以上默認(rèn)支持NIO,所以自己再實(shí)現(xiàn)異步請(qǐng)求操作沒(méi)有什么意義规哲。
? ? 然后我用wrk命令進(jìn)行壓測(cè)跟啤,看下任務(wù)線程中哪些操作是比較耗時(shí)的诽表,wrk -t 10 -c 500 -d 15s --latency -s http://127.0.0.1:2551/ping.json唉锌。10個(gè)線程500個(gè)連接,持續(xù)15秒竿奏“兰颍可以看到?jīng)]有任何業(yè)務(wù)邏輯的接口QPS只有1715。對(duì)任務(wù)線程抽樣進(jìn)行快照如圖八泛啸。展開其中一個(gè)線程任務(wù)圖九绿语。查看耗時(shí)的調(diào)用方法。如圖十中DispatcherServlet在調(diào)用doDispatch方法占用了64.2%的時(shí)間。一個(gè)doDispatch怎么會(huì)用這么多的時(shí)間呢吕粹?繼續(xù)追蹤方法內(nèi)調(diào)用getHander种柑,最后耗時(shí)在getMatchingCondition中。
查看源碼從doDispatch開始跟蹤匹耕,發(fā)現(xiàn)當(dāng)程序啟動(dòng)時(shí)會(huì)把@RequestMapping注解的path放到map集合中聚请,當(dāng)有請(qǐng)求時(shí),先去map中獲取對(duì)應(yīng)的路徑稳其,如果有則返回方法驶赏,沒(méi)有則根據(jù)設(shè)置的后綴匹配規(guī)則進(jìn)行遍歷匹配圖十三。其中畫框的屬性是不是很熟悉既鞠。對(duì)煤傍,它就是實(shí)現(xiàn)WebMvcConfigurer時(shí)設(shè)置的配置。? 如寫的是@RequestMapping(value = {"/ping"}, method = {RequestMethod.GET}) 嘱蛋,但請(qǐng)求的是/ping.json蚯姆,第一次查找在集合中沒(méi)有以/ping.json為path的方法,就會(huì)遍歷所有路徑集合進(jìn)行拆分后綴匹配洒敏。直到匹配到為止蒋失。筆者的項(xiàng)目中有300個(gè)接口,500多個(gè)路徑桐玻。如果不顯示的給出后綴篙挽,每次請(qǐng)求都會(huì)遍歷一遍這500多個(gè)路徑,造成耗時(shí)镊靴。
? ? ? 最后猜想是匹配路徑耗時(shí)導(dǎo)致吞吐量變低铣卡。于是把注解中路徑后綴顯示給出@RequestMapping(value = {"/ping.json", "/ping.xml"}, method = {RequestMethod.GET}) , 再進(jìn)行一次壓測(cè)。結(jié)果QPS為9384偏竟,翻了4倍多煮落。到此為止才算把性能提升上來(lái)。符合上線標(biāo)準(zhǔn)踊谋。
? ? ? 此次調(diào)優(yōu)過(guò)程中發(fā)現(xiàn)還有好多需要優(yōu)化的地方蝉仇,比如日志,集成的swagger殖蚕,actuator等等轿衔。都多少影響性能。但為了增加必要功能睦疫,損失些性能也是可以接受的害驹,有些不必要的損失性能還是要找到根源解決掉,筆者遇到的情況未必適合所有人蛤育。不過(guò)可以給那些想提升性能的朋友提供一些思路宛官。