文章目录
- 一、微服务网关
- 二、分布式日志追踪
- 三、微服务通信
- 四、Hystrix
一、微服务网关
1、概念
1.一款基于 SpringFramework5,Project Reactor 和SpringBoot 2 之上开发的网关
2.第一代网关 Zuul 不同的是:gateway 是异步非阻塞的(netty+webflux 实现);zuul是同步阻塞请求的
3.三大组成部分
- Route路由(ID必须唯一、URI)
- Predicate断言:路由的匹配条件
- Filter过滤器
4、网关是微服务工程架构下的唯一入口(客户端 )
5、Gateway 提供了统一的路由方式,基于Filter 链的方式提供了网关的基本功能
工作模型
解读:
第一步:请求发送到网关,经由分发器将请求匹配到相应的 HandlerMapping,第一个分析匹配的组件,匹配相应的路由配置。
第二步:请求和处理器之间有一个映射,路由到网关处理程序,即 Web Handler,读取路由配置上的所有过滤器,构造过滤器链,往过滤器上传递。
第三步:执行特定的请求过滤器链,最终到达代理的微服务
第四步:响应逆向返回客户端
2、谓词 Predicate 的原理与应用
一、概念与原理
使用的java8的Predicate(位于 java.util.function 包中,是一个 FunctionalInterface(函数式接口)),其中的接口通常用在steam的filter中实现,表示是否满足条件。
/**
* <h1>Java8 Predicate 使用方法与思想</h1>
* */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class PredicateTest {
public static List<String> MICRO_SERVICE = Arrays.asList(
"nacos", "gateway", "ribbon", "feign", "hystrix"
);
/**
* test 方法主要用于参数符不符合规则, 返回值是 boolean
* */
@Test
public void testPredicateTest() {
Predicate<String> letterLengthLimit = s -> s.length() > 5;
MICRO_SERVICE.stream().filter(letterLengthLimit).forEach(System.out::println);
//打印出 gateway、ribbon、hystrix
}
/**
* and 方法等同于我们的逻辑与 &&, 存在短路特性, 需要所有的条件都满足
* */
@Test
public void testPredicateAnd() {
Predicate<String> letterLengthLimit = s -> s.length() > 5;
Predicate<String> letterStartWith = s -> s.startsWith("gate");
MICRO_SERVICE.stream().filter(
letterLengthLimit.and(letterStartWith)
).forEach(System.out::println);
//打印出 gateway
}
/**
* or 等同于我们的逻辑或 ||, 多个条件主要一个满足即可
* */
@Test
public void testPredicateOr() {
Predicate<String> letterLengthLimit = s -> s.length() > 5;
Predicate<String> letterStartWith = s -> s.startsWith("gate");
MICRO_SERVICE.stream().filter(
letterLengthLimit.or(letterStartWith)
).forEach(System.out::println);
//打印出 gateway、ribbon、hystrix
}
/**
* negate 等同于我们的逻辑非 !
* */
@Test
public void testPredicateNegate() {
Predicate<String> letterStartWith = s -> s.startsWith("gate");
MICRO_SERVICE.stream().filter(letterStartWith.negate()).forEach(System.out::println);
//打印出 nacos、ribbon、hystrix、feign
}
/**
* isEqual 类似于 equals(), 区别在于: 先判断对象是否为 NULL,
* 不为 NULL 再使用 equals 进行比较
* */
@Test
public void testPredicateIsEqual() {
Predicate<String> equalGateway = s -> Predicate.isEqual("gateway").test(s);
MICRO_SERVICE.stream().filter(equalGateway).forEach(System.out::println);
//打印出gateway
}
}
二、Gateway中的应用
1、源码
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
public PathRoutePredicateFactory() {
super(PathRoutePredicateFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("path");
}
@Override
public Predicate<ServerWebExchange> apply(PathRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
PathContainer path = exchange.getRequest().getPath();
for (String p : config.getPath()) {
if (path.match(TextUtils.urlDecode(p))) {
return true;
}
}
return false;
}
@Override
public String toString() {
return String.format("Paths: %s", config.getPath());
}
};
}
public static class Config {
private List<String> path = new ArrayList<>();
public List<String> getPath() {
return path;
}
public void setPath(List<String> path) {
this.path = path;
}
}
}
解读:用于根据请求的路径进行路由判断和过滤。当请求的路径匹配指定的条件时,路由会被转发到相应的服务 例如:
gateway: routes: - id: path_route_example uri: http://httptest.org predicates: - Path=/user/** # 匹配 /user/ 开头的路径 #此配置将匹配所有以 /user/ 开头的请求路径, #如 /user/123 或 /user/abc,并将它们转发到 http://httptest.org/user/** ```
3、集成 Alibaba Nacos 实现路由配置
一、静态路由配置
(1)、yml文件配置
1、静态路由配置写在配置文件中(yml或者 properties 文件中),端点是:spring.cloud.gateway
2、每次改动都需要网关模块重新部署
# 静态路由
gateway:
routes:
- id: path_route # 路由的ID
uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
predicates: # 断言, 路径相匹配的进行路由
- Path=/user/{id}
(2)、代码配置
用于登录注册转发微服务
@Configuration
public class RouteLocatorConfig {
/**
* <h2>使用代码定义路由规则, 在网关层面拦截下登录和注册接口</h2>
* */
@Bean
public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {
// 手动定义 Gateway 路由规则需要指定 id、path 和 uri
return builder.routes()
.route(
"e_commerce_authority",
r -> r.path(
//配置项中的predicates
"/whqn/e-commerce/login",
"/whqn/e-commerce/register"
).uri("http://localhost:9001/")
).build();
}
}
二、动态路由配置
1、修改nacos配置
[
{
"id": "e-commerce-nacos-client",
"predicates": [
{
"args": {
"pattern": "/whqn/ecommerce-nacos-client/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-nacos-client",#微服务id
"filters": [
{
"name": "HeaderToken"
},
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
}
]
2、创建工程
【pom文件】
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ecommerce-springcloud</artifactId>
<groupId>com.whqn.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-gateway</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>e-commerce-gateway</name>
<description>Spring Cloud Gateway</description>
<dependencies>
<!-- spring cloud alibaba nacos discovery 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
<!-- 不能引入mvc依赖,gateway使用的不是tomcat-->
<dependency>
<groupId>com.whqn.ecommerce</groupId>
<artifactId>e-commerce-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<!--
SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
-->
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
【yml】
server:
port: 9001
servlet:
context-path: /whqn
spring:
application:
name: e-commerce-gateway
cloud:
nacos:
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
# 静态路由
# gateway:
# routes:
# - id: path_route # 路由的ID
# uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
# predicates: # 断言, 路径相匹配的进行路由
# - Path=/user/{id}
main:
allow-bean-definition-overriding: true # 因为将来会引入很多依赖, 难免有重名的 bean
# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 配置类中读取被监听
nacos:
gateway:
route:
config:
data-id: e-commerce-gateway-router
group: e-commerce
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
3、创建配置类, 读取 Nacos 相关的配置项, 用于配置监听器
@Configuration
public class GatewayConfig {
/** 读取配置的超时时间 */
public static final long DEFAULT_TIMEOUT = 30000;
/** Nacos 服务器地址 */
public static String NACOS_SERVER_ADDR;
/** 命名空间 */
public static String NACOS_NAMESPACE;
/** data-id */
public static String NACOS_ROUTE_DATA_ID;
/** 分组 id */
public static String NACOS_ROUTE_GROUP;
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAddr(String nacosServerAddr) {
NACOS_SERVER_ADDR = nacosServerAddr;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
public void setNacosNamespace(String nacosNamespace) {
NACOS_NAMESPACE = nacosNamespace;
}
@Value("${nacos.gateway.route.config.data-id}")
public void setNacosRouteDataId(String nacosRouteDataId) {
NACOS_ROUTE_DATA_ID = nacosRouteDataId;
}
@Value("${nacos.gateway.route.config.group}")
public void setNacosRouteGroup(String nacosRouteGroup) {
NACOS_ROUTE_GROUP = nacosRouteGroup;
}
}
4、注册网关事件监听器(事件推送 Aware: 动态更新路由网关 Service)
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
/** 写路由定义 */
private final RouteDefinitionWriter routeDefinitionWriter;
/** 获取路由定义 */
private final RouteDefinitionLocator routeDefinitionLocator;
/** 事件发布 */
private ApplicationEventPublisher publisher;
public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,
RouteDefinitionLocator routeDefinitionLocator) {
this.routeDefinitionWriter = routeDefinitionWriter;
this.routeDefinitionLocator = routeDefinitionLocator;
}
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
// 完成事件推送句柄的初始化
this.publisher = applicationEventPublisher;
}
/**
* <h2>增加路由定义</h2>
* */
public String addRouteDefinition(RouteDefinition definition) {
log.info("gateway add route: [{}]", definition);
// 保存路由配置并发布
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
// 发布事件通知给 Gateway, 同步新增的路由定义
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* <h2>更新路由</h2>
* */
public String updateList(List<RouteDefinition> definitions) {
log.info("gateway update route: [{}]", definitions);
// 先拿到当前 Gateway 中存储的路由定义
List<RouteDefinition> routeDefinitionsExits =
routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
// 清除掉之前所有的 "旧的" 路由定义
routeDefinitionsExits.forEach(rd -> {
log.info("delete route definition: [{}]", rd);
deleteById(rd.getId());
});
}
// 把更新的路由定义同步到 gateway 中
definitions.forEach(definition -> updateByRouteDefinition(definition));
return "success";
}
/**
* <h2>根据路由 id 删除路由配置</h2>
* */
private String deleteById(String id) {
try {
log.info("gateway delete route id: [{}]", id);
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
// 发布事件通知给 gateway 更新路由定义
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "delete success";
} catch (Exception ex) {
log.error("gateway delete route fail: [{}]", ex.getMessage(), ex);
return "delete fail";
}
}
/**
* <h2>更新路由</h2>
* 更新的实现策略比较简单: 删除 + 新增 = 更新
* */
private String updateByRouteDefinition(RouteDefinition definition) {
try {
log.info("gateway update route: [{}]", definition);
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception ex) {
return "update fail, not find route routeId: " + definition.getId();
}
try {
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception ex) {
return "update route fail";
}
}
}
RouteDefinition.java源码对应的是配置文件中的配置项
5、注册网关事件监听器(通过 nacos 下发动态路由配置, 监听 Nacos 中路由配置变更)
@Slf4j
@Component
@DependsOn({"gatewayConfig"}) //配置项先注入
public class DynamicRouteServiceImplByNacos {
/** Nacos 配置服务 */
private ConfigService configService;
private final DynamicRouteServiceImpl dynamicRouteService;
public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
this.dynamicRouteService = dynamicRouteService;
}
/**
* <h2>Bean 在容器中构造完成之后会执行 init 方法</h2>
* */
@PostConstruct
public void init() {
log.info("gateway route init....");
try {
// 初始化 Nacos 配置客户端
configService = initConfigService();
if (null == configService) {
log.error("init config service fail");
return;
}
// 通过 Nacos Config 并指定路由配置路径去获取路由配置
String configInfo = configService.getConfig(
GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP,
GatewayConfig.DEFAULT_TIMEOUT
);
log.info("get current gateway config: [{}]", configInfo);
List<RouteDefinition> definitionList =
JSON.parseArray(configInfo, RouteDefinition.class);
if (CollectionUtils.isNotEmpty(definitionList)) {
for (RouteDefinition definition : definitionList) {
log.info("init gateway config: [{}]", definition.toString());
dynamicRouteService.addRouteDefinition(definition);
}
}
} catch (Exception ex) {
log.error("gateway route init has some error: [{}]", ex.getMessage(), ex);
}
// 设置监听器
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP);
}
/**
* <h2>初始化 Nacos Config</h2>
* */
private ConfigService initConfigService() {
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
return configService = NacosFactory.createConfigService(properties);
} catch (Exception ex) {
log.error("init gateway nacos config error: [{}]", ex.getMessage(), ex);
return null;
}
}
/**
* <h2>监听 Nacos 下发的动态路由配置</h2>
* */
private void dynamicRouteByNacosListener(String dataId, String group) {
try {
// 给 Nacos Config 客户端增加一个监听器
configService.addListener(dataId, group, new Listener() {
/**
* <h2>自己提供线程池执行操作</h2>
* */
@Override
public Executor getExecutor() {
return null;
}
/**
* <h2>监听器收到配置更新</h2>
* @param configInfo Nacos 中最新的配置定义
* */
@Override
public void receiveConfigInfo(String configInfo) {
log.info("start to update config: [{}]", configInfo);
List<RouteDefinition> definitionList =
JSON.parseArray(configInfo, RouteDefinition.class);
log.info("update route: [{}]", definitionList.toString());
dynamicRouteService.updateList(definitionList);
}
});
} catch (NacosException ex) {
log.error("dynamic update gateway config error: [{}]", ex.getMessage(), ex);
}
}
}
4、过滤器SpringCloud Gateway Filter
一、相关概念
1、SpringCloud Gateway 基于过滤器实现,同 zuul 类似,有 pre 和 post两种方式的filter,分别处理前置逻辑和后置逻辑
2、客户端的请求先经过 pre 类型的 fiter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过 post 类型的 filter 处理,最后返回响应到客户端
3、Filter 一共有两大类:全局过滤器和局部过滤器
二、执行流程
1、过滤器有优先级之分,Order越大,优先级越低,越晚被执行
2、全局过滤器所有的请求都会执行
3、局部过滤器只有配置的请求才会执行
三、内置过滤器解读
(1)、全局过滤器RouteToRequestUrlFilter.java
作用示例:
假设你有如下的路由配置
spring:
cloud:
gateway:
routes:
- id: service1_route
uri: lb://service-1
predicates:
- Path=/api/v1/**
当请求到达 http://localhost:8080/api/v1/hello
时:
路由匹配:请求路径 /api/v1/hello
会匹配到 service1_route 路由。
路由到目标服务:RouteToRequestUrlFilter 会使用服务发现机制(如 Nacos 或 Eureka),从中查找服务 ID 为 service-1 的实例。
生成目标 URL:假设 service-1 实际运行在 http://localhost:8081
,RouteToRequestUrlFilter 会将原始请求的 URL http://localhost:8080/api/v1/hello
替换为 http://localhost:8081/api/v1/hello
。
转发请求:请求最终会被转发到 http://localhost:8081/api/v1/hello
,即目标微服务。
源码解读(看注释):
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从exchange中取路由Route对象
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
if (route == null) {
return chain.filter(exchange);
}
log.trace("RouteToRequestUrlFilter start");
// 取当前请求uri : http://localhost:8080/api/v1/hello
URI uri = exchange.getRequest().getURI();
boolean encoded = containsEncodedParts(uri);
// 路由对象中保存的uri,也就是我们在yml文件中配置的值: lb://service-1
URI routeUri = route.getUri();
if (hasAnotherScheme(routeUri)) {
exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
routeUri = URI.create(routeUri.getSchemeSpecificPart());
}
// 如果我们在yml文件中配置的uri,即不是lb开头并且host还为null,那么就抛异常
if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
throw new IllegalStateException("Invalid host: " + routeUri.toString());
}
// 转换结果为: lb://service-1/api/v1/hello
//ReactiveLoadBalancerClientFilter转化为http://localhost:8081/api/v1/hello
URI mergedUrl = UriComponentsBuilder.fromUri(uri)
.scheme(routeUri.getScheme()).host(routeUri.getHost()).
port(routeUri.getPort()).build(encoded).toUri();
// 存入exchange中,之后的LoadBalancer全局Filter中会用到
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
return chain.filter(exchange);
}
(2)、局部过滤器PrefixPathGatewayFilterFactory.java
作用示例:
spring;
cloud:
routes:
-id: whqn
uri: http://example/org
filters:
- PrefixPath=/mypath
当请求到达:http://gatewayhost:8080/hello
生成目标 URL:http://example/org/mypath/hello
源码解读(看注释):
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
boolean alreadyPrefixed = exchange.getAttributeOrDefault(GATEWAY_ALREADY_PREFIXED_ATTR, false);
if (alreadyPrefixed) {
return chain.filter(exchange);
}
exchange.getAttributes().put(GATEWAY_ALREADY_PREFIXED_ATTR, true);
ServerHttpRequest req = exchange.getRequest();
addOriginalRequestUrl(exchange, req.getURI());
//config.prefix是需要加入的前缀
String newPath = config.prefix + req.getURI().getRawPath();
ServerHttpRequest request = req.mutate()
.path(newPath)
.build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());
if (log.isTraceEnabled()) {
log.trace("Prefixed URI with: "+config.prefix+" -> "+request.getURI());
}
return chain.filter(exchange.mutate().request(request).build());
};
}
(3)、局部过滤器StripPrefixGatewayFilterFactory.java
作用示例:
spring;
cloud:
routes:
-id: whqn
uri: http://example/org
predicates:
- Path=/name/**
filters:
- StripPrefix=2
当请求到达:http://gatewayhost:8080/name/test/hello
生成目标 URL:http://example/org/hello
源码解读(看注释):
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
addOriginalRequestUrl(exchange, request.getURI());
String path = request.getURI().getRawPath();
//config.parts是int类型,表示跳过几个部分
String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(path, "/"))
.skip(config.parts).collect(Collectors.joining("/"));
ServerHttpRequest newRequest = request.mutate()
.path(newPath)
.build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
三、自定义过滤器
(1)、【局部过滤器】- 校验 Header 中的 Token
1、需要实现 GatewayFilter, Ordered,实现相关的方法
2、加入到过滤器工厂,并且将工厂注册到 Spring 容器中
3、在配置文件中进行配置,如果不配置则不启用此过滤器规则(路由规则)
作用示例:
"filters": [
{
"name": "HeaderToken" #配置之后才会生效
},
]
请求头中需要存在从 HTTP Header 中寻找 key 为 token, value 为 whqn 的键值对
步骤(看注释):
1、HTTP 请求头部携带 Token 验证过滤器
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从 HTTP Header 中寻找 key 为 token, value 为 whqn 的键值对
String name = exchange.getRequest().getHeaders().getFirst("token");
if ("whqn".equals(name)) {
return chain.filter(exchange);
}
// 标记此次请求没有权限, 并结束这次请求
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
//值越大,越迟执行
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
}
2、新建过滤器工程,不创建无法生效
@Component
//取名称中的HeaderToke作为配置项
public class HeaderTokenGatewayFilterFactory
extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new HeaderTokenGatewayFilter();
}
}
(2)、【全局过滤器】- 耗时日志
@Slf4j
@Component
public class GlobalElapsedLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 前置逻辑
StopWatch sw = StopWatch.createStarted();
String uri = exchange.getRequest().getURI().getPath();
return chain.filter(exchange).then(
// 后置逻辑
Mono.fromRunnable(() ->
log.info("[{}] elapsed: [{}ms]",
uri, sw.getTime(TimeUnit.MILLISECONDS)))
);
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
}
(2)、【全局过滤器】- 缓存HTTP请求Body
作用:将请求头的body内容全部缓存起来
定义常量:
public class GatewayConstant {
/** 登录 uri */
public static final String LOGIN_URI = "/e-commerce/login";
/** 注册 uri */
public static final String REGISTER_URI = "/e-commerce/register";
}
编写过滤器,从登录或注册请求中缓存请求中的body
@Slf4j
@Component
@SuppressWarnings("all")
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
boolean isloginOrRegister =
exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
|| exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);
if (null == exchange.getRequest().getHeaders().getContentType()
|| !isloginOrRegister) {
return chain.filter(exchange);
}
// DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
// 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
DataBufferUtils.retain(dataBuffer);
// defer、just 都是去创建数据源, 得到当前数据的副本
Flux<DataBuffer> cachedFlux = Flux.defer(() ->
Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
// 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
ServerHttpRequest mutatedRequest =
new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
// 将包装之后的 ServerHttpRequest 向下继续传递
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 1;
}
}
(3)、【全局过滤器】- 登录、注册、鉴权
1、定义常量:
鉴权微服务:context-path: /ecommerce-authority-center
id:e-commerce-authority-center
找到Ip和端口,最后进行拼接
public class GatewayConstant {
/** 登录 uri */
public static final String LOGIN_URI = "/e-commerce/login";
/** 注册 uri */
public static final String REGISTER_URI = "/e-commerce/register";
/** 去授权中心拿到登录 token 的 uri 格式化接口 */
public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT =
"http://%s:%s/ecommerce-authority-center/authority/token";
/** 去授权中心注册并拿到 token 的 uri 格式化接口 */
public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT =
"http://%s:%s/ecommerce-authority-center/authority/register";
}
2、将RestTemplate提前注入到网关中,用于远程调用微服务
@Configuration
public class GatewayBeanConf {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
3、创建全局登录鉴权过滤器
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
/** 注册中心客户端, 可以从注册中心中获取服务实例信息 */
private final LoadBalancerClient loadBalancerClient;
private final RestTemplate restTemplate;
public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,
RestTemplate restTemplate) {
this.loadBalancerClient = loadBalancerClient;
this.restTemplate = restTemplate;
}
/**
* 登录、注册、鉴权
* 1. 如果是登录或注册, 则去授权中心拿到 Token 并返回给客户端
* 2. 如果是访问其他的服务, 则鉴权, 没有权限返回 401
* */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 1. 如果是登录
if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
// 去授权中心拿 token
String token = getTokenFromAuthorityCenter(
request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
);
// header 中不能设置 null
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
// 2. 如果是注册
if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
// 去授权中心拿 token: 先创建用户, 再返回 Token
String token = getTokenFromAuthorityCenter(
request,
GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
);
response.getHeaders().add(
CommonConstant.JWT_USER_INFO_KEY,
null == token ? "null" : token
);
response.setStatusCode(HttpStatus.OK);
return response.setComplete();
}
// 3. 访问其他的服务, 则鉴权, 校验是否能够从 Token 中解析出用户信息
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
LoginUserInfo loginUserInfo = null;
try {
//TokenParseUtil是上一章学习记录编写的token解析工具类
loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
} catch (Exception ex) {
log.error("parse user info from token error: [{}]", ex.getMessage(), ex);
}
// 获取不到登录用户信息, 返回 401
if (null == loginUserInfo) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 解析通过, 则放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
/**
* 从授权中心获取 Token
* */
private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {
// service id 就是服务名字, 负载均衡
ServiceInstance serviceInstance = loadBalancerClient.choose(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("Nacos Client Info: [{}], [{}], [{}]",
serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
JSON.toJSONString(serviceInstance.getMetadata()));
String requestUrl = String.format(
uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
);
UsernameAndPassword requestBody = JSON.parseObject(
parseBodyFromRequest(request), UsernameAndPassword.class
);
log.info("login request url and body: [{}], [{}]", requestUrl,
JSON.toJSONString(requestBody));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
JwtToken token = restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(requestBody), headers),
JwtToken.class
);
if (null != token) {
return token.getToken();
}
return null;
}
/**
* 从 Post 请求中获取到请求数据
* */
private String parseBodyFromRequest(ServerHttpRequest request) {
// 获取请求体
Flux<DataBuffer> body = request.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
// 订阅缓冲区去消费请求体中的数据
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
// 一定要使用 DataBufferUtils.release 释放掉, 否则, 会出现内存泄露
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
// 获取 request body
return bodyRef.get();
}
}
四、验证过滤器
(1)、验证登录注册接口
### 登录
POST 127.0.0.1:9001/whqn/e-commerce/login
Content-Type: application/json
{
"username": "WHQN@imooc.com",
"password": "asdfdfds283aa400af464c76d713c07ad"
}
###
POST 127.0.0.1:9001/whqn/e-commerce/register
Content-Type: application/json
{
"username": "WHQN97@imooc.com",
"password": "asdfdfds283aa400af464c76d713c07ad"
}
使用了鉴权微服务并返回jwt
(2)、调用普通查询服务
### 查询nacos注册中心中网关服务的信息
GET http://127.0.0.1:9001/whqn/ecommerce-nacos-client/nacos-client/service-instance?serviceId=e-commerce-gateway
Accept: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjE3LFwidXNlcm5hbWVcIjpcIkltb29jUWlueWlAaW1vb2MuY29tXCJ9IiwianRpIjoiMGIxNzQyYWItMWU3OC00OTZjLWIyNTAtMjNkZGQ1ZGEyZTU1IiwiZXhwIjoxNjI0MjA0ODAwfQ.QKGHzohSHdYDHzUVHpe9gNPUgzfkPwrSbB-WiMWYjLlt2tr9BufzZM8bSt-whb_bd0hKoC6rkYYO0WUVR67uSML-2yaTL1xMIn8GH9Flyig3rpO4vefL3Hp2TXIpwHHa7WlKsLzcUpNk9lxWs2B5k0ICdYCH_jD5Tx6N7CzfSUG9u4fOnVeM9UFE2nX_DURupUh_DKCc2oOoMeyCSR7Ma8-Ab4WQU3r-U0YivR8G1A0kmKOIoTeRhM3LcPuxUPh3rCbrjzMg--fexRGw0O38Qsby6pz-ku2IlTyFXY6_jNOG1BZR34-jBOnaIciP1TExw9bFumeuC2GcowTHJVH1Nw
token: whqn #对应以上所述的HeaderTokenGatewayFilter过滤器
二、分布式日志追踪
1、SpringCloud日志追踪概述
一、组件
(1)、Sleuth
概要:它会自动为当前应用构建起各通信通道的跟踪机制(采用了 Brave 作为 tracer库)
1、通过诸如RabbitMQ
、Kafka
(或者其他任何SpringCloud Stream 绑定器
实现的消息中间件)传递的请求
2、通过Zuul
、Gateway
代理传递的请求
3、通过RestTemplate
发起的请求
跟踪原理:
为了实现请求跟踪: 当请求发送到分布式系统的入口端点时,为该请求创建一个唯一的跟踪标识,Trace ID
为了统计各处理单元的时间延迟,请求到达各个服务组件时,通过唯一标识来标记它的开始、具体过程以及结束,Span ID
(2)、Zipkin
(1)、概要(Sleuth可以不使用 Zipkin)
解决微服务架构中的延迟问题, 包括数据的收集、存储、查找和展现
(2)、四大核心组件构成(如下面逻辑架构图所示):
- Collector:收集器组件
- Storage:存储组件
- API:RESTFUIAPI,提供外部访问接口
- UI:Web U,提供可视化查询页面
(3)、Brave
核心的概念 (是一个 tracer 库,提供的是 tracer接口)
1、trace: 以看作是一个逻辑执行过程中的整个链条
2、span:是 trace 跟踪的基本单位
二、结构
(1)、数据结构
Tracing:工具类,用于生成 Tracer 类实例
Tracer :也是工具类,用于生成 Span
Span:实际记录每个功能块执行信息的类
TraceContext:记录 trace 的执行过程中的元数据信息类
Propagation:用于在分布式环境或者跨进程条件下的trace 跟踪时实现 TraceContext 传递的工具类
(2)、逻辑架构
三、跨服务Trace追踪原理
概述:SpringCloud Sleuth 和 Brave 提供了很多不同的分布式框架的支持,例如 gRPC、Kafka、HTTP 等
通过 TraceContext 中核心信息的传递来实现的
2、集成 SpringCloud Sleuth
一、代码集成
原则:保证微服务存在跨进程通信,否则意义不大
pom文件加入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
二、日志信息
(1)、控制台变化
(2)、自定义输出
使用brave进行代码输出
@Slf4j
@Service
public class SleuthTraceInfoService {
/** brave.Tracer 跟踪对象 */
private final Tracer tracer;
public SleuthTraceInfoService(Tracer tracer) {
this.tracer = tracer;
}
/**
* <h2>打印当前的跟踪信息到日志中</h2>
* */
public void logCurrentTraceInfo() {
log.info("Sleuth trace id: [{}]", tracer.currentSpan().context().traceId());
log.info("Sleuth span id: [{}]", tracer.currentSpan().context().spanId());
}
}
三、设置采样率、抽样收集策略
(1)、概要
需要做好权衡
1.收集的跟踪信息越多,越能反映出系统的实际运行情况
2.高并发场景下,大量的请求调用会产生海量的跟踪日志信息,性能开销太大
控制台变化中,有个布尔类型的值,就是是否收集到zipkin的标识
(2)、抽样策略
(1)、ProbabilityBasedSampler
采样率策路
1.默认使用的策略, 以请求百分比的方式配置和收集跟踪信息: 它的默认值为 0.1,代表收集 10% 的请求跟踪信息
2.spring.sleuth.sampler.probability
=0.5
(2)、RateLimitingSampler
抽样策略
1.限速采集,也就是说它可以用来限制每秒追踪请求的最大数量,优先级更高
2.spring.sleuth.sampler.rate
=10
(3)、yml文件配置
spring:
sleuth:
sampler:
# ProbabilityBasedSampler 抽样策略
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
# RateLimitingSampler 抽样策略, 设置了限速采集, spring.sleuth.sampler.probability 属性值无效
rate: 100 # 每秒间隔接受的 trace 量
(4)、代码配置
@Configuration
public class SamplerConfig {
/**
* <h2>限速采集</h2>
* */
@Bean
public Sampler sampler() {
return RateLimitingSampler.create(100);
}
/**
* <h2>概率采集, 默认的采样策略, 默认值是 0.1</h2>
* */
@Bean
public Sampler defaultSampler() {
return ProbabilityBasedSampler.create(0.5f);
}
}
3、集成 SpringCloud Zipkin
一、搭建Zipkin Server
(1)、步骤
SpringCloud Finchley 版本(包含)之后,官官方不建议自己搭建 Zipkin-Server提供了已经打包好的jar
文件(SpringBoot工程),直接下载启动即可
下载地址:curl -sSL https://zipkin.io/quickstart.sh | bash -s
1.选择自己需要的版本,我的是 zipkin-server-2.21.7-exec.jar
2.选择 exec.jar 结尾的 jar
(2)、运行
(一)、简单执行
java -jar zipkin-server-2.21.7-exec.jar
(二)、使用mysql存储执行
MySQL 中添加数据表:
https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql
java -jar zipkin-server-2.21.7-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1
--MYSQL_TCP_PORT=3306 --MYSQL_USER=root --MYSQL_PASS=root --MYSQL_DB=whqn_zipkin
(三)、使用kafka上报跟踪数据执行
步骤一、安装并运行kafka(不详细叙述)
下载 Kafka :https://kafka.apache.org/quickstart
解压、启动 ZK 和 Kafka Server 即可(使用默认配置 )
java -DKAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092 -jar zipkin-server-2.21.7-exec.jar
--STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1
--MYSQL_TCP_PORT=3306 --MYSQL_USER=root --MYSQL_PASS=root --MYSQL_DB=whqn_zipkin
二、SpringCloud Sleuth 整合 Zipkin
(1)、pom文件加入依赖
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
(2)、yml文件加入配置
spring:
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:
retries: 3
consumer:
auto-offset-reset: latest
zipkin:
sender:
type: kafka # 默认是 web
base-url: http://localhost:9411/
(3)、运行调用普通查询服务后的结果
三、微服务通信
1、通信方案
(1)、RPC
核心思想
全局注册表:将 RPC 支持的所有方法都注册进去
通过将 Java 对象进行编码(IDL json,xml 等等)+方法名传递( TCP/IP 协议)到目标服务器实现微服务通信
地址表:Server 的地址一般存储于 Zookeeper上
优点:
- 目前市面上最流行的 RPC框架有:gRPC、Thrift、Dubbo,有较多的选择性。
- 速度快、并发性能高 基于TCP进行通信
缺点:
- 实现复杂(相对 Rest而言),需要做的工作与维护上更多(需要引入和维护 ZK)
(2)、 HTTP
核心思想
标准化的 HTTP 协议(GET、POST、PUT、DELETE 等),目前主流的微服务框架通信实现都是 HTTP
简单、标准,需要做的工作和维护工作少;几乎不需要做额外的工作即可与其他的微服务集成
(3)、 Message
核心思想
通过 Kafka、RocketMO等消息队列实现消息的发布与订阅(消费)
可以实现“削峰填谷”,缓冲机制实现数据、任务处理
缺点:只能够做到最终一致性,而不能做到实时一致性;当然,这也是看业务需求
(4)、 如何选择
- SpringCloud 建议的通信方案是 OpenFeign(Rest )
- 需要最终一致性且不要求快速响应的业务场景可以选择使用Message
- SpringCloud 要有足够强的理由说明你为什么要使用 RPC
2、RestTemplate通信
(1)、 两种实现方式
(一)、在代码(或配置文件中)写死IP 和 端口号
public JwtToken getTokenFromAuthorityService(UsernameAndPassword usernameAndPassword)
{
String requestUrl = "http://127.0.0.1:7000/ecommerce-authority-center" +
"/authority/token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new RestTemplate().postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class //反序列化
);
}
(二)、通过注册中心获取服务地址
可以实现负载均衡的效果
//注入LoadBalancerClient实现负载均衡选择服务实例
private final LoadBalancerClient loadBalancerClient;
public UseRestTemplateService(LoadBalancerClient loadBalancerClient) {
this.loadBalancerClient = loadBalancerClient;
}
public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(
UsernameAndPassword usernameAndPassword
) {
//通过注册中心拿到服务的信息(是所有的实例), 再去发起调用
ServiceInstance serviceInstance = loadBalancerClient.choose(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID //服务名称
);
String requestUrl = String.format(
"http://%s:%s/ecommerce-authority-center/authority/token",
serviceInstance.getHost(),
serviceInstance.getPort()
);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new RestTemplate().postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
3、SpringCloud Netflix Ribbon 通信
提供基于 RestTemplate 的 HTTP 客户端并且支持服务负载均衡功能
(1)、 原理解读
(2)、 手动实现
public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {
String urlFormat = "http://%s/ecommerce-authority-center/authority/token";
// 1. 找到服务提供方的地址和端口号
List<ServiceInstance> targetInstances = discoveryClient.getInstances(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
// 构造 Ribbon 服务列表
List<Server> servers = new ArrayList<>(targetInstances.size());
targetInstances.forEach(i -> {
servers.add(new Server(i.getHost(), i.getPort()));
log.info("found target instance: [{}] -> [{}]", i.getHost(), i.getPort());
});
// 2. 使用负载均衡策略实现远端服务调用
// 构建 Ribbon 负载实例
BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
.buildFixedServerListLoadBalancer(servers);
// 设置负载均衡策略
loadBalancer.setRule(new RetryRule(new RandomRule(), 300));
//命令模式使用负载均衡器构造请求,提交之后,阻塞第一个请求,直到返回结果
String result = LoadBalancerCommand.builder().withLoadBalancer(loadBalancer)
.build().submit(server -> {
//将服务名称替换为IP和端口
String targetUrl = String.format(
urlFormat,
String.format("%s:%s", server.getHost(), server.getPort())
);
log.info("target request url: [{}]", targetUrl);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String tokenStr = new RestTemplate().postForObject(
targetUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
String.class
);
return Observable.just(tokenStr);//异步发射字符串,不阻塞会导致无法返回值
}).toBlocking().first().toString();//阻塞请求,直到完成响应
return JSON.parseObject(result, JwtToken.class);
}
(3)、 实战应用
- pom 文件中引入 Ribbon 依赖
<!-- Ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
- 增强 RestTemplate,添加 @LoadBalanced 注解,使之具备负载均衡的能力
@Component
public class RibbonConfig {
/**
* <h2>注入 RestTemplate</h2>
* */
@Bean
@LoadBalanced //启动时给ribbon扫描到,才能增强,不能New出来
public RestTemplate restTemplate() {
return new RestTemplate();
}
- 编写代码
public JwtToken getTokenFromAuthorityServiceByRibbon(
UsernameAndPassword usernameAndPassword) {
// 注意到 url 中的 ip 和端口换成了服务名称
String requestUrl = String.format(
"http://%s/ecommerce-authority-center/authority/token",
CommonConstant.AUTHORITY_CENTER_SERVICE_ID //服务名称
);
log.info("login request url and body: [{}], [{}]", requestUrl,
JSON.toJSONString(usernameAndPassword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 这里一定要使用自己注入的 RestTemplate
return restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
4、SpringCloud OpenFeign 通信
只需要使用注解和接口的配置即可完成对服务提供方的接口绑定
(1)、 实战应用
- pom 文件中引入 Ribbon 依赖
<!-- open feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 添加 @EnableFeignClients 注解,启用 open-feign
/**
* <h1>与 Authority 服务通信的 Feign Client 接口定义</h1>
* */
@FeignClient(
contextId = "AuthorityFeignClient", value = "e-commerce-authority-center"
)
public interface AuthorityFeignClient {
@RequestMapping(value = "/ecommerce-authority-center/authority/token",
method = RequestMethod.POST,
consumes = "application/json", produces = "application/json")
JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}
(2)、 配置应用
(一)、开启 gzip 压缩,可以压缩请求体以及响应体
feign:
# feign 开启 gzip 压缩
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 1024
response:
enabled: true
(二)、日志、重试、请求连接和响应时间限制
/**
* <h1>OpenFeign 配置类</h1>
* */
@Configuration
public class FeignConfig {
/**
* <h2>开启 OpenFeign 日志</h2>
* */
@Bean
public Logger.Level feignLogger() {
return Logger.Level.FULL; // 需要注意, 日志级别需要修改成 debug
}
/**
* <h2>OpenFeign 开启重试</h2>
* period = 100 发起当前请求的时间间隔,请求失败后多少秒进行重试, 单位是 ms
* maxPeriod = 1000 发起当前请求的最大时间间隔,由于http请求存在延迟,所以应该是个区间, 单位是 ms
* maxAttempts = 5 最多请求次数
* */
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(
100,
SECONDS.toMillis(1),
5
);
}
public static final int CONNECT_TIMEOUT_MILLS = 5000;
public static final int READ_TIMEOUT_MILLS = 5000;
/**
* <h2>对请求的连接和响应时间进行限制</h2>
* */
@Bean
public Request.Options options() {
return new Request.Options(
CONNECT_TIMEOUT_MILLS, TimeUnit.MICROSECONDS,//毫秒
READ_TIMEOUT_MILLS, TimeUnit.MILLISECONDS,
true
);
}
}
(三)、 使用 okhttp 替换 httpclient
将默认的urlconnetion的httpclient替换为okhttp
- pom配置okhttp依赖
<!-- feign 替换 JDK 默认的 URLConnection 为 okhttp -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
- yml配置
feign:
# feign 开启 gzip 压缩
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 1024
response:
enabled: true
# 禁用默认的 http, 启用 okhttp
httpclient:
enabled: false
okhttp:
enabled: true
- 自定义配置
/**
* <h1>OpenFeign 使用 OkHttp 配置类</h1>
* */
@Configuration
@ConditionalOnClass(Feign.class) //有Feign才去配置
@AutoConfigureBefore(FeignAutoConfiguration.class) //需要在自动配置之前完成配置,提前初始化
public class FeignOkHttpConfig {
/**
* <h2>注入 OkHttp, 并自定义配置</h2>
* */
@Bean
public okhttp3.OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS) // 设置连接超时
.readTimeout(5, TimeUnit.SECONDS) // 设置读超时
.writeTimeout(5, TimeUnit.SECONDS) // 设置写超时
.retryOnConnectionFailure(true) // 是否自动重连
// 配置连接池中的最大空闲线程个数为 10, 并保持 5 分钟
// 让效率升高
.connectionPool(new ConnectionPool(
10, 5L, TimeUnit.MINUTES))
.build();
}
}
(3)、 实现原理
1、将接口通过动态代理生成实现类
2、使用Contract将注解解析成feign的规则,原生api不完全兼容springcloud的请求注解
3、在Feign代理对象的方法被调用时,Feign的InvocationHandler会拦截所有方法调用。它会提取方法上的注解(如路径、请求方法、请求参数等),并将这些信息用于构造HTTP请求。
4、将对象包装成消息体
5、拦截器装饰:例如 添加的gzip配置,超时配置等
6、日志记录
7、使用http框架发送请求上面可替换为http,Feign会利用预先配置的HTTP客户端发送请求,返回响应并使用解码器将响应转换为Java对象。
(4)、 手动实现
使用 Feign 的原生 Api, 而不是 OpenFeign ( Feign + Ribbon)
private final DiscoveryClient discoveryClient;
public UseFeignApi(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
public JwtToken thinkingInFeign(UsernameAndPassword usernameAndPassword) {
// 通过反射去拿 serviceId
String serviceId = null;
Annotation[] annotations = AuthorityFeignClient.class.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(FeignClient.class)) {
serviceId = ((FeignClient) annotation).value();
break;
}
}
// 如果服务 id 不存在, 直接抛异常
if (null == serviceId) {
throw new RuntimeException("can not get serviceId");
}
// 通过 serviceId 去拿可用服务实例
List<ServiceInstance> targetInstances = discoveryClient.getInstances(serviceId);
if (CollectionUtils.isEmpty(targetInstances)) {
throw new RuntimeException("can not get target instance from serviceId: " +
serviceId);
}
// 随机选择一个服务实例: 负载均衡
ServiceInstance randomInstance = targetInstances.get(
new Random().nextInt(targetInstances.size())
);
log.info("choose service instance: [{}], [{}], [{}]", serviceId,
randomInstance.getHost(), randomInstance.getPort());
// Feign 客户端初始化, 必须要配置 encoder、decoder、contract
// 由于原生的encoder和decodr不支持对象
//Feign 的路径拼接工作确实是由 Contract 和 InvocationHandler 共同完成的
//Contract 解析
//InvocationHandler 拼接目标路径
AuthorityFeignClient feignClient = Feign.builder() // 1. Feign 默认配置初始化
.encoder(new GsonEncoder()) // 2.1 设置自定义配置
.decoder(new GsonDecoder()) // 2.2 设置自定义配置
.logLevel(Logger.Level.FULL) // 2.3 设置自定义配置
.contract(new SpringMvcContract()) // 配置contract对注解进行解析
.target( // 3 生成代理对象
AuthorityFeignClient.class,
String.format("http://%s:%s",
randomInstance.getHost(), randomInstance.getPort())
);
//代理对象调用时会被InvocationHandler拦截,并发起请求
return feignClient.getTokenByFeign(usernameAndPassword);
}
5、SpringCloud Stream 消息通信
(1)、 概要
如果没有SpringCloud Stream,当我们需要切换消息中间件时,难以变更
核心概念:
- Binder:负责与中间件交互的抽象绑定器,与外部消息系统通信,屏蔽了底层中间件的使用细节
- Input:发送消息的应用通信信道,通过发送信道向Channel发送消息
- Output:接收消息的应用通信信道,通过接收信道向Channel接受消息
- Spring Messaging:统一消息的编程模型,Channel存放消息
- 消息分类映射为通信信道,可以为不同类的消息自定义通信信道
发布订阅模型:与kafka类似
- Topic 可以认为就是 Kafka 中的 Topic 概念
- Producer 通过 Input 信道发布消息到 Topic 上
- Consumer 通过 Output 信道消费 Topic 上的消息
(2)、 实战应用
pom文件添加依赖,用不同的消息中间件需要不同的绑定器进行绑定
<!-- SpringCloud Stream-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<!-- SpringCloud Stream + Kafka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
<!-- SpringCloud Stream + RocketMQ -->
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>-->
<!-- </dependency>-->
配置文件进行配置
spring:
# 消息驱动的配置
stream:
# SpringCloud Stream + Kafka
kafka:
binder:
brokers: 127.0.0.1:9092
auto-create-topics: true # 如果设置为false, 就不会自动创建Topic, 你在使用之前需要手动创建好
# SpringCloud Stream + RocketMQ
# rocketmq:
# binder:
# name-server: 127.0.0.1:9876
bindings:
# 默认发送方
output: # 这里用 Stream 给我们提供的默认 output 信道
destination: ecommerce-stream-client-default # 消息发往的目的地, Kafka 中就是 Topic
content-type: text/plain # 消息发送的格式, 接收端不用指定格式, 但是发送端要
# 默认接收方
input: # 这里用 Stream 给我们提供的默认 input 信道
destination: ecommerce-stream-client-default
创建自定义消息对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WhqnMessage {
private Integer id;
private String projectName;
private String org;
private String author;
/**
* <h2>返回一个默认的消息, 方便使用</h2>
* */
public static WhqnMessage defaultMessage() {
return new WhqnMessage(
1,
"e-commerce-stream-client",
"imooc.com",
"Whqn"
);
}
}
使用默认的通信信道实现消息的发送
绑定默认的通信信道:Source 进行通信
@Slf4j
@EnableBinding(Source.class)
public class DefaultSendService {
private final Source source;
public DefaultSendService(Source source) {
this.source = source;
}
/**
* <h2>使用默认的输出信道发送消息</h2>
* */
public void sendMessage(WhqnMessage message) {
String _message = JSON.toJSONString(message);
log.info("in DefaultSendService send message: [{}]", _message);
// Spring Messaging, 统一消息的编程模型, 是 Stream 组件的重要组成部分之一
source.output().send(MessageBuilder.withPayload(_message).build());
}
}
使用默认的信道实现消息的接收
@Slf4j
@EnableBinding(Sink.class)
public class DefaultReceiveService {
/**
* <h2>使用默认的输入信道接收消息</h2>
* */
@StreamListener(Sink.INPUT)
public void receiveMessage(Object payload) {
log.info("in DefaultReceiveService consume message start");
WhqnMessage whqnMessage = JSON.parseObject(
payload.toString(), WhqnMessage.class
);
// 消费消息
log.info("in DefaultReceiveService consume message success: [{}]",
JSON.toJSONString(whqnMessage));
}
}
(3)、 自定义通信信道
自定义输出信道名称,需要再yml中进行声明
public interface WhqnSource {
String OUTPUT = "whqnOutput";
/** 输出信道的名称是 whqnOutput, 需要使用 Stream 绑定器在 yml 文件中声明 */
@Output(WhqnSource.OUTPUT)
MessageChannel whqnOutput(); //统一消息的编程模型通道进行输出,向MessageChannel发送消息
}
构建消息的发送服务,与默认的通信信道一样
@Slf4j
@EnableBinding(WhqnSource.class)
public class WhqnSendService {
private final WhqnSource whqnSource;
public WhqnSendService(WhqnSource whqnSource) {
this.whqnSource = whqnSource;
}
/**
* <h2>使用自定义的输出信道发送消息</h2>
* */
public void sendMessage(WhqnMessage message) {
String _message = JSON.toJSONString(message);
log.info("in WhqnSendService send message: [{}]", _message);
whqnSource.whqnOutput().send(
MessageBuilder.withPayload(_message).build()
);
}
}
自定义输入信道名称,订阅输出信道,获取channel传来到输出信道的消息
public interface WhqnSink {
String INPUT = "whqnInput";
/** 输入信道的名称是 whqnInput, 需要使用 Stream 绑定器在 yml 文件中配置*/
@Input(WhqnSink.INPUT)
SubscribableChannel whqnInput();
}
使用自定义的输入信道实现消息的接收
@Slf4j
@EnableBinding(WhqnSink.class)
public class WhqnReceiveService {
/** 使用自定义的输入信道接收消息 */
@StreamListener(WhqnSink.INPUT) //spring框架自动扫描注解
public void receiveMessage(@Payload Object payload) {
log.info("in WhqnReceiveService consume message start");
WhqnMessage whqnMessage = JSON.parseObject(payload.toString(), WhqnMessage.class);
log.info("in WhqnReceiveService consume message success: [{}]",
JSON.toJSONString(whqnMessage));
}
}
配置文件进行绑定,上面的通信信道在配置文件中进行绑定,使用上述代码中的接口进行发布与订阅
spring:
# 消息驱动的配置
stream:
bindings:
# 可以像不同类型的topic发
# Whqn 发送方
whqnOutput:
destination: ecommerce-stream-client-whqn
content-type: text/plain
# Whqn 接收方
whqnInput:
destination: ecommerce-stream-client-whqn
第一步:先在接口中定义通信信道名称
第二步:再配置文件中绑定通信信道
第三步:使用通信信道进行发送或接收消息
(4)、 消费分组
1、应用的不同实例放在一个消费者组中,每一条消息只会被一个实例消费
2、消费者组的思想是通过多实例扩展服务吞吐量,且不会造成消息的重复消费
3、不分配则每个消费者各自一个匿名消费组
,会重复处理
直接在配置文件中定义消费组
spring:
# 消息驱动的配置
stream:
bindings:
input:
group: e-commerce-whqn-default #不分配则每个消费者各自一个匿名消费组,会重复处理
(5)、 消费分区
为了确保具有共同特征标识的数据由同一个消费者实例进行处理
引入配置
spring:
# 消息驱动的配置
stream:
instanceCount: 1 # 消费者的总数
instanceIndex: 0 # 当前消费者的索引,从第一个索引开始分配
bindings:
# 默认发送方
output:
# 消息分区
producer:
partitionCount: 1 # 分区大小
- 第一步:配置消费者实例个数与分区个数,形成一对一的对应关系
例如:
假设你有 3 个分区和 3 个消费者实例,instanceIndex 将决定每个消费者实例被分配到哪个分区
instanceIndex = 0 会被分配到分区 0,instanceIndex = 1 会被分配到分区 1,instanceIndex = 2 会被分配到分区 2。
- 第二步:接收方配置分区支持
spring:
# 消息驱动的配置
stream:
bindings:
# 默认接收方
input: # 这里用 Stream 给我们提供的默认 input 信道
# 消费者开启分区支持
consumer:
partitioned: true
- 第三步:配置分区关键字(默认配置)
spring:
# 消息驱动的配置
stream:
bindings:
# 默认发送方
output:
# 消息分区
producer:
# 分区关键字, payload 指的是发送的对象, author 是对象中的属性
partitionKeyExpression: payload.author
- 第四步:配置分区关键字(自定义配置)
spring:
# 消息驱动的配置
stream:
bindings:
# 默认发送方
output:
# 消息分区
producer:
# 使用自定义的分区策略, 注释掉 partitionKeyExpression,需用注解配置
partitionKeyExtractorName: whqnPartitionKeyExtractorStrategy
partitionSelectorName: whqnPartitionSelectorStrategy
自定义从 Message 中提取 partition key 的策略
@Slf4j
@Component
public class WhqnPartitionKeyExtractorStrategy implements PartitionKeyExtractorStrategy {
@Override //接收message的消息,以此进行发送过滤分区
public Object extractKey(Message<?> message) {
WhqnMessage whqnMessage = JSON.parseObject(
message.getPayload().toString(), WhqnMessage.class
);
// 自定义提取 key
String key = whqnMessage.getProjectName();
log.info("SpringCloud Stream Whqn Partition Key: [{}]", key);
return key;
}
}
决定 message 发送到哪个分区的策略
@Slf4j
@Component
public class WhqnPartitionSelectorStrategy implements PartitionSelectorStrategy {
/**
* <h2>选择分区的策略</h2>
* */
@Override
public int selectPartition(Object key, int partitionCount) {
//自定义配置策略
int partition = key.toString().hashCode() % partitionCount;
log.info("SpringCloud Stream Whqn Selector info: [{}], [{}], [{}]",
key.toString(), partitionCount, partition);
return partition;
}
}
四、Hystrix
1、概览
概要与设计目标
Hystrix 是一个库,通过添加延迟容忍
和容错逻辑
,帮助你控制这些分布式服务之间的交互
Hystrix 通过隔离
服务之间的访问点、停止级联失败
和提供回退选项来实现服务之间的容错
-
1.对客户端访问的
延迟和故障
进行保护和控制 -
2.
隔离
在复杂的分布式系统中阻止级联故障
-
3.快速失败,快速恢复
-
4.
兜底回退
,尽可能优雅的降级
解决的问题
服务之间存在许多依赖项,依赖项可能会存在故障,如果不做故障隔离整个服务可能被拖垮
如何解决
- 对依赖项(服务)进行
包装代理
,不直接与依赖项交互 - 调用超时时间允许自行设定,
超时
之后立刻熔断报错
- 每一个依赖项都在自己的空间内(
线程池或信号量隔离
),依赖项之间不存在干扰 - 请求依赖项失败后,可以选择
出错
或者是兜底回退
工作流程
-
构造一个
HystrixCommand
或者HystrixObservableCommand
对象 -
执行命令获取响应
execute():阻塞,直到收到响应或者抛出异常
queue():返回一个 Future
observe():订阅代表响应的 Observable
toObservable():返回一个 Observable,当你订阅它以后,将会执行Hystrix 命令并且推送它的响应 -
响应是否已经被缓存,已缓存则立即返回
-
断路器是否打开,如果打开,则执行回退逻辑
-
线程池、信号量是否已经使用率 100%,是的话直接执行回退逻辑
-
HystrixObservableCommand.construct() or HystrixCommand.run()
-
计算电路健康,统计数据决定是否跳闸
-
回退或返回成功响应
2、三种模式
(1)、断路器模式
设置超时或者失败等熔断策略
(2)、后备策略模式
断路器模式触发之后,如果存在后备策略,则执行后备
(3)、船壁模式
通过线程池管理调用外部资源的,默认情况下所有服务调用都
公用
一个线程池
从公用一个线程池,变为每个远程服务使用自己的私有
线程池
3、实现容错降级
(1)、注解形式
pom文件
<!-- 集成 hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
启动类加入注解
@EnableCircuitBreaker // 启动 Hystrix
使用命令模式包装容错降级方法
@Slf4j
@Service
public class UseHystrixCommandAnnotation {
private final NacosClientService nacosClientService;
public UseHystrixCommandAnnotation(NacosClientService nacosClientService) {
this.nacosClientService = nacosClientService;
}
@HystrixCommand(
// 用于对 Hystrix 命令进行分组, 分组之后便于统计展示于仪表盘、上传报告和预警等等
// 内部进行度量统计时候的分组标识, 数据上报和统计的最小维度就是 groupKey
groupKey = "NacosClientService",
// HystrixCommand 的名字, 默认是当前类的名字, 主要方便 Hystrix 进行监控、报警等
commandKey = "NacosClientService",
// 舱壁模式
threadPoolKey = "NacosClientService",
// 后备模式
fallbackMethod = "getNacosClientInfoFallback",
// 断路器模式
commandProperties = {
// 超时时间, 单位毫秒, 超时进 fallback
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500"),
// 判断熔断的最少请求数, 默认是10; 只有在一定时间内请求数量达到该值, 才会进行成功率的计算
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
// 熔断的阈值默认值 50, 表示在一定时间内有50%的请求处理失败, 会触发熔断
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),
},
// 舱壁模式
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "101"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
// 在时间窗口中, 收集统计信息的次数; 在 1440ms 的窗口中一共统计 12 次
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
// 时间窗口, 从监听到第一次失败开始计时
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
}
)
public List<ServiceInstance> getNacosClientInfo(String serviceId) {
log.info("use hystrix command annotation to get nacos client info: [{}], [{}]",
serviceId, Thread.currentThread().getName());
return nacosClientService.getNacosClientInfo(serviceId);
}
/**
* <h2>getNacosClientInfo 的兜底策略 - Hystrix 后备模式</h2>
* */
public List<ServiceInstance> getNacosClientInfoFallback(String serviceId) {
log.warn("can not get nacos client, trigger hystrix fallback: [{}], [{}]",
serviceId, Thread.currentThread().getName());
return Collections.emptyList();
}
}
被包装的方法
public List<ServiceInstance> getNacosClientInfo(String serviceId) {
// 测试 UseHystrixCommandAnnotation 的超时
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
//
}
log.info("request nacos client to get service instance info: [{}]", serviceId);
return discoveryClient.getInstances(serviceId);
}
调用方式
@GetMapping("/hystrix-command-annotation")
public List<ServiceInstance> getNacosClientInfoUseAnnotation(
@RequestParam String serviceId) {
log.info("request nacos client info use annotation: [{}], [{}]",
serviceId, Thread.currentThread().getName());
return hystrixCommandAnnotation.getNacosClientInfo(serviceId);
}
运行结果
controller中的方法、hystrix注释的方法、后备模式的方法使用的不同的线程池
(2)、编码形式(默认使用线程池)
创建继承
HystrixCommand
的类 给 NacosClientService 实现包装
线程隔离:Hystrix 可以将 run() 方法的执行放在一个单独的线程中,避免耗时操作阻塞主线程。每个命令的执行会在独立的线程池中执行,最大程度避免高并发情况下的阻塞和影响。
信号量隔离:如果使用信号量隔离模式,run() 方法会被执行在调用线程中,通过信号量控制并发请求的数量,避免系统过载。
//泛型指定为被包装的服务的返回值类型
public class NacosClientHystrixCommand extends HystrixCommand<List<ServiceInstance>> {
/** 需要保护的服务 */
private final NacosClientService nacosClientService;
/** 方法需要传递的参数 */
private final String serviceId;
public NacosClientHystrixCommand(NacosClientService nacosClientService, String serviceId) {
super(
Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("NacosClientService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("NacosClientHystrixCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("NacosClientPool"))
// 线程池 key 配置
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(THREAD) // 线程池隔离策略
.withFallbackEnabled(true) // 开启降级
.withCircuitBreakerEnabled(true) // 开启熔断器
)
);
// 可以配置信号量隔离策略
// Setter semaphore =
// Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("NacosClientService"))
// .andCommandKey(HystrixCommandKey.Factory.asKey("NacosClientHystrixCommand"))
// .andCommandPropertiesDefaults(
// HystrixCommandProperties.Setter()
// .withCircuitBreakerRequestVolumeThreshold(10) //设置当断路器触发时,至少需要的请求数量(请求量阈值)
// .withCircuitBreakerSleepWindowInMilliseconds(5000) //断路器进入休眠状态后的时间
// .withCircuitBreakerErrorThresholdPercentage(50) //如果 10 个请求中有 5 个失败,断路器就会打开。
// .withExecutionIsolationStrategy(SEMAPHORE) // 指定使用信号量隔离
// //.....
// );
this.nacosClientService = nacosClientService;
this.serviceId = serviceId;
}
/**
* <h2>要保护的方法调用写在 run 方法中</h2>
* */
@Override
protected List<ServiceInstance> run() throws Exception {
log.info("NacosClientService In Hystrix Command to Get Service Instance: [{}], [{}]",
this.serviceId, Thread.currentThread().getName());
return this.nacosClientService.getNacosClientInfo(this.serviceId);
}
/**
* <h2>降级处理策略</h2>
* */
@Override
protected List<ServiceInstance> getFallback() {
log.warn("NacosClientService run error: [{}], [{}]",
this.serviceId, Thread.currentThread().getName());
return Collections.emptyList();
}
}
首先注入Service
private final NacosClientService nacosClientService;
public HystrixController(NacosClientService nacosClientServicecacheHystrixCommandAnnotation) {
this.nacosClientService = nacosClientService;
}
调用方式一
List<ServiceInstance> serviceInstances01 = new NacosClientHystrixCommand(
nacosClientService, serviceId
).execute(); // 同步阻塞 相当于方式二的queue+future
调用方式二
List<ServiceInstance> serviceInstances02;
Future<List<ServiceInstance>> future = new NacosClientHystrixCommand(
nacosClientService, serviceId
).queue(); // 异步非阻塞,获取将来执行的结果,这里发起调用
// 这里可以做一些别的事, 需要的时候再去拿结果
serviceInstances02 = future.get();
调用方式三
Observable<List<ServiceInstance>> observable = new NacosClientHystrixCommand(
nacosClientService, serviceId
).observe(); // 热响应调用,创建线程
//直到执行toBlocking()阻塞才发起调用
List<ServiceInstance> serviceInstances03 = observable.toBlocking().single();
调用方式四
Observable<List<ServiceInstance>> toObservable = new NacosClientHystrixCommand(
nacosClientService, serviceId
).toObservable(); // 异步冷响应调用,不创建线程
//创建线程,直到执行toBlocking()阻塞才发起调用
List<ServiceInstance> serviceInstances04 = toObservable.toBlocking().single();
测试熔断的方法
public List<ServiceInstance> getNacosClientInfo(String serviceId) {
// 测试 NacosClientHystrixCommand 熔断
throw new RuntimeException("has some error");
log.info("request nacos client to get service instance info: [{}]", serviceId);
return discoveryClient.getInstances(serviceId);
}
运行结果,以下的日志结果,在上述代码中省略,需要自己编写
(3)、编码形式(默认使用信号量隔离)
创建继承
HystrixObservableCommand
的类 给 NacosClientService 实现包装
@Slf4j
public class NacosClientHystrixObservableCommand
extends HystrixObservableCommand<List<ServiceInstance>> {
/** 要保护的服务 */
private final NacosClientService nacosClientService;
/** 方法需要传递的参数,信号量隔离可以使用多个参数 */
private final List<String> serviceIds;
//类似线程池配置
public NacosClientHystrixObservableCommand(NacosClientService nacosClientService,
List<String> serviceIds) {
super(
HystrixObservableCommand.Setter
.withGroupKey(HystrixCommandGroupKey
.Factory.asKey("NacosClientService"))
.andCommandKey(HystrixCommandKey
.Factory.asKey("NacosClientHystrixObservableCommand"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withFallbackEnabled(true) // 开启降级
.withCircuitBreakerEnabled(true) // 开启熔断器
)
);
this.nacosClientService = nacosClientService;
this.serviceIds = serviceIds;
}
/**
* <h2>要保护的方法调用写在这里,与线程池不同</h2>
* */
@Override
protected Observable<List<ServiceInstance>> construct() {
return Observable.create(new Observable.OnSubscribe<List<ServiceInstance>>() {
// Observable 有三个关键的事件方法, 分别是 onNext、onCompleted、onError
@Override
public void call(Subscriber<? super List<ServiceInstance>> subscriber) {
try {
//判断是否订阅,已订阅则可进行下一步发送
if (!subscriber.isUnsubscribed()) {
log.info("subscriber command task: [{}], [{}]",
JSON.toJSONString(serviceIds),
Thread.currentThread().getName());
serviceIds.forEach(
//onNext执行并将结果发送到订阅者
s -> subscriber
.onNext(nacosClientService.getNacosClientInfo(s))
);
subscriber.onCompleted();
log.info("command task completed: [{}], [{}]",
JSON.toJSONString(serviceIds),
Thread.currentThread().getName());
}
} catch (Exception ex) {
//若无后备策略,则通过onError返回一个异常信息
subscriber.onError(ex);
}
}
});
}
/**
* <h2>服务降级</h2>
* */
@Override
protected Observable<List<ServiceInstance>> resumeWithFallback() {
return Observable.create(new Observable.OnSubscribe<List<ServiceInstance>>() {
@Override
public void call(Subscriber<? super List<ServiceInstance>> subscriber) {
try {
if (!subscriber.isUnsubscribed()) {
log.info("(fallback) subscriber command task: [{}], [{}]",
JSON.toJSONString(serviceIds),
Thread.currentThread().getName());
//后备策略是返回一个空数组
subscriber.onNext(Collections.emptyList());
subscriber.onCompleted();
log.info("(fallback) command task completed: [{}], [{}]",
JSON.toJSONString(serviceIds),
Thread.currentThread().getName());
}
} catch (Exception ex) {
subscriber.onError(ex);
}
}
});
}
}
调用方式
List<String> serviceIds = Arrays.asList(serviceId, serviceId, serviceId);
List<List<ServiceInstance>> result = new ArrayList<>(serviceIds.size());
NacosClientHystrixObservableCommand observableCommand =
new NacosClientHystrixObservableCommand(nacosClientService, serviceIds);
// 异步执行命令
//在发出一次,方法有判断是否订阅,所以不执行
Observable<List<ServiceInstance>> observe = observableCommand.observe();
// 从发送者获取结果
observe.subscribe(
new Observer<List<ServiceInstance>>() {
// 执行 onNext 之后再去执行 onCompleted
@Override
public void onCompleted() {
log.info("all tasks is complete: [{}], [{}]",
serviceId, Thread.currentThread().getName());
}
//处理异常情况
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
//每次收到的服务实例列表,上面每次执行onNext就能获取一次结果
@Override
public void onNext(List<ServiceInstance> instances) {
result.add(instances);
}
}
);
log.info("observable command result is : [{}], [{}]",
JSON.toJSONString(result), Thread.currentThread().getName());
观察者订阅者编程方式实例
Observable<Integer> observable = Observable.create(emitter -> {
emitter.onNext(1); // 推送数据
emitter.onNext(2); // 推送数据
emitter.onComplete(); // 发出完成事件
});
observable.subscribe(
data -> System.out.println("接收到数据: " + data), // 数据接收者(Subscriber)
throwable -> System.out.println("发生错误: " + throwable), // 错误处理
() -> System.out.println("完成!") // 完成处理
);
//一旦订阅,observable 会依次发送 1 和 2,并最终触发完成事件
运行结果:用的同一个线程池,效率较高
3、请求缓存
(1)、概要
Hystrix 的结果缓存指的是在一次 Hystrix 的请求上下文
中
请求的是同一个方法
,使用同一个参数
,则会缓存结果
(2)、初始化过滤器
过滤器,初始化 Hystrix 请求上下文环境,由于工程引入了Sleuth,这会导致缓存功能实现,需要重新对hystrix进行配置
@Slf4j
@Component
@WebFilter(
filterName = "HystrixRequestContextServletFilter",
urlPatterns = "/*",
asyncSupported = true
)
public class HystrixRequestContextServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 初始化 Hystrix 请求上下文
// 在不同的 context 中缓存是不共享的
// 这个初始化是必须的
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
// 配置
hystrixConcurrencyStrategyConfig();
// 请求正常通过
chain.doFilter(request, response);
} finally {
// 关闭 Hystrix 请求上下文
context.shutdown();
}
}
/**
* <h2>配置 Hystrix 的并发策略</h2>
* */
public void hystrixConcurrencyStrategyConfig() {
try {
HystrixConcurrencyStrategy target =
HystrixConcurrencyStrategyDefault.getInstance();
HystrixConcurrencyStrategy strategy =
HystrixPlugins.getInstance().getConcurrencyStrategy();
//判断当前配置的类型是否是Sleuth的并发策略配置
if (strategy instanceof HystrixConcurrencyStrategyDefault) {
// 如果已经就是我们想要配置的
return;
}
// 将原来其他的配置保存下来
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier =
HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher =
HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
// 先重置, 再把我们自定义的配置与原来的配置写回去
HystrixPlugins.reset();
//配置自定义的配置
HystrixPlugins.getInstance().registerConcurrencyStrategy(target);
//原来的配置写回去
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
//重新注册
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
log.info("config hystrix concurrency strategy success");
} catch (Exception ex) {
log.error("Failed to register Hystrix Concurrency Strategy: [{}]",
ex.getMessage(), ex);
}
}
}
(3)、编码形式实现
创建继承
HystrixCommand
的类 给 NacosClientService 实现包装与上面不同的是需要继承响应的缓存方法
注:用编程方式,就不用注解方式,需要新建对象
@Slf4j
public class CacheHystrixCommand extends HystrixCommand<List<ServiceInstance>> {
/** 需要保护的服务 */
private final NacosClientService nacosClientService;
/** 方法需要传递的参数 */
private final String serviceId;
//由于需要清除功能,所以需要在外部指定
private static final HystrixCommandKey CACHED_KEY =
HystrixCommandKey.Factory.asKey("CacheHystrixCommand");
public CacheHystrixCommand(NacosClientService nacosClientService, String serviceId) {
super(
HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey
.Factory.asKey("CacheHystrixCommandGroup"))
//注意:这是包装类的id,不是缓存的key
.andCommandKey(CACHED_KEY)
);
this.nacosClientService = nacosClientService;
this.serviceId = serviceId;
}
@Override
protected List<ServiceInstance> run() throws Exception {
log.info("CacheHystrixCommand In Hystrix Command to get service instance:" +
" [{}], [{}]", this.serviceId, Thread.currentThread().getName());
return this.nacosClientService.getNacosClientInfo(this.serviceId);
}
//继承这个方法即可实现缓存功能
@Override
protected String getCacheKey() {
return serviceId;
}
@Override
protected List<ServiceInstance> getFallback() {
return Collections.emptyList();
}
/**
* <h2>根据缓存 key 清理在一次 Hystrix 请求上下文中的缓存</h2>
* */
public static void flushRequestCache(String serviceId) {
//赋值key和配置(使用的是过滤器的配置)
HystrixRequestCache.getInstance(
CACHED_KEY,
HystrixConcurrencyStrategyDefault.getInstance()
).clear(serviceId);//调用clear清理缓存
log.info("flush request cache in hystrix command: [{}], [{}]",
serviceId, Thread.currentThread().getName());
}
}
调用方式
// 使用缓存 Command, 发起两次请求
CacheHystrixCommand command1 = new CacheHystrixCommand(
nacosClientService, serviceId
);
CacheHystrixCommand command2 = new CacheHystrixCommand(
nacosClientService, serviceId
);
List<ServiceInstance> result01 = command1.execute();
List<ServiceInstance> result02 = command2.execute();
log.info("result01, result02: [{}], [{}]",
JSON.toJSONString(result01), JSON.toJSONString(result02));
// 清除缓存
CacheHystrixCommand.flushRequestCache(serviceId);
// 使用缓存 Command, 发起两次请求
CacheHystrixCommand command3 = new CacheHystrixCommand(
nacosClientService, serviceId
);
CacheHystrixCommand command4 = new CacheHystrixCommand(
nacosClientService, serviceId
);
List<ServiceInstance> result03 = command3.execute();
List<ServiceInstance> result04 = command4.execute();
log.info("result03, result04: [{}], [{}]",
JSON.toJSONString(result03), JSON.toJSONString(result04));
运行结果:
command1 command3 调用服务;
command2 command4 调用缓存;
(4)、注解形式实现
注解 | 描述 | 属性 |
---|---|---|
@CacheResult | 该注解用来标记请求命令返回的结果应该被缓存,它必须与@HystrixCommand注解结合使用 | cacheKeyMethod |
@CacheRemove | 该注解用来让请求命令的缓存失效,失效的缓存根据commandKey进行查找。 | commandKey,cacheKeyMethod |
@CacheKey | 该注解用来在请求命令的参数上标记,使其作为cacheKey,如果没有使用此注解则会使用所有参数列表中的参数作为 | value |
方式一
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(commandKey = "CacheHystrixCommandAnnotation")
public List<ServiceInstance> useCacheByAnnotation01(String serviceId) {
log.info("use cache01 to get nacos client info: [{}]", serviceId);
return nacosClientService.getNacosClientInfo(serviceId);
}
@CacheRemove(commandKey = "CacheHystrixCommandAnnotation",
cacheKeyMethod = "getCacheKey")
@HystrixCommand
public void flushCacheByAnnotation01(String cacheId) {
log.info("flush hystrix cache key: [{}]", cacheId);
}
public String getCacheKey(String cacheId) {
return cacheId;
}
方式二
@CacheResult
@HystrixCommand(commandKey = "CacheHystrixCommandAnnotation")
public List<ServiceInstance> useCacheByAnnotation02(@CacheKey String serviceId) {
log.info("use cache02 to get nacos client info: [{}]", serviceId);
return nacosClientService.getNacosClientInfo(serviceId);
}
@CacheRemove(commandKey = "CacheHystrixCommandAnnotation")
@HystrixCommand
public void flushCacheByAnnotation02(@CacheKey String cacheId) {
log.info("flush hystrix cache key: [{}]", cacheId);
}
方式三
@CacheResult
@HystrixCommand(commandKey = "CacheHystrixCommandAnnotation")
public List<ServiceInstance> useCacheByAnnotation03(String serviceId) {
log.info("use cache03 to get nacos client info: [{}]", serviceId);
return nacosClientService.getNacosClientInfo(serviceId);
}
@CacheRemove(commandKey = "CacheHystrixCommandAnnotation")
@HystrixCommand
public void flushCacheByAnnotation03(String cacheId) {
log.info("flush hystrix cache key: [{}]", cacheId);
}
调用测试
List<ServiceInstance> result01 =
cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);
List<ServiceInstance> result02 =
cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);
// 清除掉缓存
cacheHystrixCommandAnnotation.flushCacheByAnnotation01(serviceId);
List<ServiceInstance> result03 =
cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);
// 这里有第四次调用,重新调用服务
return cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);
4、请求合并
(1)、概要
默认情况下,每一个请求都会占用一个线程和一次网络请求,高并发场景下效率不高
使用 Hystrix 的请求合并,将多个请求 merge 为一个,提高服务的并发能力
(2)、编码形式实现
编写需要调用的合并方法
public List<List<ServiceInstance>> getNacosClientInfos(List<String> serviceIds) {
log.info("request nacos client to get service instance infos: [{}]",
JSON.toJSONString(serviceIds));
List<List<ServiceInstance>> result = new ArrayList<>(serviceIds.size());
serviceIds.forEach(s -> result.add(discoveryClient.getInstances(s)));
return result;
}
创建继承
HystrixCommand
的类 给上述方法实现包装
@Slf4j
public class NacosClientBatchCommand extends HystrixCommand<List<List<ServiceInstance>>> {
private final NacosClientService nacosClientService;
private final List<String> serviceIds;
protected NacosClientBatchCommand(
NacosClientService nacosClientService, List<String> serviceIds
) {
super(
HystrixCommand.Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("NacosClientBatchCommand")
)
);
this.nacosClientService = nacosClientService;
this.serviceIds = serviceIds;
}
@Override
protected List<List<ServiceInstance>> run() throws Exception {
log.info("use nacos client batch command to get result: [{}]",
JSON.toJSONString(serviceIds));
return nacosClientService.getNacosClientInfos(serviceIds);
}
@Override
protected List<List<ServiceInstance>> getFallback() {
log.warn("nacos client batch command failure, use fallback");
return Collections.emptyList();
}
}
创建请求合并器
@Slf4j
//第一个参数是 批量批量方法的请求结果,第二个参数是单个请求的响应结果,第三个是参数
public class NacosClientCollapseCommand
extends HystrixCollapser<List<List<ServiceInstance>>, List<ServiceInstance>, String> {
private final NacosClientService nacosClientService;
private final String serviceId;
public NacosClientCollapseCommand(NacosClientService nacosClientService, String serviceId) {
super(
HystrixCollapser.Setter.withCollapserKey(
HystrixCollapserKey.Factory.asKey("NacosClientCollapseCommand")
).andCollapserPropertiesDefaults(
//300毫秒内的请求进行合并
HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(300)
)
);
this.nacosClientService = nacosClientService;
this.serviceId = serviceId;
}
/**
* <h2>获取请求中的参数</h2>
* */
@Override
public String getRequestArgument() {
return this.serviceId;
}
/**
* <h2>创建批量请求 Hystrix Command 也就是调用上述NacosClientBatchCommand的命令</h2>
* */
@Override
protected HystrixCommand<List<List<ServiceInstance>>> createCommand(
Collection<CollapsedRequest<List<ServiceInstance>, String>> collapsedRequests) {
List<String> serviceIds = new ArrayList<>(collapsedRequests.size());
serviceIds.addAll(
collapsedRequests.stream()
.map(CollapsedRequest::getArgument)
.collect(Collectors.toList())
);
return new NacosClientBatchCommand(nacosClientService, serviceIds);
}
/**
* <h2>响应分发给单独的请求</h2>
* */
@Override
protected void mapResponseToRequests(List<List<ServiceInstance>> batchResponse,
Collection<CollapsedRequest<List<ServiceInstance>,
String>> collapsedRequests) {
int count = 0;
for (CollapsedRequest<List<ServiceInstance>, String> collapsedRequest : collapsedRequests) {
// 从批量响应集合中按顺序取出结果
List<ServiceInstance> instances = batchResponse.get(count++);
// 将结果返回原 Response 中,插入到单个响应结果中
collapsedRequest.setResponse(instances);
}
}
}
调用方式
// 前三个请求会被合并
NacosClientCollapseCommand collapseCommand01 = new NacosClientCollapseCommand(
nacosClientService, "e-commerce-nacos-client1");
NacosClientCollapseCommand collapseCommand02 = new NacosClientCollapseCommand(
nacosClientService, "e-commerce-nacos-client2");
NacosClientCollapseCommand collapseCommand03 = new NacosClientCollapseCommand(
nacosClientService, "e-commerce-nacos-client3");
Future<List<ServiceInstance>> future01 = collapseCommand01.queue();
Future<List<ServiceInstance>> future02 = collapseCommand02.queue();
Future<List<ServiceInstance>> future03 = collapseCommand03.queue();
future01.get();
future02.get();
future03.get();
Thread.sleep(2000);
// 过了合并的时间窗口, 第四个请求单独发起
NacosClientCollapseCommand collapseCommand04 = new NacosClientCollapseCommand(
nacosClientService, "e-commerce-nacos-client4");
Future<List<ServiceInstance>> future04 = collapseCommand04.queue();
future04.get();
运行结果
(3)、注解形式实现
// 使用注解实现 Hystrix 请求合并
// 抛出异常说明有问题 例如找不到合并方法
// Hystrix 会通过指定的 batchMethod(在这里是 findNacosClientInfos)来异步处理这些请求,返回Future异步处理
@HystrixCollapser(
batchMethod = "findNacosClientInfos",
scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, //对所有的请求拦截
collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "300")
}
)
public Future<List<ServiceInstance>> findNacosClientInfo(String serviceId) {
// 系统运行正常, 不会走这个方法
throw new RuntimeException("This method body should not be executed!");
}
@HystrixCommand
public List<List<ServiceInstance>> findNacosClientInfos(List<String> serviceIds) {
log.info("coming in find nacos client infos: [{}]", JSON.toJSONString(serviceIds));
return getNacosClientInfos(serviceIds);
}
调用方式
//直接返回future,上面需要使用queue方法返回future,证明queue方法封装在HystrixCollapser里面
Future<List<ServiceInstance>> future01 = nacosClientService.findNacosClientInfo(
"e-commerce-nacos-client1"
);
Future<List<ServiceInstance>> future02 = nacosClientService.findNacosClientInfo(
"e-commerce-nacos-client2"
);
Future<List<ServiceInstance>> future03 = nacosClientService.findNacosClientInfo(
"e-commerce-nacos-client3"
);
future01.get();
future02.get();
future03.get();
Thread.sleep(2000);
Future<List<ServiceInstance>> future04 = nacosClientService.findNacosClientInfo(
"e-commerce-nacos-client4"
);
future04.get();
运行效果与编码形式一致
5、openfeign集成hystrix
(1)、在配置文件中开启熔断功能
# Feign 的相关配置
feign:
# OpenFeign 集成 Hystrix
hystrix:
enabled: true
(2)、使用FeignClient 注解的 fallback属性
创建实现类,相当于commad(上述编码形式、注释形式)中的默认实现
@Slf4j
@Component
public class AuthorityFeignClientFallback implements AuthorityFeignClient {
@Override
public JwtToken getTokenByFeign(UsernameAndPassword usernameAndPassword) {
log.info("authority feign client get token by feign request error " +
"(Hystrix Fallback): [{}]", JSON.toJSONString(usernameAndPassword));
return new JwtToken("whqn");
}
}
feign方法中备注
@FeignClient(
contextId = "AuthorityFeignClient", value = "e-commerce-authority-center",
fallback = AuthorityFeignClientFallback.class
)
public interface AuthorityFeignClient {
@RequestMapping(value = "/ecommerce-authority-center/authority/token",
method = RequestMethod.POST,
consumes = "application/json", produces = "application/json")
JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}
运行结果与普通实现容错降级一致,但是无法报错异常错误,为此,需要使用fallbackFactory 属性
(3)、使用FeignClient 注解的 fallbackFactory属性
创建实现类,其中的泛型是需要包装的feign client
@Slf4j
@Component
public class AuthorityFeignClientFallbackFactory
implements FallbackFactory<AuthorityFeignClient> {
@Override
public AuthorityFeignClient create(Throwable throwable) {
log.warn("authority feign client get token by feign request error " +
"(Hystrix FallbackFactory): [{}]", throwable.getMessage(), throwable);
//需要返回一个类,但是feign是个接口,所以需要返回一个匿名内部类,实现默认方法
return new AuthorityFeignClient() {
@Override
public JwtToken getTokenByFeign(UsernameAndPassword usernameAndPassword) {
return new JwtToken("whqn-factory");
}
};
}
}
feign方法中备注
@FeignClient(
contextId = "AuthorityFeignClient", value = "e-commerce-authority-center",
fallbackFactory = AuthorityFeignClientFallbackFactory.class
)
public interface AuthorityFeignClient {
@RequestMapping(value = "/ecommerce-authority-center/authority/token",
method = RequestMethod.POST,
consumes = "application/json", produces = "application/json")
JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}
运行结果:有报错显示 如下
5、hystrix监控面板
(1)、搭建监控面板
为什么可以监控hystrix的信息
由于每个工程下都配有端点actuator,里面包含了hystrix的端点,实际上就是对这个端点的监控
是一个单独的应用,实时的对应用进行监测
创建工程,pom文件
server:
port: 9999
servlet:
context-path: /ecommerce-hystrix-dashboard
spring:
application:
name: e-commerce-hystrix-dashboard
cloud:
nacos:
# 服务注册发现
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
server-addr: 127.0.0.1:8848
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
hystrix:
dashboard:
proxy-stream-allow-list: "127.0.0.1" #对这个地址进行监控
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
创建启动类
/**
* <h1>hystrix dashboard 入口</h1>
* 127.0.0.1:9999/ecommerce-hystrix-dashboard/hystrix/
* */
@EnableDiscoveryClient
@SpringBootApplication
@EnableHystrixDashboard // 开启 Hystrix Dashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
运行结果
(2)、使用监控面板
需要监控的地址这里为
*****/actuator/hystrix.stream
使用http://127.0.0.1:8000/ecommerce-nacos-client/actuator/hystrix.stream测试
short-circuited 熔断的数量
线程池的情况