1.springCloud组件运行流程
SpringCloud核心组件:Eureka注册中心
Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
心跳(续约):提供者定期通过HTTP方式向Eureka刷新自己的状态
springCloud核心组件:Feign
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等 等操作,一切都交给Feign去做。
@FeignClient("user-service")
public interface UserClient {
//http://user-service/user/123
@GetMapping("/user/{id}")
User queryById(@PathVariable("id") Long id);
}
首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟Mybatis的mapper很像 @FeignClient ,声明这是一个Feign客户端,同时通过 value 属性指定服务名称
接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
@GetMapping中的/user,请不要忘记;因为Feign需要拼接可访问的地址
编写新的控制器类 ConsumerFeignController ,使用UserClient访问:
@RestController
@RequestMapping("/cf")
public class ConsumerFeignController {
@Autowired
private UserClient userClient;
@GetMapping("/{id}")
public User queryById(@PathVariable Long id){
return userClient.queryById(id);
}
}
开启Feign功能 在 ConsumerApplication 启动类上,添加注解,开启Feign功能
@EnableFeignClients //开启feign功能
springCloud核心组件:Ribbon
实际环境中,我们往往会开启很多个service的集群,此时我们的列表就会有很多个,到底该访问那一个呢,一般情况下我们就需要编写负载均衡算法,在多个实列中进行选择,不过Eureka中已经集成了负载均衡组件Ribbon
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码: 在RestTemplate的配置方法上添加 @LoadBalanced 注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, User.class);
}
负载均衡策略
Ribbon默认的负载均衡策略是简单的轮询,
SpringBoot也帮我们提供了修改负载均衡规则的配置入口:
再次测试,发现结果变成了随机:
SpringCloud的核心组件:Hystrix
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务,防止出现级联失败。
雪崩问题
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用 链路:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成 雪崩效应。 这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么 就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这 个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。
一个零件的波及范围不断扩大 Hystrix解决雪崩问题的手段,主要包括: 线程隔离 服务降级
线程隔离&服务降级
解读: Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加 速失败判定时间。 用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超 时,则会进行降级处理,什么是服务降级?
服务降级:可以优先保证核心服务。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回 友好的提示信息) 。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它 服务没有响应。
触发Hystrix服务降级的情况:
线程池已满 请求超时
1引入依赖
2开启熔断在启动类 ConsumerApplication 上添加注解:@EnableCircuitBreaker
3编写降级逻辑
当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻 辑,要使用HystrixCommand来完成。
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallback")
public String queryById(@PathVariable Long id) {
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
public String queryByIdFallback(Long id) {
log.error("查询用户信息失败。id:{}", id);
return "对不起,网络太拥挤了!";
}
}
要注意;因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。 失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把queryById的方法改造为返回String, 反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
说明: @HystrixCommand(fallbackMethod = "queryByIdFallBack"):用来声明一个降级逻辑的方法
4默认的fallback 刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以可以把Fallback配置加在 类上,实现默认fallback;
@RestController
@RequestMapping("/consumer")
@Slf4j
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
//@HystrixCommand(fallbackMethod = "queryByIdFallback")
@HystrixCommand
public String queryById(@PathVariable Long id) {
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
public String queryByIdFallback(Long id) {
log.error("查询用户信息失败。id:{}", id);
return "对不起,网络太拥挤了!";
}
public String defaultFallback() {
return "默认提示:对不起,网络太拥挤了!";
}
}
5超时设置
在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystrix的默认超时时长为1,我们可以通过配置 修改这个值;修改 consumer-demo\src\main\resources\application.yml 添加如下配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 请求时长
服务熔断
在服务熔断中,使用的熔断器,也叫断路器,其英文单词为:Circuit Breaker 熔断机制与家里使用的电路熔断原理 类似;当如果电路发生短路的时候能立刻熔断电路,避免发生灾难。在分布式系统中应用服务熔断后;服务调用方 可以自己进行判断哪些服务反应慢或存在大量超时,可以针对这些服务进行主动熔断,防止整个系统被拖垮。 Hystrix的服务熔断机制,可以实现弹性容错;当服务请求情况好转之后,可以自动重连。通过断路的方式,将后续 请求直接拒绝,一段时间(默认5秒)之后允许部分请求通过,如果调用成功则回到断路器关闭状态,否则继续打 开,拒绝请求的服务。
Hystrix的熔断状态机模型
疯狂访问就可以触发熔断。不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 #服务降级超时时间
circuitBreaker:
errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20
Spring Cloud Gateway网关
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对 应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息, 从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册到Eureka服务注册中心。 网关的核心功能是:过滤和路由
. 核心概念
路由(route) 路由信息的组成:由一个ID、一个目的URL、一组断言工厂、一组Filter组成。如果路由断言 为 真,说明请求URL和配置路由匹配。
断言(Predicate) Spring Cloud Gateway中的断言函数输入类型是Spring 5.0框架中的 ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于HTTP Request中的 任何信息比如请求头和参数。
过滤器(Filter) 一个标准的Spring WebFilter。 Spring Cloud Gateway中的Filter分为两种类型的Filter,分 别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址
# uri: HTTP://127.0.0.1:9091
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/user/**
eureka:
client:
service-url:
defaultZone: HTTP://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
将符合 Path 规则的一切请求,都代理到 uri 参数指定的地址 本例中,我们将路径中包含有 /user/** 开头的请求, 代理到http://127.0.0.1:9091
路由前缀
客户端的请求地址与微服务的服务地址如果不一致的时候,可以通过配置路径过滤器实现路径前缀的添加和 去除。
添加前缀:对请求地址添加前缀路径之后再作为代理的服务地址;
http://127.0.0.1:10010/8 --> http://127.0.0.1:9091/user/8 添加前缀路径/user
去除前缀:将请求地址中路径去除一些前缀路径之后再作为代理的服务地址;
http://127.0.0.1:10010/api/user/8 --> http://127.0.0.1:9091/user/8 去除前缀路径/api
添加前缀
在gateway中可以通过配置路由的过滤器PrefixPath,实现映射路径中地址的添加; 修改 lxs-gateway\src\main\resources\application.yml 文件:
server:
port: 10010
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/**
filters:
# 添加请求路径的前缀
- PrefixPath=/user
eureka:
client:
service-url:
defaultZone: HTTP://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
通过 PrefixPath=/xxx 来指定了路由要添加的前缀。
也就是:
PrefixPath=/user http://localhost:10010/8 --》http://localhost:9091/user/8 PrefixPath=/user/abc http://localhost:10010/8 --》http://localhost:9091/user/abc/8
去除前缀
在gateway中可以通过配置路由的过滤器StripPrefix,实现映射路径中地址的去除; 修改 lxs-gateway\src\main\resources\application.yml 文件:
server:
port: 10010
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/api/user/**
filters:
# 表示过滤1个路径,2表示两个路径,以此类推
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: HTTP://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
通过 StripPrefix=1 来指定了路由要去掉的前缀个数。
如:路径 /api/user/1 将会被代理到 /user/1 。
也就是:
StripPrefix=1 http://localhost:10010/api/user/8 --》http://localhost:9091/user/8
StripPrefix=2 http://localhost:10010/api/user/8 --》http://localhost:9091/8
过滤器
Gateway作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作往往是通过网关提供的过滤器来实现 的。前面的 路由前缀 章节中的功能也是使用过滤器实现的。
配置全局默认过滤器
这些自带的过滤器可以和使用 路由前缀 章节中的用法类似,也可以将这些过滤器配置成不只是针对某个路由;而 是可以对所有路由生效,也就是配置默认过滤器:
server:
port: 10010
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# 路由id,可以随意写
- id: user-service-route
# 代理的服务地址
uri: lb://user-service
# 路由断言,可以配置映射路径
predicates:
- Path=/api/user/**
filters:
# 表示过滤1个路径,2表示两个路径,以此类推
- StripPrefix=1
# 默认过滤器,对所有路由都生效
default-filters:
- AddResponseHeader=X-Response-Foo, Bar
- AddResponseHeader=abc-myname,lxs
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
上述配置后,再访问 http://localhost:10010/api/user/7 的话;那么可以从其响应中查看到如下信息:
过滤器类型:Gateway实现方式上,有两种过滤器;
局部过滤器:通过 spring.cloud.gateway.routes.filters 配置在具体路由下,只作用在当前路由上; 如果配置spring.cloud.gateway.default-filters 上会对所有路由生效也算是全局的过滤器;但是这些过滤器 的 实现上都是要实现GatewayFilterFactory接口。
全局过滤器:不需要在配置文件中配置,作用在所有的路由上;实现 GlobalFilter 接口即可。
执行生命周期
Spring Cloud Gateway 的 Filter 的生命周期也类似Spring MVC的拦截器有两个:“pre” 和 “post”。“pre”和 “post” 分别会在请求被执行前调用和被执行后调用
使用场景
请求鉴权:一般 GatewayFilterChain 执行filter方法前,如果发现没有访问权限,直接就返回空。 异常处理:一般 GatewayFilterChain 执行filter方法后,记录异常并返回。
服务调用时长统计: GatewayFilterChain 执行filter方法前后根据时间统计。
Spring Cloud Config分布式配置中心
在分布式系统中,由于服务数量非常多,配置文件分散在不同的微服务项目中,管理不方便。为了方便配置文件集 中管理,需要分布式配置中心组件。在Spring Cloud中,提供了Spring Cloud Config,它支持配置文件放在配置服 务的本地,也支持放在远程Git仓库(GitHub、码云)。
在码云上创建仓库,在仓库里编写配置文件,配置文件的命名方式:{application}-{profile}.yml 或 {application}-{profile}.properties application为应用名称 profile用于区分开发环境,测试环境、生产环境等 如user-dev.yml,表示用户微服务开发环境下使用的配置文件。
搭建配置中心微服务,添加pom,创建启动类添加@EnableConfigServer //开启配置服务
配置文件
server:
port: 12000
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/lxsong77/lxs-config.git
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
可以启动测试
配置中心搭建完成,我们就需要改造前面写的service,配置文件不再由微服务项目提供,而是在配置中心获取,在service的pom中添加依赖,修改配置文件
Spring Cloud Bus服务总线
前面已经完成了将微服务中的配置文件集中存储在远程Git仓库,并且通过配置中心微服务从Git仓库拉取配置文 件, 当用户微服务启动时会连接配置中心获取配置信息从而启动用户微服务。 如果我们更新Git仓库中的配置文 件,那用户微服务是否可以及时接收到新的配置信息并更新呢?
修改在码云上的user-dev.yml文件,添加一个属性test.name 。
修改service代码
@Value("${test.name}")
private String name;
@GetMapping("/user/{id}")
public User queryById(@PathVariable Long id){
System.out.println("配置文件中的test.name为:" + name);
return userService.queryById(id);
}
依次启动注册中心 eureka-server 、配置中心 config-server 、用户服务 user-service ;然后修改Git仓库中的配 置信息,访问用户微服务,查看输出内容。
结论:通过查看用户微服务控制台的输出结果可以发现,我们对于Git仓库中配置文件的修改并没有及时更新到用 户微服务,只有重启用户微服务才能生效。
如果想在不重启微服务的情况下更新配置该如何实现呢? 可以使用Spring Cloud Bus来实现配置的自动更新。
Spring Cloud Bus简介
Spring Cloud Bus是用轻量的消息代理将分布式的节点连接起来,可以用于广播配置文件的更改或者服务的监控管 理。也就是消息总线可以为微服务做监控,也可以实现应用程序之间相互通信。 Spring Cloud Bus可选的消息代理 有RabbitMQ和Kafka。
在 config-server 项目的pom.xml文件中加入Spring Cloud Bus相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
在 config-server 项目修改application.yml文件如下:
server:
port: 12000
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://gitee.com/lxsong77/lxs-config.git
# 配置rabbitmq信息;如果是都与默认值一致则不需要配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
management:
endpoints:
web:
exposure:
# 暴露触发消息总线的地址
include: bus-refresh
改造用户服务
1. 在用户微服务 user-service 项目的pom.xml中加入Spring Cloud Bus相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2. 修改 user-service 项目的bootstrap.yml如下
spring:
cloud:
config:
# 要与仓库中的配置文件的application保持一致
name: user
# 要与仓库中的配置文件的profile保持一致
profile: dev
# 要与仓库中的配置文件所属的版本(分支)一样
label: master
discovery:
# 使用配置中心
enabled: true
# 配置中心服务名
service-id: config-server
# 配置rabbitmq信息;如果是都与默认值一致则不需要配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3. 改造用户微服务 user-service 项目的UserControlle
前面已经完成了配置中心微服务和用户微服务的改造,下面来测试一下,当我们修改了Git仓库中的配置文件,用 户微服务是否能够在不重启的情况下自动更新配置信息。
Spring Cloud 完整体系架构