Eureka
核心流程:
- Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息
- Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务
- Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常
- 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例
- 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端
- 当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式
- Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地
- 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存
- Eureka Client 获取到目标服务器信息,发起服务调用
- Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除
Nacos
springboot获取配置
通过 bootstrap.yml 配置 基础信息,如:
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP
namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
# nacos-config-client-test.yaml ----> config.info
将通过 ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
名称寻找nacos远程配置
本地与Nacos配置优先级
当本地配置与Nacos远程配置有冲突配置时,会以Nacos配置为准
以本地配置为主 :
以下配置需要放到远程配置才生效,在本地配置不生效:
spring:
cloud:
config:
#是否允许本地配置覆盖远程配置
allow-override: true
#是否一切以本地配置为准,默认false
override-none: false
#系统环境变量或系统属性才能覆盖远程配置文件的配置
#本地配置文件中配置优先级低于远程配置,默认true
override-system-properties: true
属性动态刷新和回滚
在对应的取值类上加入@RefreshScope
整合ZooKeeper
集群
在nacos中,可以将这些实例指定到不同的集群中,比如可以通过:
spring.cloud.nacos.discovery.cluster-name=beijing
在消费端配置指定集群名称,那么此时服务消费者就只会调用到指定集群中的实例。
如果消费端没有配置cluster-name,那么则会使用所有集群。
Ribbon
Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
引入spring-cloud-starter-ribbon
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用
RestTemplate
使用入门
配置@LoadBalanced
通过RestTemplate+@LoadBalanced来完成负载均衡调用提供者。
RestTemplate想要通过服务名称来调用,那么一定要配置@LoadBalanced注解,不然会报错的,只有配置了这个注解,RestTemplate才会和Ribbon相结合。
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
服务调用者方 :
@RestController
@Slf4j
public class OrderController
{
//public static final String PAYMENT_URL = "http://localhost:8001";
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment)
{
return restTemplate.postForObject(PAYMENT_URL +"/payment/create",payment,CommonResult.class);
}
}
服务名称就是在提供者的application当中配置的
配置@RibbonClient
新建MySelfRule规则类:
@Configuration
public class MySelfRule
{
@Bean
public IRule myRule()
{
return new RandomRule();//定义为随机
}
}
主启动类添加 RibbonClient 注解,指定对应要调用的服务名称,并指定对应规则类
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了(也就是一旦被扫描到,RestTemplate直接不管调用哪个服务都会用指定的算法)。若用@ComponentScan(或@SpringBootApplication),应该采取措施来避免它被包含到扫描的范围中。
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)
public class OrderMain80
{
public static void main(String[] args)
{
SpringApplication.run(OrderMain80.class,args);
}
}
上例当RestTemplate调用服务名称为CLOUD-PAYMENT-SERVICE的时候,采用MySelfRule当中注入的负载均衡算法。
Controller调用 与 LoadBalancer 示例一致
@RibbonClients不同于@RibbonClient,它 可以为所有的Ribbon客户端提供默认配置:
@RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)
public class RibbonClientDefaultConfigurationTestsConfig {
public static class BazServiceList extends ConfigurationBasedServerList {
public BazServiceList(IClientConfig config) {
super.initWithNiwsConfig(config);
}
}
}
@Configuration
class DefaultRibbonConfig {
@Bean
public IRule ribbonRule() {
return new BestAvailableRule();
}
@Bean
public IPing ribbonPing() {
return new PingUrl();
}
@Bean
public ServerList<Server> ribbonServerList(IClientConfig config) {
return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config);
}
@Bean
public ServerListSubsetFilter serverListFilter() {
ServerListSubsetFilter filter = new ServerListSubsetFilter();
return filter;
}
}
配置文件方式
从1.2.0版本开始,Spring Cloud Netflix支持自定义Ribbon客户端配置
配置格式 :
- [clientName].ribbon.NFLoadBalancerClassName: 配置ILoadBalancer的实现类
- [clientName].ribbon.NFLoadBalancerRuleClassName:配置IRule的实现类
- [clientName].ribbon.NFLoadBalancerPingClassName: 配置IPing的实现类
- [clientName].ribbon.NIWSServerListClassName: 配置ServerList的实现类
- [clientName].ribbon.NIWSServerListFilterClassName:配置ServerListFilter的实现类
如下配置就可以取代@RibbonClient注解,注意一定要使用全类名,没有@RibbonClient级别高:
CLOUD-PAYMENT-SERVICE:
ribbon:
NFLoadBalancerRuleClassName: com.gzl.myrule.MySelfRule
CLOUD-PAYMENT-SERVICE
为调用的服务名称,NFLoadBalancerRuleClassName
指定规则
脱离Eureka使用
去掉启动类上的@EnableDiscorveryClient注解
yml配置 :
server:
port: 8081
spring:
application:
name: microservice-consumer-pay
microservice-provider-order:
ribbon:
listOfServers: node01:8080,node02:8080,node03:8080
ribbon:
eureka:
enabled: false
#false不使用Eureka
listOfServers的地址列表是microservice-provider-order提供者的Ribbin的客户端地址列表,供microservice-consumer-pay调用
其他额外使用
直接用Ribbon提供的LoadBalancerClient
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/order/${orderId}")
public String order(@PathVariable String orderId) {
ServiceInstance serviceInstance = loadBalancerClient.choose("microservice-provider-order");
LOGGER.info("ServiceId:{},Host:{}:Port:{}",serviceInstance.getServiceId(),serviceInstance.getHost(),serviceInstance.getPort());
URI url = URI.create(String.format("http://%s:%s/pay"+, serviceInstance.getHost(), serviceInstance.getPort()));
return restTemplate.getForObject(url+orderId, String.class);
}
Feign
使用入门
pom :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
测试伪代码 :
//@Component
//@FeignClient(value = "api.github.com")
interface GitHub {
// 【效果与访问该url一致:https://api.github.com/repos/OpenFeign/feign/contributors】
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}
public static class Contributor {
String login;
int contributions;
}
public static class Issue {
String title;
String body;
List<String> assignees;
int milestone;
List<String> labels;
}
public class MyApp {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// Fetch and print a list of the contributors to this library.
List<Contributor> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
调用流程图:
FeignClient示例
@FeignClient(name = "remote",
url = "http://localhost:8081",
path = "remote") //path 会在下列所有请求前加上指定前缀 ‘/remote’
public interface RemoteFeignClient {
@GetMapping("findPerson") // 多于1个参数,则必须写@RequestParam注解(并且必须写value)
Person findPerson(@RequestParam(name = "pName") String name,
@RequestParam(name = "pAge") Integer age);
@GetMapping("getPerson") // @RequestParam后面是自定义参数类型将不会封装到接口的方法参数中
Person getPerson(@RequestParam("person") Person person);
@GetMapping("getPerson1") // feign将会把url拼接成url?name=xx&age=yy&address=zz(address字符串形式)
// 这个address将会导致接口那边在获取address时,不能正常封装成Address对象而导致报错
Person getPerson1(@RequestParam(name = "name") String name,
@RequestParam(name = "age") Integer age
,@RequestParam(name = "address") Address address);
@PostMapping("getPerson2") // 可以使用Map封装(远程接口使用@RequestBody Map来接收(address属性能正常接收到))
Person getPerson2(Map<String,Object> map);
@PostMapping("addPerson") // 不能使用超过1个@RequestBody
Person addPerson(@RequestBody Person person,@RequestParam("pAge") Integer age);
@PostMapping("addPerson2") // @RequestParam后面是自定义参数Address将不会封装到接口的方法参数中
Person addPerson2(@RequestBody Person person,@RequestParam("addr") Address addr);
@PostMapping("checkPerson") // 多于1个参数,则必须写@RequestParam注解(并且必须写value)
// feign拼接url?ids=1%2C2%2C3 (%2C,即逗号)
Person checkPerson(@RequestBody Person person, @RequestParam("ids") Integer[] ids);
@PostMapping("checkPerson2") // 多于1个参数,则必须写@RequestParam注解(并且必须写value)
// feign拼接url??ids=1&ids=2&ids=3
Person checkPerson2(@RequestBody Person person, @RequestParam("ids") List<Integer> ids);
}
/**
* 127.0.0.1:8091/feign/1
*/@GetMapping("/{uid}")
public User detail(@PathVariable int uid) {
return userFeignService.detail(uid);
}
/**
* 127.0.0.1:8091/feign/users
*
*/@GetMapping("/users")
public List<User> users(@RequestHeader String token) {
return userFeignService.users(token);
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
// 同name, 用来指定FeignClient的名称,如果没有配置url,name就`作为微服务的名称`,用于服务发现
@AliasFor("name")
String value() default "";
// serviceId已经废弃了,直接使用name即可
@Deprecated
String serviceId() default "";
// 如果指定,则会被用来作为feignClient的bean的名称,否则默认会取name(同value)属性
String contextId() default "";
// 同name, 用来指定FeignClient的名称,如果没有配置url,name就`作为微服务的名称`,用于服务发现
@AliasFor("value")
String name() default "";
// url用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用
String url() default "";
// 当调用请求发生404错误时,decode404的值为true,那么会执行decoder解码,否则抛出异常。
boolean decode404() default false;
// 配置Feign的配置类,在配置类中可以自定义Feign的Encoder、Decoder、LogLevel、Contract等
// 默认使用的配置类是: FeignClientsConfiguration
Class<?>[] configuration() default {};
// 定义容错的处理类(定义为bean),也就是回退逻辑,fallback的类必须实现F@eignClient标注的接口,无法知道熔断的异常信息
Class<?> fallback() default void.class;
// 也是容错的处理(定义为bean,实现FallbackFactory接口),可以知道熔断的异常信息
Class<?> fallbackFactory() default void.class;
// path定义当前FeignClient访问接口时的统一前缀,比如接口地址是/user/get, 如果你定义了前缀是user, 那么具体方法上的路径就只需要写/get 即可。
String path() default "";
// primary对应的是@Primary注解,默认为true,官方这样设置也是有原因的。
// 当我们的Feign实现了fallback后,也就意味着Feign Client有多个相同的Bean在Spring容器中,
// 当我们在使用@Autowired进行注入的时候,不知道注入哪个,所以我们需要设置一个优先级高的,@Primary注解就是干这件事情的。
boolean primary() default true;
// 如果配置了qualifier优先用qualifier作为别名,qualifier对应的是@Qualifier注解
// 当我们在容器中定义了fallback的bean,而feignClient的bean的primary属性又被设置了false,
// 那么可以使用这个属性设置,而在注入端使用@Quarlifier("指定这个属性值")
String qualifier() default "";
}
常用的注解属性 :
contextId ,当有多个服务调用方法不想写在一个接口里,就要使用到
name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现 url:
url一般用于调试,可以手动指定@FeignClient调用的地址 fallback:
定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现 @FeignClient标记的接口
fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
path: 定义当前FeignClient的统一前缀
开启日志
级别 | 打印内容 |
---|---|
NONE(默认值) | 不记录任何日志 |
BASIC | 仅记录请求方法、URL、响应状态代码以及执行时间(生产环境最好使用这个) |
HEADERS | 记录BASIC级别的基础上,记录请求和响应的header |
FULL | 记录请求和响应的header、body和元数据 |
- 一 、在consumer(调用方)的yml文件中加入以下代码
feign:
client:
config:
feign-provider: #此处写的是服务名称,针对我们feign微服务的配置,如果是default就是全局配置
loggerLevel: full #配置Feign的日志级别,相当于代码配置方式中的Logger
#在application.yml中使用 logging.level.<Feign客户端对应的接口的全限定名> 的参数配置格式来开启指定客户端日志
logging:
level:
com.bjpowernode.feign: debug
- java代码方式,声明一个Bean:
/**
* 配饰feign日志的bean
*/
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;
}
}
想要全局生效,则在服务启动类上加上注解:@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
如果只想对局部服务生效,则注解加在调用这个服务的类中:@FeignClient(value = “user-service”,configuration = FeignClientConfiguration.class)
超时配置
Feign所有默认配置都在org.springframework.cloud.openfeign.support.FeignHttpClientProperties类中
Feign 远程服务调用的默认超时时间为 1 秒,Feign的负载均衡底层用的就是Ribbon,所以这里的请求超时配置其实就是配置Ribbon,当出现请求超时会出现 SocketTimeOutException : Read Time Out
报错。
注意 :当开启日志配置之后,feign超时问题不会出现报错情况。
针对超时问题配合日志,我们需要在之前的日志配置中加入以下 ConnectTimeout
、ReadTimeout
内容
feign:
client:
config:
feign-provider: #此处写的是服务名称,针对我们feign微服务的配置,如果是default就是全局配置
loggerLevel: full #配置Feign的日志级别,相当于代码配置方式中的Logger
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 5000 #请求处理的超时时间
如果我们同时配置了Feign的超时和Ribbon超时,那么以Feign的超时设置说了算 :
例接口请求时间为3秒
#配置这个 ribbon肯定会超时
ribbon:
connectTimeout: 2000
readTimeout: 2000
#(Feign不会超时)
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
重试配置
重试功能和超时功能密切相关,只有远程调用过程耗时超过了超时限制后,才会试图去发起重试请求操作。一旦远程调用在超时范围内返回了结果将不会发起重试操作。
Feign 重试由 Retryer 来完成,它包含了三个属性:period、maxPeriod 和maxAttempts,maxAttempts 表示重试的最大次数,包含首次调用,period 表示重试周期间隔,maxPeriod 表示最大重试周期间隔。下图是在设置 maxAttempts 为 5 的情况下,重试调用过程图:
红色方块表示的是超时时长,绿色方块表示重试间隔周期,总共发起了 5 次请求,发生 4 次重试周期间隔等待。
重试间隔周期并不是固定时长的,而是逐次增长,但间隔时长总在 period 和 maxPeriod 之间。所以如果当提供者服务的接口耗时超过了超时时长时,那么消费者服务将总共会发起 5 次调用,整个过程耗时将是上面所有红绿方块时长总和。
配置重试
public class MyFeignConfig {
/**重试策略,默认不重试*/
@Bean
public Retryer feignRetryer(){
Retryer retryer = new Retryer.Default(1000, 10000, 5);
return retryer;
}
}
再在配置类上指定Feign的配置:
@EnableFeignClients(defaultConfiguration = MyFeignConfig.class)
测试:可以将服务提供者调用时长调长点,再配置消费端超时时长小于服务提供者
# feign 配置
feign:
client:
config:
default:
connectTimeout: 1000 #连接超时时间
readTimeout: 1000 #读取结果超时
性能问题
Feign的HTTP客户端支持三种框架:HttpURLConnection、HttpClient、OkHttp;默认是HttpURLConnection
SpringBoot 可配置server.compression.enable:ture 开启 gzip 压缩
Feign底层客户端实现:
- URLConnection:默认实现,不持支连接池;
- Apache HttpClient:支持连接池;
- OKHttp:支持连接池;
Feign的性能主要包括:
- 使用连接池代替默认的URLConnection;
- 日志级别,最好用basic或none
相关问题
针对这种问题一有有两种解决方案:
方式一:指定FeignClient所在包
@EnableFeignClients(basePackages = "UserClient所在的包")
方式二:指定FeignClients字节码
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = FeignClientConfiguration.class)
此处推荐第二种写法
切换客户端
当使用 Feign 时,默认会加载 FeignRibbonClientAutoConfiguration 配置类,该配置类会通过 @Import 导入三个 HTTP 客户端实现配置类:
HttpClientFeignLoadBalancedConfiguration
、OkHttpFeignLoadBalancedConfiguration
、DefaultFeignLoadBalancedConfiguration
,分别对应了 ApacheHttpClient、OkHttpClient、Default(HttpURLConnection)
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
//......省略部分代码
}
Feign 默认使用 HttpURLConnection 作为其 HTTP 客户端实现。
切换 Client 查看原文示例
Feign 在 2.1.0 版本开始增加了一个注解 @SpringQueryMap,该注解的目的是用于在 GET 请求情况下,让消费者服务将接口参数对象转换为 GET 参数(包含请求在路径上)。在此之前的版本如果不做额外处理是无法在 GET 请求上传递对象参数的。
@FeignClient(name = "cloud-feign-provider")
public interface ProviderFeignClient {
/**@SpringQueryMap 让GET请求参数也可以是对象类型*/
@GetMapping("/provider/query")
UserInfo queryUserInfo(@SpringQueryMap UserInfo userInfo);
}
内容压缩
Feign 提供了对请求响应数据的压缩,使用该功能之后,在传输数据超过指定限制大小之后将执行压缩操作,采用 GZIP 压缩。需要注意的是压缩之后远程调用会将数据先转换成字节数组,所以结果接收需要使用 ResponseEntity<byte[]> 。通过下面的配置开启内容压缩功能:
feign:
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json # 配置压缩支持的MIME TYPE
min-request-size: 2048 # 配置压缩数据大小的下限
response:
enabled: true # 配置响应GZIP压缩
拦截器
Feign 提供了拦截器 RequestInterceptor ,它会在每次发起远程服务接口调用时执行,我们可以通过该拦截器往 HTTP 协议包上添加一些公共数据:
public class MyFeignConfig {
//拦截器
@Bean
public RequestInterceptor requestInterceptor(){
return requestTemplate -> {
//事务id
requestTemplate.header("username", "123456");
};
}
}
OpenFegn
springcloud F 及F版本以上 springboot 2.0 以上基本上使用openfeign,openfeign 如果从框架结构上看就是2019年feign停更后出现版本
GateWay
GateWay 的作用 :
- 反向代理
- 鉴权
- 限流
- 熔断
GateWay三个配置:
- Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
- Redicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由;
- Filter(过滤) - 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
调用路径:
-
客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。
-
Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
-
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post")执行业务逻辑。
-
Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
先断言判断,再过滤器,再路由。
使用入门
引入maven 依赖 :
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml
server:
port: 9527
spring:
application:
name: springcloud-gateway
cloud:
gateway:
routes:
- id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
uri: http://localhost:8001
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
eureka:
instance:
hostname: springcloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
通过euraca服务名称路由 :
spring:
application:
name: springcloud-gateway
cloud:
gateway:
# locator需要打开,不然通过 lb://.. 方式请求不到
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
routes:
- id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
#匹配后提供服务的路由地址
#uri: http://localhost:8001
#lb是一个动态路由协议,后面的SPRINGCLOUD-PAYMENT-SERVICE是要跳转的服务名称。
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
在gateway中配置中的uri配置有三种方式
- ws(websocket)方式: uri: ws://localhost:9000
- http方式: uri: http://localhost:8130/
- lb(注册中心中服务名字)方式: uri: lb://brilliance-consumer
路由route配置的组成部分:
- id: 路由的ID
- uri: 匹配路由的转发地址
- predicates: 配置该路由的断言,通过PredicateDefinition类进行接收配置。
- order: 路由的优先级,数字越小,优先级越高。
- Filter: 过滤器 过滤掉一些请求, 满足则转发
Predicates
Predicate 来源于Java8,接受输入参数,返回一个布尔值结果;
gateway通过spring.cloud.gateway.routes.predicates
字段配置该路由的断言,通过PredicateDefinition类进行接收配置。
Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则
转发的判断条件. SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等支持多个Predicate请求的转发是必须满足所有的Predicate后才可以进行路由转发。若满足规则就按规则约定进行路由放行,否则拒绝访问或报404错误。
Gateway 支持的Predicate实现类:
对应yml配置的predicates
常用predicates :
-
Path
代表gateway所支持的路由接口地址。例 /payment/get/** 其中**
代表任何级别的接口名。Path还可指定占位符接口,配置规则:- Path= /payment/get/{segment} -
Query
Query用来指定请求的查询参数,当前请求必须传递查询参数,并且传递的查询参数必须与gateway所指定的完全相同,否则断言拒绝当前请求 -
After
After只接受一个参数,即DateTime格式时间,客户端访问Gateway接口的时间在After指定之后的时间是允许访问的,否则,当前访问被拦截 -
Before
Before只接受一个参数,即DateTime格式时间,客户端访问Gateway接口的时间在Before指定之前的时间是允许访问的,否则,当前访问被拦截 -
Between
Between接受两个参数,格式依然为DateTime格式时间,客户端访问Gateway接口的时间在Between指定的时间区间之内是允许访问的,否则,当前访问被拦截 -
Cookie
Cookie用来指定当前客户端请求头的cookie设置,只有当客户端请求头传递了cookie并且值与gateway设置的相等则放行,否则请求被拦截 -
Header
Header用来设定请求头匹配属性的,当前请求头属性与gateway所指定属性规则相同时,gateway断言放行该请求,否则拒绝访问 -
Host
Host用来匹配当前请求的host规则,该参数一般为自动计算,不需要手动设置,只有当前请求头中的host满足gateway所设定支持的host规则时,断言才会放行请求,否则截断 -
Method
Method用来指定gateway断言支持的请求方式,如:GET、POST或是PUT等,只有请求方式在gateway所设定支持请求方式范围内,则放行请求 -
RemoteAddr
用来设定断言所支持的IP网段,格式:IP地址/子网掩码,如果当前请求的IP地址在RemoteAddr所指定的IP段内,则gateway断言放行该请求
断言配置选项中,涉及的时间格式是ZonedDateTime类型;如果设置的断言时间不符合日期规范,编译时会直接报错
建议断言中配置一些需要优先验证,且常规固定的HTTP参数校验,很多其它拓展功能建议使用Filter自定义处理。
断言如何工作
上面断言的实现都依赖于抽象类AbstractRoutePredicateFactory
的实现类AfterRoutePredicateFactory、PathRoutePredicateFactory及HostRoutePredicateFactory等;具体的断言判别逻辑是在各自工厂的apply方法中,通过GatewayPredicate路由断言接口的test方法判别实现。
例: 断言 Before 的实现类 BeforeRoutePredicateFactory;这个类定义断言行为是 如果当前时间大于我们配置的时候,则不允许转发。
主要方法 :
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
final ZonedDateTime now = ZonedDateTime.now();
return now.isBefore(config.getDatetime());
}
@Override
public String toString() {
return String.format("Before: %s", config.getDatetime());
}
};
}
自定义Predicates断言
- 需要声明是Springboot的Bean,添加注解@Component,名称必须以RoutePredicateFactory结尾,这个是命名约束。如果不按照命名约束来命名,那么就会找不到该断言工厂。前缀就是配置中配置的断言。
- 可以直接复制Gateway中已经实现的断言工厂,修改对应的内容,避免踩坑。
- 继承父类AbstractRoutePredicateFactory,并重写方法。
- 需要定义一个Config静态内部类,来接收断言配置的数据。
- 在重写的shortcutFieldOrder方法中,绑定Config中的属性。传入数组的内容需要与Config中的属性一致。
- 重写的apply方法中,实现具体验证逻辑。
需要实现RoutePredicateFactory接口并实例化为Spring Bean,也可以通过继承AbstractRoutePredicateFactory来实现自定义断言,名称必须以RoutePredicateFactory结尾
,示例如下:
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {
public MyRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
//使用 config 获取参数
return exchange -> {
// 获取request
ServerHttpRequest request = exchange.getRequest();
// 判断是否满足自定义需求
return matches(config, request);
};
}
public static class Config {
// 指定自定义断言的参数
}
}
import com.alibaba.cloud.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* @ClassName CustomVerifyRoutePredicateFactory
* @Description
* @Author tigerkin
* @Date 2022/3/14 15:15
*/
@Component
public class CustomVerifyRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomVerifyRoutePredicateFactory.Config> {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 验证内容 key.
*/
public static final String VERIFY_CONTENT_KEY = "verifyContentKey";
/**
* 验证内容 val.
*/
public static final String VERIFY_CONTENT_VAL = "verifyContentVal";
public CustomVerifyRoutePredicateFactory() {
super(CustomVerifyRoutePredicateFactory.Config.class);
}
/**
* 将签名 key 与 静态类Config中的属性进行绑定
* 数组里面下标对应配置文件中配置
* 将配置文件中的值按返回集合的顺序,赋值给配置类
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(VERIFY_CONTENT_KEY, VERIFY_CONTENT_VAL);
}
@Override
public Predicate<ServerWebExchange> apply(CustomVerifyRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
/**
* 断言验证逻辑,返回true,则验证成功,否则失败
*
* @param serverWebExchange
* @return
*/
@Override
public boolean test(ServerWebExchange serverWebExchange) {
HttpHeaders headers = serverWebExchange.getRequest().getHeaders();
String headerVal = headers.getFirst(config.getVerifyContentKey());
boolean result = StringUtils.equals(headerVal, config.getVerifyContentVal());
log.info("========> 自定义断言配置 key:{} val:{}", config.getVerifyContentKey(), config.getVerifyContentVal());
log.info("========> 自定义断言验证 status:{} val:{}", result, headerVal);
return result;
}
@Override
public Object getConfig() {
return config;
}
@Override
public String toString() {
return String.format("key: %s, val: %s", config.getVerifyContentKey(), config.getVerifyContentVal());
}
};
}
/**
* 定义静态类,接收自定义断言的配置信息
*/
@Data
public static class Config {
@NotNull
private String verifyContentKey;
@NotNull
private String verifyContentVal;
}
}
配置使用自定义的CustomVerifyRoutePredicateFactory断言工厂:
spring:
cloud:
gateway:
routes:
- id: user-route # 路由ID,唯一标识,自定义命名
uri: lb://gateway-user
predicates:
- Path=/user-server/**
# 自定义的断言工厂,多个参数按逗号(,)隔开,参数对应断言工厂中shortcutFieldOrder方法定义的数组,一一对应。
- CustomVerify=verify, success
Filters
Spring Cloud Gateway中的过滤器分为全局过滤器和局部过滤器两种类型,不同类型的过滤器在执行顺序上有所不同。
-
全局过滤器执行顺序
全局过滤器是指在所有路由规则中都会执行的过滤器(无需配置,全局生效),可以用于实现一些全局性的功能,如请求的日志记录、响应头信息的设置等。Spring Cloud Gateway提供了一些内置的全局过滤器,如请求路径的重写、请求日志的记录等。可实现GatewayFilter
接口实现,可实现Ordered
接口或者写@Order
注解定义过滤排序顺序值,否则,它的order则是从1开始,按照Route中定义的顺序依次排序;在Spring Cloud Gateway中,全局过滤器的执行顺序是由GatewayFilterAdapter
的ORDER常量值确定的,该常量值为-2147483648,order值越小,GlobalFilter执行的优先级越高;当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。 -
局部过滤器执行顺序
局部过滤器是指只在特定路由规则中才会执行的过滤器(需要在配置文件中配置),可以用于实现一些特定的功能,如请求鉴权、请求转发等。Spring Cloud Gateway中的局部过滤器可以通过自定义过滤器工厂类来实现,该工厂类需要继承AbstractGatewayFilterFactory
抽象类,并实现其中的apply方法和泛型参数指定配置类。在Spring Cloud Gateway中,局部过滤器的执行顺序是由配置文件中的filters属性确定的,该属性可以通过spring.cloud.gateway.routes.filters参数进行配置,不同的过滤器在列表中的位置就决定了它们的执行顺序。
示例定义全局过滤器 :
实现GlobalFilter
接口,并注册进Spring容器
@Component
public class MyTestGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("GlobalFilter before...");
String token = exchange.getRequest().getQueryParams().getFirst("token");
ServerHttpResponse response = exchange.getResponse();
if(!StringUtils.hasText(token)){
//没登录
System.out.println("没有登录");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//返回401状态
//return response.setComplete();
//如果中文乱码,需要加上响应头
//HttpHeaders httpHeaders = response.getHeaders();
//返回数据格式
//httpHeaders.setContentType(MediaType.APPLICATION_JSON);
//一般访问接口应该返回json格式
String ret = "{\"code\":401, \"msg\":\"没有登录\", \"data\":\"\"}";
byte[] bits = ret.getBytes(StandardCharsets.UTF_8);
//把字节数据转换成一个DataBuffer
DataBuffer buffer = response.bufferFactory().wrap(bits);
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println("GlobalFilter after...");
}));
}
@Override
public int getOrder() {
return -1;
}
}
示例定义局部过滤器 :
一、首先实现GatewayFilter
/**
* 计算时间过滤器
*/
public class TimeGatewayFilter implements GatewayFilter, Ordered {
/**
* 过滤逻辑
* @param exchange 转换器--封装了来自请求中所有信息,比如:请求方法,请求参数,请求路径,请求头,cookie等
* @param chain 过滤器链--使用责任链模式,决定当前过滤器是放行还是拒绝
* @return Mono 响应式编程的返回值规范,一般后置拦截才会用
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//统计时间:进来(pre前置)记当前时间, 出去(post后置)记当前时间,2者差值就是运行时间
long begin = System.currentTimeMillis();
System.out.println("前置当前时间:" + begin);
return chain.filter(exchange).then(Mono.fromRunnable(()->{
long end = System.currentTimeMillis();
System.out.println("后置当前时间:" + end);
System.out.println("两者时间差:" + (end-begin));
}));
}
@Override
public int getOrder() {
return 0;
}
}
二、实现一个 AbstractGatewayFilterFactory
并注册进spring容器,并关联实现的GatewayFilter
/**
* 注册时间统计过滤器
* 要求:
* 1>过滤器工厂名字必须按照规定格式,否则报错。
* 格式: XxxGatewayFilterFactory, 其中Xxx 就是在yml中filters配置过滤器
* 2>继承AbstractGatewayFilterFactory父类,明确指定一个泛型
* 改泛型用于接受yml-filters配置的过滤器值
*/
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new TimeGatewayFilter();
}
}
yml配置里配置 routes.filters
使用 TimeGatewayFilterFactory
前缀Time
指定过滤器
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- Time
或则一步到位,用匿名类实现GatewayFilter
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
public LocalFilter1() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
System.out.println("LocalFilter1 before...");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println("LocalFilter1 after...");
}));
};
}
public static class Config {
// 配置参数
}
}
可以使用 @Order(-1) 注解配置过滤排序
数值越大优先级越低, 负的越多, 优先级越高。
带参数过滤器
例:携带两个参数
filters:
- StripPrefix=1
- Time=111,222
定制接受参数的对象
@Data
public class TimeGatewayFilterParam {
//读取配置文件中的配置,按顺序配置参数
private String value1;
private String value2;
}
TimeGatewayFilter 中注入 TimeGatewayFilterParam
/**
* 计算时间过滤器
*/
public class TimeGatewayFilter implements GatewayFilter,Ordered {
private TimeGatewayFilterParam value;
//指定构造函数注入 TimeGatewayFilterParam
public TimeGatewayFilter(TimeGatewayFilterParam config){
this.value = config;
}
/**
* 过滤逻辑
* @param exchange 转换器--封装了来自请求中所有信息,比如:请求方法,请求参数,请求路径,请求头,cookie等
* @param chain 过滤器链--使用责任链模式,决定当前过滤器是放行还是拒绝
* @return Mono 响应式编程的返回值规范,一般后置拦截才会用
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//统计时间:进来(前置)记当前时间, 出去(后置)记当前时间,2者差值就是运行时间
long begin = System.currentTimeMillis();
System.out.println(value.getValue1() + "前置当前时间:" + begin);
return chain.filter(exchange).then(Mono.fromRunnable(()->{
long end = System.currentTimeMillis();
System.out.println(value.getValue2() + "后置当前时间:" + end);
System.out.println("两者时间差:" + (end-begin));
}));
}
@Override
public int getOrder() {
return 0;
}
}
在TimeGatewayFilterFactory 类中指定 AbstractGatewayFilterFactory 的泛型为 TimeGatewayFilterParam
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterParam> {
//指定获取配置参数之后封装成啥对象
public TimeGatewayFilterFactory(){
super(TimeGatewayFilterParam.class);
}
//将数据添加到哪个属性上,与实体类TimeGatewayFilterParam相对应
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("value1", "value2");
}
@Override
public GatewayFilter apply(TimeGatewayFilterParam config) {
return new TimeGatewayFilter(config);
}
}
Gateway的生命周期:
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择 请求的微服务、记录调试信息等。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
AbstractGatewayFilterFactory
gateway网关在运行时候,先执行断言 predicate,后执行过滤 filters
图中Gateway Handler Mapping 和 Gateway web Handler 指的就是断言,相当于地址映射和web请求
过滤器的执行顺序为:默认过滤器 → 当前路由过滤器 → 全局过滤器。
gateway最大的能力是在于转发,一些过滤最好下沉到各个服务。否则会影响性能
常见跨域解决方案
方式一:配置application.yml文件:
spring:
cloud:
gateway:
globalcors: # 全局的跨域配置
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
# options请求 就是一种询问服务器是否浏览器可以跨域的请求
# 如果每次跨域都有询问服务器是否浏览器可以跨域对性能也是损耗
# 可以配置本次跨域检测的有效期maxAge
# 在maxAge设置的时间范围内,不去询问,统统允许跨域
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 允许在请求中携带cookie
maxAge: 360000 # 本次跨域检测的有效期(单位毫秒)
# 有效期内,跨域请求不会一直发option请求去增大服务器压力
方式二:使用编码方式定义配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
public class CorsConfig {
private static final String MAX_AGE = "18000L";
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
// 使用SpringMvc自带的跨域检测工具类判断当前请求是否跨域
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}
HttpHeaders requestHeaders = request.getHeaders(); // 获取请求头
ServerHttpResponse response = ctx.getResponse(); // 获取响应对象
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod(); // 获取请求方式对象
HttpHeaders headers = response.getHeaders(); // 获取响应头
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); // 把请求头中的请求源(协议+ip+端口)添加到响应头中(相当于yml中的allowedOrigins)
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name()); // 允许被响应的方法(GET/POST等,相当于yml中的allowedMethods)
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); // 允许在请求中携带cookie(相当于yml中的allowCredentials)
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); // 允许在请求中携带的头信息(相当于yml中的allowedHeaders)
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE); // 本次跨域检测的有效期(单位毫秒,相当于yml中的maxAge)
if (request.getMethod() == HttpMethod.OPTIONS) { // 直接给option请求反回结果
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx); // 不是option请求则放行
};
}
}
Zuul
通过hystrix和ribbon的参数来调整Zuul路由请求的各种超时时间等配置
Zuul 中一共有四种不同生命周期的 Filter,分别是:
- pre:在 Zuul 按照规则路由到下级服务之前执行。如果需要对请求进行预处理,比如鉴权、限流等,都应考虑在此类 Filter 实现。
- route:这类 Filter 是 Zuul 路由动作的执行者,是 Apache Http Client 或 Netflix Ribbon 构建和发送原始 HTTP 请求的地方,目前已支持 Okhttp。
- post:这类 Filter 是在源服务返回结果或者异常信息发生后执行的,如果需要对返回信息做一些处理,则在此类 Filter 进行处理。
- error:在整个生命周期内如果发生异常,则会进入 error Filter,可做全局异常处理。
Zuul 抛错两种情况:
- 在 post Filter 抛错之前,pre、route Filter 没有抛错,此时会进入 ZuulException 的逻辑,打印堆栈信息,然后再返回 status = 500 的 ERROR 信息。
- 在 post Filter 抛错之前,pre、route Filter 已有抛错,此时不会打印堆栈信息,直接返回status = 500 的 ERROR 信息。
自定义请求过滤:
package com.web.springcloud;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
@Component
public class MyZuulFilter extends ZuulFilter {
private static final Logger log = LoggerFactory.getLogger(MyZuulFilter.class);
/**
过滤器具体逻辑
*/
@Override
public Object run() {
// TODO Auto-generated method stub
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
log.info("send {} request to {}",request.getMethod(),request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken==null) {
log.warn("access token is empty");
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
/**
判断该过滤器是否需要被执行,这里返回true,将会对所有请求生效
*/
@Override
public boolean shouldFilter() {
// TODO Auto-generated method stub
return true;
}
/**
过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行
*/
@Override
public int filterOrder() {
// TODO Auto-generated method stub
return 0;
}
@Override
public String filterType() {
// TODO Auto-generated method stub
return "pre";
}
}
Zuul 1.0 是建立在Servlet上的同步阻塞架构,故在使用时对这部分的优化工作是必要的,Zuul 2.0 是建立在Servlet上的异步非阻塞架构,已经开源,但是Spring Cloud已经明确表示不会再支持Zuul 2.0,所以,相对于第一代网关Zuul而言,Spring Cloud的第二代网关中间件Spring Cloud Gateway使用Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架,性能更高为学习重点。
Hystrix
重要概念 :
-
请求缓存:可以根据请求的URI将其返回结果暂时缓存起来,下次请求相同的URI就会从缓存中获取数据。
-
请求合并:可以将客户端多个请求,合并成一个请求,采用批处理的方式调用服务端接口。
-
服务限流:秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
-
线程池隔离:将不同的请求采用不同的线程池隔离,这样即使其中一个线程池的请求出现问题,也不会影响其他线程池的请求。
-
信号量隔离:项目中存在太多的线程池会降低性能,因为线程池需要来回切换线程,所以就提出了信号量隔离机制。
-
服务监控:Hystrix提供了一个dashboard控制面板,可以监控并查看请求的调用情况。
-
服务熔断
熔断机制是应对雪崩效应的⼀种微服务链路保护机制。当链路的某个微服务不可⽤或者响应时间太⻓时,熔断该节点微服务的调⽤(停止调用),进⾏服务的降级,快速返回错误的响应信息。当检测到该节点微服务调⽤响应正常后,恢复调⽤链路。【通常与服务降级一起使用】 -
服务降级
服务降级是从系统整体考虑,当某个服务熔断之后,服务器不再被调⽤时,客户端可以为发送的请求准备⼀个本地的fallback回调,返回⼀个与方法返回值类型相同的缺省值,这样做,虽然服务水平下降,但整体仍然可用,比直接熔断要好。
降级情况:- 超时
- 程序运行异常
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
使用示例
服务提供方做服务降级
@Service
public class PaymentService
{
/**
* 正常访问,肯定OK
* @param id
* @return
*/
public String paymentInfo_OK(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
/**
* 配置超时降级策略
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})
public String paymentInfo_TimeOut(Integer id)
{
//int age = 10/0;
try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
public String paymentInfo_TimeOutHandler(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
}
主启动类添加@EnableCircuitBreaker注解来激活Hystrix的功能
也可以使用@EnableHystrix,它继承了@EnableCircuitBreaker,并对它进行了在封装,这两个注解都是激活hystrix的功能,
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker //此处添加
public class PaymentHystrixMain8003 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8003.class,args);
}
}
降级函数要与被处理的函数参数一致
@HystrixProperty 注解配置触发降级的条件
@HystrixCommand 指定降级的方法及触发条件
@HystrixCommand的参数说明:
-
groupKey:设置HystrixCommand分组的名称。
-
commandKey:设置HystrixCommand的名称。
-
threadPollKey:设置HystrixCommand执行线程池的名称。
-
fallbackMethod:设置HystrixCommand服务降级所使用的方法名称,注意该方法需要与原方法定义在同一个类中,并且方法签名也要一致。
-
commandProperties:设置HystrixCommand属性,例如断路器失败百分比、断路器时间容器大小等。
-
ignoreException:设置HystrixCommand执行服务降级处理时需要忽略的异常,当出现异常时不会执行服务降级处理。
-
observableExecutionMode : 设 置 HystrixCommand 执 行 的 方式。
-
defaultFallback:设置HystrixCommand默认的服务降级处理方 法 , 如 果 同 时 设 定 fallbackMethod 属 性 , 会 优 先 使 用fallbackMethod属性所指定的方法。该属性所指定的方法没有参数,需要注意返回值与原方法返回值的兼容性。
除使用注解方式来完成服务降级实现外,Hystrix还提供了两个对象 来 支 持 服 务 降 级 实 现 处 理 : HystrixCommand 和HystrixObserableCommand 。
如果继承HystrixCommand 则 需 要 实 现getFallback方法,代码如下:
public class CustomFallbackCommand extends HystrixCommand<String> {
String name;
public CustomFallbackCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("custom")));
}
public CustomFallbackCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(name)));
this.name = name;
}
//具体业务实现
@Override
protected String run() throws Exception {
return null;
}
//降级实现
@Override
protected String getFallback() {
return super.getFallback();
}
}
HystrixObserableCommand用于所依赖服务返回多个操作结果的时候,在实现服务降级时,如果是继承HystrixObserableCommand,则需要实现resumeWithFallback方法,代码略。
有4种方法可以执行Hystrix命令(前两种方法只适用于简单的HystrixCommand对象,不适用于HystrixObservableCommand对象)。
-
execute:该方法与queue方法以相同的方式获取一个Future对象,然后在这个Future上调用get方法来获取可观察对象发出的单个值。
-
queue:该方法将可观察对象转换为BlockingObservable对象,以便将其转换为Future对象,然后返回此Future对象。
-
observe:该方法可以立即订阅可观察对象,并开始执行命令的流。返回一个可观察对象,当订阅该对象时,它将重新产生结果并通知订阅者。
-
toObservable:该方法返回的可观察值不变,需要订阅后才能真正开始执行命令流程。
使用通配服务降级FeignFallback(集成Feign)
配置在消费方:
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "paymentInfo_OK出现异常";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "paymentInfo_TimeOut出现异常";
}
}
FeignClient指定fallback类为PaymentFallbackService
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",
fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
开启 Hystrix 对 Feign 的支持,默认为 false 关闭
feign:
hystrix:
enabled: true
主启动类加上@EnableCircuitBreaker注解
也可以在消费方层PaymentController 加上@DefaultProperties 的 defaultFallback 属性来指定类的全局降级回退方法
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallackMethod")
public class PaymentController {
@Resource
private PaymentHystrixService paymentService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_OK(id);
log.info("*******result:" + result);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") //3秒钟以内就是正常的业务逻辑
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
//兜底方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对付支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,(┬_┬)";
}
/**
* 全局fallback处理方法
*/
public String payment_Global_FallackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对付支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,(┬_┬)";
}
}
服务熔断示例
断路器有三种状态熔断打开、熔断关闭、熔断半开
正常调用时,断路器是关闭的。当出现高并发等情况,导致某个服务瘫痪,此时断路器打开,服务发生熔断,会进行服务的降级,快速返回错误的信息。
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果能够正常运行,则恢复正常链路,此时断路器关闭
服务提供方service代码示例 :
@HystrixCommand(fallbackMethod = "payment_Hander",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期,断路器打开后,多久以后开始尝试恢复,默认5s
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
public String paymentInfo_TimeOut(Integer id)
{ if(id<0){
throw new RuntimeException("id不能为负数");
}
//try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): 3";
}
public String payment_Hander(Integer id){
return "线程池: "+Thread.currentThread().getName()+" 8003系统繁忙或者运行报错,请稍后再试,id: "+id;
}
Hystrix—隔离
资源隔离是指把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了。比如说商品服务,现在同时发起的调用量已经到了 1000,但是分配给商品服务线程池内就 10 个线程,最多就只会用这 10 个线程去执行。不会因为对商品服务调用的延迟或者失败,将 Tomcat 内部所有的线程资源全部耗尽。
资源隔离要解决的核心问题,就是将多个依赖服务的调用分别隔离到各自的资源池中,避免某一个依赖服务的调用因为延迟或者失败,导致所有的线程资源全部耗费在这个服务接口的调用上,一旦某个服务的线程资源全部耗尽,就可能导致雪崩效应。
雪崩效应:多个微服务之间进行调用的时候,假设服务A调用服务B和服务C,服务B和服务C又调用其他微服务,这就是所谓的"扇出",如果扇出的链路上某个微服务的调用响应时间过长,如线程阻塞了,容器中的线程数量就则会持续增加,这个时候服务A占用系统的线程资源就会越来越多,导致更多联级故障,进而引起系统奔溃。
使用资源隔离的好处
- 合理分配资源,把给资源分配的控制权交给用户。
- 在一定程度上防止微服务的雪崩效应。
- 有利于对接口调用的监控和分析,类似于hystrix-dashboard的使用。
Hystrix有限流、降级、熔断的功能,包含常用的容错方法:线程隔离、信号量隔离、服务降级、服务熔断。它使用Rxjava来写,它会把每一个请求的都包裹为一个HystrixCommand来同步或异步地执行,并使用滑动窗口来统计执行结果,它有两种隔离模式,一种是线程池隔离,一种是信号量隔离,线程池隔离一般一个服务使用一个专有的线程池,当这个服务响应时间很长或宕机时,它会限制服务的请求数量或对接口进行降级,打开断路器,拒绝服务来保证服务的稳定性,信号量隔离会限制服务的并发执行数量来保证服务的可用性。一般情况下,信号量隔离没有线程池隔离方式好。
-
线程池隔离模式:使用一个线程池来存储当前的请求。线程池对请求做处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池里慢慢处理)。
-
信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求到来时先判断计数器的数值,若超过设置的最大线程个数,则丢弃该类型的新请求,若不超过,则执行计数操作,请求到来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)。
基于Hystrix两种隔离方式的区别
- 基于Hystrix线程池隔离:Hystrix线程池隔离,它提供了一个抽象,叫做Command。我们可以把这个Command简单理解为一个线程池。Hystrix线程池的隔离,是用 Hystrix 自己的线程去执行调用,并不是去控制tomcat容器的线程,Hystrix线程池满后,Tomcat的线程不会因为依赖服务的接口调用延迟或者失败而阻塞,不会卡死在哪里,可以去做其他事情。
- 基于Hystrix信号量隔离:Hystrix信号量隔离,是控制Tomcat容器中的线程数,信号量有多少,就允许多少个Tomcat线程通过它,然后去执行。比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
线程池隔离和信号量隔离的使用应用场景
-
线程池隔离适用于绝大部分的场景,但是这些场景里面往往包含对依赖服务的网络请求调用,TimeOut这些问题。
-
信号量隔离适用于少部分的场景,如果你的访问不是对外部依赖服务的网络请求调用,而是对系统内部一些业务的访问,这些访问基本上不包括任何网络请求,也不会出现TimeOut的问题,那么我们就可以使用信号量的隔离来实现资源隔离
@Service
public class UserServiceImpl implements UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserFeign userFeign;
/**
* threadPoolKey:1:如果设置的groupKey值已经存在,他会使用这个groupKey值。
* 这种情况下:同一个groupKey下的依赖调用共用同一个线程池
* 2:如果groupKey值不存在,则会对于这个groupKey新建一个线程池
*
* 1:threadPoolKey的默认值是groupKey,而groupKey默认值是@HystrixCommand标注的方法所在类名
* 2:可以通过在类上加@DefaultProperties(threadPoolKey="xxx")设置默认的threadPoolKey
* 3;可以通过@HystrixCommand(threadPoolKey="xxx")指定当前HystrixCommand实例的threadPoolKey
* 4:threadPoolKey用于从线程池缓存中获取线程池和初始化创建线程池,由于默认以groupKey即类名为threadPoolKey,那么默认所有在一个类中的HystrixCommand共用一个线程池
* 5:动态配置线程池 --
* 可以通过hystrix.command.HystrixCommandKey.threadPoolKeyOverride=线程池key动态设置
* threadPoolKey,对应的HystrixCommand所使用的线程池也会重新创建,还可以继续通过
* hystrix.threadpool.HystrixThreadPoolKey.coreSize=n和hystrix.threadpool.HystrixThreadPoolKey.maximumSize=n动态设置线程池大小
* 6:commandKey的默认值是@HystrixCommand标注的方法名,即每个方法会被当做一个HystrixCommand
*/
@HystrixCommand(threadPoolKey = "time",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2"),
@HystrixProperty(name = "maxQueueSize", value = "20")}, commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "9000")
}
)
@Override
public String timeOut(Integer mills) {
log.info("-----------mills:的值为:" + mills + "--------------");
return userFeign.timeOut(mills);
}
/**
* 一个类中的HystrixCommand使用两个线程池,进行线程隔离【和上面的方法隔离】
*/
@HystrixCommand(threadPoolKey = "time_1",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2"),
@HystrixProperty(name = "maxQueueSize", value = "20")}, commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "9000")
}
)
@Override
public String timeOut_1(Integer mills) {
log.info("-----------mills:的值为:" + mills + "--------------");
return userFeign.timeOut(mills);
}
}
Hystrix隔离策略相关的参数如下。
-
execution.isolation.strategy=THREAD|SEMAPHORE:设置线程或信号量隔离模式。
-
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:设置隔离模式的超时时间,默认值是1000ms。
-
execution.isolation.semaphore.maxConcurrentRequests :设置在使用时允许到HystrixCommand.run方法的最大请求数,默认值是10。
-
execution.timeout.enabled:设置HystrixCommand.run方法执行时是否开启超时设置,默认开启。
-
execution.isolation.thread.interruptOnTimeout:发生超时时是否中断HystrixCommand.run方法,默认是true。
-
execution.isolation.thread.interruptOnCancel:取消时是否中断HystrixCommand.run方法,默认是false。
Hystrix缓存
Hystrix有两种方式来应对高并发场景,分别是请求缓存与请求合并缓存。请求缓存是在同一请求多次访问中保证只调用一次这个服务提供者的接口,同一请求第一次的结果会被缓存,保证同一请求多次访问返回结果相同。
使用@CacheResult开启请求缓存功能
使用CacheKey开启缓存
通过@CacheRemove注解来实现失效缓存清理功能
调用超时
hystrix默认配置都在com.netflix.hystrix.HystrixCommandProperties
类中
默认超时为一秒钟 :default_executionTimeoutInMilliseconds = 1000,
超时全局的设置,可以在配置文件中配置如下,默认是 1000 ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
@HystrixCommand 注解,那么可以在注解中直接指定超时时间:
@HystrixCommand(fallbackMethod="fallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000" )
}
)
@Override
public String firstLoginTimeOut(Integer id) {
return userFeign.getCouponTimeOut(id);
}
Hystrix工作流程
上图可知,一个请求有4个执行入口,分别是 execute() , queue() , observe() , toObservable() 这4种方法,但它们底层都使用 toObservable() 去创建一个 HystrixCommand 。
Hystrix整体流程
1.构造Hystrix命令
- 构造一个HystrixCommand或HystrixObservableCommand对象,用于封装请求并在构造方法中配置请求被执行需要的参数。
2.执行Hystrix命令
- 根据上文中提供的4种方式执行命令。
3.判断是否缓存了响应
- 如果你为命令启用了请求缓存,并且在缓存中命中了可用请求的响应,则缓存的响应将立即以可观察到的形式返回。
4.判断熔断电路是否打开
-
当执行命令时,Hystrix将与断路器一起检查熔断电路是否打开。
-
如果熔断电路打开,那么Hystrix将不执行命令并回退。如果熔断电路关闭,则继续执行,检查是否有可用的容量来运行命令。
5.线程池、队列、信号量是否已满
- 如果与命令关联的线程池和队列(或信号量,如果不在线程中运行)已满,那么Hystrix将不执行命令,执行逻辑跳转到第7步。
6.计算电路健康状态
- 执行HystrixObservableCommand.construct或HystrixCommand.run方法,Hystrix向断路器报告成功、失败、拒绝或超时,如果执行逻辑失败或者超,则执行逻辑跳转第7步;否则执行逻辑跳转到第8步;
7.回退
- Hystrix试图恢复你的回滚命令,并执行回退逻辑或者fallback备用逻辑。
8.返回成功的响应如果Hystrix命令成功,它将以可观察到的形式返回响应给调用者。
Hystrix - Dashboard
Config
- GitHub上新建一个用作配置中心的新仓库
- 新建Module作为配置中心服务端连接GitHub读取GitHub配置
- 新建Module作为配置中心客户端连接服务端读取服务端配置
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。
资源由三个变量参数化:
label: 分支(branch),是可选的git标签(默认为“master”)
name/application: 服务名
profiles:环境(dev/test/prod)
读取配置文件信息返回的是 json格式。不存在的配置访问结果为{} 空。
SpringCloud Config默认使用Git来存储配置文件(也有其他方式,比如SVN和本地文件),但最推荐使用的还是Git,而且使用的是http/https访问的形式。这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
Zookeeper
Bus消息总线
Stream
当我们在一个企业中可能存在多种中间件,但是掌握所有中间件的大牛不是很多,就引出了Stream,让我们不在关注具体MQ细节,我们只需要用一种适配器绑定的方式,自动给我们在各种MQ内切换
基础使用步骤:
- pom文件依赖
<!-- 主要依赖,版本由导入的springcloud帮我们控制-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
- yml配置
发送方 :
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: #配置要绑定的rabbitmq的服务信息
testRabbit: #表示定义的一个binder名称,用于bingding的整合
type: rabbit #binder类型这里是rabbit
enviroment:
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
output: # 这个名字是一个通道的名称 output表示输出/写入MQ中
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: testRabbit # 设置要绑定的消息服务的具体binder名称
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
destination
在RabbitMQ就是Exchange,在Kakfa中就是Topic。
Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。
发送方代码 :
// 可以理解为是一个消息的发送管道的定义
@EnableBinding(Source.class)
public class MessageProviderImpl implements IMessageProvider {
// 消息的发送管道
@Resource
private MessageChannel output;
@Override
public String send() {
String serial = UUID.randomUUID().toString();
// 创建并发送消息
this.output.send(MessageBuilder.withPayload(serial).build());
System.out.println("***serial: " + serial);
return serial;
}
}
消费Yml示例 :
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称,input表示输入/读取MQ
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: test #消费者组名称,同一消费组内一条消息只会被一个消费者消费,且会被持久化
group
可以设置消费组名称和开启持久化
消费者代码 :
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListener {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者1号,------->接收到的消息:" + message.getPayload() + "\t port: " + serverPort);
}
}
Sleuth
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案。他会将服务与服务之间的调用给记录起来。可以快速的知道调用 用户服务,到底涉及到了哪些微服务,方便我们快速排查问题,Sleuth可提供服务:
- 提供链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。
- 可视化各个阶段耗时,进行性能分析
- 各个调用环节的可用性、梳理服务依赖关系以及优化
- 数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。
sleuth :链路追踪器
zipkin:链路分析器。可以理解为可视化界面,配合Sleuth可以清晰定位请求流程。
Sleuth 主要关键词 :
-
Span (跨度)
工作的基本单位。通俗的理解span就是一次请求信息,发送一个远程调度任务就会产生一个Span。 Span 由一个64位ID唯一标识的,Trace 是用另一个64位ID唯一标识的,Span 还有其他数据信息,比如摘要、时间戳事件、Span的ID、进度ID、键值注释(标签),导致它们的跨度的ID以及进程ID(通常是IP地址)。创建跨度后,必须在将来的某个时刻停止。 -
Trace(跟踪)
类似于树结构的span集合,表示一条调用链路,存在唯一标识,是一个完整的请求流程,包括从客户端发起请求到服务器处理完毕并返回响应的整个过程。Trace 由一个或多个 Span 组成。。请求一个微服务系统的API接口,这个API接口,需要调用多个微服务,调用每个微服务都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace。 -
Annotation(标注)
用来及时记录一个事件的存在,一些核心注解用来定义一个请求的开始和结束。可以理解成 Span 生命周期中 重要时刻 的 数据快照,比如一个 Annotation 中一般包含 发生时刻(timestamp)、事件类型(value)、端点(endpoint)等信息。其中 Annotation 的 事件类型包含以下四类:- cs(Client Sent) - 客户端发送 - 客户端发送一个请求,这个注解描述了这个跨度的开始
- sr(Server Received) - 服务器接收 - 服务器端得到请求,并将开始处理它。如果从此时间戳中减去cs时间戳,可得到网络传输的时间。
- ss(Server Sent) - 服务器发送 - 该注解表明请求处理的完成(当请求返回客户端)。如果从此时间戳中减去sr时间戳,就可以得到服务器请求的时间。
- cr(Client Received) - 客户端接收 - 表示跨度的结束。客户端已成功接收到服务器端的响应。如果从此时间戳中减去cs时间戳,则会收到客户端从服务器接收响应所需的整个时间,即整个请求所消耗的时间。
Trace ID 和 Span ID
- Trace ID:每个 Trace 都有一个全局唯一的 ID,用于跨服务和组件追踪请求的整个执行过程。Trace ID 在整个 Trace 内保持不变。
- Span ID:每个 Span 都有一个唯一的 ID,用于在特定 Trace 内标识一个特定的操作。Span ID 在同一个 Trace 内是唯一的,但在不同 Trace 之间可能重复。
入门示例
POM:
SpringCloud从F版开始起就不需要自己构建ZipKin Server了,只需要调用jar包即可:
-
从网络上下载Zipkin服务端的可执行jar包,下载地址:
https:/repo1.maven.org/maven2/io/zipkin/java/zipkin-server/2.9.4/zipkin-server-2.9.4-exec.jar。 -
将zipkin-server-2.9.4-exec.jar修改为zipkin.jar。
-
执行下面的命令进入zipkin.jar所在目录:
java -jar zipkin.jar
Zipkin服务端的默认启动端口为9411,浏览器访问localhost:9411即可进入Zipkin服务端管理界面
服务提供者(payment)和消费者(order)工程均需要添加依赖
<!--依赖包含了sleuth,所以不需要再单独引入sleuth-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
如果上边的依赖飘红引不进来,那么原因可能是你使用的cloud版本已经移除了spring-cloud-starter-zipkin,需要使用下边的来替代:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
yml :
服务提供者(payment)和消费者(order)工程均需要配置
server:
port: 8000
spring:
zipkin:
#zipkin服务所在地址
base-url: http://localhost:9411/
sender:
type: web #使用http的方式传输数据
#配置采样百分比
sleuth:
sampler:
probability: 1 # 将采样比例设置为 1.0,也就是全部都需要。默认是0.1也就是10%,一般情况下,10%就够用了
rate: 10 # 每秒采集的数量,默认是10,通过设置这个可以有效的避免消息过大
##打开debug日志
logging:
level:
org.springframework.web.servlet.DispatcherServlet: DEBUG
type=web也就是通过 HTTP 的方式发送数据到 Zipkin ,如果请求量比较大,这种方式其实性能是比较低的,一般情况下我们都是通过消息中间件来发送
采样率 ··
采样率决定了有多少请求被 Spring Cloud Sleuth 追踪。采样率的值介于 0 到 1 之间,其中 0 表示不追踪任何请求,1 表示追踪所有请求。通过调整采样率,我们可以根据需要平衡追踪数据的详细程度和系统性能的开销。
服务提供者代码:
@RestController
@RequestMapping("/payment")
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/zipkin")
public CommonResult<Object> paymentZipKin() {
return new CommonResult<>(200, "hi,I am paymentZipKin server O(∩_∩)O哈哈~", serverPort);
}
}
服务消费者Controller代码:
@RestController
@RequestMapping("/payment")
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/zipkin")
public CommonResult<Object> paymentZipKin() {
return new CommonResult<>(200, "hi,I am paymentZipKin server O(∩_∩)O哈哈~", serverPort);
}
}
更改Zipkin发送消息的方式
zipkin当中所展示的数据实际上都是由我们服务发送给zipkin他才将数据清洗,并展示出来的。默认采用的是HTTP请求方式来进行向zipkin发送的。
在实际开发当中HTTP请求方式,有时候势必会给我们服务器带来一些压力,并发量特别大的情况下,会占用大量线程。HTTP请求讲究的是,我发送给你,然后并且收到你的消息回复,这条连接才算结束。
所以基于这一点zipkin也给我们提供了可以通过消息中间件来进行发送,发送给消息中间件我们就不用管了,这样可以避免线程拥堵,目前支持RabbitMQ和Kafka、ActiveMQ!
以rabbit为例
POM依赖:
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
yml配置:
spring:
application:
name: sleuth-demo
zipkin:
base-url: http://localhost:9411/
sender:
type: rabbit
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
listener: # 这里配置了重试策略
direct:
retry:
enabled: true
sleuth:
sampler:
#采样率值介于 0 到 1 之间,1 则表示全部采集
probability: 1
rate: 10
修改Zipkin启动命令:
java -jar zipkin-server-2.23.18-exec.jar --RABBIT_ADDRESSES=127.0.0.1:5672 --RABBIT_USER=guest --RABBIT_PASSWORD=guest8
会自动创建队列 zipkin ,通过这个队列进行发送消息的