一次漫長的dubbo網關內存泄露排查經歷

背景介紹

在微服務架構中,不同的微服務有不同的網絡地址,而客戶端則是通過統一的地址進行調用,在客戶端與服務端之間需要有一個通信的橋樑,這就產生了微服務網關。微服務網關可以連接客戶端與微服務,提供統一的認證方式,管理接口的生命週期,做更好的負載均衡、熔斷限流,提供方便的監控,提供統一的風控入口。

今天要介紹的主角是dubbo微服務網關,來自公司內部自研的提供http協議到dubbo協議轉換的微服務網關,跟本文相關的就是它的核心點:dubbo泛化調用。dubbo官網對泛化調用的描述爲

“泛化接口調用方式主要用於客戶端沒有 API 接口及模型類元的情況,參數及返回值中的所有 POJO 均用map表示,通常用於框架集成。“

dubbo最常見的調用方式是引入服務提供方定義的jar包,用於描述接口,但如果是網關引入所有的dubbo提供者的jar包就很不現實,況且如果需要新增接口就需要重新發佈網關,所以需要使用泛化調用來解決這個問題,官網提供的示例代碼截圖如下:

image

問題描述

這款網關上線以來一直運行穩定,直到某一天出現了問題,頻繁full GC,cpu上漲,錯誤率飆升,然而接口的調用量並沒有上漲。立馬重啓機器,並且保留了一份內存dump文件,分析了一週沒有結論,最近又出現了一次,情形類似。

image

full gc頻繁

image

old區被填滿

image

cpu上漲,錯誤率上升

問題排查

  • 從內存dump文件查起

從監控上基本能斷定是內存問題,那就分析一下當初dump出來的內存文件,使用eclipse的mat插件分析

image

image

RegistryDirectory對象多達7000多個,直接定位爲RegistryDirectory可能存在內存泄露。再跟進一下這個對象的持有者

image

發現是com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient$CuratorWatcherImpl,搜索一下該類的對象

image

這個對象也非常多。到這裏就去查看dubbo的源碼(本文dubbo源碼基於2.6.6)。首先查找com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient$CuratorWatcherImpl創建的地方

image

只有一個地方創建,接着跟進addChildListener

image

只有在訂閱zookeeper的節點時會調用,繼續查找訂閱zookeeper的地方,發現有兩處

  • 一處是ReferenceConfig的createProxy會調用RegistryProtocol的doRefer,進而訂閱zookeeper;
  • 另一處是FailbackRegistry中會有一個線程不停地對訂閱失敗的path進行重試,在zk重新連接時會recover。

首先懷疑第二處

image

dubbo應用在與zookeeper斷開連接重新連上的時候會recover,recover裏面執行的就是重新訂閱zookeeper。這很好模擬,線下打斷點測試一下在zookeeper斷開重連的時候是否會重新生成CuratorWatcherImpl對象。結果當然是沒有生成,因爲dubbo應用會緩存CuratorWatcherImpl對象,對於相同的URL訂閱,返回的都是相同的CuratorWatcherImpl對象,並不會重新生成。

  • 從網絡尋求答案

換個思路重新開始,去網上看看有沒有人遇到相同的問題,網上搜了一下,發現了 https://github.com/apache/dubbo/issues/376 跟我的問題幾乎一樣,難道是發佈導致的問題?但經過幾次對比否決了,因爲平時發佈都沒事,偏偏這時候出事?而且兩次爆發時網卡流量並不高。

image

否決了這個issue後又發現了一個issue, https://github.com/apache/dubbo/issues/4587

image

但這個很快又被否決了,因爲這裏他的主要問題是reference未緩存,在dubbo的文檔中針對這個是有提及的

image

出問題的dubbo網關是對reference做了緩存(以interface+version+group+timeout作爲key,timeout是爲了能動態調整接口的超時時間),理論上不會重複生成reference

  • “守株待兔“

問題真的陷入僵局,好在看到了這麼一篇文章,《netty堆外內存泄露排查盛宴》(點擊原文查看),作者在面對無法排查的問題時,在線上植入一段監控代碼來協助定位問題。於是在想能不能也植入一段代碼,看看這麼多訂閱到底是什麼。通過對dubbo的源碼翻看,找到這麼一個可以獲取訂閱的點

image

image

ZookeeperRegistry中有個zkListeners存儲了訂閱了哪些URL,如果能檢測這個字段是不是就可以了?簡單點,定時檢測並打印日誌。通過反射來獲取這個變量

image

代碼改好,測試一下,放到線上一臺機器守株待兔。經過一下午,終於抓到了

image

發現其中一個service,被重複訂閱了很多次,而且訂閱的URL只有timestamp不一樣,且只有一個服務會這樣,懷疑跟服務本身有關,查看該服務,發現其沒有provider

image

難道沒有provider的服務會出現重複訂閱?線下復現試試,使用網關去調用一個沒有provider的服務(省略復現過程),果然問題出現了!問題能復現就很好排查了,打斷點根據調用棧就能找到訂閱的地方

image

在生成referenceConfig時會初始化proxy,如果初始化過就會忽略,問題就在createProxy

image

如果check=true,且provider不存在,createProxy就會拋異常,createProxy底層去訂閱了zookeeper,緩存了RegistryDirectory對象,如果不停訂閱,內存就會被撐爆,看下這個報錯在那幾天是否很多

image

好像也不是很多,單機加起來幾百次,爲什麼會產生這麼多RegistryDirectory對象,通過調試發現

image

每次訂閱的URL是生成的,也就是timestamp不同,且會被塞入urls這個變量緩存起來,然後循環這個urls進行refer(即訂閱),也就是說第一次urls中有1個URL需要訂閱,第二次就成了2,第三次是3,也就是高斯計算的那個1+2+3+4+…+100的問題,請求100次會產生5050個RegistryDirectory對象。

解決辦法很簡單,將check=true改爲check=false。

總結

  • 這個是dubbo的bug,在2.7.5版本中已經將訂閱的URL中的timestamp去掉了,只會對一個URL訂閱一次,issue即之前提到的https://github.com/apache/dubbo/issues/4587,但當時這個issue並沒有解決我們的疑問;
  • 泛化調用時將reference的check設置爲false,否則可能會出現內存泄露;普通調用(xml配置)則無影響,因爲check=true如果沒有provider應用會啓動失敗;
  • 問題排查難易程度:通過監控、代碼、日誌直接定位的問題 < 可穩定復現的問題 < 不可穩定復現的問題(偶爾出現) < 不可復現的問題(只出現一次),本文的問題屬於不可穩定復現的問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章