赞
踩
上面这张图小伙伴们如果有些地方不太理解的话,接下来看完我这篇文章会理解。
Spring Cloud 是一个服务治理平台,是若干个框架的集合,提供了全套的分布式系统解决方案,在平时我们构建微服务的过程中需要做如服务发现注册、消息总线、负载均衡、配置中心、断路器、数据监控等操作,而 Spring Cloud 为我们提供了一套简易的编程模型,使我们能在 Spring Boot 的基础上轻松地实现微服务项目的构建。
Spring Cloud 通过 Spring Boot 风格的封装,屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、容易部署的分布式系统开发工具包。开发者可以快速的启动服务或构建应用、同时能够快速和云平台资源进行对接。
微服务是可以独立部署、水平扩展、独立访问的服务单元,Spring Cloud 就是这些微服务的大管家,采用了微服务这种架构之后,项目的数量会非常多,Spring Cloud 做为大管家需要管理好这些微服务,自然需要很多小弟来帮忙。
Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。
Eureka包含两个组件:Eureka Server(Eureka 服务注册中心) 和 Eureka Client(Eureka 客户端 )。
在应用启动后会向 Eureka Server 发送心跳,默认周期为30秒,如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,Eureka Server 将会从服务注册表中把这个服务节点移除(默认90秒)。
Eureka Server 之间通过复制的方式完成数据的同步,Eureka 提供了缓存机制,即便所有的Eureka Server都挂掉了,客户端依然可以利用缓存中的信息消费其他服务的API。
服务消费的流程如下:
关于 Eureka 的一些基础概念了,我们以上面的图进行讲解大家可能就更加容易明白了,理解起来会更简单。
当 Eureka Client 向 Eureka Service 注册时,Eureka Client 提供自身的元数据,比如 IP 地址、端口、运行状态指标的 URL、主页地址等信息。
结合上图理解:房东(提供者-Eureka Client Provider)在中介(服务器-Eureka Server)那里登记房屋的信息,比如面积,价格,地段等等。
Eureka Client 在默认的情况下会每隔30s发送一次心跳来进行服务续约。通过服务续约来告知 Eureka Service 该Eureka Client 仍然可用,没有出现故障。正常情况下,如果 Eureka Service 在 90 s内没有收到 Eureka Client 的心跳,Eureka Service 会将 Eureka Client 实例从注册列表中删除。
这里需要注意:官网不建议更改续约间隔。
结合上图理解:房东(提供者-Eureka Client Provider)定期告诉中介(服务器-Eureka Server)我的房子还租(续约) ,中介 (服务器-Eureka Server)收到之后继续保留房屋的信息。
Eureka Client 从 Eureka Service 获取服务注册信息,并将其缓存在本地。Eureka Client 会使用服务注册列表信息查找其它服务信息,从而进行远程调用。该注册列表信息定时(每30s)更新一次,每次返回注册列表信息可能与Eureka Client 的缓存信息不同,Eureka Client 会自己处理这些信息。如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 会重新获取整个注册表信息。Eureka Service 缓存了所有的服务注册列表信息,并将整个注册列表以及每个应用程序的信息进行压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Service 可以使用 JSON 和 XML 数据格式进行通信。在默认的情况下,Eureka Client 使用 JSON 格式的方式来获取服务注册列表的信息。
结合上图理解:租客(消费者-Eureka] Client Consumer)去中介 (服务器-Eureka Server) 那里获取所有的房屋信息列表 (客户端列表-Eureka Client List) ,而且租客为了获取最新的信息会定期向中介 (服务器-Eureka Server) 那里获取并更新本地列表。
Eureka Client 在程序关闭时可以向 Eureka Service 发送下线请求。发送请求后,该客户端的示例信息将从 Eureka Service 的服务注册列表中删除。该下线请求不会自动完成,需要在程序中关闭时调用代码:
DiscoveryManager.getInstance().shutdownComponent();
结合上图理解:房东 (提供者-Eureka Client Provider) 告诉中介 (服务器-Eureka Server) 我的房子不租了,中介之后就将注册的房屋信息从列表中剔除。
在默认情况下,当 Eureka Client 连续 90s 没有向 Eureka Service 发送服务续约(即心跳)时,Eureka Service 会将该服务实例从服务注册列表中删除,即服务剔除。
结合上图理解:房东(提供者-Eureka Client Provider) 会定期联系 中介 (服务器-Eureka Server) 告诉他我的房子还租(续约),如果中介 (服务器-Eureka Server) 长时间没收到提供者的信息,那么中介会将他的房屋信息给下架(服务剔除)。
以下图是官网描述的一个架构图:
讲解Ribbon之前我们先讲一下RestTemplate,如果看过我之前的文章的小伙伴应该知道服务直接的调用是通过RestTemplate实现的。微服务之间的调用是使用的RestTemplate,我们上面所讲的 Eureka 框架中的注册、续约等,如果看一下源码,就知道底层都是使用的RestTemplate实现,比如以下的代码:
@RestController
public class ProviderController {
@Autowired
private RestTemplate restTemplate ;
@RequestMapping("/getInfo1002")
public String get1001Info (){
return restTemplate.getForObject("http://why-provider-1002/getInfo/55555",String.class) ;
}
}
这是一个请求如果被Ribbon代理之后,请求的执行流程如下图所示
Ribbon 是 Netflix 公司的一个开源的具有负载均衡项目,Ribbon 是一个 http 客户端,它具备了负载均衡,失败重试,ping等功能,通过分析源码可知。
比如我们经常使用的 httpClient 就是一个 http 客户端,它就是用来发送 http 请求的,但是 Ribbon 在 httpClient 上做了更多的封装,满足更好的使用。
综上所述,Ribbon 是一个具有负载均衡等功能的 http 客户端,而并不只是负载均衡工具。
Ribbon是运行在消费者端的,它的工作原理就是 Consumer 端获取到了所有的服务列表之后,在其内部使用负载均衡算法,进行对多个系统的调用。如下图:
Nignx:集中式的负载均衡器,就是将所有请求都集中起来,然后再进行负载均衡
Ribbon:消费者端进行负载均衡
下面我们用两种图对比:
Nignx:
Ribbon:
这里两个的区别在于:
Ribbon 的几种负载均衡算法:
负载均衡不管是 Nginx 还是 Ribbon 都需要算法的支持,Nginx 使用的是轮询和加权轮询算法,而在Ribbon中有更多的负载均衡调度算法,默认的是RoundRobinRule轮询策略,其他算法比如:
Spring Cloud的 OpenFeign 用于 Spring Boot 应用程序的声明式 REST客户端。 Feign 创建一个用 JAX-RS 或 Spring MVC 注释修饰的接口的动态实现。
使用Ribbon我们就可以愉快地进行服务间的调用了,但是使用 RestTemplate 还是有不足的地方,比如下面的代码:
@RestController
public class ProviderController {
@Autowired
private RestTemplate restTemplate ;
@RequestMapping("/getInfo1002")
public String get1001Info (){
return restTemplate.getForObject("http://why-provider-1002/getInfo/55555",String.class) ;
}
}
每次都调用 RestRemplate 的 API 那也太麻烦了,那能不能像调用原来代码一样进行各个服务间的调用呢?
当然是利用 OpenFeign 来实现,与 Ribbon 不同的是,通过 feign 只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
// 使用 @FeignClient 注解来指定提供者的名字
@FeignClient("why-nacos-server")
public interface MsgFeign {
// 这里需要注意使用的是提供者端的请求相对路径,这里就相当于映射了
@GetMapping("/web/getMsg")
String getMsg (@RequestParam(name = "name") String name);
}
然后在 Controller 层就可以像原来调用 Service 层一样调用它了。
@RestController
public class ServerWeb {
@RequestMapping(value = "/web/getMsg",method = RequestMethod.GET)
public String getMsg (@RequestParam("name") String name){
log.info("服务被调用...");
return "Hello:" + name ;
}
}
在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现这一点,所有这些都可以提高系统的整体弹性。
总的来说 Hystrix 就是一个能进行熔断和降级的库,通过使用它能提高整个系统的弹性。
分布式系统环境下,服务间依赖非常常见,一个业务调用通常依赖多个基础服务。如下图:
上面各个服务正常运行,但如果其中一个服务崩坏掉会出现什么样的情况呢?
以订单为例:对于同步调用,当库存服务不可用时,商品服务请求线程被阻塞,当有大批量请求调用库存服务时,最终可能导致整个商品服务资源耗尽,无法继续对外提供服务。并且这种不可用可能沿请求调用链向上传递,这种现象被称为么服务雪崩,如下图:
上面说到为什么阻塞会崩溃,这是因为这些请求会消耗占用系统的线程、IO 等资源,消耗完这个系统服务器不就崩了吗?
所谓熔断就是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过断路器直接将此请求链路断开。
那有小伙伴就问了,上面说熔断和降级好好的,怎么又扯出一个服务雪崩的问题来?
其实熔断是服务雪崩的一种有效解决方案。当在指定时间内的请求失败率达到设定阈值时,系统将通过断路器直接将此请求链路断开。
也就是说上面的商品服务调用库存服务在指定时间内,调用的失败率到达了一定的值,那么Hystrix则会自动将商品服务与库存服务之间的请求都断了,以免导致服务雪崩现象。
这里使用@HystrixCommand注解来标识getOrder()方法,这样Hystrix就会使用断路器来包装这个方法,每当调用时间超过指定时间时(默认为1000ms),断路器将会中断对这个方法的调用。
@HystrixCommand(
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")}
)
public List<Order> getOrder() {
// ...省略
}
而降级是为了更好的用户体验,当一个方法调用异常时,通过执行另一种代码逻辑来给用户友好的回复。
比如大量用户同时访问可能会导致系统崩溃,那么我们就进行服务降级,一些请求会做一些降级处理比如当前人数太多请稍后查看等提示给用户。
@HystrixCommand(fallbackMethod = "getHystrixNews")
@GetMapping("/get/User")
public News getUser(@PathVariable("id") int id) {
//大量用户服务的代码逻辑
}
public News getHystrixUser(@PathVariable("id") int id) {
// 做服务降级
// 返回当前人数太多,请稍后查看
}
Zuul是 Spring Cloud 中的微服务网关,而网关是一个网络整体系统中的前置门户入口。请求首先通过网关,进行路径的路由,定位到具体的服务节点上。
我们都知道在微服务架构中,系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?难道要一个个的去调用吗?很显然这是不太实际的,我们需要有一个统一的接口与这些微服务打交道,这就是我们需要服务网关的原因。
我们知道在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用 N 个微服务的接口完成一个用户请求。比如:用户查看一个商品的信息,它可能包含商品基本信息、价格信息、评论信息、库存信息等等,而这些信息获取则来源于不同的微服务,诸如商品系统、价格系统、评论系统、库存系统等等,那么要完成用户信息查看则需要调用多个微服务,这样会带来以下几个问题:
如下图所示:
那么我们该如何解决这些问题呢?试想一下不要让前端直接知道后台诸多微服务的存在,我们的系统本身就是从业务领域的层次上进行划分,形成多个微服务,这是后台的处理方式。对于前台而言,后台应该仍然类似于单体应用一样,一次请求即可,于是我们可以在客户端和服务端之间增加一个API网关,所有的外部请求先通过这个微服务网关。它只需跟网关进行交互,而由网关进行各个微服务的调用。
这样我们就可以解决上面提到的问题,还可以有如下优点:
API网关它是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等等。
有网关和无网关的区别如下图:
服务网关大概就是四个功能:统一接入、流量管控、协议适配、安全维护。而在目前的网关解决方案里,有Nginx+ Lua、Kong、Tyk以及Spring Cloud Zuul等等。这里以Zuul为例进行说明,它是Netflix公司开源的一个API网关组件,Spring Cloud对其进行二次封装做到开箱即用。同时,Zuul还可以与Spring Cloud中的Eureka、Ribbon、Hystrix等组件配合使用。
Zuul中最关键的就是路由和过滤器了:
比如这个时候我们向 Eureka Server 注册了两个 Consumer、三个 Provicer,这个时候我们加个 Zuul 网关,如下图所示:
首先,Zuul 向 Eureka 进行注册,Zuul 就能拿到所有 Consumer 的信息了。能拿到信息,我们是不是可以获取所有的 Consumer 的元数据(例如名称,ip,端口)。
那拿到这些数据我们有什么用呢?
拿到了我们就可以做路由映射,比如原来用户调用 Consumer1 的接口:localhost:8001/Info/update这个请求,我们是不是可以这样进行调用了呢?localhost:8000/consumer1/Info/update呢?到这里小伙伴是不是就明白了呢?
在yml中加上以下的配置,就能Zuul注册到Eureka:
server:
port: 3001
spring:
application:
name: why-cloud-parent
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:2001/eureka/
然后在启动类上加入 @EnableZuulProxy 注解就行了。
zuul:
# 前缀,可以用来做版本控制
prefix: /pre
就是我们在访问路径前面加一个统一的前缀,比如我们刚刚调用的是localhost:8000/consumer1/Info/update,现在变成localhost:8000/pre/consumer1/Info/update来进行访问了。
zuul:
# 前缀,可以用来做版本控制
prefix: /pre
# 禁用默认路由,执行配置的路由
ignored-services: "*"
routes:
# 配置1001接口微服务
pro6001:
serviceId: why-provider-1001
path: /api-1001/**
# 配置1002接口微服务
pro6002:
serviceId: why-provider-1002
path: /api-1002/**
我们可以发现前面的访问方式是直接使用服务名,需要将微服务名称暴露给用户,会存在安全性问题。所以,可以自定义路径来替代微服务名称,即自定义路由策略。这个时候就可以使用localhost:8000/pre//api-1001//Info/update进行访问了。
这里有个问题,就是在配置完路由策略之后使用微服务名称还是可以访问得到的,这个时候需要将服务名进行屏蔽,如下:
zuul:
ignore-services: "*"
zuul:
ignore-patterns: **/auto/**
Zuul还可以指定屏蔽掉的路径 URI,即只要用户请求中包含指定的 URI 路径,那么该请求将无法访问到指定的服务。通过该方式可以限制用户的权限。
** 代表匹配多级任意路径
*代表匹配一级任意路径
可看这篇文章 SpringCloud微服务-实现ZUUL路由网关控制/服务限流和降级
通过前面的学习,我们知道 Zuul 包含了两个核心功能:对请求的路由和过滤。其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础。
而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。其实,路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以,过滤器可以说是 Zuul 实现 API 网关功能最为核心的部件,每一个进入 Zuul 的 http 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景:
请求到达首先会经过 pre 类型过滤器,而后到达 routing 类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达 post 过滤器。而后返回响应。
整个过程中,pre 或者 routing 过滤器出现异常,都会直接进入 error 过滤器,再由 error 处理完毕后,会将请求交给 post 过滤器,最后返回给用户。
如果是 error 过滤器自己出现异常,最终也会进入 post 过滤器,而后返回。
如果是 post 过滤器出现异常,会跳转到 error 过滤器,但是与 pre 和 routing 不同的是,请求不会再到达 post 过滤器了。
接下来我们来自定义一个过滤器:
@Component
public class FilterConfig extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(FilterConfig.class) ;
@Override
// 指定该过滤器的过滤类型
// 此时是后置过滤器
public String filterType() {
return "pre";
}
@Override
// SEND_RESPONSE_FILTER_ORDER 是最后一个过滤器
// 我们此过滤器在它之前执行
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
// 过滤时执行的策略
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext() ;
try {
doBizProcess(requestContext);
} catch (Exception e){
LOGGER.info("异常:{}",e.getMessage());
}
return null;
}
public void doBizProcess (RequestContext requestContext) throws Exception {
HttpServletRequest request = requestContext.getRequest() ;
// 这里我可以获取HttpServletRequest来获取URI并且打印出来
String reqUri = request.getRequestURI() ;
if (!reqUri.contains("getInfo")){
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(401);
requestContext.getResponse().getWriter().print("访问的路径不存在");
}
}
}
上面就简单实现了请求地址错误的功能,到这里小伙伴们有感受到Zuul过滤器功能强大之处了吗
当我们的微服务系统开始慢慢地庞大起来,那么多 Consumer、Provider、Eureka Server、Zuul 系统都会持有自己的配置,这个时候我们在项目运行的时候可能需要更改某些应用的配置,如果我们不进行配置的统一管理,我们只能去每个应用一个个找配置文件然后修改配置文件再重启应用。
首先对于分布式系统而言我们就不应该去每个应用下去分别修改配置文件,再者对于重启应用来说,服务无法访问所以直接抛弃了可用性,这是我们更不愿见到的。
那么有没有一种方法既能对配置文件统一地进行管理,又能在项目运行时动态修改配置文件呢?
当然有了,那就是接下来要介绍的 Spring Cloud Config。
Spring Cloud Config为分布式系统中的外部化配置提供服务器和客户端支持。使用Config服务器,可以在中心位置管理所有环境中应用程序的外部属性。
简单来说,Spring Cloud Config就是能将各个 应用/系统/模块 的配置文件存放到统一的地方然后进行管理(Git 或者 SVN)。
下图是一个 Spring Cloud Config 配置结构图:
当然,这里还有一个问题是:如果我在应用运行时去更改远程配置仓库(Git)中的对应配置文件,那么依赖于这个配置文件的已启动的应用会不会进行其相应配置的更改呢?肯定是不会的。
那这么解决呢?我们可以使用 Webhooks ,这是 github 提供的功能,它能确保远程库的配置文件更新后客户端中的配置信息也得到更新。
这里需要注意一点是:Webhooks 虽然能解决,但是你了解一下会发现它根本不适合用于生产环境,所以基本不会使用它的。
一般会使用 Bus 消息总线 + Spring Cloud Config 进行配置的动态刷新。
用于将服务和服务实例与分布式消息系统链接在一起的事件总线。在集群中传播状态更改很有用,比如配置更改。
简单点理解 Spring Cloud Bus 的作用是管理和广播分布式系统中的消息,也就是消息引擎系统中的广播模式。当然作为消息总线的Spring Cloud Bus可以做很多事而不仅仅是客户端的配置刷新功能,
而拥有了Spring Cloud Bus之后,我们只需要创建一个简单的请求,并且加上 @ResfreshScope 注解就能进行配置的动态修改了。
下面一张图让小伙伴更容易理解:
上面这张图大家发现没有,会存在一些问题:
我们换种方式实现,配置中心 Server 端承担起配置刷新的职责,如下图所示:
执行流程:
总结
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。