一示损、問(wèn)題背景
一次生產(chǎn)事故寿弱,線上服務(wù)響應(yīng)慢犯眠;
作為常規(guī)操作,服務(wù)的VM啟動(dòng)參數(shù)有配置OOM提取內(nèi)存DUMP信息:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dump-path/
這是個(gè)好習(xí)慣症革。
使用Eclipse MAT分析dump文件筐咧,大對(duì)象視圖如下:
一種對(duì)象占據(jù)了1.8G的JVM內(nèi)存空間,程序配置的最大堆大小是2G噪矛;很明顯量蕊,這是由于程序問(wèn)題引起的單一對(duì)象大量產(chǎn)生,而又一直引用可達(dá)艇挨,造成JVM無(wú)法GC引起的OOM残炮。
二、MAT分析
接下來(lái)繼續(xù)使用MAT缩滨,分析對(duì)象產(chǎn)生的堆棧:
這是一個(gè)總結(jié)性的描述势就,意思是一個(gè)zipkin2.reporter.InMemoryReporterMetrics
類的實(shí)例占據(jù)了96.09%的堆空間,而內(nèi)存的增加是由于java.util.concurrent.ConcurrentHashMap$Node[]
實(shí)例的堆積引起的脉漏。
通過(guò)這個(gè)總結(jié)性的描述信息苞冯,大概能夠知道去InMemoryReporterMetrics
這個(gè)類找問(wèn)題了。
1)到內(nèi)存積累點(diǎn)的最短路徑
MAT還提供了視圖Shortest Paths to the Accumulation Point
來(lái)定位大對(duì)象產(chǎn)生的引用關(guān)系:
通過(guò)這個(gè)視圖侧巨,大對(duì)象的引用關(guān)系是:
AsyncReporter.Builder
->
AsyncReporter.BoundedAsyncReporter(metrics屬性)
->
InMemoryReporterMetrics(messagesDropped屬性)
2)大對(duì)象內(nèi)容
既然大對(duì)象是ConcurrentHashMap$Node
的實(shí)例舅锄,那么可以通過(guò)了解Node的具體內(nèi)容,來(lái)定位問(wèn)題司忱;
通過(guò)MAT巧娱,還可以看到堆積的大對(duì)象的具體內(nèi)容碉怔。
操作方式是:
得到大對(duì)象內(nèi)容:
任意選取一個(gè)對(duì)象烘贴,通過(guò)查看Map的Node內(nèi)容禁添,發(fā)現(xiàn):
- key是一個(gè)異常類,具體是
ResourceAccessException
- value是一個(gè)自動(dòng)Long
AtomicLong
- key這個(gè)異常的產(chǎn)生原因是:對(duì)
http://localhost:9411/api/v2/spans
這個(gè)地址的POST被拒絕
三桨踪、源碼分析
使用MAT工具分析DUMP老翘,已經(jīng)得出了很多信息,甚至已經(jīng)知道問(wèn)題原因锻离。但是還需要進(jìn)一步分析源碼铺峭,詳細(xì)了解問(wèn)題的產(chǎn)生,以及解決方法汽纠。
1)InMemoryReporterMetrics
通過(guò)MAT分析得出的大對(duì)象引用關(guān)系卫键,查看類InMemoryReporterMetrics
:
private final ConcurrentHashMap<Throwable, AtomicLong> messagesDropped =
new ConcurrentHashMap<Throwable, AtomicLong>();
messagesDropped
是一個(gè)key 為Throwable
,value為AtomicLong
的ConcurrentHashMap
虱朵。
InMemoryReporterMetrics
莉炉,看名字,它是一個(gè)內(nèi)存報(bào)告度量碴犬。具體對(duì)是sleuth發(fā)送到zipkin服務(wù)器的所有消息的一個(gè)統(tǒng)計(jì)絮宁,包括發(fā)送成功的消息,發(fā)送失敗的消息服协。注意這個(gè)統(tǒng)計(jì)信息是存在內(nèi)存里的绍昂。
而這個(gè)度量中的messagesDropped
就是存儲(chǔ)發(fā)送異常的消息,key是具體異常信息偿荷,value是出現(xiàn)次數(shù)窘游。
那么推斷如果發(fā)送zipkin異常不斷產(chǎn)生,那么messagesDropped
的不斷堆積跳纳,勢(shì)必會(huì)造成OOM忍饰。
2)AsyncReporter
從引用關(guān)系上來(lái)看,InMemoryReporterMetrics
是由AsyncReporter.BoundedAsyncReporter
中的屬性metrics
引用的:
static final class BoundedAsyncReporter<S> extends AsyncReporter<S> {
final ReporterMetrics metrics;
}
在這個(gè)類的flush()
方法中棒旗,有這樣一段代碼:
void flush(BufferNextMessage<S> bundler) {
try {
sender.sendSpans(nextMessage).execute();
} catch (IOException | RuntimeException | Error t) {
// In failure case, we increment messages and spans dropped.
metrics.incrementMessagesDropped(t);
}
}
可以看到喘批,當(dāng)sender
發(fā)送消息到zipkin產(chǎn)生異常時(shí),就會(huì)將異常實(shí)例本身铣揉,存入metrics
的messagesDropped
中饶深。
AsyncReporter
類使用了build模式,來(lái)創(chuàng)建異步報(bào)告者(AsyncReporter)逛拱,而這個(gè)異步報(bào)告者的具體類敌厘,就是AsyncReporter
的內(nèi)部類BoundedAsyncReporter
。
在AsyncReporter.Builder
的builder()
方法中朽合,啟動(dòng)了一個(gè)線程俱两,在一個(gè)while
循環(huán)中饱狂,不斷將消息隊(duì)列中的消息flush到zipkin。這就是異步reporter的由來(lái)宪彩。
3)zipkin自動(dòng)配置
SpringBoot的自動(dòng)配置休讳,其實(shí)就是根據(jù)相關(guān)必須條件,將具備各種功能的bean注入到spring上下文中尿孔。zipkin的自動(dòng)配置也不例外:
自動(dòng)配置類ZipkinAutoConfiguration
創(chuàng)建異步報(bào)告者的方法如下:
@Bean
@ConditionalOnMissingBean
public Reporter<Span> reporter(
ReporterMetrics reporterMetrics,
ZipkinProperties zipkin,
Sender sender,
BytesEncoder<Span> spanBytesEncoder
) {
return AsyncReporter.builder(sender)
.queuedMaxSpans(1000) // historical constraint. Note: AsyncReporter supports memory bounds
.messageTimeout(zipkin.getMessageTimeout(), TimeUnit.SECONDS)
.metrics(reporterMetrics)
.build(spanBytesEncoder);
}
這個(gè)類中俊柔,還創(chuàng)建了發(fā)送到zipkin所需的sender,以及我們的關(guān)注點(diǎn)ReporterMetrics
:
@Bean
@ConditionalOnMissingBean
ReporterMetrics sleuthReporterMetrics() {
return new InMemoryReporterMetrics();
}
四活合、問(wèn)題原因
服務(wù)在開發(fā)測(cè)試時(shí)雏婶,使用了zipkin的調(diào)用鏈追蹤。但是投產(chǎn)時(shí)白指,由于某些原因留晚,無(wú)法使用zipkin,于是將zipkin的相關(guān)配置注釋掉了告嘲。
因此服務(wù)有zipkin的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
但是沒(méi)有zipkin的配置:
# 調(diào)用鏈
# zipkin:
# base-url: http://172.20.6.23:9412
# sleuth:
# sampler:
# probability: 1.0 # 采樣率, 默認(rèn)為0.1, 采樣10%的請(qǐng)求
通過(guò)觀察zipkin的自動(dòng)配置類ZipkinAutoConfiguration
:
@EnableConfigurationProperties({ZipkinProperties.class, SamplerProperties.class})
@ConditionalOnProperty(value = "spring.zipkin.enabled", matchIfMissing = true)
public class ZipkinAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Reporter<Span> reporter(
ReporterMetrics reporterMetrics,
ZipkinProperties zipkin,
Sender sender,
BytesEncoder<Span> spanBytesEncoder
) {
return AsyncReporter.builder(sender)
.queuedMaxSpans(1000) // historical constraint. Note: AsyncReporter supports memory bounds
.messageTimeout(zipkin.getMessageTimeout(), TimeUnit.SECONDS)
.metrics(reporterMetrics)
.build(spanBytesEncoder);
}
}
即使沒(méi)有任何zipkin的配置错维,都會(huì)創(chuàng)建一個(gè)異步報(bào)告者,默認(rèn)的采樣率是:
private float probability = 0.1f;
所以即使不配置相關(guān)配置項(xiàng)状蜗,也會(huì)以默認(rèn)采樣率10%需五,發(fā)送到zipkin,這是默認(rèn)的地址是:
@ConfigurationProperties("spring.zipkin")
public class ZipkinProperties {
/**
* URL of the zipkin query server instance. You can also provide
* the service id of the Zipkin server if Zipkin's registered in
* service discovery (e.g. http://zipkinserver/)
*/
private String baseUrl = "http://localhost:9411/";
}
此時(shí)發(fā)送到localhost顯然會(huì)連接拒絕轧坎。導(dǎo)致度量中的異常實(shí)例堆積宏邮,從而OOM。
五缸血、問(wèn)題解決
通過(guò)MAT分析和源碼分析蜜氨,可以容易得到問(wèn)題原因是zipkin地址的問(wèn)題,那么把地址配置正確應(yīng)該就可以解決問(wèn)題捎泻。
更深層次的問(wèn)題
通過(guò)分析得出飒炎,其實(shí)隨異步發(fā)送者創(chuàng)建的InMemoryReporterMetrics
是有缺陷的隔箍;
因?yàn)槿粲捎谝恍┎豢深A(yù)知的原因?qū)е掳l(fā)送zipkin產(chǎn)生異常惠险,那么這個(gè)異常信息會(huì)存放到內(nèi)存度量中(InMemoryReporterMetrics
)五慈,而且又沒(méi)有機(jī)制去刪除导而。若不斷堆積,還是會(huì)產(chǎn)生OOM洽沟。
這一點(diǎn)扼睬,不知道是不是zipkin的設(shè)計(jì)缺陷床嫌。
解決辦法
同事提出可以創(chuàng)建一個(gè)空的度量哄孤,來(lái)替換原來(lái)的內(nèi)存度量:
@Bean
public ReporterMetrics metrics() {
return new ReporterMetrics() {
@Override
public void incrementMessages() {
}
@Override
public void incrementMessagesDropped(Throwable cause) {
}
@Override
public void incrementSpans(int quantity) {
}
@Override
public void incrementSpanBytes(int quantity) {
}
@Override
public void incrementMessageBytes(int quantity) {
}
@Override
public void incrementSpansDropped(int quantity) {
}
@Override
public void updateQueuedSpans(int update) {
}
@Override
public void updateQueuedBytes(int update) {
}
};
}