Spring Cloud

  • 服务发现
  • 分类
  • 注册中心需要考虑的问题
  • 服务调用
  • 负载均衡
  • 客户端负载均衡
  • 服务容错
  • 容错策略
  • 容错设计模式
  • 服务网关
  • 限流算法
  • 流量统计指标
  • 限流设计模式
  • 消息总线和配置
  • 常用的框架
  • Nacos
  • Feign
  • 分布式负载均衡算法
  • Hystrix
  • Gateway
  • 高可用和高并发
  • 高并发情况下如何保证接口的幂等性



Spring Cloud 是一系列框架的集合,它里面的框架解决了微服务需要解决的各种问题

而这一系列框架分别实现了如服务发现注册 、配置中心 、消息总线 、负载均衡 、断路器 、数据监控等操作

本文简单说明一下,各个组件的思想、为什么会出现这些组件以及组建的简单实现

服务发现

所有的远程服务调用都是使用全限定名(Fully Qualified Domain Name,FQDN (opens new window))、端口号与服务标识所构成的三元组来确定一个远程服务的精确坐标的

全限定名代表了网络中某台主机的精确位置,端口代表了主机上某一个提供了 TCP/UDP 网络服务的程序,服务标识则代表了该程序所提供的某个具体的方法入口

那我们如何让程序知道哪些服务器提供了哪些服务、哪些服务不能用呢

分类

服务发现一般分为以下两种:

第一种是类似 DNS 服务一样的服务发现,即将用户的域名转换为网络上的真实地址全限定名加端口

第二种是以 UDDI 为代表的“百科全书式”的服务发现,上至提供服务的企业信息(企业实体、联系地址、分类目录等等),下至服务的程序接口细节(方法名称、参数、返回值、技术规范等等)都在服务发现的管辖范围之内

由于注册中心是整个服务协调的灵魂,如果注册中心挂了那么就算提供了服务也无法被调用,需要在高准确性的同时满足高性能,以此必须同时满足可用性和可靠性

注册中心需要考虑的问题

RPC 中的注册中心,用于让不同服务器之间了解对方的方法,因此需要有一套完整的注册、校验、删除机制,万一提供服务的机器挂了或者想在集群中加一台机器,此时应该做些什么呢

服务注册:当客户端向注册中心注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL 等

服务续约:心跳检验机制,为了了解客户仍然存在,没有出现问题,如果注册中心没有在一定时间收到服务提供方的消息,它会将实例从其注册表中删除

获取注册列表信息:客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期更新一次(客户端负载均衡)

服务务发现:这里的发现是特指狭义上消费者从服务发现框架中,把一个符号转换为服务实际坐标的过程,这个过程现在一般是通过 HTTP API 请求或者通过 DNS Lookup 操作来完成,也还有一些相对少用的方式

服务下线:客户端在程序关闭时向服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除

服务剔除:客户端出现故障时,注册中心会通过心跳校验了解情况,并自动将其从注册表中剔除

服务调用

服务发现之后,需要使用一些框架进行不同服务器之间的方法相互调用

服务调用就和 RPC 过程相似,使用序列化协议与传输协议来进行数据的传输,springcloud 的框架就是解决这一问题

负载均衡

在服务调用时,一般会进行负载均衡的判断,由此来选择最为合适的服务器。因为微服务本身就是解决大量请求的方案,因此为了整个系统的高可用 ,我们需要将服务器做一个集群,为了不让集群中的某一个机器承受更多请求,负载均衡器也是必须的

客户端负载均衡

服务端负载均衡:之前说到的服务调用负载均衡在客户端进行负载均衡才进行请求的服务网关负载均衡在网关或者 nginx 进行负载均衡,这是一种集中式的负载均衡,也叫客户端负载均衡

客户端负载均衡:客户端均衡器是和服务实例一一对应的,而且与服务实例并存于同一个进程之内,均衡器与服务之间信息交换是进程内的方法调用,不存在任何额外的网络开销,以此调用方法会少一次网络请求

服务容错

在微服务中有一些特征是无法做出妥协的,其中的典型就是容错性设计。一个大的服务集群中,程序可能崩溃、节点可能宕机、网络可能中断,这些意外情况其实全部都在意料之中,发生错误之后如何才能让整个服务正常向外部提供服务,这个问题便是重中之重

容错策略

容错策略指的是面对故障,我们该做些什么,这些策略提供一些指导思想,让我们遇到错误时不至于漫无目的

1,故障转移

故障转移是指如果调用的服务器出现故障,系统不会立即向调用者返回失败结果,而是自动切换到其他服务副本,尝试其他副本能否返回成功调用的结果

故障转移的实现应当有一定的调用次数限制,以防止过多的错误调用影响系统性能

同时,被调用的接口应当有幂等性,比如 get、remove、put,不然可能会生成脏数据

2,快速失败

快速失败一般使用于一些在与金额支付相关的操作中,比如在支付场景中,需要调用银行的扣款接口,如果该接口返回的结果是网络异常,程序是很难判断到底是扣款指令发送给银行时出现的网络异常,还是银行扣款后返回结果给服务时出现的网络异常的。为了避免这种情况,程序应当尽快抛出异常,由调用者自行处理

3,安全失败

对于一些不重要的业务,一种理想的容错策略是即使旁路逻辑调用实际失败了,也当作正确来返回,如果需要返回值的话,系统就自动返回一个符合要求的数据类型的对应零值,然后自动记录一条服务调用出错的日志备查即可,这种策略被称为安全失败

4,沉默失败

在大量的请求需要等到超时(或者长时间处理后)才宣告失败的时候,很容易由于某个远程服务的请求堆积而消耗大量的线程、内存、网络等资源。此时我们让机器在一段时间内不在对外提供同类型服务,因为该次失败很可能下一次调用也失败。沉默失败会让系统不再向错误机器分配请求流量,将错误隔离开来,避免对系统其他部分产生影响

5,并行调用和广播调用

这两种算是以性能获取准确性的方法,希望在调用之前就开始考虑如何获得最大的成功概率。并行调用是指一开始就同时向多个服务副本发起调用,只要有其中任何一个返回成功,那调用便宣告成功。而广播调用则是要求所有的请求全部都成功,这次调用才算是成功

6,故障恢复

故障恢复一般不单独存在,而是作为其他容错策略的补充措施,一般在微服务管理框架中,如果设置容错策略为故障恢复的话,通常默认会采用快速失败加上故障恢复的策略组合。它是指当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用

由于故障恢复可能会发送多次请求,因此他与故障转移有一些相同的限制条件,比如服务必须具备幂等性的,有最大重试限制,同时故障恢复策略一般用于对实时性要求不高的主路逻辑,同时也适合处理那些不需要返回值的旁路逻辑

容错设计模式

讲完了思想我们来说一下实现:

1,断路器模式

又称为服务熔断,断路器模式是为了处理服务雪崩而出现的,是快速失败策略的一种实现。简单来说处理过程是,当满足某种条件时(一定时间内响应请求的成功率较小并且数量较大,比如10秒内请求成功率为百分之五十,发了三百个请求),系统将通过断路器直接将此请求的所有链路都断开

断路器一般使用代理来实现,代理转发请求到处理事务的机器上,如果判断被熔断了,那么直接返回失败

服务被熔断之后,熔断器将“自动”(一般是由下一次请求而不是计时器触发的,所以这里自动带引号)切换到半开启状态。该状态下,会放行一次远程调用,然后根据这次调用的结果成功与否,转换自身的状态,以实现断路器的弹性恢复

下面来说一下两个基础概念:

服务雪崩:当某一台机器出现请求更多或者自身故障时,它不能及时处理请求,而调用这个方法的机器不得不等待回应。这个等待时间是需要消耗 IO 和线程资源的。由于这一系列的连锁反应导致整个系统都会崩掉,这种情况叫服务雪崩

熔断和降级的区别:服务熔断对应断路器模式,满足条件则拒绝所有的服务请求。服务降级对应后备处理模式、安全失败,这种模式面向用户,为了给用户一个好的体验,它会执行另外一种方法来给用户友好的回应,在服务熔断之后,上游应当处理这个错误,给用户返回合理的回应,这里的处理就是服务降级。降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障

2,舱壁隔离模式

又称服务隔离,时沉默失败的一种实现,它原本的意思是设计舰船时,要在每个区域设计独立的水密舱室,一旦某个舱室进水,也只是影响这个舱室中的货物,而不至于让整艘舰艇沉没

在微服务的场景下,舱室进水指的是,一些请求的处理时间相当长,占用了线程资源,一般来说,普通的 java 程序只会设置 200 到 300 条线程,如果该类型请求过多,导致所有的线程被占满,此时该服务就无法处理其他的请求了。水密舱室指的是处理方法,在这种情况出现时,我们一般有以下两种处理方法:

  • 使用线程池来接受该类型请求,线程池设置了最大线程数,就算接受了过多的请求,也不会占用太多机器线程资源
  • 使用信号量来统计该类型线程数,并且设置一个最大阈值,达到这个阈值就不给该资源分配线程资源

3,重试模式

请求返回错误后放进消息队列再发一次请求,是服务恢复的实现。重试模式实现并不困难,即使完全不考虑框架的支持,靠程序员自己编写十几行代码也能够完成。在实践中,重试模式面临的风险反而大多来源于太过简单而导致的滥用。我们判断是否应该且是否能够对一个服务进行重试时,应同时满足以下几个前提条件

  • 仅在主路逻辑的关键服务上进行同步的重试,不是关键的服务,一般不把重试作为首选容错方案,尤其不该进行同步重试
  • 仅对由瞬时故障导致的失败进行重试
  • 仅对具备幂等性的服务进行重试
  • 重试必须有明确的终止条件,最好一下两种都配上,一是超时终止,并不限于重试,所有调用远程服务都应该要有超时机制避免无限期的等待;二是次数终止、重试必须要有一定限度,不能无限制地做下去

服务网关

网关是一个系统对外的唯一出入口,网关最重要的是路由功能,即负载均衡,当然你也可以加其他的功能,比如限流、过滤、安全、认证、授权、监控、缓存等等

因此,简单来说

网关 = 路由器(基础职能) + 过滤器(可选职能)

由于网关是所有服务对外的总出口,是流量必经之地,所以网关的路由性能将导致全局的、系统性的影响,如果经过网关路由会有 1 毫秒的性能损失,就意味着整个系统所有服务的响应延迟都会增加 1 毫秒。因此我们应当额外关注网关的性能与可用性

限流算法

当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。这种情况常见于热点业务或者黑客攻击,早期的 12306 系统就明显存在这样的问题,全国人民都上去抢票的结果是全国人民谁都买不上票。限流算法可以解决这一问题,利用流量控制减少进入系统的请求量,以避免系统的崩溃

所有的限流算法都是当请求到达一定数目之后,丢弃溢出的请求来实现的,那么我们如何实现这一功能呢

流量统计指标

每秒事务数:TPS 是衡量信息系统吞吐量的最终标准。事务可以理解为一个逻辑上具备原子性的业务操作

每秒请求数:HPS 是指每秒从客户端发向服务端的请求数,如果只要一个请求就能完成一笔业务,那 HPS 与 TPS 是等价的,但在一些场景(尤其常见于网页中)里,一笔业务可能需要多次请求才能完成

每秒查询数:QPS 是指一台服务器能够响应的查询次数

限流设计模式

常用的限流算法有四种:

1,固定窗口:又称流量计数器模式,这种算法将每一个时间段设置为一个窗口,当窗口容纳请求的数量满了之后,丢弃所有的溢出请求。这种算法有一个缺点,如下图所示

spring cloud 配置 kingbase8 数据源_spring cloud


2,滑动窗口:这个算法使用先进先出解决上面的问题,在不断向前流淌的时间轴上,漂浮着一个固定大小的窗口,窗口与时间一起平滑地向前滚动。任何时刻静态地通过窗口内观察到的信息,都等价于一段长度与窗口大小相等、动态流动中时间片段的信息

假如我们准备观察时间片段为 10 秒,并以 1 秒为统计精度的话,那可以设定一个长度为 10 的数组(设计通常是以双头队列去实现,这里简化一下)和一个每秒触发 1 次的定时器。这也说明,滑动窗口其实和固定窗口差不多,但是将每一个窗口都切的更细以获取精确度

3,漏桶算法:处理完一个请求代表从桶中流出一颗水滴,每接受一个请求代表向桶中加入一颗水滴,桶可以接受的水滴有限。漏桶以固定速率向外漏出水滴,当水桶已满时便拒绝新的请求进入

现实中系统的处理速度往往受到其内部拓扑结构变化和动态伸缩的影响,所以能够支持变动请求处理速率的令牌桶算法往往可能会是更受程序员青睐的选择

4,令牌桶算法:和漏桶算法相反,固定时间段内向桶中加入一个令牌,令牌满了则丢弃令牌,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求就可以执行;如果桶空了,那么尝试取令牌的请求会被直接丢弃

消息总线和配置

消息总线用于将服务和服务实例与分布式消息系统链接在一起的事件总线。在集群中传播状态更改很有用(例如配置更改事件)

配置为分布式系统中的外部化配置提供服务器和客户端支持,可以在中心位置管理所有环境中应用程序的外部属性

常用的框架

Nacos

这个框架承当了服务注册、消息总线、配置三种功能,非常方便实用

Feign

这个框架用于解决服务调用相关问题,内嵌 Ribbon 框架用于解决负载均衡

分布式负载均衡算法

ribbon 作为一个负载均衡框架肯定有很多独特的实现,这里说一下常见的负载均衡算法,分轮询、hash、随机、最小活跃数这四类,这些算法都可以优化,比如加权轮询、随机权重、一致性 hash 等算法

负载均衡算法实现都需要考虑一个问题,算法如何在服务列表变化的情况下正确执行。比如在感知到服务列表变化时,应通过一些同步机制,及时的更新正在进行选择时的服务列表以及权重比例,及时重新计算相关数组以及总权重数

Hystrix

Hystrix 就是一个能进行熔断和降级的库,通过使用它能提高整个系统的弹性

Gateway

这个框架充当服务网关,比较重要的功能为请求转发和过滤器

下面是 gateway 中的重要概念:

1,路由:包含url、ID、一组断言和一组过滤器
2,断言:全称叫断言函数,这个函数中写了匹配规则
3,过滤器:一层层过滤器会对请求或者响应做出修改

gateway 一般由以下部分组成:

1,Mapping:映射器,映射名称与url的组件
2,handler:执行器,发送请求
3,filter:过滤器

高可用和高并发

在微服务系统中如何保持高可用?

1,集群:相同的服务部署多份,避免单点故障
2,限流:为了避免服务雪崩,限流为了对服务端的接口接受请求的频率进行限制,防止服务挂掉,一般由网关实现,也可以由业务程序实现
3,降级/熔断:当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的应对,以此释放服务器资源以保证核心任务的正常运行。由熔断器实现

在微服务系统中如何保持高并发?

1,消息队列:消息队列在分布式系统中主要是为了解耦和削峰
2,读写分离:读写分离主要是为了将数据库的读和写操作分不到不同的数据库节点上。主服务器负责写,从服务器负责读
3,负载均衡:负载均衡系统通常用于将任务比如用户请求处理分配到多个服务器处理以提高网站、应用或者数据库的性能和可靠性

高并发情况下如何保证接口的幂等性

1,悲观锁:对数据上悲观锁,同一时间只有一个线程对数据进行操作,之后再有线程对其修改的话,通过一系列判断来拒绝该请求。比如说我们有150块,需要取出100块,第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。如果余额不足,说明是重复请求,则直接返回成功。高并发场景下,应当加分布式锁

2,乐观锁:加个版本号,或者一致性非锁定读,MVCC基础,就不再重复过程了。拒绝策略为如果版本发生变动拒绝之后的更改

3,修改前先查询:查询数据库是否有这行数据,如果有说明已经执行了一次操作,拒绝执行第二次操作

4,唯一索引:为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报异常,表示数据有冲突