此篇笔记包含的技术:Consul,OpenFeign,LoadBancer,Resilience4J,Micrometer+ZipKin,Gateway。
=======注意,这里有用!!!!!=======
笔记案例代码已经同步到:gitee.com/yanghaokun0...
=======注意,这里有用!!!!!=======
Consul服务发现与注册中心
Consul软件安装
Docker方式:
shell
docker run -d --name consul -p 8500:8500 consul agent -dev -server -bootstrap -client 0.0.0.0 -ui
安装验证:
安装成功。以上安装没有解决数据持久化,数据都存储在内存中。
客户端注册到Consul中
导入坐标:
xml
<!--consul服务发现注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--web健康检测-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml配置:
yaml
# consul服务地址
consul:
host: cloud.demo
port: 8500
yaml
# consul注册发现配置
spring:
cloud:
consul:
host: ${consul.host}
port: ${consul.port}
discovery:
# 注册到consul的服务名称
service-name: ${spring.application.name}
prefer-agent-address: true
# 心跳检查
heartbeat:
# 开启
enabled: true
# 在注册时使用ip地址而不是主机名
prefer-ip-address: true
启动类开启服务注册注解:
java
@EnableDiscoveryClient
启动微服务组件项目,观察Consul控制台:
Consul分布式配置中心(浅用)
Consul配置云yaml文件内容
解释:在Consul服务中配置了微服务 cloud-product 开发环境(dev)的云配置信息。
客户端读取云配置内容
导入云配置坐标:
xml
<!--consul云配置读取-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
创建 bootstrap.yaml 系统级别项目启动文件
内容如下:
yaml
---
spring:
application:
name: cloud-product
profiles:
active: dev
---
# consul服务地址
consul:
host: cloud.demo
port: 8500
---
# consul注册发现配置
spring:
cloud:
consul:
host: ${consul.host}
port: ${consul.port}
discovery:
# 注册到consul的服务名称
service-name: ${spring.application.name}
prefer-agent-address: true
# 心跳检查
heartbeat:
# 开启
enabled: true
# 在注册时使用ip地址而不是主机名
prefer-ip-address: true
---
spring:
cloud:
consul:
# 配置中心
config:
# 配置文件名称已 - 进行分割
profile-separator: '-'
# 配置文件类型
format: yaml
watch:
# 监听更新时间间隔 默认 55秒 测试 设置为 1秒
wait-time: 1
配置解释:项目一启动就去注册到Consul服务中,并且读取cloud-product-dev.yaml 文件的内容到本地。
Spring中读取配置文件内容实现动态刷新的两种方式:
-
@Value()注解 + @RefreshScope注解 实现动态刷新。
-
@Component注解 + @ConfigurationProperties()注解,实现动态刷新,例子如下:
java
@ConfigurationProperties(prefix = "dateformat")
@Component
@Data
public class DateFormat {
private String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
private String dateFormat = "yyyy-MM-dd";
}
创建一个访问接口进行测试,代码如下:
java
@RestController("/yun")
public class YunConfigController {
@Resource
private DateFormat dateFormat;
@GetMapping("/getInfo")
public String getInfo(){
return dateFormat.getDateTimeFormat();
}
}
访问:
更新Consul上的云配置文件内容:
观察idea控制台打印:
再次访问测试接口:
已实现云配置更新影响程序结果的效果。
OpenFeign远程调用接口
维护base-openfeign-api项目接口
根据以往项目开发经验,将OpenFeign维护的接口,单独创建出一个工程项目进行维护,步骤如下:
创建一个base-openfeign-api的项目。
导入openfeign坐标,以及基础公共实体坐标。
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
创建维护某个微服务的openfeign接口:
java
@FeignClient("cloud-product")
public interface ProductApi {
/**
* 根据商品id查询商品信息
*/
@GetMapping("/product/getProductById/{id}")
public ResponseResult<ProductVO> getProductById(@PathVariable("id") Integer id);
}
接口上核心注解:@FeignClient() ,表明次api接口面向注册中心哪个服务。
使用base-openfeign-api项目进行远程访问
导入base-openfeign-api坐标到使用工程中:
xml
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--远程调用服务接口项目-->
<dependency>
<groupId>com.haokun</groupId>
<artifactId>base-openfeign-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
启动类上激活你需要的api接口所在包:
java
@EnableFeignClients(basePackages = {"com.haokun.api.product"})
业务中使用:
java
@Resource
private ProductApi productApi;
// 调用接口
ResponseResult<ProductVO> productRes = productApi.getProductById(order.getProductId());
上述步骤已实现,在微服务一个体系内,可以使用openfeign的方式进行远程调用。
复制一份商品微服务程序,形成集群服务,使用openfeign远程调用商品服务,测试 LoadBancer 负载均衡:
启动:
LoadBancer 默认实现的是轮训算法进行访问:
经过上述测试,已经实现openfeign + LoadBancer + consul 服务的注册发现,负载均衡,远程访问功能。
LoadBancer的依赖由consul带入。
经过上面的步骤已经完成了案例的第一个版本,下面逐步加入微服务组件,代码地址如下: gitee.com/yanghaokun0...
openfeign的高级特性
调用时间超时机制
yaml配置内容:
yaml
# openfeign超时时间配置
spring:
cloud:
openfeign:
client:
config:
# 默认调用全部接口的时间为1.5秒
default:
connect-timeout: 1500
read-timeout: 1500
# 指定微服务组件的超时时间
cloud-product:
connect-timeout: 3000
read-timeout: 3000
超时会报错抛出异常,内容如下:
重试机制:
java
/**
* openfeign配置
*/
@SpringBootConfiguration
public class FeignConfig {
/**
* 重试配置
*
* @return
*/
@Bean
public Retryer myRetryer() {
// openfeign 默认配置是不走重试策略的
// return Retryer.NEVER_RETRY;
// 最大请求次数为3 (1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100, 1, 3);
}
}
可以实现在请求超时时,重新发送两次请求。
请求日志
观察请求日志:
请求连接池
openfeign 默认使用的是 java.base java.net.HttpURLConnection,没有连接池,替换为 httpClient 连接池,为openfeign来提供连接供给。
引入连接池坐标:
xml
<!--请求连接池-->
<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
yaml配置内容:
yaml
# 配置请求连接池
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
请求观察:
开启请求响应压缩
yaml配置:
yaml
# 开启请求响应压缩模式
spring:
cloud:
openfeign:
# 压缩
compression:
# 请求
request:
enabled: true
# 最小压缩要求
min-request-size: 2048
# 压缩数据类型
mime-types: text/xml, application/xml, application/json
# 响应
response:
enabled: true
Resilience4j熔断降级,限流,隔离
我对熔断降级,限流,隔离的理解:熔断就是在请求访问一个服务时,触发了设置的阈值,然后开始熔断,不再请求,并且快速返回数据,称为降级处理,隔离和限流很类似,是设置访问一个服务的请求数量保持在多少,多的请求会进行降级处理。
-
设定一个需求:在访问一个请求时,请求数量6个,出现了3次异常信息,错误率达到50%,进行熔断降级处理,并返回系统繁忙请稍后再试的数据内容。
-
设定第二个需求:访问一个请求,这个请求1秒只能并发5个,多的请求会降级处理,并返回当前人数较多,请稍后再试的数据。
-
设定第三个需求:访问一个请求,这个请求1秒只能通过10个,多的请求会降级处理,并返回当前人数较多,请稍后再试的数据。
针对resilience4j熔断的方式两种:超时,异常,我将使用openfeign来控制超时,若超时抛出异常,让resilience4j来统计,达到阈值时熔断。
熔断降级
导入坐标:
xml
<!--熔断降级resilience4j-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!--由于断路保护等需要AOP实现,所以必须导入AOP包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
yaml配置内容:
yaml
# 熔断降级前提配置
spring:
cloud:
openfeign:
# 断路器
circuitbreaker:
enabled: true
group:
enabled: true
坑:
yaml
# 延长TimeLimiter默认的请求超时时长 它 1秒的
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s
主要内容:
yaml
# 配置断路器
resilience4j:
circuitbreaker:
configs:
# 默认断路器配置
default:
# 断路器方式 计数的方式
sliding-window-type: count_based
# 统计数量周期
sliding-window-size: 6
# 最小统计数量
minimum-number-of-calls: 6
# 阈值 百分比
failure-rate-threshold: 50
#熔断时长 秒
wait-duration-in-open-state: 10s
# 需要开启半开状态来测试
automatic-transition-from-open-to-half-open-enabled: true
# 半开测试访问请求数量
permitted-number-of-calls-in-half-open-state: 2
# 计数的异常
record-exceptions:
- java.lang.Exception
instances:
cloud-product:
base-config: default
Java代码入侵加强:
java
@CircuitBreaker(name = "cloud-product", fallbackMethod = "nextOrderFallbackMethod")
@Override
public Boolean nextOrder(Order order) {
}
java
public Boolean nextOrderFallbackMethod(Order order,Throwable throwable){
if (true){
throw new ServiceException("系统繁忙请稍后再试");
}
return false;
}
降级的方法参数有要求,如上:需要与配置了断路器的方式参数一致时,后面还需要加入Throwable参数类型。
开始压测:
其他请求在熔断的时间内将直接降级处理:
隔离
限制对下游的并发数量。
导入坐标:
xml
<!--对下游访问的访问隔离-->
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
yaml配置:
yaml
# 对下游访问的访问隔离
resilience4j:
bulkhead:
configs:
default:
# 最大允许数量
max-concurrent-calls: 5
# 时间周期
max-wait-duration: 1s
instances:
cloud-product:
base-config: default
Java入侵增强:
java
@Bulkhead(name = "cloud-product",fallbackMethod = "nextOrderBulkheadFallbackMethod",type = Bulkhead.Type.SEMAPHORE)
降级方法:
开始压测:
允许的范围内成功:
失败:
限流
导入坐标:
xml
<!--限流-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
yaml配置内容:
yaml
# 限流
resilience4j:
ratelimiter:
configs:
# 默认每秒只能通过1个请求
default:
limit-for-period: 10
limit-refresh-period: 1s
timeout-duration: 1
instances:
cloud-product:
base-config: default
Java代码入侵增强:
压测触发,降级处理:
总结:resilience4j不好用,配置繁琐,代码入侵,难维护!
分布式链路追踪
micrometer(收集数据) + zipkin(数据展示)
搭建 zipkin
Docker的方式搭建:
shell
docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin
浏览器访问:你的服务地址:9411
引入micrometer
父工程版本控制内容:
xml
<properties>
<!--链路追踪版本控制-->
<micrometer-tracing.version>1.2.0</micrometer-tracing.version>
<micrometer-observation.version>1.12.0</micrometer-observation.version>
<feign-micrometer.version>12.5</feign-micrometer.version>
<zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
</properties>
xml
<dependencyManagement>
<dependencies>
<!--链路追踪版本控制-->
<!--micrometer-tracing-bom导入链路追踪版本中心 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bom</artifactId>
<version>${micrometer-tracing.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--micrometer-tracing指标追踪 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-observation 4-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<version>${micrometer-observation.version}</version>
</dependency>
<!--feign-micrometer 5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
<version>${feign-micrometer.version}</version>
</dependency>
<!--zipkin-reporter-brave 6-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
<version>${zipkin-reporter-brave.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
微服务项目工程引入坐标:
xml
<!--链路追踪-->
<!--micrometer-tracing指标追踪 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
yaml配置内容:
yaml
# 链路追踪zipkin配置
management:
zipkin:
tracing:
# zipkin服务端地址
endpoint: http://cloud.demo:9411/api/v2/spans
tracing:
sampling:
# 默认0.1 10次请求采集一次,值越大采集越及时
probability: 1.0
发送请求,观察zipkin控制台
可以发现第一次请求访问很慢。
点击一个请求详细内容:SHOW按钮点击
发送一个会报异常的请求:
Gateway网关
网关HelloWorld
创建网关微服务独立项目,引入坐标:
xml
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
需要引入所使用的服务注册中心,并注册进入。
yaml配置一个路由:
yaml
# 配置网关路由
spring:
cloud:
gateway:
routes:
# 唯一id
- id: cloud-order
# 动态访问服务地址
uri: lb://cloud-order
# 断言
predicates:
- Path=/order/**
网关三大组件:路由:routes,断言:predicates,过滤器:filters
通过网关的服务地址+端口号进行访问,有效的屏蔽了真正微服务服务的地址。
网关动态获取服务地址
yaml
uri: lb://cloud-order
这里的lb,就是:LoadBancer,从服务注册中心拉去下来,负载均衡。
断言predicates
项目启动也加载了这些断言。
最常用的就是Path断言了。
测试一下头信息需要携带的内容:需要请求携带头信息authentication,值为存数字才可以断言成功
yaml
predicates:
- Header=authentication, \d+
测试不携带authentication头信息:
携带:
自定义断言 与 自定义过滤器类似。
过滤器
对已断言成功的请求,锦上添花:
参考官网地址: docs.spring.io/spring-clou...
使用内置过滤器工厂往请求头中添加信息,证明请求是经过了网关,若客户端直接绕开网关,则请求失败:
yaml
spring:
cloud:
gateway:
routes:
# 唯一id
- id: cloud-order
# 动态访问服务地址
uri: lb://cloud-order
# 断言
predicates:
- Path=/order/**
# - Header=authentication, \d+
filters:
- AddRequestHeader=me, yang
自定义全局过滤器
往请求头中添加客户端请求的IP地址。
java
@Component
public class ClientIpFilter implements GlobalFilter, Ordered {
private static final String CLIENT_IP_HEADER = "X-Client-IP";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取客户端IP
String clientIp = exchange.getRequest().getRemoteAddress().getHostString();
// ReadOnlyHttpHeaders 报错
// exchange.getRequest().getHeaders().add(CLIENT_IP_HEADER, clientIp);
ServerHttpRequest request = exchange.getRequest().mutate().header(CLIENT_IP_HEADER, clientIp).build();
return chain.filter(exchange.mutate().request(request).build());
}
/**
* 值越小,优先级越高
*/
@Override
public int getOrder() {
return -1;
}
}
网关发送到微服务时:
统计接口调用时间
java
@Component
@Slf4j
public class GlobalTimeTestFilter implements GlobalFilter, Ordered {
//开始访问时间
private static final String BEGIN_VISIT_TIME = "begin_visit_time";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginVisitTime != null) {
log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
log.info("分割线: ###################################################");
System.out.println();
}
}));
}
@Override
public int getOrder() {
return -2;
}
}
自定义条件过滤器
请求头中携带指定id
java
@Component
@Slf4j
public class CheckGatewayFilterFactory extends AbstractGatewayFilterFactory<CheckGatewayFilterFactory.Config> {
public CheckGatewayFilterFactory() {
super(CheckGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("经过了校验条件过滤器");
List<String> ids = List.of("2670", "2690");
String id = exchange.getRequest().getHeaders().getFirst("id");
if (Objects.isNull(id) || id.isEmpty() || !ids.contains(id)) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("id");
}
@Data
public static class Config {
private String id;
}
}
yaml配置它:
yaml
# 配置网关路由
spring:
cloud:
gateway:
routes:
# 唯一id
- id: cloud-order
# 动态访问服务地址
uri: lb://cloud-order
# 断言
predicates:
- Path=/order/**
# - Header=authentication, \d+
filters:
- AddRequestHeader=me, yang
- Check
请求访问:
失败:
成功:
以上的笔记就结束了对原生SpringCloud的一些组件学习与练习。代码已标签: gitee.com/yanghaokun0...