升級spring-cloud-gateway后GC惡化的問題定位及解決

spring-cloud-gateway是spring出品的微服務(wù)網(wǎng)關(guān),我們公司基于此進(jìn)行了二次開發(fā)并承載公司所有web流量锌仅,之前我們一直用的是spring-cloud?Finchley.SR2版本對應(yīng)的gateway摄凡,最近我們嘗試升級到spring-cloud?Greenwich.RELEASE版本時發(fā)現(xiàn)GC惡化了不少蝎宇,于是排查之旅就此開始徽龟。

先在兩臺升級前后的機(jī)器上執(zhí)行jstat命令贤旷,這個命令可以查看GC概況广料,在排查GC問題時非常有用,如下兩圖分別是升級前后的GC概況:

圖1. 升級前GC概況
圖2. 升級后GC概況

從這兩張圖中我們可以得到如下結(jié)論:

young gc頻率差不多幼驶,都是大約4秒一次艾杏,但是每次young gc后,圖2中survivor區(qū)占用內(nèi)存明顯比圖1高很多盅藻,圖2中每次晉升到old區(qū)的內(nèi)存量也明顯比圖1高购桑;這說明升級后存活對象數(shù)增加了很多,導(dǎo)致gc惡化氏淑。圖中也可以看出升級后FGCT(筆者用的是CMS垃圾回收算法)明顯比升級前多了很多勃蜘。

那么到底是哪些存活對象變多了導(dǎo)致GC惡化了呢?樓主在分析問題的時候用了一款瑞士軍刀:https://github.com/DarLiner/vjtools/tree/master/vjmap

在升級前后的機(jī)器上分別執(zhí)行命令:./vjmap.sh -sur PID > histo-sur.log假残,這個命令可以輸出survivor區(qū)的對象統(tǒng)計(jì)信息(這個命令會STW元旬,執(zhí)行前別忘了先切斷流量),兩臺機(jī)器執(zhí)行結(jié)果分別如下:

圖3. 升級前survivor區(qū)對象統(tǒng)計(jì)信息
圖4. 升級后survivor區(qū)對象統(tǒng)計(jì)信息

從圖中可以看出升級后java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask這個類的對象占用了8.7M內(nèi)存守问,而升級前這個類的對象非常少匀归,看來李鬼就在這里了。

那么這些對象到底是哪里來的呢耗帕,從以上概況已經(jīng)看不出更多信息了穆端,這個時候就該用jmap命令dump內(nèi)存看看了,balabala仿便,十幾分鐘過后体啰,樓主打開新鮮出爐的內(nèi)存dump文件,經(jīng)過一番分析嗽仪,發(fā)現(xiàn)了如下關(guān)鍵信息:

內(nèi)存dump分析

如上圖所示荒勇,可以看到大量java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask對象存在ScheduledThreadPoolExecutor的工作隊(duì)列中,看源碼可以知道ScheduledThreadPoolExecutor會把所有提交的任務(wù)包裝成ScheduledFutureTask放入queue中闻坚,并在執(zhí)行完成后刪除沽翔,從這里也能看出ScheduledThreadPoolExecutor的threadFactory使用了reactor.core.schedule.ReactorThreadFactory, 看起來是有地方在使用reactor-java提交大量延時任務(wù)導(dǎo)致的。

目前使用上述手段也已經(jīng)無法獲得更多信息窿凤,繼續(xù)從其他地方著手繼續(xù)排查仅偎。樓主發(fā)現(xiàn)在斷開流量后,過五六分鐘去重新執(zhí)行命令查看survivor區(qū)對象雳殊,ScheduledFutureTask對象實(shí)例幾乎沒有了橘沥,這說明ScheduledFutureTask這個對象的創(chuàng)建基本是由流量觸發(fā)的。于是我打開本地代碼夯秃,在ScheduledThreadPoolExecutor類中所有提交任務(wù)的代碼中加入斷點(diǎn)座咆,并加入斷點(diǎn)條件:this.getThreadFactory().getClass().getSimpleName().equals("ReactorThreadFactory")


Idea增加條件斷點(diǎn)

然后通過瀏覽器打開頁面痢艺,果然觸發(fā)了斷點(diǎn)。斷點(diǎn)所在堆棧很長介陶,樓主截取了關(guān)鍵信息如下:

"reactor-http-kqueue-4" #340 daemon prio=5 os_prio=31 tid=0x00007fe61cb62800 nid=0x15207 at breakpoint[0x000070000fa71000] java.lang.Thread.State: RUNNABLE at java.util.concurrent.ScheduledThreadPoolExecutor.schedule(ScheduledThreadPoolExecutor.java:544) at reactor.core.scheduler.Schedulers.directSchedule(Schedulers.java:838) at reactor.core.scheduler.ParallelScheduler.schedule(ParallelScheduler.java:158) at reactor.core.scheduler.Schedulers$CachedScheduler.schedule(Schedulers.java:767) at reactor.core.publisher.MonoDelay.subscribe(MonoDelay.java:58) at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44) at reactor.core.publisher.Mono.subscribe(Mono.java:3710) at reactor.core.publisher.FluxTimeout.subscribe(FluxTimeout.java:89) at reactor.core.publisher.FluxOnErrorResume.subscribe(FluxOnErrorResume.java:47) at reactor.core.publisher.Flux.subscribe(Flux.java:7793)
?......
?......
?at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:3710) at rx.RxReactiveStreams$1.call(RxReactiveStreams.java:60) at rx.RxReactiveStreams$1.call(RxReactiveStreams.java:57)
?......
?......
at rx.Observable.subscribe(Observable.java:10095) at org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory.lambda$null$0(HystrixGatewayFilterFactory.java:102) at org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory$$Lambda$948/2078441774.accept(Unknown Source) at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:57) at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44) at reactor.core.publisher.Mono.subscribe(Mono.java:3710) at reactor.core.publisher.MonoIgnorePublisher.subscribe(MonoIgnorePublisher.java:41)
?......
?......
at reactor.netty.http.server.HttpServerHandle.onStateChange(HttpServerHandle.java:64) at reactor.netty.tcp.TcpServerBind$ChildObserver.onStateChange(TcpServerBind.java:234) at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:436) at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:141) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:162) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
?......
?......
?at java.lang.Thread.run(Thread.java:745)


注意其中的粗體部分腹备,F(xiàn)luxTimeout,從這個類可以看出調(diào)用來自Flux.timeout方法斤蔓,類似reactor的框架素來就對調(diào)試非常不友好植酥,從這個堆棧完全看不出是哪里調(diào)用了Flux.timeout, 經(jīng)過一番查找和調(diào)試,筆者發(fā)現(xiàn)Flux.timeout來自源碼org.springframework.cloud.gateway.filter.NettyRoutingFilter:

spring源碼

spring在這里設(shè)置了超時時間弦牡,如果從上游的響應(yīng)超過了超時時間友驮,則拋出異常。筆者這里的超時時間設(shè)置了59秒驾锰。

結(jié)合以上分析卸留,答案自然浮出了水面:spring-cloud-gateway每次處理一個http請求時都會調(diào)用Flux.timeout往ScheduledThreadPoolExecutor中提交一個延時任務(wù)(在這個例子中是延遲59秒后執(zhí)行)并生成一個ScheduledFutureTask放入工作隊(duì)列中,大部分情況下上游的響應(yīng)都會在十幾ms內(nèi)響應(yīng)椭豫,reactor-java會調(diào)用ScheduledFutureTask.cancel來取消這個延時任務(wù)的觸發(fā)耻瑟,然而這個ScheduledFutureTask對象仍然會存在于工作隊(duì)列中,直到59秒后才會被從隊(duì)列中剔除赏酥。

由于大量ScheduledFutureTask對象都會存活59秒喳整,這些對象很容易就熬過了幾次young gc,并最終晉升到老年代裸扶,這樣不僅導(dǎo)致了young gc時間變長框都,還導(dǎo)致CMS GC頻次的大幅增加。

那么怎么解決這個問題呢呵晨?筆者在閱讀ScheduledThreadPoolExecutor源碼時發(fā)現(xiàn)其中一個有意思的方法:

ScheduledThreadPoolExecutor源碼

從注釋可以看出魏保,如果把removeOnCancel屬性設(shè)置為true,那么調(diào)用ScheduledFutureTask.cancel方法的同時也會把ScheduledFutureTask從工作隊(duì)列中移除摸屠,從而可以盡快被gc回收掉(removeOnCancel屬性默認(rèn)為false)谓罗。

那么有什么辦法修改reactor-java內(nèi)部創(chuàng)建的ScheduledThreadPoolExecutor的屬性呢?reactor-java已經(jīng)提供了hook給我們季二,只要在適當(dāng)?shù)牡胤剑ū热鐒倖訒r)調(diào)用如下代碼就可以了:

Schedulers.addExecutorServiceDecorator("setRemoveOnCancel", (scheduler, scheduledExecutorService) -> {
?if (scheduledExecutorService instanceof ScheduledThreadPoolExecutor) {? ? ? ? ? ? ? ? ????((ScheduledThreadPoolExecutor)scheduledExecutorService) ????????.setRemoveOnCancelPolicy(true); }
?return scheduledExecutorService;
});


最后拋出個問題留給讀者自己思考:為什么升級前不會有這個問題呢檩咱?讀者可自行閱讀源碼研究一下。

后續(xù):筆者順便看了下另一款框架RxJava的實(shí)現(xiàn)戒傻,其內(nèi)部創(chuàng)建的ScheduledThreadPoolExecutor默認(rèn)都是把removeOnCancel設(shè)置為true的税手,筆者個人覺得這算是一個問題蜂筹,故給reactor-java提交了一個PR:https://github.com/reactor/reactor-core/pull/1674

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末需纳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子艺挪,更是在濱河造成了極大的恐慌不翩,老刑警劉巖兵扬,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異口蝠,居然都是意外死亡器钟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門妙蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來傲霸,“玉大人,你說我怎么就攤上這事眉反£甲模” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵寸五,是天一觀的道長梳凛。 經(jīng)常有香客問我,道長梳杏,這世上最難降的妖魔是什么韧拒? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮十性,結(jié)果婚禮上叛溢,老公的妹妹穿的比我還像新娘。我一直安慰自己劲适,他們只是感情好雇初,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著减响,像睡著了一般靖诗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上支示,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天刊橘,我揣著相機(jī)與錄音,去河邊找鬼颂鸿。 笑死促绵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嘴纺。 我是一名探鬼主播败晴,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼栽渴!你這毒婦竟也來了尖坤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤闲擦,失蹤者是張志新(化名)和其女友劉穎慢味,沒想到半個月后场梆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纯路,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年或油,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驰唬。...
    茶點(diǎn)故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡顶岸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叫编,到底是詐尸還是另有隱情蜕琴,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布宵溅,位于F島的核電站凌简,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恃逻。R本人自食惡果不足惜雏搂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寇损。 院中可真熱鬧凸郑,春花似錦、人聲如沸矛市。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浊吏。三九已至而昨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間找田,已是汗流浹背歌憨。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留墩衙,地道東北人务嫡。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像漆改,于是被迫代替她去往敵國和親心铃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容