spring boot 微服务作为一项在云中部署应用和服务的新技术是当下比较热门话题,而微服务的特点决定了功能模块的部署是分布式的,运行在不同的机器上相互通过服务调用进行交互,业务流会经过多个微服务的处理和传递,在这种框架下,微服务的监控显得尤为重要。我们知道,spring boot 在引入
org.springframework.boot
spring-boot-starter-actuator
这个jar包之后就拥有了多个自带的mapping映射端口,其中最常用的是/health接口,我们可以通过这个接口查看包括磁盘空间,redis集群连接情况,es集群连接情况,rabbit连接情况,mysql连接情况在内的众多信息,查看效果如下:
可以发现,只要有一个服务DOWN之后,整个实例的状态就会呈现DOWN状态,其中不仅响应体是这样,响应码也会根据DOWN的状态返回不同的值,例如这次的请求,状态码就是503,而正常情况下是200响应码
所以,这些东西到底有什么用吗,我们知道,某个实例,在连接比如es服务在在超时后,会导致相关功能不可用,介入人工解决的话,对开发人员来说简直是一场噩梦,得时刻盯着服务的健康状态,那么有没有什么好办法呢,那就是云端每几秒检测一下实例的/health接口,看下实例的健康状态,如果呈现DOWN的状态就将该实例kill,从而重启另一个实例代替原有实例(重启大发好(滑稽)),而且因为我们服务大多是集群对外提供服务的,所以一个实例挂掉,对整体并无大碍。这样就可以实现高可用和稳定性。
Actuator监控分成两类:原生端点和用户自定义扩展端点,原生的主要有:
路径 | 描述 |
---|---|
/autoconfig | 提供了一份自动配置报告,记录哪些自动配置条件通过了,哪些没通过 |
/beans | 描述应用程序上下文里全部的Bean,以及它们的关系 |
/env | 获取全部环境属性 |
/configprops | 描述配置属性(包含默认值)如何注入Bean |
/dump | 获取线程活动的快照 |
/health | 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供 |
/info | 获取应用程序的定制信息,这些信息由info打头的属性提供 |
/mappings | 描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系 |
/metrics | 报告各种应用程序度量信息,比如内存用量和HTTP请求计数 |
/shutdown | 关闭应用程序,要求endpoints.shutdown.enabled设置为true |
/trace | 提供基本的HTTP请求跟踪信息(时间戳、HTTP头等) |
安全措施
如果上述请求接口不做任何安全限制,安全隐患显而易见。实际上Spring Boot也提供了安全限制功能。比如要禁用/env接口,则可设置如下:
endpoints.env.enabled= false
如果只想打开一两个接口,那就先禁用全部接口,然后启用需要的接口:
endpoints.enabled = false
endpoints.health.enabled = true
另外也可以引入spring-boot-starter-security依赖
org.springframework.boot
spring-boot-starter-security
在application.properties中指定actuator的端口以及开启security功能,配置访问权限验证,这时再访问actuator功能时就会弹出登录窗口,需要输入账号密码验证后才允许访问。
management.port=8099
management.security.enabled=true
security.user.name=admin
security.user.password=admin
actuator暴露的health接口权限是由两个配置: management.security.enabled
和 endpoints.health.sensitive
组合的结果进行返回的。
management.security.enabled | endpoints.health.sensitive | Unauthenticated | Authenticated |
---|---|---|---|
false | false | Full content | Full content |
false | true | Status only | Full content |
true | false | Status only | Full content |
true | true | No content | Full content |
安全建议
在使用Actuator时,不正确的使用或者一些不经意的疏忽,就会造成严重的信息泄露等安全隐患。在代码审计时如果是springboot项目并且遇到actuator依赖,则有必要对安全依赖及配置进行复查。也可作为一条规则添加到黑盒扫描器中进一步把控。
安全的做法是一定要引入security依赖,打开安全限制并进行身份验证。同时设置单独的Actuator管理端口并配置不对外网开放。
实现原理
首先是接口HealthIndicator
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.health;
/**
* Strategy interface used to provide an indication of application health.
*
* @author Dave Syer
* @see ApplicationHealthIndicator
*/
public interface HealthIndicator {
/**
* Return an indication of health.
* @return the health for
*/
Health health();
}
可以看到这个接口有很多实现类,默认的实现类就是/health接口返回的那些信息,包括
RabbitHealthIndicator
MongoHealthIndicator
ElasticsearchHealthIndicator
等等…..
其中接口的返回值是Health类
包括了整体的状态Status和健康明细details,Status有4个状态描述:
其中
UNKNOWN
UP
都会返回200,而剩下都是返回503服务不可用
一般情况不会直接实现这个接口,而是现实它的抽象类AbstractHealthIndicator
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.health;
import org.springframework.boot.actuate.health.Health.Builder;
/**
* Base {@link HealthIndicator} implementations that encapsulates creation of
* {@link Health} instance and error handling.
*
* This implementation is only suitable if an {@link Exception} raised from
* {@link #doHealthCheck(org.springframework.boot.actuate.health.Health.Builder)} should
* create a {@link Status#DOWN} health status.
*
* @author Christian Dupuis
* @since 1.1.0
*/
public abstract class AbstractHealthIndicator implements HealthIndicator {
@Override
public final Health health() {
Health.Builder builder = new Health.Builder();
try {
doHealthCheck(builder);
}
catch (Exception ex) {
builder.down(ex);
}
return builder.build();
}
/**
* Actual health check logic.
* @param builder the {@link Builder} to report health status and details
* @throws Exception any {@link Exception} that should create a {@link Status#DOWN}
* system status.
*/
protected abstract void doHealthCheck(Health.Builder builder) throws Exception;
}
可以看到抽象类有一个final标志的health()方法,代表着这个方法是不可以用来重写的,我们包括自定义的健康检查项目都可以用doHealthCheck来重写我们具体的实现,下面是一个es的原生实现类:
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.health;
import java.util.List;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.Requests;
/**
* {@link HealthIndicator} for an Elasticsearch cluster.
*
* @author Binwei Yang
* @author Andy Wilkinson
* @since 1.3.0
*/
public class ElasticsearchHealthIndicator extends AbstractHealthIndicator {
private static final String[] allIndices = { "_all" };
private final Client client;
private final ElasticsearchHealthIndicatorProperties properties;
public ElasticsearchHealthIndicator(Client client,
ElasticsearchHealthIndicatorProperties properties) {
this.client = client;
this.properties = properties;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
List indices = this.properties.getIndices();
ClusterHealthResponse response = this.client.admin().cluster()
.health(Requests.clusterHealthRequest(indices.isEmpty() ? allIndices
: indices.toArray(new String[indices.size()])))
.actionGet(this.properties.getResponseTimeout());
switch (response.getStatus()) {
case GREEN:
case YELLOW:
builder.up();
break;
case RED:
default:
builder.down();
break;
}
builder.withDetail("clusterName", response.getClusterName());
builder.withDetail("numberOfNodes", response.getNumberOfNodes());
builder.withDetail("numberOfDataNodes", response.getNumberOfDataNodes());
builder.withDetail("activePrimaryShards", response.getActivePrimaryShards());
builder.withDetail("activeShards", response.getActiveShards());
builder.withDetail("relocatingShards", response.getRelocatingShards());
builder.withDetail("initializingShards", response.getInitializingShards());
builder.withDetail("unassignedShards", response.getUnassignedShards());
}
}
可以看到它继承了AbstractHealthIndicator并且实现了doHealthCheck方法,通过检测es集群的健康状态来映射实例的es健康状态,我们知道es的绿色和黄色代表正常和预警,红色代表有问题,之后在拼接详细明细到builder这个构造器中。
同理,我们也可以自定义我们自己的实现,例如:
@Component
public class CusDiskSpaceHealthIndicator extends AbstractHealthIndicator {
private final FileStore fileStore;
private final long thresholdBytes;
@Autowired
public CusDiskSpaceHealthIndicator(
@Value("${health.filestore.path:/}") String path,
@Value("${health.filestore.threshold.bytes:10485760}") long thresholdBytes)
throws IOException {
fileStore = Files.getFileStore(Paths.get(path));
this.thresholdBytes = thresholdBytes;
}
// 检查逻辑
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
long diskFreeInBytes = fileStore.getUnallocatedSpace();
if (diskFreeInBytes >= thresholdBytes) {
builder.up();
} else {
builder.down();
}
long totalSpaceInBytes = fileStore.getTotalSpace();
builder.withDetail("disk.free", diskFreeInBytes);
builder.withDetail("disk.total", totalSpaceInBytes);
}
}
实际问题
在公司的一个实际项目中,由于监控平台会定时检查实例的健康状态,如果不健康的话会将实例的docker进行kill,之后进行重启,最近每天晚上会发送一个报警信息,显示实例被删除重启,时间点是固定的,大概在晚上的凌晨0点到2点之间,于是介入调查问题原因。
首先重启报警是因为实例健康状态的原因,也就是/health接口返回503,这点是肯定的,而且每次发生的时间点是晚上0-2点,不是白天,肯定是定时任务搞得鬼,于是开始查找所有的定时job,排除每隔一段时间就进行的定时任务,那肯定就是只有在某个点才触发的操作,于是点位到了一个job,之后就是看看具体的实现了。
查看实现后,发现该实现依赖了两个外部的服务,一个是mogodb一个是es服务器,问题被缩小了,之后该怎么办呢?
由于每天发生故障的时间点是半夜,无法直接查看/health接口的返回((⊙﹏⊙)),而且springBoot1.X版本也没有相关的日志打印(坑),所以看到了health接口的具体源码实现后,决定自己写个aop来代理打印出具体失败时候的日志。。。
要实现aop代理的话我们注意到Health类,它包含了所以的接口返回信息,于是决定找到返回是Health的类的方法,并且该方法是可以重写的方法才行,因为我们的项目是基于cgLib的代理,cgLib是基于继承的,如果方法被final标注的话,意味着这个方法代理对象是没有的,说的是谁呢,对,就是你。。。
于是开始像上找调用这个方法的类,找到了它:
可以看到这个是个avaBean,被spring托管,且返回值有health,可以很方便的实现代理,于是对他进行@AfterReturn的代理
@Pointcut("execution(* org.springframework.boot.actuate.endpoint.HealthEndpoint.invoke(..))")
public void healthCheckFacade() {
}
很容易就拿到了Health类并在Status是Down和OUT_OF_SERVICE时进行了打印,之后就是等待复现,找出这个臭虫(bug)
第二天,如期而至,查找找历史日志,可以看到
[{"details":{"error":"org.elasticsearch.ElasticsearchTimeoutException: java.util.concurrent.TimeoutException: Timeout waiting for task."},"status":{"code":"DOWN","description":""}}]
这么一段话,没错,就是那个定时任务搞得鬼,导致es服务器Red状态,健康检查DOWN状态才重启的
解决方法有两个,首先看这个错误java.util.concurrent.TimeoutException: Timeout waiting for task,在从es服务器上对应的时间段看日志,可以发现这个时间点,有很多的看到blukload的的错误日志,而我们使用的es服务器是5.X版本的,log4j是有一个bug的,导致内存泄露,贸然升级的话,对生产数据时有风险的,于是通过调整参数重启来解决,此外为啥会有这多日志,原来是因为这个点进行了大量的update操作,而且有些update是没有mapppingId的,导致es大量报错,所以通过修改定时任务的频率和过滤非法数据来保证服务的可用性。