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