Spring Cloud是微服务架构一站式解决方案,在平时构建项目时需要用到服务发现注册,负载均衡,断路器,数据监控,配置中心,消息总线等操作,而 Spring Cloud 为我们提供了一套简易的编程模型,使我们能在 Spring Boot 的基础上轻松地实现微服务项目的构建。
总的来说,Eureka
就是一个服务发现框架。何为服务,何又为发现呢?
举一个生活中的例子,就比如我们平时租房子找中介的事情。
在没有中介的时候我们需要一个一个去寻找是否有房屋要出租的房东,这显然会非常的费力,凭一个人的能力是找不到很多房源供你选择,再者你也懒得这么找下去(找了这么久,没有合适的只能将就)。这里的我们就相当于微服务中的 Consumer
,而那些房东就相当于微服务中的 Provider
。消费者 Consumer
需要调用提供者 Provider
提供的一些服务,就像我们现在需要租他们的房子一样。
但是如果只是租客和房东之间进行寻找的话,他们的效率是很低的,房东找不到租客赚不到钱,租客找不到房东住不了房。所以,后来房东肯定就想到了广播自己的房源信息(比如在街边贴贴小广告),这样对于房东来说已经完成他的任务(将房源公布出去),但是有两个问题就出现了。第一、其他不是租客的都能收到这种租房消息,这在现实世界没什么,但是在计算机的世界中就会出现 资源消耗 的问题了。第二、租客这样还是很难找到你,试想一下我需要租房,我还需要东一个西一个地去找街边小广告,麻不麻烦?
那怎么办呢?我们当然不会那么傻乎乎的,第一时间就是去找 中介 呀,它为我们提供了统一房源的地方,我们消费者只需要跑到它那里去找就行了。而对于房东来说,他们也只需要把房源在中介那里发布就行了。
但是,这个时候还会出现一些问题。
1.房东注册之后如果不想卖房子了怎么办?我们是不是需要让房东 定期续约 ?如果房东不进行续约是不是要将他们从中介那里的注册列表中 移除 。
2.租客是不是也要进行 注册 呢?不然合同乙方怎么来呢?
3.中介可不可以做 连锁店 呢?如果这一个店因为某些不可抗力因素而无法使用,那么我们是否可以换一个连锁店呢?
针对上面的问题我们来重新构建一下上面的模式图
服务发现:其实就是一个“中介”,整个过程中有三个角色:服务提供者(出租房子的)、服务消费者(租客)、服务中介(房屋中介)。
服务提供者: 就是提供一些自己能够执行的一些服务给外界。
服务消费者: 就是需要使用一些服务的“用户”。
服务中介: 其实就是服务提供者和服务消费者之间的“桥梁”,服务提供者可以把自己注册到服务中介那里,而服务消费者如需要消费一些服务(使用一些功能)就可以在服务中介中寻找注册在服务中介的服务提供者。
服务注册 Register:
官方解释:当 Eureka
客户端向 Eureka Server
注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。
结合中介理解:房东 (提供者 Eureka Client Provider
)在中介 (服务器 Eureka Server
) 那里登记房屋的信息,比如面积,价格,地段等等(元数据 metaData
)。
服务续约 Renew:
官方解释:Eureka
客户会每隔 30 秒(默认情况下)发送一次心跳来续约。 通过续约来告知 Eureka Server
该 Eureka
客户仍然存在,没有出现问题。 正常情况下,如果 Eureka Server
在 90 秒没有收到 Eureka
客户的续约,它会将实例从其注册表中删除。
结合中介理解:房东 (提供者 Eureka Client Provider
) 定期告诉中介 (服务器 Eureka Server
) 我的房子还租(续约) ,中介 (服务器Eureka Server
) 收到之后继续保留房屋的信息。
获取注册列表信息 Fetch Registries:
官方解释:Eureka
客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每 30 秒钟)更新一次。每次返回注册列表信息可能与 Eureka
客户端的缓存信息不同, Eureka
客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka
客户端则会重新获取整个注册表信息。 Eureka
服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka
客户端和 Eureka
服务器可以使用 JSON / XML 格式进行通讯。在默认的情况下 Eureka
客户端使用压缩 JSON
格式来获取注册列表的信息。
结合中介理解:租客(消费者 Eureka Client Consumer
) 去中介 (服务器 Eureka Server
) 那里获取所有的房屋信息列表 (客户端列表 Eureka Client List
) ,而且租客为了获取最新的信息会定期向中介 (服务器 Eureka Server
) 那里获取并更新本地列表。
服务下线 Cancel:
官方解释:Eureka 客户端在程序关闭时向 Eureka 服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:DiscoveryManager.getInstance().shutdownComponent();
结合中介理解:房东 (提供者 Eureka Client Provider
) 告诉中介 (服务器 Eureka Server
) 我的房子不租了,中介之后就将注册的房屋信息从列表中剔除。
服务剔除 Eviction:
官方解释:在默认的情况下,当 Eureka 客户端连续 90 秒(3 个续约周期)没有向 Eureka 服务器发送服务续约,即心跳,Eureka 服务器会将该服务实例从服务注册列表删除,即服务剔除。
结合中介理解:房东(提供者 Eureka Client Provider
) 会定期联系 中介 (服务器 Eureka Server
) 告诉他我的房子还租(续约),如果中介 (服务器 Eureka Server
) 长时间没收到提供者的信息,那么中介会将他的房屋信息给下架(服务剔除)。
假如一个订单服务只分布在一台服务器上,那么用户的请求都从这台服务器走,这样显然当某一时刻大量访问时造成服务器崩溃,因此考虑到高可用,可以采用集群的方式,就是把一个服务分布在多台机器上,这样就能减小服务器压力,在用户请求时,“太闲”的服务器给用户响应请求,“太忙”的服务器说明此时正在被其他大量用户访问,自己都忙不过来,那么就会叫其他服务器接待,这个过程就是负载均衡。其工作原理就是 Consumer
端获取到了所有的服务列表之后,在其内部使用负载均衡算法,进行对多个系统的调用,注意:Ribbon
是运行在消费者端的负载均衡器
提到 负载均衡 就不得不提到大名鼎鼎的 Nignx
了,而和 Ribbon
不同的是,它是一种集中式的负载均衡器。何为集中式呢?简单理解就是 将所有请求都集中起来,然后再进行负载均衡。如下图。
我们可以看到 Nginx
是接收了所有的请求进行负载均衡的,而对于 Ribbon
来说它是在消费者端进行的负载均衡。如下图。
请注意 Request
的位置,在 Nginx
中请求是先进入负载均衡器,而在 Ribbon
中是先在客户端进行负载均衡才进行请求的。
负载均衡,不管 Nginx
还是 Ribbon
都需要其算法的支持,如果我没记错的话 Nginx
使用的是 轮询和加权轮询算法。而在 Ribbon
中有更多的负载均衡调度算法,其默认是使用的 RoundRobinRule
轮询策略。
RoundRobinRule
:轮询策略。Ribbon
默认采用的策略。若经过一轮轮询没有找到可用的 provider
,其最多轮询 10 轮。若最终还没有找到,则返回 null
。RandomRule
: 随机策略,从所有可用的 provider
中随机选择一个。RetryRule
: 重试策略。先按照 RoundRobinRule
策略获取 provider
,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。- providerName:
- ribbon:
- NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
所谓 熔断 就是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过 断路器 直接将此请求链路断开。
降级是为了更好的用户体验,当一个方法调用异常时,通过执行另一种代码逻辑来给用户友好的回复。这也就对应着 Hystrix
的 后备处理 模式。你可以通过设置 fallbackMethod
来给一个方法设置备用的代码逻辑。比如这个时候有一个热点新闻出现了,我们会推荐给用户查看详情,然后用户会通过 id 去查询新闻的详情,但是因为这条新闻太火了(比如最近什么*易对吧),大量用户同时访问可能会导致系统崩溃,那么我们就进行 服务降级 ,一些请求会做一些降级处理比如当前人数太多请稍后查看等等。
- // 指定了后备方法调用
- @HystrixCommand(fallbackMethod = "getHystrixNews")
- @GetMapping("/get/news")
- public News getNews(@PathVariable("id") int id) {
- // 调用新闻系统的获取新闻api 代码逻辑省略
- }
- //
- public News getHystrixNews(@PathVariable("id") int id) {
- // 做服务降级
- // 返回当前人数太多,请稍后查看
- }
那么什么是 熔断和降级 呢?再举个例子,此时我们整个微服务系统是这样的。服务 A 调用了服务 B,服务 B 再调用了服务 C,但是因为某些原因,服务 C 顶不住了,这个时候大量请求会在服务 C 阻塞。 服务 C 阻塞了还好,毕竟只是一个系统崩溃了。但是请注意这个时候因为服务 C 不能返回响应,那么服务 B 调用服务 C 的的请求就会阻塞,同理服务 B 阻塞了,那么服务 A 也会阻塞崩溃。(为什么阻塞会崩溃。因为这些请求会消耗占用系统的线程、IO 等资源,消耗完你这个系统服务器不就崩了么。)
首先,Zuul
需要向 Eureka
进行注册,此时Consumer
也向 Eureka Server
进行注册,这样就可以获取所有的 Consumer
的元数据(名称,ip,端口)拿到这些元数据有什么好处呢?拿到了我们是不是直接可以做路由映射?比如原来用户调用 Consumer1
的口 localhost:8001/studentInfo/update
这个请求,我们是不是可以这样进行调用了呢?localhost:9000/consumer1/studentInfo/update
呢?你这样是不是恍然大悟了?
- server:
- port: 9000
- eureka:
- client:
- service-url:
- # 这里只要注册 Eureka 就行了
- defaultZone: http://localhost:9997/eureka
然后在启动类上加入 @EnableZuulProxy
注解就行了
统一前缀
这个很简单,就是我们可以在前面加一个统一的前缀,比如我们刚刚调用的是 localhost:9000/consumer1/studentInfo/update
,这个时候我们在 yaml
配置文件中添加如下。
- zuul:
- prefix: /zuul
这样我们就需要通过 localhost:9000/zuul/consumer1/studentInfo/update
来进行访问了。
路由策略配置
你会发现前面的访问方式(直接使用服务名),需要将微服务名称暴露给用户,会存在安全性问题。所以,可以自定义路径来替代微服务名称,即自定义路由策略。
- zuul:
- routes:
- consumer1: /FrancisQ1/**
- consumer2: /FrancisQ2/**
这个时候你就可以使用 ``localhost:9000/zuul/FrancisQ1/studentInfo/update` 进行访问了。
服务名屏蔽
这个时候你别以为你好了,你可以试试,在你配置完路由策略之后使用微服务名称还是可以访问的,这个时候你需要将服务名屏蔽。
- zuul:
- ignore-services: "*"
路径屏蔽
Zuul
还可以指定屏蔽掉的路径 URI,即只要用户请求中包含指定的 URI 路径,那么该请求将无法访问到指定的服务。通过该方式可以限制用户的权限。
- zuul:
- ignore-patterns: **/auto/**
这样关于 auto 的请求我们就可以过滤掉了。
Zuul 的过滤功能
如果说,路由功能是 Zuul
的基操的话,那么过滤器就是 Zuul
的利器了。毕竟所有请求都经过网关(Zuul),那么我们可以进行各种过滤,这样我们就能实现 限流,灰度发布,权限控制 等等
过滤器类型:Pre
、Routing
、Post
。前置Pre
就是在请求之前进行过滤,Routing
路由过滤器就是我们上面所讲的路由策略,而Post
后置过滤器就是在 Response
之前进行过滤的过滤器。你可以观察上图结合着理解
令牌桶限流
首先我们会有个桶,如果里面没有满那么就会以一定 固定的速率 会往里面放令牌,一个请求过来首先要从桶中获取令牌,如果没有获取到,那么这个请求就拒绝,如果获取到那么就放行
下面我们就通过 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;
- }
- }