一次zuul版本升級(jí)產(chǎn)生的問題排查記錄
起因
事情的起因是由于早期的一些服務(wù)版本放到現(xiàn)在太低了,基本上都是 SpringBoot1.5.x ,因此準(zhǔn)備統(tǒng)一對(duì)服務(wù)進(jìn)行一次版本升級(jí),升級(jí)到 2.1.x , SpringCloud 版本升級(jí)到Greenwich。當(dāng)然我們用的舊版本的 zuul 相關(guān)的都需要升級(jí)。
意外的 Bug
我們網(wǎng)關(guān)使用的是 zuul,使用的是 spring-cloud-netflix 封裝的包,此次版本升級(jí)同步升級(jí)了相關(guān)的包。但是意外的情況發(fā)生了,在測(cè)試環(huán)境上我們發(fā)現(xiàn)上傳文件會(huì)出現(xiàn)異常。具體表現(xiàn)是這樣的:當(dāng)上傳的文件超出一定大小后,在經(jīng)過 zuul 網(wǎng)關(guān)并向其他服務(wù)轉(zhuǎn)發(fā)的時(shí)候,之前上傳的包就不見了。這個(gè)情況十分奇怪,因此馬上開始排查。
Bug 的排查
出現(xiàn)這樣的問題,第一反應(yīng)是測(cè)試是不是根本沒有上傳包所以當(dāng)然包沒法轉(zhuǎn)發(fā)到下一層,當(dāng)然這種想法很快被否定了。好吧,那就認(rèn)真的排查吧。
首先先去追蹤了一下路由以及出現(xiàn)的具體日志,將問題定位到 zuul 服務(wù),排除了上游 nginx 和下游業(yè)務(wù)服務(wù)出現(xiàn)問題的可能。但是 zuul 服務(wù)沒有任何異常日志出現(xiàn),所以非常困擾。檢查過后發(fā)現(xiàn)文件確實(shí)有通過 zuul,但是之后憑空消失沒有留下一點(diǎn)痕跡。
明明當(dāng)初考慮上傳文件的問題給 zuul 分配了兩個(gè) g 的內(nèi)存,怎么上傳 500m 的文件就出問題了呢?不對(duì)!此時(shí)我靈光一閃,會(huì)不會(huì)和垃圾回收機(jī)制有關(guān)。我們的文件是非常大的,這樣的大文件生成的大對(duì)象是會(huì)保存在 java 的堆上的,并且由于垃圾回收的機(jī)制,這樣的對(duì)象不會(huì)經(jīng)歷年輕代,會(huì)直接分配到老年代,會(huì)不會(huì)是由于我們內(nèi)存參數(shù)設(shè)置不合理導(dǎo)致老年代太小而放不下呢?想到做到,我們通過調(diào)整 jvm 參數(shù),保證了老年代至少有一個(gè) G 的空間,并且同步檢測(cè)了 java 的堆內(nèi)存的狀態(tài)。然而讓人失望的是居然沒有奏效。不過此時(shí)事情和開始不同,我們有了線索。在剛才的堆的內(nèi)存監(jiān)控中發(fā)現(xiàn)了一些異常,隨即合理懷疑是堆中內(nèi)存不夠?qū)е铝?oom。隨后加大內(nèi)存嘗試并且再次運(yùn)行,發(fā)現(xiàn)居然上傳成功了。果然是老年代內(nèi)存不足導(dǎo)致的 oom,不過雖然上傳成功,但是老年代中的內(nèi)存居然被占用了 1.6G 左右,明明是 500M 的文件,為什么會(huì)占用了這么大的內(nèi)存呢?
雖然找到了原因,但是增加內(nèi)存顯然不是解決問題的方法,因此,我們?cè)趩?dòng)參數(shù)上新增了 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data 準(zhǔn)備查看 oom 的具體分析日志。

查看堆棧信息可以發(fā)現(xiàn),溢出是發(fā)生在 byte 數(shù)組的拷貝上,我們迅速定位代碼,可以找到如下的代碼:
public?InputStream?getRequestEntity()?{
if?(requestEntity?==?null)?{
return?null;
}
if?(!retryable)?{
return?requestEntity;
}
????????try?{
????????????if?(!(requestEntity?instanceof?ResettableServletInputStreamWrapper))?{
????????????????requestEntity?=?new?ResettableServletInputStreamWrapper(
????????????????????????StreamUtils.copyToByteArray(requestEntity));
????????????}
????????????requestEntity.reset();
????????}
????????finally?{
????????????return?requestEntity;
????????}
????}
這段代碼源自 RibbonCommandContext 是在 zuul 中進(jìn)行請(qǐng)求轉(zhuǎn)發(fā)的時(shí)候調(diào)用到的,具體的 OOM 是發(fā)生在調(diào)用 StreamUtils.copyToByteArray(requestEntity)); 的時(shí)候。繼續(xù)進(jìn)入方法查找源頭。最終經(jīng)過排查找到了溢出的源頭。ribbon 轉(zhuǎn)發(fā)中的用到了 ByteArrayOutputStream 的拷貝,代碼如下:
public?synchronized?void?write(byte?b[],?int?off,?int?len)?{
if?((off?0)?||?(off?>?b.length)?||?(len?0)?||
((off?+?len)?-?b.length?>?0))?{
throw?new?IndexOutOfBoundsException();
}
ensureCapacity(count?+?len);
System.arraycopy(b,?off,?buf,?count,?len);
count?+=?len;
}
可以看到這邊有一個(gè) ensureCapacity ,查看源碼:
private?void?ensureCapacity(int?minCapacity)?{
//?overflow-conscious?code
if?(minCapacity?-?buf.length?>?0)
grow(minCapacity);
}
????private?void?grow(int?minCapacity)?{
????????//?overflow-conscious?code
????????int?oldCapacity?=?buf.length;
????????int?newCapacity?=?oldCapacity?<1;
????????if?(newCapacity?-?minCapacity?0)
????????????newCapacity?=?minCapacity;
????????if?(newCapacity?-?MAX_ARRAY_SIZE?>?0)
????????????newCapacity?=?hugeCapacity(minCapacity);
????????buf?=?Arrays.copyOf(buf,?newCapacity);
????}
可以看到 ensureCapacity 做了一件事,就是當(dāng)流拷貝的時(shí)候 byte 數(shù)組的大小不夠了,那就調(diào)用 grow 進(jìn)行擴(kuò)容,而 grow 的擴(kuò)容和 ArrayList 不同,他的擴(kuò)容是每一次將數(shù)組擴(kuò)大兩倍。
至此溢出的原因就很清楚了,500m 文件占用 1.6g 是因?yàn)閯偤糜|發(fā)擴(kuò)容,導(dǎo)致用了多一倍的空間來容納拷貝的文件,再加上源文件,所以占用了文件的 3 倍空間。
解決方案
至于解決方案,調(diào)整內(nèi)存占用或者是老年代的占比顯然不是合理的解決方案。我們?cè)倩仡^查看源代碼,可以看到這個(gè)部分
if?(!retryable)?{
return?requestEntity;
}
如果設(shè)置的不重試的話,那么 body 中的信息就不會(huì)被保存。所以,我們決定臨時(shí)先去除上傳文件涉及到的服務(wù)的重試,之后再修改上傳機(jī)制,在以后的上傳文件時(shí)繞過 zuul。
追根溯源
雖然找到的原因,并且也有了解決方案,但是我們?nèi)匀徊恢罏槭裁磁f版本是 ok 的,因此本著追根究底的態(tài)度,找到了舊版的 zuul 的源碼。
新版的 ribbon 代碼集成 spring-cloud-netflix-ribbon ,而舊版的 ribbon 的代碼集成在 spring-cloud-netflix-core 中,所以稍稍花費(fèi)點(diǎn)時(shí)間才找到對(duì)應(yīng)的代碼,檢查不同,發(fā)現(xiàn)舊版的 getRequestEntity 沒有任何的處理,直接返回了 requestEntity
public?InputStream?getRequestEntity()?{
return?requestEntity;
}
而在之后的版本中馬上就加上了拷貝機(jī)制。于是我們?nèi)?github 上找到了當(dāng)初的那個(gè) commit
之后我們順著 commit 中給出的信息找到了最初的 issue
查看過 issue 之后發(fā)現(xiàn)這原來是舊版的一個(gè) bug,這個(gè) bug 會(huì)導(dǎo)致舊版的 post 請(qǐng)求在 retry 的時(shí)候有 body 丟失的情況,因此在新版本中進(jìn)行了修復(fù),當(dāng)請(qǐng)求為 post 的時(shí)候會(huì)對(duì)于 body 進(jìn)行緩存以便于重試。
總結(jié)
至此,我們?cè)颈镜膹?fù)原了這個(gè) bug 的全貌以及形成的歷史和原因。并且找到適當(dāng)?shù)慕鉀Q方案。最后提一句:真的不要用 zuul 來上傳大文件,真的會(huì)很糟糕!
后臺(tái)回復(fù)?學(xué)習(xí)資料?領(lǐng)取學(xué)習(xí)視頻
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
