SpringCloud 入门
更新时间:2022.6.15 11:34 AM
SpringCloud 是在 SpringBoot 的基础上实现构建微服务中需要的 服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控 等操作的解决方案。
SpringCloud 的版本号不是使用数字命名的,而是伦敦的地铁站名称。
本文基于 SpringCloudNetflix 而非 SpringCloudAlibaba
发布于 CSDN 与个人博客 https://www.jdassd.top
参考 JavaGuide 中 SpringCloud 入门部分,进行少量修改,便于自身理解和阅览
此外 JavaGuide 中 SpringCloud 入门部分,出处为 https://juejin.im/post/5de2553e5188256e885f4fa3
SpringCloud 的服务发现框架——Eureka
原则为:消费者 Consumer 调用提供者 Provider 提供的服务;提供一个服务中心作为中介。
类似于房东与租客通过中介进行联系。
房东提供的房源需要进行注册,而租客需要通过中介获取房源信息。
服务提供方需要定期续约,以保障提供的服务存在(确保房屋存在);服务消费方也需要进行注册(租房合同的乙方);服务中心可能存在无法使用的问题,因此要提供多个服务中心(中介连锁店)。
服务注册 Register 过程
当 Eureka 客户端向 Eureka Server 注册时,提供自身的元数据。比如 IP 地址、端口、URL 等。
服务续约 Renew 过程
Eureka 客户端默认每间隔 30 秒发送一次心跳来进行续约。
Eureka Server 默认在 90 秒内没有收到续约,会将实例从注册表中删除。此过程也称服务剔除 Eviction。
获取注册列表信息 Fetch Registries
Eureka 客户端从服务器获取注册表信息,并缓存在本地。该注册表信息每 30 秒更新一次,针对 Eureka Server 返回注册列表信息与 Eureka 客户端本地缓存信息是否相同,Eureka 客户端进行自动处理。并且具有错误规避方案,如果由于某种原因导致注册列表信息不能及时匹配,Eureka 客户端会重新获取整个注册表信息。
Eureka 客户端与 Eureka Server 间默认采用 JSON 格式进行通信,Eureka Server 会对数据进行压缩处理,此外还可以采用 XML 进行通信。
服务下线 Cancel
Eureka 客户端在程序关闭时向 Eureka Server 发送取消请求。发送请求后,该客户端实例将从 Eureka Server 实例注册表中删除。
负债均衡 Ribbon
RestTemplate 是 Spring 提供的一个访问 Http 服务的客户端类,微服务之间的调用使用的就是 RestTemplate。Eureka 注册、续约等操作,实质使用的也是 RestTemplate。
Ribbon 的需求性
Ribbon 是 Netflix 公司开源的负载均衡项目,是客户端的负载均衡器,运行在消费者端。
秒杀系统,为了确保系统的高可用,常常需要为这个系统做一个集群。
如果仅对秒杀系统1进行大量调用,而另外两个基本不请求,那么就失去了集群的意义,也无法保证高可用。用户多了,秒杀系统1服务器可能会崩溃。
经过 Ribbon 的负载均衡算法,在 Consumer 端获取到服务列表后,进行多个系统的调用,使得集群中的机器调用更加合理,从而保证高并发时的高可用。
付简单面试题:分布式的三高为:高性能、高并发、高可用。
Ribbon 和 Nginx 负载均衡的区别
Nginx 是集中式的负载均衡器,会将所有请求都集中起来,再进行负载均衡。
Ribbon 是在消费者端进行的负载均衡。
Nginx 的请求是先进入负载均衡器,Ribbon 是先进行负载均衡后在进行请求,(相当于是一种输出了)。
Ribbon 的负载均衡算法
Nginx 使用的是轮询和加权轮询算法,Ribbon 默认使用的也是轮询策略。
-
RoundRobinRule:
默认的轮询策略,若没有找到可用的 provider 最多轮询 10 轮,若最终还是没有找到,则返回 null 。
-
RandomRule:
随机策略,从所有可用的 provider 中随机选择一个。
-
RetryRule:
重试策略,先按照默认策略获取 provider ,若失败,则在指定的时间内重试。默认为 500 ms。
除此之外,还有很多。
更换默认负载均衡策略,需要在配置文件中进行修改。
providerName:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
也可以自定义负载均衡策略,只需要实现 IRule 接口,然后修改配置文件;或者自定义 Java Config 类。
Open Feign
OpenFeign 是运行在消费者端,使用 Ribbon 进行负载均衡的服务映射调用方式。
// 使用 @FeignClient 注解来指定提供者的名字
@FeignClient(value = "eureka-client-provider")
public interface TestClient {
// 这里一定要注意需要使用的是提供者那端的请求相对路径,这里就相当于映射了
@RequestMapping(value = "/provider/xxx",
method = RequestMethod.POST)
CommonResponse<List<Plan>> getPlans(@RequestBody planGetRequest request);
}
然后在 Controller 就可以像原来调用 Service 一样调用它:
@RestController
public class TestController {
// 这里就相当于原来自动注入的 Service
@Autowired
private TestClient testClient;
// controller 调用 service 层代码
@RequestMapping(value = "/test", method = RequestMethod.POST)
public CommonResponse<List<Plan>> get(@RequestBody planGetRequest request) {
return testClient.getPlans(request);
}
}
Hystrix
Hystrix 是能进行 熔断 和 降级 的库,使用它可以提高整个系统的弹性。
假设有一个微服务系统,是服务 A 调用服务 B ,服务 B 调用服务 C ,但是因为某些原因,此时服务 C 阻塞,那么大量由服务 B 传递到 C 的请求得不到响应,因此服务 B 也会阻塞,同理服务 A 也会阻塞。
出现所有服务都阻塞的现象,称为服务雪崩。
而 熔断 就是服务雪崩的一种解决方案,当指定时间内的请求失败率达到设定阈值时,系统通过断路器将请求链路直接断开。
请求断开,避免了服务雪崩现象。
这里的 熔断 指的是 Hystrix
中的 断路器模式 ,使用 @HystrixCommand
注解来标注某个方法,这样 Hystrix
就会使用 断路器 来“包装”这个方法,每当调用时间超过指定时间时(默认为 1000ms),断路器将会中断对这个方法的调用。
设置超时时间:
@HystrixCommand(
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")}
)
public List<Xxx> getXxxx() {
// ...省略代码逻辑
}
而降级是指,当一个方法执行异常时,通过执行另一段代码,来给用户友好的回复。既 Hystrix 的后备处理模式。
通过设置 fallbackMethod
来进行服务降级,给一个方法设置备用的代码逻辑。
比如这个时候有一个热点新闻出现了,给用户推送详情,然后用户会通过 id 去查询新闻的详情,但是大量用户同时访问可能会导致系统崩溃,那么我们就进行 服务降级 ,一些请求会做一些降级处理,比如提示当前人数太多请稍后查看等等。
// 指定了后备方法调用
@HystrixCommand(fallbackMethod = "getHystrixNews")
@GetMapping("/get/news")
public News getNews(@PathVariable("id") int id) {
// 调用新闻系统的获取新闻api 代码逻辑省略
}
//
public News getHystrixNews(@PathVariable("id") int id) {
// 做服务降级
// 返回当前人数太多,请稍后查看
}
此外 Hystrix 还有舱壁模式和仪盘表,仪盘表用来实时监控 Hystrix 的各项指标信息。
微服务网关——Zuul
在 Eureka 中,服务提供者是消费者通过 Eureka Server 进行访问的,即 Eureka Server 是服务提供者的统一入口。但是消费者却没有统一的管理入口,因此需要引入一个消费者管理入口,而这个入口就是 Zuul,称为微服务网关。
Zuul 的功能类似于 Vue 中的路由 Router。
网关的概念
网关是系统唯一的对外入口,介于客户端与服务器端之间,用于请求的鉴权、限流、路由和监控等功能。
在 Zuul 中,最关键的功能为路由和过滤器。
Zuul 路由功能
向 Eureka Server
注册两个 Consumer
、三个 Provicer
,再加个 Zuul
网关后如图所示:
Zuul 向 Eureka 进行注册,注册后便可以通过获取注册列表得到所有 Consumer 信息,因此可以得到它们的元数据(如名称、IP、端口等),有了这些元数据,便可以实现路由映射了。
比如原来用户调用 Consumer1
的接口 localhost:8001/studentInfo/update
这个请求与调用 Consumer2
的接口 localhost:8002/studentInfo/update
这个请求,可以通过映射的方式,统一成:localhost:9000/consumer1/studentInfo/update
与localhost:9000/consumer2/studentInfo/update
。
因此,Zuul 最基本的配置,就是向 Eureka 注册自己本身:
server:
port: 9000
eureka:
client:
service-url:
# 这里只要注册 Eureka 就行了
defaultZone: http://localhost:9997/eureka
然后在启动类上加入@EnableZuulProxy
注解,开启 Zuul 代理即可使用 Zuul 的路由功能。
此外,还可以进行统一前缀的配置:
zuul:
prefix: /zuul
localhost:9000/consumer1/studentInfo/update
变为localhost:9000/zuul/consumer1/studentInfo/update
。
自定义路由策略的配置:
zuul:
routes:
consumer1: /FrancisQ1/**
consumer2: /FrancisQ2/**
使用:localhost:9000/FrancisQ1/studentInfo/update
进行访问。
自定义路由配置用来解决直接将微服务名称暴露给用户的问题。
配置完成自定义路由策略后,如果要停用原有的服务名称访问方式,需要进行如下配置:
zuul:
ignore-services: "*"
此外,还可以进行路径屏蔽配置:
路径屏蔽用于识别 URI 路径,只要请求中存在该配置的路径,那么就将无法访问到指定的服务。可以通过此方式限制用户的权限。
zuul:
ignore-patterns: **/auto/**
** 代表匹配多级任意路径
*代表匹配一级任意路径
默认情况下,Cookie、Set-Cookie 等敏感请求头信息会被 zuul 屏蔽。
Zuul 过滤功能
路由功能是进行地址映射,那么过滤功能,就是对请求进行限制和操作。
配置了 Zuul 网关后,所有的请求都要经过 Zuul ,那么可以进行限流、灰度发布、权限控制等等操作。
要实现自己定义的 Filter ,要继承 ZuulFilter 然后将这个过滤器用 @Component 注解加入到 Spring 容器即可。
过滤器类型:Pre
、Routing
、Post
。Pre
是前置过滤,也就是在请求之前进行过滤,Routing
路由过滤器就是路由策略,而Post
是后置过滤,是在 Response
前(响应前)过滤。
// 加入Spring容器
@Component
public class PreRequestFilter extends ZuulFilter {
// 返回过滤器类型 这里是前置过滤器
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
// 指定过滤顺序 越小越先执行,这里第一个执行
// 当然不是只真正第一个 在Zuul内置中有其他过滤器会先执行
// 那是写死的 比如 SERVLET_DETECTION_FILTER_ORDER = -3
@Override
public int filterOrder() {
return 0;
}
// 什么时候该进行过滤
// 这里我们可以进行一些判断,这样我们就可以过滤掉一些不符合规定的请求等等
@Override
public boolean shouldFilter() {
return true;
}
// 如果过滤器允许通过则怎么进行处理
@Override
public Object run() throws ZuulException {
// 这里我设置了全局的RequestContext并记录了请求开始时间
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("startTime", System.currentTimeMillis());
return null;
}
}
// lombok的日志
@Slf4j
// 加入 Spring 容器
@Component
public class AccessLogFilter extends ZuulFilter {
// 指定该过滤器的过滤类型
// 此时是后置过滤器
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
// SEND_RESPONSE_FILTER_ORDER 是最后一个过滤器
// 我们此过滤器在它之前执行
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
// 任何请求都执行
@Override
public boolean shouldFilter() {
return true;
}
// 过滤时执行的策略
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
// 从RequestContext获取原先的开始时间 并通过它计算整个时间间隔
Long startTime = (Long) context.get("startTime");
// 这里我可以获取HttpServletRequest来获取URI并且打印出来
String uri = request.getRequestURI();
long duration = System.currentTimeMillis() - startTime;
log.info("uri: " + uri + ", duration: " + duration / 100 + "ms");
return null;
}
}
上面就简单实现了请求时间日志打印功能,可以根据请求,打印日志。
令牌桶限流
因为会以一定的速率往令牌桶中放入令牌,而每次请求都要从桶中获取令牌,当获取不到令牌时,请求就被拒绝,因此完成了限流操作。
通过 Zuul 前置过滤器实现令牌桶限流:
package com.lgq.zuul.filter;
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
// 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -5;
}
@Override
public Object run() throws ZuulException {
log.info("放行");
return null;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()) {
log.warn("访问量超载");
// 指定当前请求未通过过滤
context.setSendZuulResponse(false);
// 向客户端返回响应码429,请求数量过多
context.setResponseStatusCode(429);
return false;
}
return true;
}
}
这样请求数量就控制在一秒两个。
Zuul
作为网关存在 单点问题 ,如果要保证 Zuul
高可用,需要配置 Zuul
集群,这个时候可以借助额外的一些负载均衡器比如 Nginx
。
SpringCloud 配置管理——Config
对于分布式的应用,会涉及到很多的框架和系统,如果一个个进行配置,那么将相当繁琐。同时修改配置后,需要将应用重新启动,那么就丢失了可用性,失去了分布式的意义。
基于以上两点,需要有一个既能对配置文件进行统一管理,又能在项目运行时进行动态修改和配置的方案。
SpringCloud 默认提供的方案为:Spring Cloud Config。(默认的 Config 方案有些地方实现的不那么尽如人意,因此可以选用 disconf 或者 阿波罗 等其他配置管理方案)。
图中的 Server 为 Config 提供的统一管理服务器,Git 为配置文件具体存放的统一位置。每次客户端启动时,通过请求 Config Server 拉取配置文件,然后再进行启动操作。
但是通过 Git 管理(即使通过 git 提供的 webhooks),也不能达到远程配置文件更新后本地同步更新的操作。
因此需要 Bus 消息总线 与 Spring Cloud Config 配合使用,进行配置文件的动态刷新。
Spring Cloud Bus
Spring Cloud Bus 消息总线的作用是管理和广播分布式系统中的消息,也就是消息引擎系统总的广播模式。
使用 Spring Cloud Bus 消息总线配合 Spring Cloud Config 只需要创建一个简单的请求,加上@ResfreshScope
注解即可。
总结
此文介绍了 Spring Cloud 中的以下组件:
Eureka
服务发现框架Ribbon
进程内负载均衡器Open Feign
服务调用映射Hystrix
服务降级熔断器Zuul
微服务网关Config
微服务统一配置中心Bus
消息总线
附上 Spring Cloud 架构图: