开发完成后,生产就绪需要做哪些工作呢?我认为,以下三方面的工作最重要。
提供健康检测接口。传统采用 ping 的方式对应用进行探活检测并不准确。有的时候,应用的关键内部或外部依赖已经离线,导致其根本无法正常工作,但其对外的 Web 端口或管理端口是可以 ping 通的。我们应该提供一个专有的监控检测接口,并尽可能触达一些内部组件。
暴露应用内部信息。应用内部诸如线程池、内存队列等组件,往往在应用内部扮演了重要的角色,如果应用或应用框架可以对外暴露这些重要信息,并加以监控,那么就有可能在诸如 OOM 等重大问题暴露之前发现蛛丝马迹,避免出现更大的问题。
建立应用指标 Metrics 监控。Metrics 可以翻译为度量或者指标,指的是对于一些关键信息以可聚合的、数值的形式做定期统计,并绘制出各种趋势图表。这里的指标监控,包括两个方面:一是,应用内部重要组件的指标监控,比如 JVM 的一些指标、接口的 QPS 等;二是,应用的业务数据的监控,比如电商订单量、游戏在线人数等。
Spring Boot 有一个 Actuator 模块,封装了诸如健康检测、应用内部信息、Metrics 指标等生产就绪的功能。今天这一讲后面的内容都是基于 Actuator 的,因此我们需要先完成 Actuator 的引入和配置
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
Actuator 自带了很多开箱即用提供信息的端点(Endpoint),可以通过** JMX 或 Web **两种方式进行暴露。考虑到有些信息比较敏感,这些内置的端点默认不是完全开启的,你可以通过官网查看这些默认值。在这里,为了方便后续 Demo,我们设置所有端点通过 Web 方式开启。
默认情况下,Actuator 的 Web 访问方式的根地址为 /actuator
,可以通过management.endpoints.web.base-path
参数进行修改
management:
server:
port: 45678
endpoints:
web:
exposure:
include: "*"
base-path: /admin
现在访问http://localhost:45678/admin
可以查看 Actuator 的所有功能 URL
大部分端点提供的是只读信息,比如查询 Spring 的 Bean、ConfigurableEnvironment、定时任务、SpringBoot 自动配置、Spring MVC 映射等;少部分端点还提供了修改功能,比如优雅关闭程序、下载线程 Dump、下载堆 Dump、修改日志级别等。
我们可以访问这里,查看所有这些端点的功能,详细了解它们提供的信息以及实现的操作。此外推荐一个很好的工具, Spring Boot 管理工具Spring Boot Admin,它把大部分 Actuator 端点提供的功能封装为了 Web UI。可以参考:Spring Boot Admin服务监控
健康检测接口可以让监控系统或发布工具知晓应用的真实健康状态,比 ping 应用端口更可靠。不过,要达到这种效果最关键的是,我们能确保健康检测接口可以探查到关键组件的状态。好在 Spring Boot Actuator
帮我们预先实现了诸如数据库、InfluxDB、Elasticsearch、Redis、RabbitMQ 等三方系统的健康检测指示器 HealthIndicator。
通过 Spring Boot 的自动配置,这些指示器会自动生效。当这些组件有问题的时候,HealthIndicator 会返回 DOWN
或 OUT_OF_SERVICE
状态,health 端点 HTTP 响应状态码也会变为 503,我们可以以此来配置程序健康状态监控报警。
management:
server:
port: 45678
endpoints:
web:
exposure:
include: "*"
base-path: /admin
endpoint:
health:
show-details: always
我们可以修改配置文件,把 management.endpoint.health.show-details 参数设置为 always,让所有用户都可以直接查看各个组件的健康情况(如果配置为 when-authorized,那么可以结合 management.endpoint.health.roles 配置授权的角色)。访问 health 端点可以看到,数据库、磁盘、RabbitMQ、Redis 等组件健康状态是 UP,整个应用的状态也是 UP
如果程序依赖一个很重要的三方服务,我们希望这个服务无法访问的时候,应用本身的健康状态也是 DOWN,首先创建User类以及配置bean
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private long userId;
private String userName;
}
@Configuration
public class Config {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
比如三方服务有一个 user 接口,出现异常的概率是 50%:
@Slf4j
@RestController
@RequestMapping("user")
public class UserServiceController {
@GetMapping
public User getUser(@RequestParam("userId") long id) {
//一半概率返回正确响应,一半概率抛异常
if (ThreadLocalRandom.current().nextInt() % 2 == 0) {
return new User(id, "name" + id);
} else {
throw new RuntimeException("error");
}
}
}
要实现这个 user 接口是否正确响应和程序整体的健康状态挂钩的话,很简单,只需定义一个 UserServiceHealthIndicator
实现 HealthIndicator
接口即可。
在 health 方法中,我们通过 RestTemplate 来访问这个 user 接口,如果结果正确则返回 Health.up()
,并把调用执行耗时和结果作为补充信息加入 Health 对象中。如果调用接口出现异常,则返回 Health.down()
,并把异常信息作为补充信息加入 Health 对象中:
@Component
@Slf4j
public class UserServiceHealthIndicator implements HealthIndicator {
@Autowired
private RestTemplate restTemplate;
@Override
public Health health() {
long begin = System.currentTimeMillis();
long userId = 2L;
User user = null;
try {
user = restTemplate.getForObject("http://localhost:8080/user?userId=" + userId, User.class);
if (user != null && user.getUserId() == userId) {
return Health.up()
.withDetail("user", user)
.withDetail("took", System.currentTimeMillis() - begin)
.build();
} else {
return Health.down().withDetail("took", System.currentTimeMillis() - begin).build();
}
} catch (Exception ex) {
log.warn("health check failed!", ex);
return Health.down(ex).withDetail("took", System.currentTimeMillis() - begin).build();
}
}
}
此时访问http://localhost:45678/admin/health
即可发现UserService已经成功被检测
我们再来看一个聚合多个 HealthIndicator 的案例,也就是定义一个 CompositeHealthContributor 来聚合多个 HealthContributor,实现一组线程池的监控
首先,在 ThreadPoolProvider 中定义两个线程池,其中 demoThreadPool 是包含一个工作线程的线程池,类型是 ArrayBlockingQueue,阻塞队列的长度为 10;还有一个 ioThreadPool 模拟 IO 操作线程池,核心线程数 10,最大线程数 50
public class ThreadPoolProvider {
//一个工作线程的线程池,队列长度10
private static ThreadPoolExecutor demoThreadPool = new ThreadPoolExecutor(
1, 1,
2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new CustomizableThreadFactory("springThread-pool-"));
//核心线程数10,最大线程数50的线程池,队列长度50
private static ThreadPoolExecutor ioThreadPool = new ThreadPoolExecutor(
10, 50,
2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new CustomizableThreadFactory("io-Thread-pool-"));
public static ThreadPoolExecutor getDemoThreadPool() {
return demoThreadPool;
}
public static ThreadPoolExecutor getIOThreadPool() {
return ioThreadPool;
}
}
然后,我们定义一个接口,来把耗时很长的任务提交到这个 demoThreadPool 线程池,以模拟线程池队列满的情况:
@GetMapping("slowTask")
public void slowTask() {
ThreadPoolProvider.getDemoThreadPool().execute(() -> {
try {
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
}
});
}
做了这些准备工作后,让我们来真正实现自定义的 HealthIndicator
类,用于单一线程池的健康状态。我们可以传入一个 ThreadPoolExecutor
,通过判断队列剩余容量来确定这个组件的健康状态,有剩余量则返回 UP,否则返回 DOWN,并把线程池队列的两个重要数据,也就是当前队列元素个数和剩余量,作为补充信息加入 Health
public class ThreadPoolHealthIndicator implements HealthIndicator {
private ThreadPoolExecutor threadPool;
public ThreadPoolHealthIndicator(ThreadPoolExecutor threadPool) {
this.threadPool = threadPool;
}
@Override
public Health health() {
Map<String, Integer> detail = new HashMap<>();
detail.put("queue_size", threadPool.getQueue().size());
detail.put("queue_remaining", threadPool.getQueue().remainingCapacity());
if (threadPool.getQueue().remainingCapacity() > 0) {
return Health.up().withDetails(detail).build();
} else {
return Health.down().withDetails(detail).build();
}
}
}
再定义一个 CompositeHealthContributor
,来聚合两个 ThreadPoolHealthIndicator
的实例,分别对应 ThreadPoolProvider
中定义的两个线程池:
@Component
public class ThreadPoolsHealthContributor implements CompositeHealthContributor {
private Map<String, HealthContributor> contributors = new HashMap<>();
ThreadPoolsHealthContributor() {
this.contributors.put("demoThreadPool", new ThreadPoolHealthIndicator(ThreadPoolProvider.getDemoThreadPool()));
this.contributors.put("ioThreadPool", new ThreadPoolHealthIndicator(ThreadPoolProvider.getIOThreadPool()));
}
@Override
public HealthContributor getContributor(String name) {
return contributors.get(name);
}
@Override
public Iterator<NamedContributor<HealthContributor>> iterator() {
return contributors.entrySet().stream()
.map((entry) -> NamedContributor.of(entry.getKey(), entry.getValue())).iterator();
}
}
启动后当看到一个 demoThreadPool
为 DOWN 导致父 threadPools
为 DOWN,进一步导致整个程序的 status
为 DOWN:
除了可以把线程池的状态作为整个应用程序是否健康的依据外,我们还可以通过 Actuator 的 InfoContributor
功能,对外暴露程序内部重要组件的状态数据
@Component
public class ThreadPoolInfoContributor implements InfoContributor {
private static Map threadPoolInfo(ThreadPoolExecutor threadPool) {
Map<String, Object> info = new HashMap<>();
info.put("poolSize", threadPool.getPoolSize());
info.put("corePoolSize", threadPool.getCorePoolSize());
info.put("largestPoolSize", threadPool.getLargestPoolSize());
info.put("maximumPoolSize", threadPool.getMaximumPoolSize());
info.put("completedTaskCount", threadPool.getCompletedTaskCount());
return info;
}
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("demoThreadPool", threadPoolInfo(ThreadPoolProvider.getDemoThreadPool()));
builder.withDetail("ioThreadPool", threadPoolInfo(ThreadPoolProvider.getIOThreadPool()));
}
}
访问 /admin/info 接口,可以看到这些数据
如果开启了JMX,即spring.jmx.enabled=true
,可以通过 jconsole 工具,在 org.springframework.boot.Endpoint
中找到 Info
这个 MBean,然后执行 info 操作可以看到,我们刚才自定义的 InfoContributor
输出的有关两个线程池的信息:
指标是指一组和时间关联的、衡量某个维度能力的量化数值。通过收集指标并展现为曲线图、饼图等图表,可以帮助我们快速定位、分析问题
健康检测可以帮我们实现负载均衡的联动;应用信息以及 Actuaor 提供的各种端点,可以帮我们查看应用内部情况,甚至对应用的一些参数进行调整;而指标监控,则有助于我们整体观察应用运行情况,帮助我们快速发现和定位问题