系統(tǒng)監(jiān)控實(shí)戰(zhàn)經(jīng)驗(yàn)
Spring Boot是一款優(yōu)秀的開源框架,可以快速引導(dǎo)和開發(fā)應(yīng)用程序。隨著應(yīng)用程序的規(guī)模逐漸擴(kuò)大,當(dāng)功能和傳入的請(qǐng)求不斷增加時(shí),SpringBoot應(yīng)用程序的性能就會(huì)受到影響。
這是所有Web應(yīng)用的正常情況。
在本節(jié)中,我們將討論與Spring Boot應(yīng)用程序性能優(yōu)化相關(guān)的開發(fā)技巧。
通過替換默認(rèn)組件提升Spring Boot性能
Spring Boot提供了以JAR文件的形式運(yùn)行Web應(yīng)用程序的嵌入式服務(wù)器。
一些可用的嵌入式服務(wù)器包括Tomcat、Undertow和Jetty。Spring Boot默認(rèn)使用的是Tomcat,我們建議使用Undertow作為嵌入式服務(wù)器。與Tomcat和Jetty相比,Undertow提供了更高的吞吐量和更少的內(nèi)存消耗。
想要在Spring Boot中使用Undertow,需要調(diào)整相關(guān)的Maven依賴,如代碼清單12-26所示。
代碼清單12-26 使用Undertow時(shí)的Maven依賴包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
可以看到,我們移除了對(duì)
spring-boot-starter-tomcat的依賴并添加了spring-boot-starter-undertow依賴。
一旦使用了Undertow,我們也可以在配置文件中添加對(duì)該服務(wù)器的專屬配置,如代碼清單12-27所示。
代碼清單12-27 Undertow相關(guān)配置項(xiàng)
server:
port: 8081
undertow: # 下面是配置Undertow作為服務(wù)器的參數(shù)
io-threads: 4 #設(shè)置I/O線程數(shù)
worker-threads: 20 #設(shè)置工作線程數(shù)
buffer-size: 1024 #設(shè)置buffer大小
direct-buffers: true # 是否分配直接內(nèi)存
另外,@SpringBootApplication注解是由@ComponentScan、@EnableAutoConfiguration和@SpringBootConfiguration這三個(gè)注解所組成的一個(gè)復(fù)合型注解,如代碼清單12-28所示。
代碼清單12-28 @SpringBootConfiguration注解定義代碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(...)
因此,@SpringBootApplication注解相當(dāng)于使用三個(gè)默認(rèn)配置的注解。
而其中的@ComponentScan注解會(huì)掃描基本包(Spring Boot應(yīng)用程序主類的包)和所有子包中定義的JAVA類。當(dāng)應(yīng)用程序的規(guī)模顯著擴(kuò)大時(shí),這會(huì)減慢系統(tǒng)的啟動(dòng)速度。
為了解決這個(gè)問題,我們可以用單獨(dú)的注解替換@SpringBootApplication注解,并明確指定使用@ComponentScan掃描的包路徑。我們還可以考慮只使用@Import注解導(dǎo)入所需的組件、Bean或配置。
使用PerformanceMonitorInterceptor
讓我們看看如何對(duì)方法執(zhí)行過程進(jìn)行分析或監(jiān)視。針對(duì)這個(gè)問題,我們可以使用Spring AOP所提供的
PerformanceMonitorInterceptor類。
在前面,我們已經(jīng)系統(tǒng)學(xué)習(xí)了AOP的相關(guān)內(nèi)容。而Spring AOP的
PerformanceMonitorInterceptor類是一個(gè)攔截器,可以綁定到任何想要執(zhí)行的自定義方法。PerformanceMonitorInterceptor會(huì)使用StopWatch實(shí)例記錄方法執(zhí)行的開始和結(jié)束時(shí)間。
想要在Spring Boot應(yīng)用程序中使用
PerformanceMonitorInterceptor,我們需要實(shí)現(xiàn)一個(gè)配置類,如代碼清單12-29所示。
代碼清單12-29
PerformanceMonitorInterceptor配置示例代碼
@Configuration
public class PerformanceMonitorConfiguration {
@Pointcut("execution(*com.springboot.aop.service.AccountService.doAcco
untTransaction(..))")
public void monitor() {
}
@Bean
public PerformanceMonitorInterceptorperformanceMonitorInterceptor() {
return new PerformanceMonitorInterceptor();
}
@Bean
public Advisor performanceMonitorAdvisor() {
AspectJExpressionPointcut pointcut = new
AspectJExpressionPointcut();
pointcut.setExpression("com.springboot.aop.
PerformanceMonitorConfiguration.monitor()");
return new DefaultPointcutAdvisor(pointcut,
performanceMonitorInterceptor());
}
}
可以看到,在該配置類中,我們首先通過@Pointcut注解聲明了一個(gè)切點(diǎn),該切點(diǎn)的目標(biāo)方法是
com.springboot.aop.service.AccountService.doAccountTransaction()。
然后,我們通過編碼的方式實(shí)現(xiàn)了一個(gè)Advisor,用于將這個(gè)切點(diǎn)與
PerformanceMonitorInterceptor關(guān)聯(lián)起來。
現(xiàn)在,當(dāng)我們執(zhí)行
com.springboot.aop.service.AccountService.doAccountTransaction()方法,系統(tǒng)日志中就會(huì)出現(xiàn)如代碼清單12-30所示的一行日志。
代碼清單12-30
PerformanceMonitorInterceptor執(zhí)行效果
o.s.a.i.PerformanceMonitorInterceptor : StopWatch
'com.springboot.aop.service.AccountService.doAccountTransaction':
running time (millis) = 11
顯然,通過
PerformanceMonitorInterceptor,我們就能獲取目標(biāo)方法的實(shí)時(shí)執(zhí)行時(shí)間,并根據(jù)這一度量結(jié)果執(zhí)行對(duì)應(yīng)的優(yōu)化和調(diào)整操作。
實(shí)現(xiàn)自定義的性能度量指標(biāo)
監(jiān)控系統(tǒng)的背后是各種度量指標(biāo),在Spring Boot 2.x版本中,Actuator組件使用內(nèi)置Micrometer庫(kù)來實(shí)現(xiàn)度量指標(biāo)的收集和分析。Micrometer是一個(gè)監(jiān)控指標(biāo)的度量類庫(kù),為Java平臺(tái)提供了一套通用的API,應(yīng)用程序只需要使用這些API來收集度量指標(biāo)即可。
在Micrometer中定義了一個(gè)計(jì)量器組件Meter。在日常開發(fā)過程中,我們常用的計(jì)量器主要是計(jì)數(shù)器Counter、計(jì)量?jī)xGauge和計(jì)時(shí)器Timer這三種。其中Counter是一個(gè)不斷遞增的累加器,我們可以通過它的increment()方法來實(shí)現(xiàn)累加邏輯;Gauge則與Counter不同,Gauge所度量的值并不一定是累加的,我們可以通過它的gauge()方法來指定數(shù)值;而Timer比較簡(jiǎn)單,就是用來記錄事件的持續(xù)時(shí)間。
我們以計(jì)數(shù)器為例,看看如何實(shí)現(xiàn)一個(gè)自定義的Counter,如代碼清單12-31所示。
代碼清單12-31 CustomCounter類實(shí)現(xiàn)代碼
public class CustomCounter {
private String name;
private String tagName;
private MeterRegistry registry;
private Map<String, Counter> counters = new HashMap<>();
public CustomCounter(String name, String tagName, MeterRegistry
registry) {
this.name = name;
this.tagName = tagName;
this.registry = registry;
}
public void increment(String tagValue){
Counter counter = counters.get(tagValue); if(counter == null) {
counter = Counter.builder(name).tags(tagName,
tagValue).register(registry);
counters.put(tagValue, counter);
}
counter.increment();
}
}
注意,這里使用了一個(gè)MeterRegistry類,該類是Micrometer提供的一個(gè)計(jì)量器注冊(cè)表,其作用就是負(fù)責(zé)創(chuàng)建和維護(hù)各種計(jì)量器。然后,我們看到可以使用Counter的builder()方法來創(chuàng)建一個(gè)Counter,并通過它的register()方法將其注冊(cè)到MeterRegistry中。
當(dāng)執(zhí)行CustomCounter的increment()方法時(shí),如果當(dāng)前已經(jīng)存在一個(gè)相同Tag的Counter,那么就對(duì)其值進(jìn)行遞增;反之先創(chuàng)建一個(gè)新的Counter,再執(zhí)行遞增操作。
讓我們編寫一個(gè)簡(jiǎn)單的測(cè)試用例來驗(yàn)證CustomCounter的正確性,如代碼清單12-32所示。
代碼清單12-32 CustomCounter類測(cè)試代碼
SimpleMeterRegistry registry = new SimpleMeterRegistry();
CustomCounter customCounter= new CustomCounter("pencil", "color",
registry);
customCounter.increment("black");
customCounter.increment("white");
customCounter.increment("white");
customCounter.increment("black");
customCounter.increment("brown");
customCounter.increment("black");
customCounter.increment("black");
customCounter.increment("black");
顯然,這時(shí)候CustomCounter中的Counter數(shù)應(yīng)該是3,而不同顏色對(duì)應(yīng)的數(shù)量也都正確。
系統(tǒng)監(jiān)控面試題分析
面試題1:Spring Boot Actuator組件為開發(fā)人員提供了哪些有用的監(jiān)控端點(diǎn)?
答案:Spring Boot Actuator為我們提供的監(jiān)控端點(diǎn)非常豐富。默認(rèn)情況下,一個(gè)Spring Boot應(yīng)用程序會(huì)暴露10余個(gè)常用端點(diǎn),包括info、beans、env、health、metrics、threaddump等。這些端點(diǎn)大多屬于配置類和度量指標(biāo)類這兩大類別,其中配置類端點(diǎn)與程序開發(fā)人員關(guān)系比較密切,而度量指標(biāo)類端點(diǎn)則更多面向系統(tǒng)運(yùn)維人員。當(dāng)然,在特定情況下,我們還可以執(zhí)行shutdown等操作控制類的端點(diǎn),但通常這類端點(diǎn)很少使用。
面試題2:如果想要實(shí)現(xiàn)一個(gè)自定義的Actuator端點(diǎn),你應(yīng)該怎么做?
答案:為了幫助開發(fā)人員實(shí)現(xiàn)自定義的Actuator端點(diǎn),Spring Boot專門提供了一個(gè)@Endpoint注解,該注解用于設(shè)置端點(diǎn)ID以及是否默認(rèn)啟動(dòng)標(biāo)志位。同時(shí),Spring Boot還提供了@ReadOperation注解、@WriteOperation和@DeleteOperation注解,分別用于標(biāo)識(shí)對(duì)監(jiān)控?cái)?shù)據(jù)的讀取、寫入和刪除操作。通常,在實(shí)現(xiàn)一個(gè)自定義Actuator端點(diǎn)時(shí),我們需要將@Endpoint端點(diǎn)以及這些數(shù)據(jù)操作端點(diǎn)組合起來使用。
面試題3:如果想要提升Spring Boot應(yīng)用程序的性能,從默認(rèn)組件優(yōu)化的角度我們可以做哪些事情?
答案:這是一道比較經(jīng)典的面試題,考查的是我們對(duì)Spring Boot應(yīng)用程序運(yùn)行機(jī)制的掌握程度。對(duì)于這一問題,一般的回答思路是兩個(gè)方面,第一個(gè)方面是服務(wù)器類型,另一個(gè)方面是包掃描范圍。針對(duì)服務(wù)器類型,SpringBoot默認(rèn)使用的是Tomcat,而性能最好的實(shí)際上是Undertow,通過修改默認(rèn)配置可以把Tomcat轉(zhuǎn)換為Undertow。而針對(duì)包掃描范圍,因?yàn)镾pring Boot默認(rèn)情況下會(huì)掃描當(dāng)前類路徑下的所有包結(jié)構(gòu),因此對(duì)那些不需要在應(yīng)用程序啟動(dòng)時(shí)就掃描的包結(jié)構(gòu)而言是一種浪費(fèi),可以通過顯式指定包結(jié)構(gòu)的方式來提升性能。
面試題4:在Spring Boot中,如果我們想要自己實(shí)現(xiàn)一些性能度量指標(biāo),可以怎么做?
答案:Spring Boot為開發(fā)人員實(shí)現(xiàn)自定義性能度量指標(biāo)提供了高度的擴(kuò)展性,這種擴(kuò)展性來自其內(nèi)置的Micrometer框架。Micrometer對(duì)度量指標(biāo)管理過程進(jìn)行了高度的抽象,并內(nèi)置了計(jì)數(shù)器Counter、計(jì)量?jī)xGauge和計(jì)時(shí)器Timer這三種計(jì)量器組件。通常情況下,開發(fā)人員只需要使用這三種計(jì)量器組件所提供的構(gòu)建方法完成目標(biāo)計(jì)量器的構(gòu)建,并基于對(duì)應(yīng)的工具方法完成數(shù)據(jù)采集即可。