在 Spring Boot 项目中,我们使用的信息采集器主要就是 Spring Boot Actuator,这个模块由 Spring Boot 官方提供。
它包含了许多生产级别的功能,例如健康检查、审计、指标收集、HTTP 请求追踪等,Spring Boot Actuator 将这些信息收集起来后,通过 HTTP 和 JMX 两种方式暴露给外部模块。
例如 Spring Boot Actuator 通过 /health 端点(endpoints)提供了应用的健康信息,开发者只需要访问该端点就可以看到应用的健康信息,但是这些端点返回的数据是 JSON 格式的,不方便查看,也不方便分析,所以一般情况下,Spring Boot Actuator 都是和一些外部模块一起使用。
Actuator 端点
Spring Boot Security 保护 Actuator 端点信息
我们可以使用 Security 来保护 Actuator,需要登陆才可以访问我们打开的端点。
- 首先添加 Security 依赖。
- 创建 config/SecurityConfig 文件来配置 Security,重写 Security 方法。
- 在配置文件中,配置用户基本登陆信息。
- 重启项目,Postman 中需要选择 Authorization,登陆类型选择 Basic Auth,输入用户名密码,重新请求远程暂停端点查看效果。
修改 Actuator 路径映射
只需要在配置文件中修改即可。
1 2 3 4 5
| management: endpoints: web: base-path: /
|
也可以针对某一个端点进行修改路径映射
1 2 3 4 5 6
| management: endpoints: web: path-mapping: beans: bs
|
Actuator 跨域支持
在配置文件中配置跨域支持
1 2 3 4 5 6 7 8
| management: endpoints: web: cors: allowed-origins: * allowed-methods: GET,POST
|
Actuator 健康指示器
健康指示器显示信息,默认为 never,它有三个参数:
- always: 总是显示
- never:从来不限时
- when_authorized:显示给认证用户
1 2 3 4
| management: endpoint: health: show-details: when_authorized
|
访问健康指示器,指定所需角色:
1 2 3 4
| management: endpoint: health: roles: ADMIN
|
重启项目后,访问 GET 请求: http://localhost:8080/actuator/health
Actuator 健康指示器也支持生成很多技术详细,比如 redis:
Actuator 自定义健康指示器
新建一个配置类 /HealthConfig。
配置配置文件,自定义健康指示器和响应码。
1 2 3 4 5 6 7 8 9
| management: endpoint: health: status: order: FATAL,DOWN,OUT_OF_SERVER,UP,UNKNOWN http-mapping: FATAL: 503
|
Actuator 自定义应用信息
通过配置文件定义
1 2 3 4 5 6 7 8
| info: app: encoding: @project.build.sourceEncoding@ java: source: @java.version@ target: @java.version@ author: name: sihai
|
访问:http://localhost:8080/actuator/info
通过类定义
创建一个类 AppInfoConfig,继承自 InfoContributor。
1 2 3 4 5 6 7 8 9 10
| @Component public class AppInfoConfig implements InfoContributor { @Override public void contribute(Info.Builder builder) { Map<String, String> link = new HashMap<>(); link.put("site", "https://tsihai.github.io/"); link.put("site-2", "https://github.com/Tsihai"); builder.withDetail("link", link); } }
|
Actuator Info 端点查看 Git 提交信息
Actuator Info 端点查看项目构建信息
首先需要配置一下插件
1 2 3 4 5 6 7 8 9 10 11
| <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin>
|
- 在 maven 中的 spring-boot 插件 build-info 信息。
- 生成数据后,重启项目后访问 info 端点。
Actuator 单体应用监控信息可视化
Actuator Server
- 首先另外创建一个 Spring Boot 项目 /actuator-server,添加 web、Admin (Server) 依赖。
- 在启动类上添加上 @EnableAdminServer。
- 在配置类上配置端口号为 8081。
- 由于我们在 client 上配置了 security 进行了端点保护,所以需要在配置类上配置 security 的账号密码。
- 启动项目后在浏览器打开:http://localhost:8081
Actuator Client
- 在 actuator 项目中添加 Admin (Client) 依赖
- 在配置文件中配置连接的 server 地址。
1 2 3 4 5
| spring: boot: admin: client: url: http://localhost:8081
|
- 将 HealthConfig 类设置成正常返回。
1 2 3 4 5 6 7
| @Component public class HealthConfig implements HealthIndicator { @Override public Health health() { return Health.up().withDetail("msg", "正常").build(); } }
|
- 重新访问 http://localhost:8081
- 就可以看见 actuator-client 配置的详细信息 /info。
Actuator 邮件报警
如果服务掉线后,可以及时发送邮件报警
- 在 actuator-server 中,添加上 mail,fastjson 依赖。
- 在配置文件中配置信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| spring: boot: admin: instance-auth: default-user-name: sihai default-password: root notify: mail: from: 1075991706@qq.com to: 13169186610@163.com ignore-changes: mail: host: smtp.qq.com port: 465 username: 1075991706@qq.com password: default-encoding: utf-8 properties: mail: smtp: socketFactory: class: javax.net.ssl.SSLSocketFactory
|
- 添加一个后端控制台的提示信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| package com.sihai.actuatorserver.config;
import com.alibaba.fastjson.JSONObject; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono;
import java.util.Arrays;
@Component public class AdminNotifier extends AbstractStatusChangeNotifier {
private static final Logger log = LoggerFactory.getLogger(AdminNotifier.class);
private static final String template = "<<<%s>>> \n 【服务名】: %s(%s) \n 【状态】: %s(%s) \n 【服务ip】: %s \n 【详情】: %s";
private String titleAlarm = "系统告警";
private String titleNotice = "系统通知";
private String[] ignoreChanges = new String[]{"UNKNOWN:UP","DOWN:UP","OFFLINE:UP"};
public AdminNotifier(InstanceRepository repository) { super(repository); }
@Override protected boolean shouldNotify(InstanceEvent event, Instance instance) { if (!(event instanceof InstanceStatusChangedEvent)) { return false; } else { InstanceStatusChangedEvent statusChange = (InstanceStatusChangedEvent)event; String from = this.getLastStatus(event.getInstance()); String to = statusChange.getStatusInfo().getStatus(); return Arrays.binarySearch(this.ignoreChanges, from + ":" + to) < 0 && Arrays.binarySearch(this.ignoreChanges, "*:" + to) < 0 && Arrays.binarySearch(this.ignoreChanges, from + ":*") < 0; } }
@Override protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
return Mono.fromRunnable(() -> {
if (event instanceof InstanceStatusChangedEvent) { log.info("Instance {} ({}) is {}", instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus());
String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(); String messageText = null; switch (status) { case "DOWN": log.info("发送 健康检查没通过 的通知!"); messageText = String .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "健康检查没通过通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; case "OFFLINE": log.info("发送 服务离线 的通知!"); messageText = String .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务离线通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; case "UP": log.info("发送 服务上线 的通知!"); messageText = String .format(template,titleNotice, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务上线通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; case "UNKNOWN": log.info("发送 服务未知异常 的通知!"); messageText = String .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务未知异常通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; default: break; } } else { log.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(), event.getType()); } }); } }
|
Actuator + Admin + nacos + Sentinel
Sentinel 安装
- 使用 Sentinel 把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
首先下载控制台 jar,这是一个 Spring boot 工程,下载好了之后,直接使用 Spring boot 命令启动即可。
官方下载地址:https://github.com/alibaba/Sentinel/releases
执行启动命令:nohup java -Dserver.port=9999 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-2.0.0-alpha-preview.jar >/dev/null &
访问浏览器:http://localhost:9999/#/dashboard/home ,默认登陆用户名密码 sentinel/sentinel。
Admin Client
依赖项
1 2 3 4 5 6
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> <relativePath/> </parent>
|
1 2 3 4
| <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR1</spring-cloud.version> </properties>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.4.RELEASE</version> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.2.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.2.4.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2.2.4.RELEASE</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>pl.project13.maven</groupId> <artifactId>git-commit-id-plugin</artifactId> </plugin> </plugins> </build>
|
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| server: port: 8093 spring: application: name: nacos-consumer security: user: name: sihai password: root roles: ADMIN cloud: nacos: server-addr: localhost:8848 discovery: register-enabled: true sentinel: transport: dashboard: localhost:9999 port: 8819 log: dir: logs/sentinel datasource: ds: nacos: data-id: sentinel-rule group-id: DEFAULT_GROUP rule-type: flow redis: password: boot: admin: client: url: localhost:8091 management: info: git: mode: full endpoints: web: cors: allowed-origins: '*' exposure: include: '*' endpoint: health: show-details: when_authorized roles: ADMIN status: order: FATAL,DOWN,OUT_OF_SERVER,UP,UNKNOWN http-mapping: FATAL: 503 shutdown: enabled: true info: app: encoding: @project.build.sourceEncoding@ java: source: @java.version@ target: @java.version@ author: name: sihai
|
配置类
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Component public class AppInfoConfig implements InfoContributor { @Override public void contribute(Info.Builder builder) { Map<String, String> link = new HashMap<>(); link.put("site", "https://tsihai.github.io/"); link.put("site-2", "https://github.com/Tsihai"); builder.withDetail("link", link); } }
|
1 2 3 4 5 6 7 8 9 10
|
@Component public class HealthConfig implements HealthIndicator { @Override public Health health() { return Health.up().withDetail("msg", "正常").build(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatcher(EndpointRequest.toAnyEndpoint()) .authorizeRequests() .anyRequest().hasRole("ADMIN") .and() .httpBasic() .and() .csrf().disable(); } }
|
启动类
1 2 3 4 5 6 7 8 9 10 11 12
| @SpringBootApplication public class NacosConsumerApplication { public static void main(String[] args) { SpringApplication.run(NacosConsumerApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } }
|
接口
1 2 3 4 5 6 7 8 9 10 11
| @RestController public class HelloController {
@Autowired RestTemplate restTemplate;
@GetMapping("/hello") public String hello() { return restTemplate.getForObject("http://nacos-server/hello", String.class); } }
|
Admin Server
依赖项
1 2 3 4 5 6
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> </parent>
|
1 2 3 4
| <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR1</spring-cloud.version> </properties>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>2.7.10</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.28</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <dependencyManagement> <dependencies> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-dependencies</artifactId> <version>2.3.1</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.4.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
|
1 2 3 4 5 6 7 8
| <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
|
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| server: port: 8091 spring: application: name: nacos-server boot: admin: instance-auth: default-user-name: sihai default-password: root notify: mail: from: 1075991706@qq.com to: 13169186610@163.com ignore-changes: mail: host: smtp.qq.com port: 465 username: 1075991706@qq.com password: iukjuhuybkrpfjcd default-encoding: utf-8 properties: mail: smtp: socketFactory: class: javax.net.ssl.SSLSocketFactory cloud: nacos: server-addr: localhost:8848 management: endpoint: health: show-details: always endpoints: web: exposure: include: '*' cors: allowed-origins: '*' logging: file: name: /home/java/admin.log
|
配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| package com.sihai.nacosserver.config;
import com.alibaba.fastjson.JSONObject; import de.codecentric.boot.admin.server.domain.entities.Instance; import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; import de.codecentric.boot.admin.server.domain.events.InstanceEvent; import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono;
import java.util.Arrays;
@Component public class AdminNotifier extends AbstractStatusChangeNotifier {
private static final Logger log = LoggerFactory.getLogger(AdminNotifier.class);
private static final String template = "<<<%s>>> \n 【服务名】: %s(%s) \n 【状态】: %s(%s) \n 【服务ip】: %s \n 【详情】: %s";
private String titleAlarm = "系统告警";
private String titleNotice = "系统通知";
private String[] ignoreChanges = new String[]{"UNKNOWN:UP","DOWN:UP","OFFLINE:UP"};
public AdminNotifier(InstanceRepository repository) { super(repository); }
@Override protected boolean shouldNotify(InstanceEvent event, Instance instance) { if (!(event instanceof InstanceStatusChangedEvent)) { return false; } else { InstanceStatusChangedEvent statusChange = (InstanceStatusChangedEvent)event; String from = this.getLastStatus(event.getInstance()); String to = statusChange.getStatusInfo().getStatus(); return Arrays.binarySearch(this.ignoreChanges, from + ":" + to) < 0 && Arrays.binarySearch(this.ignoreChanges, "*:" + to) < 0 && Arrays.binarySearch(this.ignoreChanges, from + ":*") < 0; } }
@Override protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
return Mono.fromRunnable(() -> {
if (event instanceof InstanceStatusChangedEvent) { log.info("Instance {} ({}) is {}", instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus());
String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(); String messageText = null; switch (status) { case "DOWN": log.info("发送 健康检查没通过 的通知!"); messageText = String .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "健康检查没通过通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; case "OFFLINE": log.info("发送 服务离线 的通知!"); messageText = String .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务离线通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; case "UP": log.info("发送 服务上线 的通知!"); messageText = String .format(template,titleNotice, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务上线通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; case "UNKNOWN": log.info("发送 服务未知异常 的通知!"); messageText = String .format(template,titleAlarm, instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), "服务未知异常通知", instance.getRegistration().getServiceUrl(), JSONObject.toJSONString(instance.getStatusInfo().getDetails())); log.info(messageText); break; default: break; } } else { log.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(), event.getType()); } }); } }
|
使用