提出需求
要写一个微服务监控组件,来监控所有业务微服务是否健康。但是 不能侵入微服务的代码(实际上业务开发人员也不会在代码层面加你的监控组件),作为架构师的你,如何完成这一功能 ?
实践流程
新建一个monitor.jar(监控组件第三方jar) 。我们使用oshi进行linux机器的cpu,内存监控。引入maven:
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>3.5.0</version>
</dependency>
编写业务监控Bean:
流程:实现InitializingBean,在初始化afterPropertiesSet方法里面 开启监控线程,线程循环获取当前cpu,内存信息,进行预警。
新建一个Monitor.java类:
import java.text.DecimalFormat;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
import oshi.hardware.GlobalMemory;
import org.springframework.beans.factory.InitializingBean;
public class Monitor implements InitializingBean, Runnable {
private Thread thread;
@Override
public void afterPropertiesSet() throws Exception {
// 开启监控线程
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
while (thread.isInterrupted()) {
SystemInfo systemInfo = new SystemInfo();
CentralProcessor processor = systemInfo.getHardware().getProcessor();
long[] prevTicks = processor.getSystemCpuLoadTicks();
long[] ticks = processor.getSystemCpuLoadTicks();
long nice =
ticks[CentralProcessor.TickType.NICE.getIndex()] - prevTicks[CentralProcessor.TickType.NICE.getIndex()];
long irq =
ticks[CentralProcessor.TickType.IRQ.getIndex()] - prevTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()]
- prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()]
- prevTicks[CentralProcessor.TickType.STEAL.getIndex()];
long cSys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()]
- prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long user =
ticks[CentralProcessor.TickType.USER.getIndex()] - prevTicks[CentralProcessor.TickType.USER.getIndex()];
long iowait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()]
- prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long idle =
ticks[CentralProcessor.TickType.IDLE.getIndex()] - prevTicks[CentralProcessor.TickType.IDLE.getIndex()];
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
System.err.println("CPU总数:" + processor.getLogicalProcessorCount());
System.err.println("CPU利用率:" + new DecimalFormat("#.##%").format(1.0 - (idle * 1.0 / totalCpu)));
// 内存 监控
GlobalMemory memory = systemInfo.getHardware().getMemory();
long totalByte = memory.getTotal();
long acaliableByte = memory.getAvailable();
System.err.println("内存大小 : " + formatByte(totalByte));
System.err
.println("内存使用率:" + new DecimalFormat("#.##%").format((totalByte - acaliableByte) * 1.0 / totalByte));
// 不让其 一直跑监控 减少性能消耗
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
}
private static String formatByte(long byteNumber) {
double FORMAT = 1024.0;
double kbNumber = byteNumber / FORMAT;
if (kbNumber < FORMAT) {
return new DecimalFormat("#.##KB").format(kbNumber);
}
double mbNumber = kbNumber / FORMAT;
if (mbNumber < FORMAT) {
return new DecimalFormat("#.##MB").format(mbNumber);
}
double gbNumber = mbNumber / FORMAT;
if (gbNumber < FORMAT) {
return new DecimalFormat("#.##GB").format(gbNumber);
}
double tbNumber = gbNumber / FORMAT;
return new DecimalFormat("#.##TB").format(tbNumber);
}
}
上述监控bean写完了,问题时,如何注入到业务服务里面去呢?
在此之前 ,我们先来来了解一个技术: SpringBoot的自动配置。
SpringBoot的自动配置技术
springboot在加载的时候,会扫描环境变量下的 META-INF/spring.factories 文件,比如有个配置如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.uc.A,com.uc.B
然后就构建一个Map, key为org.springframework.boot.autoconfigure.EnableAutoConfiguration , 值为一个list,A,B 。 然后 将这个map所有的value都自动注入到Spring容器里面。
SpringBoot自动配置原理
我们复习下springboot的知识, 看下这个启动类的注解SpringBootApplication:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//.....
}
- @SpringBootConfiguration :
就是@Configuration,支持JavaConfig的方式来进行配置(使用Configuration配置类等同于XML文件)。 - @ComponentScan:
扫描注解,默认是扫描当前类下的package。将@Controller/@Service/@Component/@Repository等注解加载到IOC容器中。 - @EnableAutoConfiguration: 开启自动配置功能。
EnableAutoConfiguration注解
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
@Import 注解
@Import 可以引入ImportSelector的实现类,把ImportSelector接口selectImports()方法返回的Class名称都定义为bean。注意selectImports()方法的参数AnnotationMetadata,通过这个参数我们可以获取到@Import标注的Class的各种信息。这一点特别有用,用于做一些参数的传递。在SpringBoot的自动化配置和@EnableXXX(功能性注解)都有它的存在。
public interface ImportSelector {
/**
* 用于指定需要注册为bean的Class名称
* 当在@Configuration标注的Class上使用@Import引入了一个ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。
*
* 通过其参数AnnotationMetadata importingClassMetadata可以获取到@Import标注的Class的各种信息,
* 包括其Class名称,实现的接口名称、父类名称、添加的其它注解等信息,通过这些额外的信息可以辅助我们选择需要定义为Spring bean的Class名称
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
AutoConfigurationImportSelector 实现
@Override
public String[] selectImports(AnnotationMetadata metadata) {
try {
AnnotationAttributes attributes = getAttributes(metadata);
// 这里就是加载META-INF/spring.factories 配置下所有的类名称
List<String> configurations = getCandidateConfigurations(metadata,
attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(metadata, attributes);
configurations.removeAll(exclusions);
configurations = sort(configurations);
recordWithConditionEvaluationReport(configurations, exclusions);
// 返回 就注入到spring了
return configurations.toArray(new String[configurations.size()]);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
// 这个loadFactoryNames相当于从map中取数据,key为EnableAutoConfiguration.class
return SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, getBeanClassLoader());
}
我们只看关键的加载哪些class定义成bean, 于是要关注getCandidateConfigurations 这个加载的方法,最终会追踪到下面的loadSpringFactories方法:
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 这个就是上面我说的那个Map。 key为EnableAutoConfiguration.class,值就是用户A,B类
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources("META-INF/spring.factories") :
ClassLoader.getSystemResources("META-INF/spring.factories"));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
主要就是 加载 环境变量下的 META-INF/spring.factories 路径下的配置 ,取像下面这种:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.uc.A,com.uc.B
配置的A,B类,然后返回,最终是selectImports 的返回,就注入到spring了。到此原理结束。
回到我们的业务监控
编写自动配置类
我们继续在Monitor.jar中编写一个MonitorAutoConfiguration类:
package com.uc.neighbour.trace.client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.uc.neighbour.trace.client.container.Container;
@Configuration
public class MonitorAutoConfiguration {
/***
*
* title: 初始化容器
*
* @return
* @author HadLuo 2021-3-2 14:34:43
*/
@Bean // 注入spring
// 判断当前需要注入Spring容器中的bean的实现类是否已经含有,有的话不注入,没有就注入
@ConditionalOnMissingBean(Container.class)
public Monitor registerMonitor() {
// Monitor 就是文章开头 嗯写的业务监控 bean
return new Monitor();
}
}
写好了我们的自动配置类,然后将其放到配置路径下,在 Monitor.jar 的 resources目录下新建 META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.uc.neighbour.trace.client.MonitorAutoConfiguration
这样就完成了我们的监控 Monitor.jar 。
然后业务微服务在maven中引入我们的 Monitor.jar 就可以了。可以在你们的base工程中直接引入,业务微服务就无需改任务代码拉!!!!!
文章最后,有一个问题
业务微服务都是集群的,这样会导致每台机器都启动了监控线程,所以在监控bean中要把当前机器的ip打出来,这样就定位到哪个服务的cpu,内存情况了。