文章目录
SpringCloud简介
对于官方网站中的介绍:
Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state).
Spring Cloud
就是微服务系统架构的一站式解决方案,在平时我们构建微服务的过程中需要做如 服务发现注册 、配置中心 、消息总线 、负载均衡 、断路器 、数据监控 等操作,而 Spring Cloud
为我们提供了一套简易的编程模型,使我们能在 Spring Boot
的基础上轻松地实现微服务项目的构建。
总结来说就是:
SpringCloud=分布式微服务架构的站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶
先来看看Spring Cloud
有多少技术。
需要充分的认识到微服务不等于SpringCloud, Spring Cloud只是微服务落地的一种方式。具体看:微服务和SpringCloud的关系
为了更好的理解SpringCloud
, 把微服务的所有技术进行对比。
Dubbo | Spring Cloud | Spring Cloud Alibaba | |
---|---|---|---|
注册中心 | zookeeper、Redis | Eureka、Consul | Nacos、Eureka |
服务远程调用 | Dubbo协议 | Feign(http协议) | Dubbo、Feign |
配置中心 | SpringCloudConfig | SpringCloudConfig、Nacos | |
服务网关 | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul | |
服务监控和保护 | dubbo-admin,功能弱 | Hystix | Sentinel |
我们来看看一个微服务架构的例子架构图:
架构图展示了一个典型的微服务系统所具备的解决方案。
由于本文是一偏大致的介绍和总结文章,所以对一些组件粗略的进行介绍,其他的在后续会进行更新。
Spring Cloud 的服务发现框架——Eureka
Eureka是基于REST(代表性状态转移)的服务,主要在AWS云中用于定位服务,以实现负载均衡和中间层服务器的故障转移。我们称此服务为Eureka服务器。Eureka还带有一个基于Java的客户端组件Eureka Client,它使与服务的交互变得更加容易。客户端还具有一个内置的负载平衡器,可以执行基本的循环负载平衡。在Netflix,更复杂的负载均衡器将Eureka包装起来,以基于流量,资源使用,错误条件等多种因素提供加权负载均衡,以提供出色的弹性。
总的来说,Eureka
就是一个服务发现框架。
【Eureka】全网最形象生动的服务治理简介,5分钟带你入门eureka
平时找工作:
没有招聘平台(牛客,Boss等)的时候我们需要一个一个去寻找是否有要招聘的企业,这显然会非常的费力。
这里,我们就相当于是微服务中的 Consumer ,而需要招聘的企业相当于是微服务中的 Provider。
消费者 Consumer 需要调用提供者 Provider 提供的一些服务,就像我们需要去问他有没有工作岗位。
但是,如果是求职者和应聘者之间进行寻找的话,效率很低,企业招人的效率低,应聘者应聘的公司也可能找不到公司。后来,应聘者想到了在招聘平台发布自己的招聘信息,求职者在招聘平台投简历。而招聘平台就相当于是一个中介,统一提供了招聘信息,求职者也去招聘平台中求职。
现在我们的模式就是这样:
这时候会出现几个问题:
- 公司注册之后,没有发布岗位需求,就需要公司定期发布岗位信息,如果公司不发布就需要从招聘平台中剔除。
- 求职者也要注册。
- 招聘平台可以有多个,如果一个平台挂了,另一个平台可以顶上。
服务发现: 其实就是一个招聘平台,整个招聘过程角色有三个:服务提供者(招聘方)、服务消费者(求职者)、服务中介(求职中介平台)。
服务提供者: 能提供一些自己能够执行的一些服务给外界。
服务消费者: 需要使用一些服务的“用户”。
服务中介: 服务提供者和服务消费者之间的“桥梁”,服务提供者可以把自己注册到服务中介那里,而服务消费者如需要消费一些服务(使用一些功能)就可以在服务中介中寻找注册在服务中介的服务提供者。
服务注册 Register: Eureka 客户端向 [Eureka] Server 注册时,它提供自身的元数据 ,比如IP地址、端口,运行状况指示符URL,主页等。
服务续约 Renew: Eureka 客户会每隔30秒(默认情况下)发送一次心跳来续约 。通过续约来告知 [Eureka]Server 该 Eureka 客户仍然存在,没有出现问题。正常情况下,如果 [Eureka] Server 在90秒没有收到 Eureka 客户的续约,它会将实例从其注册表中删除。
获取注册列表信息 Fetch Registries: Eureka 客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka 客户端的缓存信息不同, Eureka 客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka 客户端则会重新获取整个注册表信息。Eureka 服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka 客户端和 Eureka 服务器可以使用JSON / XML格式进行通讯。在默认的情况下 Eureka 客户端使用压缩JSON 格式来获取注册列表的信息。
服务下线 Cancel: Eureka客户端在程序关闭时向Eureka服务器发送取消请求。发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:DiscoveryManager.getInstance().shutdownComponent();
服务剔除 Eviction : 在默认的情况下,当Eureka客户端连续90秒(3个续约周期)没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除 ,即服务剔除。
看看下图的Eureka
架构图:
当然,关于服务注册和发现组件有非常多:Zookeeper 、Consul 、 Eureka 等
负载均衡之 Ribbon
要弄明白Ribbon就不得不从RestTemplate
开始说起。
RestTemplate
RestTemplate是Spring提供的一个访问Http服务的客户端类 ,微服务之间的调用就是使用RestTemplate 。假如这时候 消费者B 需要调用 提供者A 所提供的服务,就需要使用下面代码来进行调用。
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2. 创造http请求url
String url = "http://localhost:9001/user/" +order.getUserId();
// 3.发送请求并且转换格式
User user = restTemplate.getForObject(url, User.class);
// 4.返回
order.setUser(user);
return order;
}
上面的代码是没有使用服务注册发现组件的,这时候就相当于求职者是向每一个公司都发出求职申请。
这个时候引入了Eureka
之后,就可以使用下面代码;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2. 创造http请求url
String url = "http://userservice/user/" +order.getUserId();
// 3.发送请求并且转换格式
User user = restTemplate.getForObject(url, User.class);
// 4.返回
order.setUser(user);
return order;
}
替换成服务名,也就是直接使用你想投递的岗位类型去集中投就行了,不需要一家一家的找公司,及时实际的公司地址一直在变。
如果源码有兴趣,会发现Eureka 框架中的 注册 、续约等,底层都是使用的 RestTemplate
Ribbon起的作用
Ribbon 是 Netflix 公司的一个开源的负载均衡 项目,是一个客户端/进程内负载均衡器,运行在消费者端 。
从Ribbon的作用上,可以配合Nginx一起理解,作用都是一样的,都是作为负载均衡组件来嵌入到系统中。其工作原理就是 Consumer 端获取到了所有的服务列表之后,在其内部 使用负载均衡算法 ,进行对多个系统的调用。
Nginx 和 Ribbon 的对比
Nginx和 Ribbon 不同的是,它是一种集中式 的负载均衡器。
什么是集中式?简单的理解就是把所有的请求都集中起来,然后再进行负载均衡。
Nginx 是接收了所有的请求进行负载均衡的,而对于 Ribbon 来说它是在消费者端进行的负载均衡。
在 Nginx 中请求是先进入负载均衡器,而在 Ribbon 中是先在客户端进行负载均衡才进行请求的
Ribbon 的几种负载均衡算法
负载均衡,不管 Nginx
还是 Ribbon
都需要其算法的支持, Nginx
使用的是 轮询和加权轮询算法。而在 Ribbon
中有更多的负载均衡调度算法,其默认是使用的 RoundRobinRule
轮询策略。
- RoundRobinRule :轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
- RandomRule : 随机策略,从所有可用的 provider 中随机选择一个。
- RetryRule : 重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
这里不进行一一列举了,只需要知道各个算法的大致工作原理即可,在配置文件中可以很方便的进行更换。
providerName:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
当然,Ribbon
中你还可以自定义负载均衡算法 ,你只需要实现IRule
接口,然后修改配置文件或者自定义 Java Config
类
Open Feign
有了Eureka,RestTemplate,Ribbon基本上就已经完成了初步的微服务拆分,但是使用 RestTemplate 还是不方便,我们每次都要进行这样的调用。
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2. 创造http请求url
String url = "http://userservice/user/" +order.getUserId();
// 3.发送请求并且转换格式
User user = restTemplate.getForObject(url, User.class);
// 4.返回
order.setUser(user);
return order;
}
每次开发都需要调用RestTemplate
的API是不是太麻烦了,我们能否像以前单体应用那样,直接进行服务端的调用,而不使用RestTemplate好像污染了代码的方式一样。
Open Feign
运势而生。
像我们上面写的代码一样,能否也和步骤2中一样使用映射的方法,将被调用的服务代码映射到消费者端,这样我们就可以 “无缝开发”。
OpenFeign 和Ribbon一样也是运行在消费者端,使用 Ribbon 进行负载均衡,所以 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
调用远程服务:
@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);
}
}
就相当于调用了一个Service
,丝毫不知道调用了远程服务。
Hystrix之熔断和降级
人称“豪猪哥”,Spring Cloud Netflix
的容错管理组件,为服务中出现的延迟和故障提供强大的容错能力。
在微服务架构中,一个应用往往由多个服务组成,这些服务之间相互依赖,依赖关系错综复杂。
假设在一个微服务的应用中存在服务A、B、C、D、E、F,依赖关系如下图:
明眼人都能看出,如果服务E挂了,对系统的危害非常的大。
现在假设我们的调用链路如下图所示:
当E发生故障的时候,会出现的情况有:
- 即使其他所有服务都可用,由于服务 E 的不可用,那么用户请求 1、2、3 都会处于阻塞状态,等待服务 E 的响应。在高并发的场景下,会导致整个服务器的线程资源在短时间内迅速消耗殆尽。
- 所有依赖于服务 E 的其他服务,例如服务 B、D 以及 F 也都会处于线程阻塞状态,等待服务 E 的响应,导致这些服务的不可用。
- 所有依赖服务B、D 和 F 的服务,例如服务 A 和服务 C 也会处于线程阻塞状态,以等待服务 D 和服务 F 的响应,导致服务 A 和服务 C 也不可用。
从以上过程可以看出,当微服务系统的一个服务出现故障时,故障会沿着服务的调用链路在系统中疯狂蔓延,最终导致整个微服务系统的瘫痪,这就是“雪崩效应”。
为了防止此类事件的发生,微服务架构引入了“熔断器”的一系列服务容错和保护机制
熔断器
熔断器(Circuit Breaker)一词来源物理学中的电路知识,它的作用是当线路出现故障时,迅速切断电源以保护电路的安全。
在微服务领域,熔断器最早是由 Martin Fowler 在他发表的 《Circuit Breaker》一文中提出。与物理学中的熔断器作用相似,微服务架构中的熔断器能够在某个服务发生故障后,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。
Spring Cloud Hystrix
所以熔断 就是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过 断路器 直接将此请求链路断开。 而使用Hystrix
组件的使用就是上述服务调用E在一定的时间窗口内,调用的失败率达到的一定的值,那么[Hystrix]
则会自动将 其他服务与E 之间的请求断了,以免导致服务雪崩现象。
这里的 熔断 就是指的 [Hystrix]中的 断路器模式 ,你可以使用简单的 @[Hystrix]Command
注解来标注某个方法,这样 [Hystrix]就会使用 断路器 来“包装”这个方法,每当调用时间超过指定时间时(默认为1000ms),断路器将会中断对这个方法的调用。
@HystrixCommand(
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")}
)
public List<Xxx> getXxxx() {
// ...省略代码逻辑
}
而关于降级操作是:为了更好的用户体验,当一个方法调用异常时,通过执行另一种代码逻辑来给用户友好的回复。 这也就对应着 [Hystrix]的 后备处理 模式。你可以通过设置fallbackMethod
来给一个方法设置备用的代码逻辑。比如有什么大瓜,由于访问的人数太多,为了避免系统崩溃,所以一些请求会做一些降级处理比如当前人数太多请稍后查看等等。
// 指定了后备方法调用
@HystrixCommand(fallbackMethod = "getHystrixNews")
@GetMapping("/get/news")
public News getNews(@PathVariable("id") int id) {
// 调用新闻系统的获取新闻api 代码逻辑省略
}
//
public News getHystrixNews(@PathVariable("id") int id) {
// 做服务降级
// 返回当前人数太多,请稍后查看
}
Gateway: Spring Cloud API网关组件
在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。
这种客户端直接请求服务的方式存在以下问题:
- 当服务数量众多时,客户端需要维护大量的服务地址,这对于客户端来说,是非常繁琐复杂的。
- 在某些场景下可能会存在跨域请求的问题。
- 身份认证的难度大,每个微服务需要独立认证。
我们可以通过 API 网关来解决这些问题,下面就让我们来看看什么是 API 网关。
API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。
API 网关就像整个微服务系统的门面一样,相当于学校的保安室、大门,所有的外来人都需要经过大门,
常见的API网关实现方案有一下5种:
Spring Cloud Gateway
、Spring Cloud Netflix Zuul
、Kong
、Nginx+Lua
、Traefik
Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
下面我们来理解Spring Cloud Gateway的核心概念:
核心概念 | 描述 |
---|---|
Route(路由) | 网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(Filter)组成。 |
Predicate(断言) | 路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。 |
Filter(过滤器) | 过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。 |
注意:其中 Route 和 Predicate 必须同时声明。
我们来看看Spring Cloud Gateway的工作流程:
- 客户端将请求发送到
Spring Cloud Gateway
上。 Spring Cloud Gateway
通过Gateway Handler Mapping
找到与请求相匹配的路由,将其发送给Gateway Web Handler
。Gateway Web Handler
通过指定的过滤器链(Filter Chain
),将请求转发到实际的服务节点中,执行业务逻辑返回响应结果。- 过滤器之间用虚线分开是因为过滤器可能会在转发请求之前(pre)或之后(post)执行业务逻辑。
- 过滤器(
Filter
)可以在请求被转发到服务端前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等。 - 过滤器可以在响应返回客户端之前,对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。
- 响应原路返回给客户端。
总而言之,客户端发送到 Spring Cloud Gateway
的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),我们还可以对请求和响应进行一些精细化控制。
Predicate
就是路由的匹配条件,而 Filter
就是对请求和响应进行精细化控制的工具。有了这两个元素,再加上目标 URI
,就可以实现一个具体的路由了。
Predicate 断言
Predicate
断言来实现Route
路由的匹配规则。简单点说,Predicate
是路由转发的判断条件,请求只有满足了 Predicate
的条件,才会被转发到指定的服务上进行处理。
需要注意的是:
- Route 路由与 Predicate 断言的对应关系为“一对多”,一个路由可以包含多个不同断言。
- 一个请求想要转发到指定的路由上,就必须同时匹配路由上的所有断言。
- 当一个请求同时满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。
是不是还是很懵?让我们来看看一些常见的断言例子。
Spring Cloud Gateway 动态路由
默认情况下,Spring Cloud Gateway
会根据服务注册中心(例如 Eureka Server
)中维护的服务列表,以服务名(spring.application.name
)作为路径创建动态路由进行转发,从而实现动态路由功能。
我们可以在配置文件中,将 Route
的 uri
地址修改为以下形式。
lb://service-name
Filter 过滤器
这里的过滤器可以和Spring Security
结合起来一起理解。出于安全方面的考虑,服务端提供的服务往往都会有一定的校验逻辑,例如用户登陆状态校验、签名校验等.
在微服务架构中,系统由多个微服务组成,所有这些服务都需要这些校验逻辑,此时我们就可以将这些校验逻辑写到Spring Cloud Gateway
的 Filter
过滤器中。
Filter 的分类
Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。
过滤器类型 | 说明 |
---|---|
Pre 类型 | 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。 |
Post 类型 | 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。 |
按照过滤器的作用范围划分,可以分为两类:
GatewayFilter 应用在单个路由或者一组路由上的过滤器
GlobalFilter 应用在所有的路由上的过滤器
GatewayFilter
GatewayFilter
是 Spring Cloud Gateway
网关中提供的一种应用在单个或一组路由上的过滤器。它可以对单个路由或者一组路由上传入的请求和传出响应进行拦截,并实现一些与业务无关的功能,比如登陆状态校验、签名校验、权限校验、日志输出、流量监控等。
对于Gateway Filter配置文件的写法和断言类似:
spring:
cloud:
gateway:
routes:
- id: xxxx
uri: xxxx
predicates:
- Path=xxxx
filters:
- AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 1024
- PrefixPath=/dept #在请求路径前面加上 /dept
……
Spring Cloud Gateway
内置了多达 31 种 GatewayFilter
,可以让我们程序开发者进行定制化处理。
GlobalFilter
GlobalFilter
是一种作用于所有的路由上的全局过滤器,通过它,我们可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter
组合成一个过滤器链。
关于默认的全局过滤器的详细内容,参考 Spring Cloud 官网。
Spring Cloud配置管理——Config
首先提出一个问题,为什么Config
会出现?
当我们的微服务系统开始慢慢地庞大起来,那么多 Consumer 、Provider 、[Eureka] Server 、Spring Gateway,系统都会持有自己的配置,这个时候我们在项目运行的时候可能需要更改某些应用的配置,如果我们不进行配置的统一管理,我们只能去每个应用下一个一个寻找配置文件然后修改配置文件再重启应用 。
首先对于分布式系统而言我们就不应该去每个应用下去分别修改配置文件,再者对于重启应用来说,服务无法访问所以直接抛弃了可用性,这是我们更不愿见到的。
那么有没有一种方法既能对配置文件统一地进行管理,又能在项目运行时动态修改配置文件呢?
能进行配置管理的框架不止 Spring Cloud Config 一种,大家可以根据需求自己选择(disconf,阿波罗等等)。而且对于 Config 来说有些地方实现的不是那么尽人意。
Config 是什么
Spring Cloud Config 为分布式系统中的外部化配置提供服务器和客户端支持。使用Config 服务器,可以在中心位置管理所有环境中应用程序的外部属性。
简单来说,Spring Cloud Config
就是能将各个 应用/系统/模块 的配置文件存放到 统一的地方然后进行管理 (Git
或者 SVN
)。
想一下,我们的应用是不是只有启动的时候才会进行配置文件的加载,那么我们的Spring Cloud Config
就暴露出一个接口给启动应用来获取它所想要的配置文件,应用获取到配置文件然后再进行它的初始化工作。就如下图。
tips: 这里使用 Webhooks ,这是 github 提供的功能,它能确保远程库的配置文件更新后客户端中的配置信息也得到更新。但是,如果没有使用webhooks, 在应用运行时去更改远程配置仓库(Git)中的对应配置文件,那么依赖于这个配置文件的已启动的应用进行其相应配置是不会进行更改的。
但是,问题出来了。Webhooks
虽然能解决,但是你了解一下会发现它根本不适合用于生产环境,所以基本不会使用它的
一般我们会使用 Bus 消息总线 + Spring Cloud Config 进行配置的动态刷新。
Spring Cloud Bus
用于将服务和服务实例与分布式消息系统链接在一起的事件总线。在集群中传播状态更改很有用(例如配置更改事件)。
你可以简单理解为 Spring Cloud Bus
的作用就是管理和广播分布式系统中的消息 ,也就是消息引擎系统中的广播模式。当然作为 消息总线 的 Spring Cloud Bus
可以做很多事而不仅仅是客户端的配置刷新功能。
而拥有了 Spring Cloud Bus
之后,我们只需要创建一个简单的请求,并且加上@ResfreshScope
注解就能进行配置的动态修改了。
以上就是我对Spring Cloud
一系列组件的总结。