分布式系统面临问题
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。
什么是Spring Cloud Config
Spring Cloud Config项目是一个解决分布式系统的配置管理方案。
整个结构包括三个部分,客户端(各个微服务应用),服务端 (中介者),配置仓库(可以是本地文件系统或者远端仓库, 包括git,svn等)。
- 配置仓库中放置各个配置文件(.yml 或者.properties)
- 服务端指定配置文件存放的位置
- 客户端指定配置文件的名称
Config能干什么
对比主流配置中心
开源的配置中心有很多,比如,360的QConf、淘宝的 nacos、携程的Apollo等。在Spring Cloud中,有分布式配置中心组件spring cloud config,它功能全面、强大,可以无缝地和Spring体系相结合,使用方便简单。
服务端开发
服务端开发最主要的任务是配置从哪里读取对应的配置文件,我们将配置从Git仓库读取配置文件。
在码云新建一个名为cloud-config的新的仓库
项目开源
仓库中新建3个文件
config-dev.yml
config:
info: "master branch,config-dev.yml version=1"
config-prod.yml
config:
info: "master branch,config-prod.yml version=1"
config-test.yml
config:
info: "master branch,config-test.yml version=1"
新建模块cloud-config-server3344
POM文件引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
编写YML文件
新增application.yml
server:
port: 3344
spring:
application:
name: cloud-config-server
cloud:
config:
server:
git:
# git仓库地址
uri: https://gitee.com/lxxkobe/cloud-config.git
# 搜索目录
search-paths:
- cloud-config
# 默认分支
default-label: master
#读取分支名称
label: master
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: cloud-config-server3344
编写主启动类
@Slf4j
@SpringBootApplication
@EnableConfigServer
public class ConfigServerMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigServerMain3344.class, args);
log.info("*********** 配置中心服务启动成功 *************");
}
}
测试通过config微服务是否可以从码云上获取配置
http://locahost:3344/config-dev.yml
Config支持的请求的参数规则
注意:
- {application} 就是应用名称,对应到配置文件上来,就是配置文件的名称部分,例如我上面 创建的配置文件。
- {profile} 就是配置文件的版本,我们的项目有开发版本、测试环境版本、生产环境版本,对 应到配置文件上来就是以 application-{profile}.yml 加以区分,例如application-dev.yml、 application-test.yml、application-prod.yml。
- {label} 表示 git 分支,默认是 master 分支,如果项目是以分支做区分也是可以的,那就可 以通过不同的 label 来控制访问不同的配置文件了。
新建cloud-config-client3355
POM文件添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud
groupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
新增bootstap.yml配置
server:
port: 3355
spring:
application:
name: cloud-config-client
cloud:
config:
# 读取分支名称
label: master
# 配置文件名称
name: config
# 读取后缀名称/环境名称
profile: dev
# 上述3个综合: master分支上config-dev.yml的配置文件被读取
# http://localhost:3344/master/config-dev.yml
#配置中心地址
uri: http://localhost:3344
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: cloud-config-client3355
注意:
- applicaiton. yml:是用户级的资源配置项
- bootstrap.yml:是系统级的,优先级更加高
bootstrap.yml优先级高于application.yml
为什么要引入bootstrap
注意:
要将Client模块下的application.yml文件改为bootstrap.yml,这 是很关键的, 因为bootstrap.yml是比application.yml先加载的。
分析:
Spring Cloud会创建一个"Bootstrap Context",作为Spring应用的Application Context’的父上下文。初始化的时候," Bootstrap Context"负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的"Environment"。"Bootstrap"属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context和Application Context有着不同的约定,所以新增了一个bootstrap.ymI文件, 保证Bootstrap Context和Application Context配置的分离。
必抗指南
错误提示:
Application failed to start due to an exception
org.springframework.cloud.config.client.ConfigServerConfigDataMissingEnvironmentPostProcessor$ImportException: No spring.config.importset
诞生原因:
springcloud2020 版本 把Bootstrap被默认禁用,同时 spring.config.import加入了对解密的支持。
解决办法
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
编写主启动类
@SpringBootApplication
@Slf4j
@EnableEurekaClient
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
log.info("*********** ConfigClientMain3355 服务启动成功 *********");
}
}
业务类controller编写
@RestController
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
/**
* 获取配置
*
* @return
*/
@GetMapping("/configinfo")
public String getConfigInfo() {
return configInfo;
}
}
启动config配置中心3355测试
http://localhost:3355/configinfo
分布式配置的动态刷新问题
修改码云上的配置文件内容做调整。
问题:
刷新3344,发现ConfigServer配置中心立刻响应
刷新3355,发现ConfigServer客户端没有任何响应
3355没有变化除非自己重启或者重新加载
难道每次运维修改配置文件,客户端都需要重启?
cloud-config-client3355工程引入actuator监控
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
修改bootstrap.yml暴露监控端口
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
业务类Controller修改 加入注解@RefreshScope
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
/**
* 获取配置
*
* @return
*/
@GetMapping("/configinfo")
public String getConfigInfo() {
return configInfo;
}
}
手动刷新配置
http://localhost:3355/actuator/refresh
注意:
必须是post请求。
Config配置中心遇到的问题
当我们在更新码云上面的配置以后,如果想要获取到最新的配置, 需要手动刷新机制每次提交代码发送请求来刷新客户端,客户端越来越多的时候,需要每个客户端都执行一遍,这种方案就不太适合了。Spring Cloud作为微服务架构的一个综合解决方案,也提供了对应的解决方案Spring Cloud Bus,即消息总线。
什么是Spring Cloud Bus
Spring Cloud Bus通过建立多个应用之间的通信频道,管理和传播应用间的消息,从技术角度来说,应用了AMQP消息代理作为通道,通过MQ的广播机制实现消息的发送和接收。Bus支持两种消息代理:RabbitMQ和Kafka 。
Spring Cloud Bus做配置更新的步骤:
- 修改配置文件,提交代码触发post给客户端A发送bus/refresh
- 客户端A接收到请求从Server端更新配置并且发送给Spring Cloud Bus
- Spring Cloud bus接到消息并通知给其它客户端
- 其它客户端接收到通知,请求Server端获取最新配置
- 全部客户端均获取到最新的配置
下载镜像
docker pull docker.io/macintoshplus/rabbitmq-management
启动容器
docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest -p 15672:15672 -p 5672:5672 docker.io/macintoshplus/rabbitmq-management
参数:
- -d: 守护进行运行
- –name:容器名字
- -e:配置用户名和密码
- -p:设置端口号
测试
http://192.168.66.113:15672
新增cloud-config-client3366工程
POM文件新增依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud
groupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
新增yml配置文件bootstrap.yml
server:
port: 3366
spring:
application:
name: cloud-config-client
cloud:
config:
# 读取分支名称
label: master
# 配置文件名称
name: config
# 读取后缀名称/环境名称
profile: dev
# 上述3个综合: master分支上config-dev.yml的配置文件被读取
# http://localhost:3344/master/config-dev.yml
#配置中心地址
uri: http://localhost:3344
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: cloud-config-client3366
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
主启动类
@Slf4j
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3366 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3366.class, args);
log.info("********** ConfigClient3366 启动成功 *********");
}
}
业务controller
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
/**
* 获取配置
*
* @return
*/
@GetMapping("/configinfo")
public String getConfigInfo() {
return configInfo;
}
}
注意:
利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置。
给3344,3355,3366工程添加消息总线支持
POM文件添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
YML文件添加配置
spring:
rabbitmq:
host: 192.168.66.113
port: 5672
username: guest
password: guest
一次发送出处处生效
http://localhost:3355/actuator/busrefresh
Bus动态刷新定点通知
指具体某个实例生效而不是全部
示例
http://localhost:3355/actuator/busrefresh/cloud-config-client:3366
为什么使用Spring Cloud Stream
流行的消息中间件过多,有可能一个工程中使用MQ,比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic,partitions分区, 些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的, 一大堆东西都要重新推倒重新做,因为它跟我们的系统耦了,这时候springcloud Stream给我们提供了一种解耦合的方式。
我们之前使用的数据库链接工具
注意:
Stream解决了开发人员无感知的使用消息中间件的问题,因为 Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件,使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
什么是Spring Cloud Stream
官方定义Spring Cloud Stream 是一个构建消息驱动微服务的框架。实现了一套轻量级的消息驱动的微服务框架。通过使用Spring Cloud Stream,可以有效简化开发人员对消息中间件的使用复杂度, 让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。
绑定器
Binder 绑定器是Spring Cloud Stream中一个非常重要的概念。
注意:
- Source:当需要发送消息时,我们就需要通过Source.java,它会把我们所要发送的消息进行序列化(默认转换成JSON格式字符串),然后将这些数据发送到channel 中;
- Sink:当我们需要监听消息时就需要通过Sink.java,它负责从消息通道中获取消息,并将消息反序列化成消息对象,然后交给具体的消息监听处理;
- Channel:通常我们向消息中间件发送消息或者监听消息时需要指定主题(Topic)和消息队列名称,一旦我们需要变更主题的时候就需要修改消息发送或消息监听的代码。通过 Channel对象,我们的业务代码只需要对应Channel就可以了,具体这个Channel对应的是哪个主题,可以在配置文件中来指定,这样当主题变更的时候我们就不用对代码做任何修改, 从而实现了与具体消息中间件的解耦;
- Binder:通过不同的 Binder可以实现与不同的消息中间件整合,Binder提供统一的消息收发接口,从而便得我们可以根据实际需要部署不同的消息中间件,或者根据实际生产中所部署的消息中间件来调整我们的配置。
Message
生产者/消费者之间靠消息媒介传递消息内容
消息通道MessageChannel
消息必须走特定的通道。
常用注解
新建Module工程cloud-stream-rabbitmq-provider8001
POM添加依赖
要使用RabbitMQ绑定器,可以通过使用以下Maven坐标将其添加到 Spring Cloud Stream应用程序中
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-stream-binder-rabbitartifactId>
dependency>
或者
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
完整依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
编写YML文件
spring:
application:
# 设置应用名字
name: cloud-provider
rabbitmq:
port: 5672
host: 114.117.183.67
username: guest
password: guest
cloud:
stream:
bindings:
# 广播消息
# 生产者绑定名称(myBroadcast是自定义的绑定名称) - out代表生产者 - 0是固定写法
myBroadcast-out-0:
# 对应的真实的RabbitMQ交换机
destination: my-broadcast-topic
server:
port: 8001
eureka:
client:
service-url:
# Eureka Server地址
defaultZone: http://localhost:7001/eureka/
instance:
# 实例名称
instance-id: cloud-provider8001
主启动类
/**
* 主启动类
*/
@SpringBootApplication
@EnableEurekaClient
@Slf4j
public class ProviderMain8001 {
public static void main(String[] args) {
SpringApplication.run(ProviderMain8001.class, args);
log.info("****************ProviderMain8001启动成功**************");
}
}
消息实体类
/**
* 消息实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyMessage implements Serializable {
//消息体
private String payload;
}
消息生产接口与实现类
/**
* 发送消息的接口
*/
public interface IMessageProvider {
/**
* 发送消息
*
* @param message 消息内容
* @return
*/
String send(String message);
}
/**
* 定义消息推送的管道
*/
@Service
public class MessageProviderImpl implements IMessageProvider {
//直接装配桥 用来连接消息中间件(rabbitmq/kafka)
@Autowired
private StreamBridge streamBridge;
@Override
public String send(String message) {
MyMessage myMessage = MyMessage.builder().payload(message).build();
/**
* 第一个参数:绑定名称 格式:自定义的绑定名称-out-0
* 第二个参数:发送消息实体
*/
streamBridge.send("myBroadcast-out-0", myMessage);
return "success";
}
}
编写业务类
@RestController
public class ProviderController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping("/send")
public String sendMessage(String message) {
return messageProvider.send(message);
}
}
测试
新建Module工程cloud-stream-rabbitmq-consumer8002,8003
POM添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
编写YML文件
spring:
application:
# 设置应用名字
name: cloud-consumer
rabbitmq:
port: 5672
host: 114.117.183.67
username: guest
password: guest
cloud:
function:
# 定义消费者,多个用分号分隔,当存在大于1个的消费者时,不定义不会生效
definition: myBroadcast
stream:
bindings:
# 广播消息
# 消费者绑定名称(myBroadcast是自定义的绑定名称) - in代表消费者 - 0是固定写法
myBroadcast-in-0:
# 对应的真实的RabbitMQ交换机
destination: my-broadcast-topic
server:
port: 8002
eureka:
client:
service-url:
# Eureka Server地址
defaultZone: http://localhost:7001/eureka/
instance:
# 实例名称
instance-id: cloud-consumer8002
注意: spring.cloud.function.definition 属性非常重要,务必配置。
编写主启动类
/**
* 主启动类
*/
@SpringBootApplication
@EnableEurekaClient
@Slf4j
public class ConsumerMain8002 {
public static void main(String[] args) {
SpringApplication.run(ConsumerMain8002.class, args);
log.info("****************ConsumerMain8002启动成功**************");
}
消息实体类
/**
* 消息实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyMessage implements Serializable {
//消息体
private String payload;
}
消费者
@Slf4j
@Component
// 消费者
public class Consumer {
// 消费广播消息
// 方法名myBroadcast与配置文件中的spring.cloud.function.definition:myBroadcast保持一致
@Bean
public java.util.function.Consumer<MyMessage> myBroadcast() {
return message -> {
log.info("接收广播消息:{}", message.getPayload());
};
}
}
8003跟8002除端口号外配置一致
测试
将 Stream 工程 启动两个实例,调用发送广播消息接口,查看消息消费情况。
发送广播消息接口:GEThttp://localhost:8001/send?message=hello
这是一条广播消息!
实例的消费情况
什么是消息分组
比如在电商场景中,订单系统我们做集群部署,都会从RabbitMQ 中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。
注意:
我们可以使用Stream中的消息分组来解决。在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。
服务生产者工程8001
编写消息生产接口与实现类新增方法
public interface IMessageProvider {
String send(String message);
public String groupSend(String message);
}
@Service
public class MessageProviderImpl implements IMessageProvider {
//直接装配桥 用来连接消息中间件(rabbitmq/kafka)
@Autowired
private StreamBridge streamBridge;
@Override
public String send(String message) {
MyMessage myMessage = MyMessage.builder().payload(message).build();
streamBridge.send("myBroadcast-out-0", myMessage);
return "success";
}
@Override
public String groupSend(String message) {
MyMessage myMessage = MyMessage.builder().payload(message).build();
streamBridge.send("myGroup-out-0",myMessage);
return "success";
}
}
编写controller新增接口
/**
* 发送分组消息
*
* @param message 消息内容
* @return
*/
@GetMapping("/groupsend")
public String groupsend(String message) {
return messageProvider.groupSend(message);
}
application.yml 配置
spring:
application:
# 设置应用名字
name: cloud-provider
rabbitmq:
port: 5672
host: 114.117.183.67
username: guest
password: guest
cloud:
stream:
bindings:
# 广播消息
# 生产者绑定名称(myBroadcast是自定义的绑定名称) - out代表生产者 - 0是固定写法
myBroadcast-out-0:
# 对应的真实的RabbitMQ交换机
destination: my-broadcast-topic
# 分组消费
# 生产者绑定名称
myGroup-out-0:
# 对应的真实的RabbitMQ交换机
destination: my-group-topic
server:
port: 8001
eureka:
client:
service-url:
# Eureka Server地址
defaultZone: http://localhost:7001/eureka/
instance:
# 实例名称
instance-id: cloud-provider8001
消费者工程8002,8003
新增接收信息方法
@Slf4j
@Component
// 消费者
public class Consumer {
// 消费广播消息
// 方法名myBroadcast与配置文件中的spring.cloud.function.definition:myBroadcast保持一致
@Bean
public java.util.function.Consumer<MyMessage> myBroadcast() {
return message -> {
log.info("接收广播消息:{}", message.getPayload());
};
}
// 消费分组消息
@Bean
public java.util.function.Consumer<MyMessage> myGroup() {
return message -> {
log.info("接收分组消息:{}", message.getPayload());
};
}
}
application.yml 配置
spring:
application:
# 设置应用名字
name: cloud-consumer
rabbitmq:
port: 5672
host: 114.117.183.67
username: guest
password: guest
cloud:
function:
# 定义消费者,多个用分号分隔,当存在大于1个的消费者时,不定义不会生效
definition: myBroadcast;myGroup
stream:
bindings:
# 广播消息
# 消费者绑定名称(myBroadcast是自定义的绑定名称) - in代表消费者 - 0是固定写法
myBroadcast-in-0:
# 对应的真实的RabbitMQ交换机
destination: my-broadcast-topic
# 分组消息
# 消费者绑定名称(myGroup-in-0是自定义的绑定名称) - in代表消费者 - 0是固定写法
myGroup-in-0:
# 对应的真实的RabbitMQ交换机
destination: my-group-topic
# 同一分组的消费服务,只能有一个消费者消费到消息
group: my-group-1
server:
port: 8002
eureka:
client:
service-url:
# Eureka Server地址
defaultZone: http://localhost:7001/eureka/
instance:
# 实例名称
instance-id: cloud-consumer8002
验证分组消息
将 Stream 工程启动两个实例,调用发送分组消息接口,查看消息消费情况。
发送分组消息接口:GEThttp://localhost:8001/groupsend?message=hello
这是一条分组消息
自动生成的 Exchange
自动生成的 Queue
在这个微服务系统中,用户通过浏览器的H5页面访问系统,这个用户请求会先抵达微服务网关组件,然后网关再把请求分发给各个微服务。所以你会发现,用户请求从发起到结束要经历很多个微服务的处理,这里面还涉及到消息组件的集成。
存在的问题:
- 服务之间的依赖与被依赖的关系如何能够清晰的看到?
- 出现异常时如何能够快速定位到异常服务?
- 出现性能瓶颈时如何能够迅速定位哪个服务影响的?
解决:
为了能够在分布式架构中快速定位问题,分布式链路追踪应运而生。将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
常见链路追踪技术有那些
市面上有很多链路追踪的项目,其中也不乏一些优秀的,如下:
Spring Cloud Sleuth实现了一种分布式的服务链路跟踪解决方案, 通过使用Sleuth可以让我们快速定位某个服务的问题。简单来说, Sleuth相当于调用链监控工具的客户端,集成在各个微服务上,负责产生调用链监控数据。
注意:
Spring Cloud Sleuth只负责产生监控数据,通过日志的方式展示出来,并没有提供可视化的UI界面。
Sleuth核心概念
通过这些Sleuth 的特殊标记,我们就可以根据时间顺序,将一次服务请求经过的调用节点都梳理出来,这样你就能迅速发现报错信息发生在哪个阶段。这是使用Zipkin生成的链路追踪的可视化信息。 你可以看出,每个服务调用都以时间先后顺序规整好了,红色的部分就是发生线上Exception的服务。
除了Trace和Span之外,Sleuth还有一个特殊的数据结构,叫做 Annotation,被用来记录一个具体的“事件”。我把 Sleuth所支持的四种事件做成了一个表格,你可以参考一下。
在这里我举个例子,来帮你理解怎么使用这四种事件。
流程:
如果你用服务B的ss减去 sr,你就可以得到请求在服务B阶段的处理时间。如果用服务B的sr减去服务A的cs,就可以得到服务A到服务B之间的网络调用延迟时间。如果用服务A的cr减去 cs, 就可以得到当次请求从发起到结束所花费的总时间。
整合Spring Cloud Sleuth其实没什么的难的,在这之前需要准备以下三个服务:
三个服务的调用关系如下图:
流程: 客户端请求网关发起订单的请求,网关路由给订单服务,订单服务调用支付服务进行支付。
实现步骤
第一步,我们需要将 Sleuth 的依赖项添加到三个微服务的 pom.xml文件
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
第⼆步,我们打开微服务模块的 application.yml 配置⽂件,在配置文件中添加采样率和每秒 采样记录条数。
spring:
sleuth:
sampler:
# 采样率的概率,100%采样
# 日志数据采样百分比,默认0.1(10%),这里为了测试设置成了100%,生产环境只需要0.1即可
probability: 1.0
# 每秒最高采样数字
rate: 1000
注意:
在配置文件里设置了⼀个 probability,它应该是⼀个0到1的浮点数,用来表示采样率。我这⾥设置的 probability 是 1,就 表示对请求进⾏ 100% 采样。如果 我们把 probability 设置成小于 1 的数,就说明有的请求不会被采样。如果⼀个请求未被采样,那么它将不会被调⽤链追踪系统 Track 起来。
三个服务中调整日志级别
由于sleuth并没有UI界面,因此需要调整一下日志级别才能在控制 台看到更加详细的链路信息。在三个服务的配置文件中添加以下配置:
## 设置openFeign和sleuth的日志级别为debug,方便查看日志信息
logging:
level:
org.springframework.cloud.openfeign: debug
org.springframework.cloud.sleuth: debug
演示接口完善
http://localhost:9527/order/index
日志格式中总共有四个参数,含义分别如下:
- 第一个:服务名称
- 第二个:traceId,唯一标识一条链路
- 第三个:spanId,链路中的基本工作单元id
什么是Zipkin
Zipkin是Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
Zipkin4个核心的组件
注意: zipkin分为服务端和客户端,服务端主要用来收集跟踪数据并且展示,客户端主要功能是发送给服务端,微服务的应用也就是客户端,这样一旦发生调用,就会触发监听器将sleuth日志数据传输给服务端。
为了搭建⼀条高可用的链路信息传递通道,我将使用RabbitMQ作 为中转站,让各个应用服务器将服务调用链信息传递给 RabbitMQ,而Zipkin 服务器则通过监听 RabbitMQ 的队列来获取调用链数据。相比于让微服务通过 Web 接口直连 Zipkin,使用消息队列可以大幅提高信息的送达率和传递效率。
安装RabbitMQ服务
docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest -p 15672:15672 -p 5672:5672 docker.io/macintoshplus/rabbitmq-management
下载Zipkin镜像
docker pull openzipkin/zipkin
启动容器
docker run --name rabbit-zipkin -d -p 9411:9411 --link rabbitmq -e RABBIT_ADDRESSES=rabbitmq:5672 -e RABBIT_USER=guest -e RABBIT_PASSWORD=guest openzipkin/zipkin
测试
此时可以访问zipkin的UI界面,地址:http://114.117.183.67:9411,界面如下:
最后,我们只需要验证消息监听队列是否已就位就可以了。我们使 ⽤ guest 账号登录 RabbitMQ,并切换到"Queues"面板,如果 Zipkin 和 RabbitMQ 的对接⼀切正常,那么你会在 Queues 面板下 看到⼀个名为 zipkin 的队列。
三个服务添加依赖
首先,我们需要在每个微服务模块的 pom.xml 中添加 Zipkin 适配插件和 Stream 的依赖。其中,Stream 是 Spring Cloud 中专门对接消息中间件的组件。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-sleuth-zipkinartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-stream-binder-rabbitartifactId>
dependency>
配置文件需要配置一下zipkin服务端的地址,配置如下:
spring:
application:
# 设置应用名词
name: cloud-payment-provider
sleuth:
sampler:
# 采样率的概率,100%采样
# 日志数据采样百分比,默认0.1(10%),这里为了测试设置成了100%,生产环境只需要0.1即可
probability: 1.0
# 每秒最⾼采样数字
rate: 1000
zipkin:
sender:
type: rabbit
rabbitmq:
addresses: 114.117.183.67:5672
queue: zipkin
测试
请求http://localhost:9527/order/index调用接口之后,再次访问 zipkin的UI界面
可以看到刚才调用的接口已经被监控到了,点击 SHOW 进入详情查看,如下图:
什么是SkyWalking
SkyWalking是中国人吴晟(华为)开源的一款APM工具是一个开源的可观测平台,主要用于收集,分析,聚合和可视化服务和云原生基础设施的数据。SkyWalking提供了一种简单的方式来维护分布式系统的视图关系,它甚至能够支持跨云的服务。 特别为微服务、云原生和基于容器(Docker, Kubernetes, Mesos)体系结构而设计。
SkyWalkIng与ZipKin对比
Zipkin
Twitter公司开源的一个分布式追踪工具,被Spring Cloud Sleuth集成,使用广泛而稳定。
优点: 轻量级,SpringCloud集成,使用人数多,成熟
缺点: 侵入性; 功能简单; 欠缺APM报表能力(能力弱)
SkyWalking
SkyWalking是开源的一款分布式追踪,分析,告警的工具,现已属 于Apache旗下开源项目, SkyWalking为服务提供了自动探针代理, 将数据通过gRPC或者HTTP传输给后端平台,后端平台将数据存储在Storage中,并且分析数据将结果展示在UI中。
优点:
- 多种监控手段多语言自动探针,Java,.NET Core 和 Node.JS
- 轻量高效,不需要大数据
- 模块化,UI、存储、集群管理多种机制可选,
- 支持告警
- 社区活跃
缺点:
- 较为新兴,成熟度不够高
SkyWalking能够解决什么问题
- 宿主应用:被探针通过字节码技术引入应用。从探针的视角来看,应用是探针寄生的宿主。
- 探针:收集从应用采集到的链路数据,并将其转换为SkyWalking能够识别的数据格式。
- RPC:宿主应用和平台后端之间的通信渠道。
- 平台后端:支持数据聚合、分析和流处理,包括跟踪,指标和日志等。
- 存储:通过开放的,可插拔的接口来存储SkyWalking的链路数据。 SkyWalking目前支持 ElasticSearch、H2、MySQL、TiDB、InfluxDB。
- UI:一个可定制的基于WEB的界面,允许SkyWalking终端用户管理和可视化SkyWalking的链路数据。
探针产生的背景
在开发过程中,开发人员经常会使用IDEA的Debug功能(包含本地 和远程)调试应用,在JVM进程期间获取应用运行的JVM信息,变量信息等。这些个技术通过Java Agent来实现的,那么Java Agent到底是啥,为啥这么吊?
什么是探针
JavaAgent提供了一种在加载字节码时对字节码进行修改的方式。通常使用ASM Javassist字节码工具修改class文件。
注意:
javassist是一个库,实现ClassFileTransformer接口中的 transform()方法。ClassFileTransformer 这个接口的目的就是在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。
Java探针工具技术原理
流程:
- 在JVM加载class二进制文件的时候,利用ASM动态的修改加载的class文件,在监控的方法前后添加计时器功能,用于计算监控方法耗时;
- 将监控的相关方法和耗时及内部调用情况,按照顺序放入处理器;
- 处理器利用栈先进后出的特点对方法调用先后顺序做处理,当一个请求处理结束后,将耗时方法轨迹和入参map输出到文件中;
- 然后区分出耗时的业务,转化为xml格式进行解析和分析。
Java探针工具功能
1、支持方法执行耗时范围抓取设置,根据耗时范围抓取系统运行时出现在设置耗时范围的代码运行轨迹。
2、支持抓取特定的代码配置,方便对配置的特定方法进行抓取,过滤出关系的代码执行耗时情况。
3、支持APP层入口方法过滤,配置入口运行前的方法进行监控,相当于监控特有的方法耗时,进行方法专题分析。
4、支持入口方法参数输出功能,方便跟踪耗时高的时候对应的入参数。
5、提供WEB页面展示接口耗时展示、代码调用关系图展示、方法耗时百分比展示、可疑方法凸显功能。
新建springboot项目helloagent
引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
编写HelloController
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello agent";
}
}
打包SpringBoot项目
如何使用Java Agent
JDK1.5引入了java.lang.instrument包,开发者可以很方便的实现字节码增强。其核心功能由java.lang.instrument.Instrumentation接口提供。
Instrumentation 有两种使用方式:
注意:
javassist是一个库,实现ClassFileTransformer接口中的 transform()方法。ClassFileTransformer 这个接口的目的就是 在class被装载到JVM之前将class字节码转换掉,从而达到动态注入代码的目的。
新建Maven项目javaagent
Pom引入依赖
<dependencies>
<dependency>
<groupId>org.javassistgroupId>
<artifactId>javassistartifactId>
<version>3.22.0-GAversion>
dependency>
dependencies>
<build>
<finalName>javaagentfinalName>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<version>3.0.0version>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>com.lxx.TestAgentPremain-Class>
manifestEntries>
transformer>
transformers>
configuration>
execution>
executions>
plugin>
plugins>
build>
Maven插件引入失败
在IDEA中使用maven-shade-plugin插件时会提示"Plugin ‘mavenshade-plugin:’ not found"
解决方法:
File -> Setting -> Build, Execution, Deployment -> Build Tools -> Maven -> 勾选Use plugin registry选项
File -> Invalidate Caches -> Invalidate and Restart
编写探针类TestAgent
public class TestAgent {
/**
* 在main方法运行前,与main方法运行于同一JVM中
*
* @param agentArgs agentArgs是premain函数得到的程序参数,随同"-javaagent"一同传入,
* 与main函数不同的是,该参数是一个字符串而不是一个字符串数组,
* 如果程序参数有多个,程序将自行解析这个字符串
* @param inst 一个java.lang.instrument.Instrumentation实例,由JVM自动传入,
* java.lang.instrument.Instrumentation是instrument包中的一个接口,
* 也是核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("===============premain执行了================");
System.out.println("agentArgs: " + agentArgs);
// 添加Transformer
inst.addTransformer(new TestTransformer());
}
}
编写TestTransformer
public class TestTransformer implements ClassFileTransformer {
public final String CLASS_NAME = "com.lxx.controller.HelloController";
public final String METHOD_NAME = "hello";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 1.获得类名称 className的值为=>com/lxx/controller/HelloController
String finalClassName = className.replace("/", ".");
// 2.判断是否是我要修改的类
if (CLASS_NAME.equals(finalClassName)) {
// 3.javassist是处理java字节码的类库
// java字节码存储在class file的二进制文件里
// 每个class文件包含一个java类或者接口
// javassist.CtClass就是class文件的抽象表示
CtClass ctClass;
try {
// 4.获得到helloController类
ctClass = ClassPool.getDefault().get(finalClassName);
System.out.println("ctClass is OK !");
// 5.获得类中得方法
CtMethod ctMethod = ctClass.getDeclaredMethod(METHOD_NAME);
System.out.println("ctMethod is OK !");
// 6.进行方法增强
ctMethod.insertBefore("System.out.println(\"字节码添加成功,打印日志 !\");");
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
}
return null;
}
}
项目测试打包
添加javaagent启动参数
java -javaagent:agent路径 -jar 项目名.jar
例如:
java -javaagent:javaagent.jar -jar helloagent-0.0.1-SNAPSHOT.jar
启动,打印出日志
发送请求http://localhost:8080/hello
SkyWalking运行需要java环境
# 搭建jdk环境
# 解压jdk安装包
tar -zxvf jdk-8u201-linux-x64.tar.gz -C /usr/local
# 配置环境变量
vim /etc/profile
# 底部加入如下配置
export JAVA_HOME=/usr/local/jdk1.8.0_201
export PATH=$PATH:$JAVA_HOME/bin
# 生效环境变量
source /etc/profile
下载SkyWalking包
解压SkyWalking包
tar -zxvf apache-skywalking-apm-es7-8.5.0.tar.gz -C /usr/local/
SkyWalking包目录介绍
webapp: Ul前端(web 监控页面)的jar包和配置文件
oap-libs:后台应用的jar包,以及它的依赖jar包
config:启动后台应用程序的配置文件,是使用的各种配置
bin:各种启动脚本,一般使用脚本startup.*来启动web页面和对应的后台应用
agent:代理服务jar包
修改端口号
vim /usr/local/apache-skywalking-apm-bin-es7/webapp/webapp.yml
启动服务
注意:
启动成功后会启动两个服务,一个是skywalking-oap-server, 一个是skywalking-web-ui : 8068,skywalkng-oap-server服务 启动后会暴露11800和12800两个端口,分别为收集监控数据的端口11800和接受前端请求的端口12800,修改端口可以修改 config/applicaiton.yml。
测试服务
请求http://114.117.183.67:8068
探针,用来收集和发送数据到归集器。
下载官方提供探针
网址https://skywalking.apache.org/downloads/
或者直接去解压好的skyWalking里面取
拷贝探针文件到项目中
创建coud-provider-skywalking-payment8001
创建cloud-consumer-fegin-skywalking-order80
修改项目的VM运行参数
点击菜单栏中的 Run -> EditConfigurations… ,此处我们以 OrderFeignSkyWalkingMain80 项目为例,修改参数如下:
-javaagent:F:\001-after-end\code\cloud\agent\skywalking-agent.jar
-DSW_AGENT_NAME=consumer-order
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=114.117.183.67:11800
参数:
-javaagent:用于指定探针路径。
-DSW_AGENT_NAME:服务名字
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES:连接地址
Java 命令行启动方式
java -javaagent:/path/to/skywalkingagent/skywalking-agent.jar -DSW_AGENT_NAME=cloud-provider -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=localhost:11800 -jar yourApp.jar
测试监控
拉取镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.1.1
启动容器
docker run -d --name es -p 9200:9200 -p 9300:9300 -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.1.1
参数:
-d:守护进程运行
ES_JAVA_OPTS:设置堆内存
discovery.type:设置单节点启动
补充
我们会发现在ElasticSearch启动时,会占用两个端口9200和9300。
他们具体的作用如下:
- 9200 是ES节点与外部通讯使用的端口。它是http协议的RESTful接口(各种CRUD操作都是走的该端口,如查询:http://localhost:9200/user/_search)。
- 9300是ES节点之间通讯使用的端口。它是tcp通讯端口,集群间和TCPclient都走的它。(java程序中使用ES时,在配置文件中要配置该端口)
测试
访问地址: http://114.117.183.67/:9200
修改config目录下application.yml
/usr/local/apache-skywalking-apm-bin-es7/config
修改selector
修改命名空间
修改ES数据连接地址
重启服务
[root@localhost bin]# ./startup.sh
SkyWalking OAP started successfully!
SkyWalking Web Application started
successfully!
当我们工程中,有些重要的方法,没有添加在链路中,而我们又需要时,就可以添加自定义链路追踪的Span。
工程cloud-provider-skywalking-payment8001引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.apache.skywalkinggroupId>
<artifactId>apm-toolkit-traceartifactId>
<version>8.5.0version>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
<version>2.7.8version>
dependency>
dependencies>
YML文件添加配置
spring:
application:
# 设置应用名词
name: cloud-sky-walking-payment-provider
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/student?characterEncoding=utf-8
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 3
auto-commit: true
idle-timeout: 10000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
server:
port: 8001
eureka:
client:
service-url:
# Eureka Server 地址
defaultZone: http://localhost:7001/eureka/
instance:
#实例名称(根据需要自己起名字)
instance-id: cloud-sky-walking-payment-provider8001
编写主启动类
@EnableEurekaClient
@SpringBootApplication
@Slf4j
@MapperScan("com.lxx.mapper")
public class PaymentSkyWalkingMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentSkyWalkingMain8001.class, args);
log.info("********* PaymentSkyWalkingMain8001启动成功 ******");
}
}
编写实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private Integer id;
private String name;
private String sex;
private String address;
}
编写Mapper
public interface StudentMapper extends BaseMapper<Student> {
}
编写Service接口以及实现类
public interface StudentService {
List<Student> findByAllStudent();
}
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
@Trace
@Override
public List<Student> findByAllStudent() {
return studentMapper.selectList(null);
}
}
编写控制层
@RequestMapping("/student")
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("findAll")
public List<Student> findAll() {
return studentService.findByAllStudent();
}
}
测试
http://localhost:8001/student/findAll
POM中引入相关依赖
Skywalking8.4.0版本开始才支持收集日志功能,同时pom需引用以下依赖。
<dependency>
<groupId>org.apache.skywalkinggroupId>
<artifactId>apm-toolkit-logback-1.xartifactId>
<version>8.5.0version>
dependency>
Logback配置
在logback.xml中加入配置
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level logger_name:%logger{36} - [%tid] - message:%msg%npattern>
layout>
encoder>
appender>
<root level="info">
<appender-ref ref="console"/>
root>
configuration>
Skywalking通过gRPC上报日志
gRPC报告程序可以将收集到的日志发送给Skywalking OAP服务器上。
创建logback.xml文件中添加
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level logger_name:%logger{36} - [%tid] - message:%msg%npattern>
layout>
encoder>
appender>
<appender name="log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level logger_name:%logger{36} - [%tid] - message:%msg%npattern>
layout>
encoder>
appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="log"/>
root>
configuration>
打开你的agent/config/agent.config配置文件,添加如下配置信息, 注意skywalking的log通信用的grpc:
# 指定要向其报告日志数据的grpc服务器的主机
plugin.toolkit.log.grpc.reporter.server_host=${SW_GRPC_LOG_SERVER_HOST:114.117.183.67}
# 指定要向其报告日志数据的grpc服务器的端口
plugin.toolkit.log.grpc.reporter.server_port=${SW_GRPC_LOG_SERVER_PORT:11800}
# 指定grpc客户端要报告的日志数据的最大大小
plugin.toolkit.log.grpc.reporter.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760}
# 客户端向上游发送数据时将超时多长时间。单位是秒
plugin.toolkit.log.grpc.reporter.upstream_timeout=${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30}
注:gRPC报告程序可以将收集到的日志转发到SkyWalking OAP服务器或SkyWalking Satellite卫星。
测试
告警基本流程
每隔一段时间轮询Skywalking-collector收集到的链路追踪的数据, 再根据所配置的告警规则(如服务响应时间、服务响应时间百分比)等,如果达到阈值则发送响应的告警信息。发送告警信息是以线程池异步的方式调用webhook接口完成,从而开发者可以在指定的webhook接口中自行编写各种告警方式,钉钉告警、邮件告警等 等。
Skywalking默认支持7中通知:
web、grpc、微信、钉钉、飞书、华为weLink、slack
默认规则
Skywalking默认提供的 alarm-settings.yml ,定义的告警规则如下:
告警规则
注意:
这些预定义的告警规则,打开config/alarm-settings.yml文件即可看到。
Webhook
Webhook表达的意思是,当告警发生时,将会请求的地址URL(用 POST方法)。警报消息将会以 application/json 格式发送出去。
举个栗子:
比如你的好友发了一条朋友圈,后端将这条消息推送给所有其他好友的客户端,就是 Webhook 的典型场景。
[{
"scopeId": 1,
"scope": "SERVICE",
"name": "serviceA",
"id0": 12,
"id1": 0,
"ruleName": "service_resp_time_rule",
"alarmMessage": "alarmMessage xxxx",
"startTime": 1560524171000
}]
参数:
- scopeId、scope:作用域
- name:目标作用域下的实体名称;
- id0:作用域下实体的ID,与名称匹配;
- id1:暂不使用; ruleName:
- alarm-settings.yml 中配置的规则名称;
- alarmMessage:告警消息体;
- startTime:告警时间(毫秒),时间戳形式。
默认规则
自定义告警规则
service_response_time_rule:
metrics-name: service_resp_time
op: ">"
# 阈值
threshold: 1 # 单位毫秒
# 多久检查一次当前的指标数据是否符合告警规则
period: 5
# 达到多少次告警后,发送告警消息
count: 1
# 告警消息内容
message: 服务{name}最近5分钟以内响应时间超过了 1ms
测试
Wbhooks网络钩子
Webhok可以简单理解为是一种Web层面的回调机制。告警就是一个事件,当事件发生时Skywalking会主动调用一个配置好的接口,这个接口就是所谓的Webhook;
注意:
Skywalking的告警消息会通过借HTTP请求进行发送,请求方法为 POST (Content-Type 为application/json。其JSON数据实基于List
进行序列化的。
JSON数据示例
[{
"scopeId": 1,
"scope": "SERVICE",
"name": "serviceA",
"id0": "12",
"id1": "",
"ruleName": "service_resp_time_rule",
"alarmMessage": "alarmMessage xxxx",
"startTime": 1560524171000
}]
创建项目cloud-alarm9090
引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
dependency>
创建接收实体类AlarmMessageDto
https://github.com/apache/skywalking/blob/v8.5.0/docs/en/setup/backend/backend-alarm.md
@Data
public class AlarmMessage {
private int scopeId;
private String scope;
private String name;
private String id0;
private String id1;
// 规则名字
private String ruleName;
// 告警信息
private String alarmMessage;
private List<Tag> tags;
// 时间
private long startTime;
private transient int period;
private transient boolean onlyAsCondition;
@Data
public static class Tag{
private String key;
private String value;
}
}
编写钩子接口
/**
* 告警控制层
*/
@RestController
@RequestMapping("/alarm")
public class AlarmController {
@PostMapping("/dingding")
public void message(@RequestBody List<AlarmMessage> alarmMessages) {
StringBuilder stringBuilder = new StringBuilder();
alarmMessages.forEach(info -> {
stringBuilder.append("\nscopeId:").append(info.getScopeId())
.append("\nScope实体:").append(info.getScope())
.append("\n告警消息:").append(info.getAlarmMessage())
.append("\n告警规则:").append(info.getRuleName())
.append("\n\n----------------------\n\n ");
});
System.out.println(stringBuilder);
}
}
配置网络钩子
alarm-settings.yml 增加alarm接口
回调失败
关闭windows防火墙
搜索防火墙
关闭防火墙
前言
缺点:
实际项目中,我们不会一直看着告警菜单。希望有告警信息产生时,将告警信息通过邮件或者短信发送给相关负责人。
钉钉告警
创建群聊
添加智能助手
添加机器人
选择机器人
配置加签
POM引入钉钉工具包依赖
<dependency>
<groupId>com.aliyungroupId>
<artifactId>alibaba-dingtalk-service-sdkartifactId>
<version>2.0.0version>
dependency>
创建application.yml
dingding:
# 钉钉网路钩子地址
webhook: https://oapi.dingtalk.com/robot/send?access_token=37a2fc4d24d9c5bc752644fda450d2741768c7eed0520f87f58a6072c00aa4f9
# 秘钥
secret: SEC899d5d856685d59a612acc020e239ad404bcd5ba4be362ca1c6c077bd03f01a5
编写发送接口
/**
* 告警控制层
*/
@RestController
@RequestMapping("/alarm")
public class AlarmController {
@Value("${dingding.webhook}")
private String webhook;
@Value("${dingding.secret}")
private String secret;
@Autowired
private JavaMailSender javaMailSender;
/**
* 钉钉报警
* @param alarmMessages
*/
@PostMapping("/dingding")
public void message(@RequestBody List<AlarmMessage> alarmMessages) {
// info放的是 告警信息
alarmMessages.forEach(info -> {
try {
// 1. 当前时间戳
Long timestamp = System.currentTimeMillis();
String stringToSign = timestamp + "\n" + secret;
/**
* Mac算法是带有密钥的消息摘要算法
* 初始化HmacMD5摘要算法的密钥产生器
*/
Mac mac = Mac.getInstance("HmacSHA256");
// 初始化mac
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
// 执行消息摘要
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
// 拼接签名
String sign = "×tamp=" + timestamp + "&sign=" + URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
// 构建钉钉发送客户端工具
DingTalkClient client = new DefaultDingTalkClient(webhook + sign);
// 设置消息类型
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("text");
// 设置告警信息
OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
text.setContent("业务告警:\n" + info.getAlarmMessage());
request.setText(text);
// 接受人
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(Arrays.asList("所有人"));
request.setAt(at);
OapiRobotSendResponse response = client.execute(request);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
测试
邮件发送原理
SMTP 协议全称为 Simple Mail Transfer Protocol,译作简单邮件传输协议,它定义了邮件客户端软件与 SMTP 服务器之间,以及 SMTP 服务器与 SMTP 服务器之间的通信规则
授权过程
所以在使用springboot发送邮件之前,要开启POP3和SMTP协议, 需要获得邮件服务器的授权码,这里以qq邮箱为例,展示获取授权码的过程:
在账户的下面有一个开启SMTP协议的开关并进行密码验证:
成功后会出现
POM引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
配置邮箱基本信息
spring:
mail:
# 配置 SMTP 服务器地址
host: smtp.qq.com
# 发送者邮箱
username: [email protected]
# 配置密码,注意不是真正的密码,而是刚刚申请到的授权码
password: updkummbtnuxbbga
# 默认的邮件编码为UTF-8
default-encoding: UTF-8
properties:
mail:
smtp:
#需要验证用户名密码
auth: true
starttls:
# 设置为配置SMTP连接的属性。要使用STARTTLS,必须设置以下属性
enable: true
required: true
注意:
126邮箱SMTP服务器地址:smtp.126.com,端口号:465或者994
163邮箱SMTP服务器地址:smtp.163.com,端口号:465或者994
yeah邮箱SMTP服务器地址:smtp.yeah.net,端口号:465或者994
qq邮箱SMTP服务器地址:smtp.qq.com,端口号465或587
编写接口
/**
* 邮件告警
*/
@PostMapping("sendMail")
public void sendMail(@RequestBody List<AlarmMessage> alarmMessages){
alarmMessages.forEach(info -> {
// 邮件客户端
SimpleMailMessage mailMessage = new SimpleMailMessage();
// 发件人
mailMessage.setFrom("[email protected]");
// 收件人
mailMessage.setTo("[email protected]");
// 邮件主题
mailMessage.setSubject(info.getName());
// 邮件内容
mailMessage.setText(info.getAlarmMessage());
javaMailSender.send(mailMessage);
});
}
前言
一个服务上线了后,你想知道这个服务是否可用,需要监控。假如线上出故障了,你要先于顾客感知错误,你需要监控。等等各层面的监控。
什么是Prometheus
Prometheus (音标:/pro’miθiəs/)是一套开源的系统监控报警框架。
具体流程:
- Prometheus Server 用于定时抓取数据指标(metrics)、存储时间序列数据(TSDB)
- Jobs/exporte 收集被监控端数据并暴露指标给Prometheus
- Pushgateway 监控端的数据会用push的方式主动传给此组件,随后被Prometheus 服务定时pull此组件数据即可
- Alertmanager 报警组件,可以通过邮箱、微信等方式 Web UI 用于多样的UI展示,一般为Grafana 还有一些例如配置自动发现目标的小组件和后端存储组件
Prometheus的特点
Grafana介绍
Grafana(读音: /grəˈfɑːnˌɑː/)是一个跨平台的开源的度量分析和可视化工具,可以通过将采集的数据查询然后可视化的展示。Grafana提供了对 prometheus的友好支持,各种工具帮助你构建更加炫酷的数据可视化。
Grafana特点
Prometheus下载
Prometheus下载地址:https://prometheus.io/download
解压Prometheus
tar -zxvf prometheus-2.34.0-rc.1.linux-amd64.tar.gz -C /usr/local
Prometheus启动服务
运行,指定prometheus.yml配置文件
./prometheus --config.file=prometheus.yml
Prometheus关闭服务
pgrep -f prometheus
kill -9 xxx
Prometheus验证
启动后在浏览器中访问:http://114.117.183.67:3000/,用户名密 码默认都是admin
下载Grafana镜像
docker pull grafana/grafana-enterprise
运行Grafana镜像
docker run -d --name=grafana -p 3000:3000 grafana/grafana-enterprise
Grafana验证
启动后在浏览器中访问:http://114.117.183.67:3000/,用户名密 码默认都是admin
Grafana datasouce配置
进入到Data Sources配置页面
添加监控数据源配置
点击add data souce按钮,进入添加监控数据源配置页面,数据源类型选择Prometheus(因为Grafana UI的数据来源于Prometheus)
配置Prometheus地址
保存配置
填写URL后,点击页面最下方的save & Test按钮
点击左侧import按钮
配置Grafana模板
在输入框中填写12856,然后点击load。12856是Grafana模板ID,更多模板请参考:https://grafana.com/grafana/dashboards
选择数据源对象
进入DashBoard
就可以查看JVM监控大盘了
创建工程cloud-provider-prometheus-payment8001
引入依赖
<dependency>
<groupId>io.micrometergroupId>
<artifactId>micrometer-registry-prometheusartifactId>
dependency>
修改application.yml配置文件
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
metrics:
tags:
application: ${spring.application.name}
注意:
management.endpoints.web.exposure.include=* 配置为开启 Actuator 服务,因为Spring Boot Actuator 会自动配置一个 URL 为 /actuator/Prometheus 的 HTTP 服务来供 Prometheus 抓取数据,不过默认该服务是关闭的,该配置将 打开所有的 Actuator 服务。
management.metrics.tags.application 配置会将该工程应用名称添加到Grafana UI,方便后边根据应用名称来区分不同的服 务。
修改主启动类
@EnableEurekaClient
@SpringBootApplication
@Slf4j
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
log.info("***********PaymentMain8001服务启动成功 ******* ");
}
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(
@Value("${spring.application.name}") String applicationName) {
return registry ->registry.config().commonTags("application",applicationName);
}
}
关闭Prometheus服务
因为 Prometheus 是一个 Unix 二进制程序,我们可以向 Prometheus 进程发送 SIGTERM 关闭信号。
使用 pgrep -f prometheus 找到运行的 Prometheus进程号
使用 kill -9 1234 来关闭
修改prometheus.yml配置文件
- job_name: "payment-provider"
scrape_interval: 5s
metrics_path: "/actuator/prometheus/"
static_configs:
- targets: ["192.168.66.10:8001"]
启动Prometheus服务
./prometheus --config.file=prometheus.yml
查看微服务大盘