skywalking 引起 spring-cloud-gateway 的内存溢出 skywalking的bug
大家好,我是烤鴨:
???又是個線上問題記錄,這次坑慘了,開源軟件也不是萬能的,還是要做好壓測和灰度。
問題
上游反饋大量超時,不止某一個服務,查看服務沒有問題,猜測是網絡或者環境問題。
想到網關接入了skywaling(已接入24小時),回滾后問題消失。
堆內存在某個時間點后上升且無法回收。
Full GC 時間變得特別長…這個就是上游超時的原因
環境
cloud版本
<groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-gateway</artifactId> <version>2.2.3.RELEASE</version>skywalking 版本
<artifactId>java-agent-sniffer</artifactId> <groupId>org.apache.skywalking</groupId> <version>8.9.0</version>復現
說實話,當時我本地起了,壓測了一天,并沒有出現OOM的情況,事實證明,還是量不夠大。
后來同事找到了病根(下面兩種情況原因是一樣的)
或者是
最終的結果都是 ContextManager.Context/RuntimeContext 未清空,導致內存泄露
調試
可以參考這篇文章 https://www.jianshu.com/p/ba9254f38fa5
因為我只想調試網關相關包,把下載失敗的包和編譯失敗的包都注釋掉了,再在網關項目的導入module。
導入完了,結構如下圖,該注釋的注釋,能編譯打包就行。
斷點打在 gateway-2.1.x-plugin的幾個攔截器,可以看到debug成功
源碼解析
剩下就跟著代碼一步一步走了。
幾個攔截器的順序是 NettyRoutingFilterInterceptor -> HttpClientFinalizerSendInterceptor -> HttpClientFinalizerResponseConnectionInterceptor
可以看到 NettyRoutingFilterInterceptor 初次進入會執行 ContextManager.createLocalSpan
創建span對象(全鏈路用到的流轉對象,感興趣的可以看看谷歌的dapper https://blog.csdn.net/ruizhikai_ztq/article/details/123663633)
@Override public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,MethodInterceptResult result) throws Throwable {ServerWebExchange exchange = (ServerWebExchange) allArguments[0];EnhancedInstance enhancedInstance = getInstance(exchange);AbstractSpan span = ContextManager.createLocalSpan("SpringCloudGateway/RoutingFilter");if (enhancedInstance != null && enhancedInstance.getSkyWalkingDynamicField() != null) {ContextManager.continued((ContextSnapshot) enhancedInstance.getSkyWalkingDynamicField());}span.setComponent(SPRING_CLOUD_GATEWAY); }createLocalSpan,這里的兩個實現,是否忽略trace,由于我引入了
apm-trace-ignore-plugin-8.9.0.jar 這個包,實現會走 ignore的,也就是復現里的第二種情況
這個方法里有一個棧深度 stackDepth 字段,只要創建span就會自增,刪除span就會自減。
@Override public AbstractSpan createLocalSpan(String operationName) {stackDepth++;return NOOP_SPAN; }@Override public boolean stopSpan(AbstractSpan span) {stackDepth--;if (stackDepth == 0) {ListenerManager.notifyFinish(this);}return stackDepth == 0; }一般來說的話,方法的Interceptor的 beforeMethod 會執行
ContextManager.createLocalSpan();afterMethod 會執行
AbstractSpan span = ContextManager.activeSpan(); ContextManager.stopSpan(span);但是很多中間件的某些場景都是異步的,尤其網關是響應式的,所以入口和出口也可能在不同的類里。
比如網關的 createLocalSpan 是在 NettyRoutingFilterInterceptor
而 stopSpan 是在 HttpClientFinalizerSendInterceptor
再看下上面的 stopSpan 方法的調用的地方
stopSpan 方法返回值是根據 stackDepth 是否為0來的,如果 stackDepth != 0,返回false
那這種就有點危險了,如果有方法觸發了 createLocalSpan 而后續沒有執行 stopSpan 就會出現內存無法回收。
比如只執行了 NettyRoutingFilterInterceptor 而沒有執行 HttpClientFinalizerSendInterceptor
網關異常代碼
這種問題很長時間都沒有人反饋,說明還是小眾的。主要是我們寫的不規范也有一定的原因。(不要問,問就是開源全鍋)
public class CorsResponseHeaderFilter implements GlobalFilter{@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return chain.filter(exchange).then(Mono.defer(() -> {exchange.getResponse().getHeaders().entrySet().stream().filter(kv -> !CollectionUtils.isEmpty(kv.getValue())).filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_MAX_AGE))).forEach(kv -> {kv.setValue(new ArrayList<String>() {{add(kv.getValue().get(0));}});});return chain.filter(exchange);}));}}這段代碼主要是解決網關跨域的問題,記得有一些后臺頁面對返回的頭有限制,所以做了這個邏輯處理,過濾一些響應頭和指定格式。
乍一看沒啥問題,問題就出現在 Mono.defer,一般我們使用的多的是Mono.just。
看一下官方文章這倆有啥區別 https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html#create-java.util.function.Consumer-
簡單介紹一下常用的api:
Mono.just 餓漢式:立即執行
Mono.defer 懶漢式:發布之后,等待訂閱者來執行(有延遲)
Mono.create 完全自主控制:發布之后,自己添加/移除監聽器,并且手動寫回調
問題解決
饒了一大圈,在本應該skywalking 的gateway interceptor 走完了之后,stackDepth 為0。
而 Mono.defer 操作,又進入了 NettyRoutingFilterInterceptor,執行了 createLocalSpan,stackDepth ++,再之后的CONTEXT就無法remove了,造成內存泄漏。
訪問兩次之后就會出現這種情況了。
同事已經給修了。
https://github.com/apache/skywalking-java/pull/133
同一個鏈路上 ServerWebExchange.getAttributes() 是一直有的,進入的時候放一次,再次進入判斷一下如果是同一個鏈路的話,就不再執行后面的代碼了(避免重復創建span)
總結
開源項目就是有這樣的魅力,發現其中問題,再fix提交。
不過線上運行也確實是坑啊,之前有別的網關已經接過了,沒問題,就直接上了。
但是每個網關項目本身也不一樣,一個小小的過濾器有這么大的能量。
額外說一句,一定要灰度!!!
總結
以上是生活随笔為你收集整理的skywalking 引起 spring-cloud-gateway 的内存溢出 skywalking的bug的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下创建的符号链接的权限
- 下一篇: account.php,account.