前言
看到文章的题目了吗?就是这么抽象和笼统的一个问题,确实是我面试中真实被问到的,某共享货车平台的真实面试问题。
SpringCloud确实是用过,但是那是三四年前了,那个时候SpringCloud刚开始流行没多久,我们技术总监让我们调研一下,然后算上我在内的三个同事就一人买了一本SpringCloud的书籍,开始看,开始研究,正好那个时候DDD也比较火,然后我们就一边研究的SpringCloud一边按照DDD的模型搭建自己的项目。但是这个项目最后做了三个月,才完成了一期。后面二期还没开始,我就撤了。所以SpringCloud总共的使用时间就两三个月,所以对这部分知识掌握的并不扎实,而且入职了新公司之后,都是使用公司自己封装的框架,也已经三年没有用过SpringCloud了,这次是要面试换工作了,所以决定将这方面的知识,总结一下。
服务治理 Spring Cloud Eureka
我们之前在使用服务之间相互调用的时候,一般是靠一些静态配置来完成的。比如服务A,要调用服务B来完成一个业务操作时,为了实现服务B的高可用,一般是通过手动配置来完成服务B的服务实例清单的维护。
随着业务的发展,系统功能越来越复杂,相应的服务不断增加,而且服务的IP还一直在变化,静态配置来维护各服务,就会变得越来越困难。
这个时候就出现了服务治理框架,Spring Cloud Eureka。
Spring Cloud Eureka 主要是围绕着服务注册与服务发现机制来完成对微服务的自动化管理的。
服务注册
Eureka提供了服务端组件,我们也称为注册中心。每个服务都向Eureka的服务注册中心,登记自己提供服务的元数据,包括服务的ip地址、端口号、版本号、通信协议等。这样注册中心,就将各个服务维护在了一个服务清单中(双层Map,第一层key是服务名,第二层key是实例名,value是服务地址加端口)。
服务注册中心,还会以心跳的方式去监听清单中的服务是否可用(默认30秒),若不可用(服务续约时间默认90秒),需从清单中剔除,达到排除故障服务的效果。
Eureka注册中心提供了高可用方案,可以支持集群部署注册中心,然后多个注册中心实例之间又相互注册,这样每个实例中都有一样的服务清单了。
服务发现
Eureka不但提供服务端,还提供了客户端,客户端是在各个服务中运行的。
Eureka客户端主要有两个作用:
- 向注册中心注册自身提供的服务,并周期性的发送心跳来更新它非服务租约。
- 同时,也能从服务端查询当前注册的服务信息,并把他们缓存到本地,并周期性的刷新服务状态。
在Eureka Server中注册的服务,相互之间调用,不再是通过指定的具体实例地址,而是通过向服务名发请求实现调用,因为每个服务服务都是多实例,并且实例地址还有可能经常变。
但是通过服务名称调用,并不知道具体的服务实例位置,因此需要向注册中心咨询,并获取所有服务实例清单,然后实现服务的请求访问。
举例
服务A的一个业务操作,需要调用服务B和服务C来完成。
那么服务A和服务B和服务C都将自己注册到Eureka的注册中心,然后服务A通过咨询注册中心,将注册中心的服务列表清单缓存到自己本地。
通过服务名称获取到服务B和服务C的服务实例地址,最后通过一种轮询策略取出一个具体的服务实例地址来进行调用。
总结一下
Eureka Client : 主要是将服务本身注册到Eureka Server中,同时查询Eureka Server的注册服务列表缓存到本地。
Eureka Server:注册中心,保存了所有注册服务的元数据,包括ip地址,端口等信息。
客户端负载均衡 Spring Cloud Ribbon
服务的调用方,在通过Eureka Client缓存到本地的注册表之后,通过服务名称,找到具体的服务对应的实例地址,但是被调用方的服务地址是有多个的,那么该用那个地址去进行调用呢?
服务A:
192.168.12.10:9001
192.168.12.11:9001
192.168.12.12:9001
这个时候Spring Cloud Ribbon就出现了,它是专门解决这个问题的,它的作用就是做负载均衡,会均匀的把请求分发到每台机器上。
Ribbon默认使用Round Ribbon的策略进行负载均衡,具体就是采用轮询的方式进行请求。
Ribbon除了有Round Ribbon这种轮询策略,还有其他策略以及自定义策略。
主要有:
- RandomRole: 从服务实例清单中随机选择一个服务实例。
- RoundRobinRule: 按照线性轮询的方式依次选择每个服务实例。
- RetryRule:根据轮询方式进行,且具备重试机制进行选择实例。
- WeightedResponseTimeRule:对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例。
- ZoneAvoidanceRule:根据服务方的zone区域和可用性来轮询选择。
Spring Cloud Ribbon具体的执行示例如下:
实例代码
下面的代码就是通过Ribbon调用服务的代码实例。
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/ribbon-consumer")
public String helloConsumer(){
return restTemplate.getForEntity("http://example-service/index",String.class).getBody();
}
}
可以看到Ribbon也是通过发起http请求,来进行的调用,只不过是通过调用服务名的地址来实现的。虽然说Ribbon不用去具体请求服务实例的ip地址或域名了,但是每调用一个接口都还要手动去发起Http请求,也是比较繁琐的,而且返回类型也比较抽象,所以Spring Cloud对调用方式进行了升级封装。
声明式服务调用 Spring Cloud Feign
Spring Cloud 为了简化服务间的调用,在Ribbon的基础上进行了进一步的封装。单独抽出了一个组件,就是Spring Cloud Feign。在引入Spring Cloud Feign后,我们只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。
Spring Cloud Feign具备可插拔的注解支持,并扩展了Spring MVC的注解支持。
下面我们来看一个具体的例子:
服务方具体的接口定义与实现代码如下:
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 接口定义
*/
@FeignClient(value="service-hi",fallback = TestFeignServiceImpl.class)
public interface TestFeignService {
@RequestMapping(value="/hi",method = RequestMethod.GET)
String sayHi(@RequestParam("name") String name);
}
/**
* 具体的服务实现
*/
@Component
public class TestFeignServiceImpl implements TestFeignService {
@Override
public String sayHi(String name) {
return "你好,"+name;
}
}
调用方的使用代码如下:
@RestController
public class TestController
{
@Resource
private TestFeignService testFeignService;
@RequestMapping(value="/hi",method = RequestMethod.GET)
public String sayHi(@RequestParam String name)
{
// 调用远程服务
return testFeignService.sayHi(name);
}
}
通过上面的代码,我们可以看到,调用方通过feign进程远程服务调用的时候,非常简单,就向是在调用本地服务一样。
像之前的建立连接,构造请求,发起请求,获取响应,解析响应等等操作,对使用者来说都是透明化的,使用者不用关心服务是怎么实现调用的,直接使用即可。
那么feign是如何实现这套封装逻辑的呢?
其实feign底层主要是靠动态代理来实现这整个服务的调用过程的。
主要逻辑如下:
- 如果一个接口上定义了@FeignClient注解,Feign就会根据这个接口生成一个动态代理类。
- 如果调用方,在调用这个定义了@FeignClient注解的接口时,本质上是会调用Feign生成的代理类。
- Feign生成的动态代理类,会根据具体接口方法中的@RequestMapping等注解,来动态构造出需要请求的服务地址。
- 最后针对这个地址,再通过Ribbon发起服务调用,解析响应等操作。
因为Spring Cloud Feign的使用方式比Spring Cloud Ribbon更方便,所以一般项目中都是使用Feign,而且Feign还有继承特性,可以将远程服务接口继承过来然后再进行自己的个性化扩展。因此Feign的使用范围以及普及率更高一些。
服务容错保护 Spring Cloud Hystrix
在微服务架构中,我们将系统拆分成多个服务单元,各个服务之间通过服务注册与订阅的方式互相依赖。
我们以一个电商网站下单的过程来举例,在下单的业务操作过程中需要调用库存服务,支付服务,积分、物流等服务。假设订单服务最多同一时间只能处理50个请求,这个时候如果积分服务挂了,那么每次订单服务去调用积分服务的时候,都会卡这么一段时间,然后才返回超时异常。
在这种场景下会有什么问题呢?
如果目前电商网站正在搞活动,进行抢购活动,下单的人非常多,这种高并发的场景下,订单服务的已经同时在处理50个下单请求了,并且都卡在了请求积分服务的过程中。订单服务已经没有能力去处理其他请求了。
那么其他服务再来调用订单服务时,发订单服务无响应,这样就导致订单服务也不可用了。然后其他依赖订单服务的服务,也最终会导致不可用。这就是微服务架构中的服务雪崩。
就上图所示,如果多个服务之间相互调用,而不做任何保护措施的话,那么一个服务挂了,就会产生连锁反应,导致其他服务也挂了。
其实就算是积分服务挂了,也并不应该导致订单服务也挂了,积分服务挂了,我们可以跳过积分服务,或者是放一个默认值,然后继续往下走,等着积分服务恢复了,可以手动恢复一下数据。
那么Spring Cloud Hystrix就是解决这个问题的组件,他主要是起到熔断,隔离,降级的作用。
Spring Cloud Hystrix其实是会为每一个服务开辟一个线程池,然后每个线程池中的线程用于对服务的调用请求。这样就算是积分服务挂了,那也只是调用积分服务的线程池出现问题了,而其他服务的线程池还正常工作。这就是服务的隔离。
这样订单服务在的调用积分服务的时候,如果发现有问题了,积分服务可以通过Hystrix返回一个默认值(默认是5秒内20次调用失败就熔断)。这样订单服务就不用在这里卡住了,可以继续往下调用其他服务进行业务操作了。这就是服务的熔断。
虽然说是积分服务挂了,并且也返回了默认值了,但是后续如果积分服务恢复了,想恢复数据怎么办呢?这个时候积分服务可以将姐收到的请求记录下来,或者是打到日志中,能为后面恢复数据提供依据就行。这就是服务的降级。
整个过程大致如下图所示:
API网关服务Spring Cloud Zuul
通过上面几个组件的结合使用,已经能够完成一个基本的微服务架构了。但是当一个系统中微服务的数量逐渐增多时,一些通用的逻辑,例如:权限校验机制,请求过滤,请求路由,限流等等,这些每个服务对外提供能力的时候都要考虑到的逻辑,就会变得冗余。
这个时候API网关的概念应运而生,它类似于面向对象设计模式中的Facade模式(门面模式/外观模式),所有的外部客户端访问都需要经过它来进行调度和过滤。主要实现请求路由、负载均衡、校验过滤、服务限流等功能。
Spring Cloud Zuul就是Spring Cloud提供的API网关组件,它通过与Eureka进行整合,将自身注册为Eureka下的应用,从Eureka下获取所有服务的实例,来进行服务的路由。
Zuul还提供了一套过滤器机制,开发者可以自己指定哪些规则的请求需要执行校验逻辑,只有通过校验逻辑的请求才会被路由到具体服务实例上,否则返回错误提示。
Spring Cloud Zuul的依赖包spring-cloud-starter-zuul
本身就包含了对spring-cloud-starter-hystrix
和spring-cloud-starter-ribbon
模块的依赖,所以Zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载功能。
Zuul的路由实现是通过Path和serviceId还实现的,path是一个http请求去除ip和端口号后的方法路径,例如:http://192.168.20.12:9001/api-zuul/123
,那么path就是/api-zuul/123
,Zuul在配置时支持模糊匹配,若123是动态参数,可以将path配置成/pai-zuul/**
,serviceId就是服务在Eureka中注册的服务名称。
zuul.routes.api-zuul.path= /api-zuul/**
zuul.routes.api-zuul.serviceId= service-jimoer
有了统一的网关后,再做统一的鉴权、限流、认证授权、安全等方面的工作就会变的更加方便了。
总结
上面总结了Spring Cloud的几个核心组件,其实Spring Cloud 除了这几个组件还有一些其他的组件,例如:
- 分布式配置中心:
Spring Cloud Config
; - 消息总线:
Spring Cloud Bus
; - 消息驱动:
Spring Cloud Stream
; - 分布式服务跟踪:
Spring Cloud Sleuth
。
主要是后面这些组件我们平时用的不多,而且对于微服务来说有些是有替代品的,所以我暂时就没有总结。还有一点毕竟我这次总结是为了解决面试的问题,所以后面如果在实际的工作中用到了剩下的这些组件,我会继续总结的。
好了,总结一下这次的几个组件的内容吧。
- Spring Cloud Eureka 各个微服务在启动时将自己注册到Eureka Server中,并且各个服务中的Eureka Client又能从注册中心获取各个服务的实例地址清单。
- Spring Cloud Ribbon 各个服务相互调用的时候,通过Ribbon来进行客户端的负载均衡,从多个实例中根据一定的策略选择一台进行请求。
- Spring Cloud Feign 基于动态代理机制,根据注解和参数拼接URL,选择具体的服务实例发起请求,简化了服务间相互调用的开发工作。
- Spring Cloud Hystrix 调用每个服务的时候都是通过线程池中的线程来发起的,不同的服务走不同的线程池,实现了服务的隔离,而且服务不可用时还提供了熔断机制以及支持降低措施。
- Spring Cloud Zuul 外部请求统一通过Zuul网关来进入,支持自定义路由规则,自定义过滤规则,可以实现同一的鉴权、限流、认证等功能。
最后来一个整体的架构图,将各个组件串起来。
转载:https://blog.csdn.net/qq_35165000/article/details/112129492