本文以Spring Web的後臺開發講解。
上一篇講解了如何使用jvisualvm
監控Java程式。jvisualvm
雖然已經挺強大了,但是在實際的應用中依然不滿足我們的需求。現在,我們想要監控應用程式中所有Controller
提供的介面的訪問數量,頻次,響應時長。Service
層方法的執行次數,執行時長,頻次等等。以便之後對系統的效能優化做準備。這個時候jvisualvm
已經不能滿足我們的需求了。
1 方法級監控Java程式的方案
這是我對於方法級監控Java程式的方案:
- 付費的,比如YourKit,JProfile等。我嘗試了YourKit,功能確實強大,但是現在效能並不是我們現在的瓶頸,我們儘量使用不付費的。
- Metrics-Spring。Metrics-Spring需要在每個方法上使用註解。我們採用微服務架構,20多個服務,每個工程預計平均有100左右個方法要監控。如果是一開始就用這個我覺得還可以。
- Metrics+Spring AOP。從Metrics-Spring中可以看到,Metrics統計的資訊基本滿足我們的需求。我們的專案需求是統計
Controller
層和Service
層的方法。那麼可以通過Spring中的切面完成我們的需求。
我調查的方案和分析基本這樣,其他人如果有更好的方案可以提出一起探討。
下面是講解+部分程式碼,本次講解還有優化篇。
2 Metrics的功能
關於Metrics的使用方法,已經有很多文章介紹了,我在這裡推薦我認為還不錯的給大家,然後我再介紹的使用方法.
- Metrics介紹。這篇文章對Metrics的基本功能介紹的已經很全面了。
- Metrics-Spring官方文件。這篇文章介紹了Metrics與Spring的整合,但是文件感覺不全呀。
其他的文章我就不多分享了,感覺大同小異。沒什麼太大差別。
3 將Metrics相關類裝載到Spring容器
要使用Metric,那麼首先需要MetricRegistry
。
我們需要提供Http的報表,所以我們需要將MetricsServlet
註冊到Spring中,以便可以通過Http介面獲取監控結果。下面程式碼我們將監控介面定義為:/monitor/metrics
。
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.servlets.MetricsServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MonitorConfig {
@Bean
public MetricRegistry metricRegistry() {
return new MetricRegistry();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(MetricRegistry metricRegistry) {
return new ServletRegistrationBean(new MetricsServlet(metricRegistry), "/monitor/metrics");
}
}
複製程式碼
4 提供可控的終端報表
另外,為了方便除錯,我希望支援終端輸出報表的方式。但是要可以配置開啟和關閉,於是我使用另外一個配置類,ConditionalOnProperty
註解,讓配置根據配置屬性載入:
import com.codahale.metrics.ConsoleReporter;
import com.codahale.metrics.MetricRegistry;
import lombok.extern.java.Log;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.util.concurrent.TimeUnit;
@Configuration
@Log
@ConditionalOnProperty(prefix = "monitor.report", name = "console", havingValue = "true")
@Import(MonitorConfig.class)
public class MonitorReportConfig {
@Bean
public ConsoleReporter consoleReporter(MetricRegistry metrics) {
ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
reporter.start(10, TimeUnit.SECONDS);
return reporter;
}
}
複製程式碼
這樣可以在工程中的application.properties
檔案中,通過下面配置開啟終端報表,10秒鐘輸出一次:
monitor.report.console = true
複製程式碼
5 為要監控的方法準備Timer
Metrics中可以統計的資訊很多,其中Timer已經滿足了我們需要的資訊。
我為什麼要先為監控的方法準備Timer,而不是在方法執行的時候再建立呢?原因有兩點。
- 我們既關心方法被調,也關心它從來沒有被呼叫,如果是在方法執行的時候再建立,那麼我們就不知道是方法沒有被監控還是方法沒有被呼叫了。
- 我們之後打算直接對@RestController,@Controller和@Service註解進行切面。這種類級別的切面力度會包含我們不關心的方法,例如toString等方法,所以準備好關心的方法,呼叫的時候發現不是我們關心的方法直接放過。
我們使用MethodMonitorCenter
類來收集我們想要監控的方法。通過實現ApplicationContextAware
介面,在Spring容器裝載完畢之後,會回掉setApplicationContext
方法,我們通過getBeansWithAnnotation
方法找到包含指定註解的類。然後對其進行過濾,並獲取我們想要監控的方法。在最後我們通過metricRegistry.timer(method.toString());
方法為我們的關心的方法準備一個timer。
@Component
@Getter
@Log
public class MethodMonitorCenter implements ApplicationContextAware {
public static final String PACKAGE_NAME = "com.sinafenqi"; // 這裡換成自己的包名
@Autowired
private MetricRegistry metricRegistry;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> monitorBeans = new HashMap<>();
monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Controller.class));
monitorBeans.putAll(applicationContext.getBeansWithAnnotation(Service.class));
monitorBeans.putAll(applicationContext.getBeansWithAnnotation(RestController.class));
log.info("monitor begin scan methods");
monitorBeans.values().stream()
.map(obj -> obj.getClass().getName())
.map(this::trimString)
.map(clzName -> {
try {
return Class.forName(clzName);
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.filter(aClass -> aClass.getName().startsWith(PACKAGE_NAME))
.forEach(this::getClzMethods);
}
private void getClzMethods(Class<?> clz) {
Stream.of(clz.getDeclaredMethods())
.filter(method -> method.getName().indexOf('$') < 0)
.forEach(method -> {
log.info("add method timer, method name :" + method.toGenericString());
metricRegistry.timer(method.toString());
});
}
private String trimString(String clzName) {
if (clzName.indexOf('$') < 0) return clzName;
return clzName.substring(0, clzName.indexOf('$'));
}
}
複製程式碼
6 在切面中對方法進行監控
然後我們可以在切面中監控我們關心的方法。這裡使用環繞式切面對RestController
,Controller
,和Service
三個註解做切面。這樣就可以在方法之前和之後加一些監控程式碼。當進入around函式的時候,我們先去MetricRegistry中查詢有沒有對應的timer,如果沒有說明不是我們關心的方法,那麼我們就可以直接執行,如果存在,那麼我就對其進行監控。詳情可見程式碼:
@Component
@Aspect
@Log
public class MetricsMonitorAOP {
@Autowired
private MetricRegistry metricRegistry;
@Pointcut("@within(org.springframework.stereotype.Controller)" +
"||@within(org.springframework.stereotype.Service)" +
"||@within(org.springframework.web.bind.annotation.RestController)")
public void monitor() {
}
@Around("monitor()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String target = joinPoint.getSignature().toLongString();
Object[] args = joinPoint.getArgs();
if (!metricRegistry.getNames().contains(target)) {
return joinPoint.proceed(args);
}
Timer timer = metricRegistry.timer(target);
Timer.Context context = timer.time();
try {
return joinPoint.proceed(args);
} finally {
context.stop();
}
}
}
複製程式碼
7 效果
之後訪問/monitor/metrics
介面,就可以以Json的資料格式獲取監控結果。大家實驗的時候記得把MethodMonitorCenter
類中的PACKAGE_NAME
常量換成自己的。
現在基本已經實現監控所有Controller,和Service層我們定義的方法了,但是程式碼依然有很大的優化空間。這些程式碼是我從Git的版本庫中找出來的,自己沒有再去嘗試,如有問題歡迎留言。請諒解。目前我已經對程式碼進行了多處優化,優化內容將在下一篇講解,並會附上原始碼。
最後歡迎關注我的個人公眾號。提問,嘮嗑,都可以。