微服务简介
2014,微服务:架构风格(服务微化)
一个应用应该是一组小型服务;可以通过HTTP的方式进行互通;
对应的是过去的单体应用:ALL IN ONE
微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元;(和ABB包想法有点儿像,进一步粒化。软件工程在原子化的方向上越走越远doge)
详细参照微服务文档
微服务Microservices之父,马丁.福勒(martin fowler),对微服务大概的概述如下:
就目前而言,对于微服务业界并没有一个统一的、标准的定义(While there is no precise definition of this architectural style ) 。但通在其常而言,微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API ) 。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务。可以使用不同的语言来编写服务,也可以使用不同的数据存储。
对于微服务问题有不同的、成套的“解决方案”,如
- Dubbo + ZooKeeper
- SpringCloud NetFlix
- SpringCloud Alibaba
- 霸王硬上弓般的直接手撸http
他们将多个中间件、框架组合在一起,为构建微服务项目要面临的各种问题(网关、熔断、服务注册与发现等)提供成套的解决方案
SOA:面向服务体系架构 微服务
服务治理,服务监控是关键
通信方式:http/rpc
rpc效率高
- 细嗦效率高
相关理论
CAP 理论
CAP 理论指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性:在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性,等同于所有节点访问同一份最新的数据副本。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。
- 可用性: 每次请求都能获取到正确的响应,但是不保证获取的数据为最新数据。
- 分区容错性: 分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
P 是必须的,因此只能在 CP 和 AP 中选择
根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类
CA:单点集群,满足一致性,可用性的系统,通常可扩展性较差
CP:满足一致性,分区容错的系统,通常性能不是特别高
AP:满足可用性,分区容错的系统,通常可能对一致性要求低一些
5. 作为分布式服务注册中心,Eureka比Zookeeper好在哪里?
著名的CAP理论指出,一个分布式系统不可能同时满足C (一致性) 、A (可用性) 、P (容错性),由于分区容错性P再分布式系统中是必须要保证的,因此我们只能再A和C之间进行权衡。
Zookeeper 保证的是 CP —> 满足一致性,分区容错的系统,通常性能不是特别高
Eureka 保证的是 AP —> 满足可用性,分区容错的系统,通常可能对一致性要求低一些
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接收服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但zookeeper会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30-120s,且选举期间整个zookeeper集群是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因为网络问题使得zookeeper集群失去master节点是较大概率发生的事件,虽然服务最终能够恢复,但是,漫长的选举时间导致注册长期不可用。
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保住注册服务的可用性,只不过查到的信息可能不是最新的,除此之外,Eureka还有之中自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
Eureka不在从注册列表中移除因为长时间没收到心跳而应该过期的服务
Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上 (即保证当前节点依然可用)
当网络稳定时,当前实例新的注册信息会被同步到其他节点中
因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪
BASE 理论
BASE 是 Basically Available(基本可用)、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。
- **基本可用:**在分布式系统出现故障,允许损失部分可用性(服务降级、页面降级)。
- **软状态:**允许分布式系统出现中间状态。而且中间状态不影响系统的可用性。这里的中间状态是指不同的 data replication(数据备份节点)之间的数据更新可以出现延时的最终一致性。
- **最终一致性:**data replications 经过一段时间达到一致性。
BASE 理论是对 CAP 中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
康威定律
中文直译大概的意思就是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构
注册中心-服务发现
zookeeper, eureka, nacos, consul
一般都分为server和client。每个服务实例都要有自己的服务名(服务id),通常写在配置文件中
spring.application.name
服务中心server端要暴漏服务注册地址,client在配置文件中配置注册地址,在启动时向注册地址注册;
当服务调用发生时,注册中心要向client端提供服务列表(ribbon,不知道Dubbo是怎么样的);
- 服务列表是谁在向中心获取?ribbon还是client?
服务注册中心,具有以下几个功能:
- 服务注册。
- 服务地址的管理。
- 服务动态感知。
配置中心
一个服务部署部署多个应用实例,每个实例都要单独修改配置文件的话太麻烦了,搞个配置中心统一管理;
配置中心也要集群,也要高可用等;
配置中心也要有版本管理,借助远程仓库(如github)
- nacos
- config
- zookeeper
很多配置中心都集成注册中心,虽然springcloud 中config 和eruka分别担任这两个功能
springclou和hadoop,微服务和分布式概念上什么区别?一个是java组件一个是解决分布式计算?
服务调用
RPC方式: Dubbo,gRpc
Http方式: 朴素的http客户端, Ribbon,OpenFeign, Eureka-Client, Consul-Client
这里就比较复杂了,因为Ribbon就是基于朴素的Http,而feign和Eureka-Client, Consul-Client又集成了Ribbon,,,所以底层都是同一个Http包,但是集成了不同的功能、用法?是那个包,template又是哪个包?
主要问题
服务注册、服务发现、负载均衡、流量削峰、版本兼容、服务熔断、服务降级、服务限流等方面的问题,都是因服务拆分所引发的一系列问题。如何解决这些问题,让服务更稳定地运行,就叫作服务治理。
**(1)服务:**它是分布式架构下的基础单元,包括一个或一组软件功能,其目的是不同的客户端通过网络获取相应的数据,而不用关注底层实现的具体细节。以用户服务为例,当客户端调用用户服务的注册功能时,注册信息会被写入数据库、缓存并发送消息来通知其他关注注册事件的系统,但是调用方并不清楚服务的具体处理逻辑。
**(2)注册中心:**它是微服务架构中的“通讯录”,记录了服务和服务地址的映射关系,主要涉及服务的提供者、服务注册中心和服务的消费者。在数据流程中,服务提供者在启动服务之后将服务注册到注册中心;服务消费者(或称为服务消费方)在启动时,会从注册中心拉取相关配置,并将其放到缓存中。注册中心的优势在于解耦了服务提供者和服务消费者之间的关系,并且支持弹性扩容和缩容。当服务需要扩容时,只需要再部署一个该服务。当服务成功启动后,会自动被注册到注册中心,并推送给消费者。
**(3)服务注册与发布:**服务实例在启动时被加载到容器中,并将服务自身的相关信息,比如接口名称、接口版本、IP地址、端口等注册到注册中心,并使用心跳机制定期刷新当前服务在注册中心的状态,以确认服务状态正常,在服务终止时将其从注册表中删除。服务注册包括自注册模式和第三方注册模式这两种模式。
**◎自注册模式:**服务实例负责在服务注册表中注册和注销服务实例,同时服务实例要发送心跳来保证注册信息不过期。其优点是,相对简单,无须其他系统功能的支持;缺点是,需要把服务实例和服务注册表联系起来,必须在每种编程语言和框架内部实现注册代码。
**◎第三方注册模式:**服务实例由另一个类似的服务管理器负责注册,服务管理器通过查询部署环境或订阅事件来跟踪运行服务的改变。当管理器发现一个新的可用服务时,会向注册表注册此服务,同时服务管理器负责注销终止的服务实例。第三方注册模式的主要优势是服务与服务注册表是分离的,无须为每种编程语言和架构都完成服务注册逻辑。相应地,服务实例是通过一个集中化管理的服务进行管理的;缺点是,需要一个高可用系统来支撑。
(4)服务发现:使用一个注册中心来记录分布式系统中全部服务的信息,以便其他服务快速找到这些已注册的服务。其目前有客户端发现模式和服务器端发现模式这两种模式。
**◎客户端发现模式:**客户端从服务注册服务中查询所有可用服务实例的地址,使用负载均衡算法从多个服务实例中选择一个,然后发出请求。其优势在于客户端知道可用服务注册表的信息,因此可以定义多种负载均衡算法,而且负载均衡的压力都集中在客户端。
**◎服务器端发现模式:**客户端通过负载均衡器向某个服务提出请求,负载均衡器从服务注册服务中查询所有可用服务实例的地址,将每个请求都转发到可用的服务实例中。与客户端发现一样,服务实例在服务注册表中注册或者注销。我们可以将HTTP服务、Nginx的负载均衡器都理解为服务器端发现模式。其优点是,客户端无须关注发现的细节,可以减少客户端框架需要完成的服务发现逻辑;客户端只需简单地向负载均衡器发送请求。其缺点是,在服务器端需要配置一个高可用的负载均衡器。
服务雪崩
不同服务之间相互依赖,假如一个被依赖的服务不能向上游的服务提供服务,则很可能造成雪崩效应
,最后导致整个服务不可访问
。
如订单->商品->库存这样的调用链,然后调用库存超时,导致整个超时
让我想起了流水线的游戏,突然拆掉一处导致生产线堵塞
关键词:调用链路
方案
出问题前预防:限流、主动降级、隔离
出问题后修复:熔断、被动降级
熔断_被动降级
降级处理:“替身文学”,为服务准备一个挡灾的替身,当服务响应太慢时,与其等待正常响应,不如先返回“请稍后再试”这样的降级处理
比如现实生活中排队太长了,提前告诉排在后面的人“商品已卖完”
如果在某段时间内,调用某个服务非常慢甚至超时,就可以将这个服务熔断,后续其他服务再调用这个服务就直接返回,告诉其他服务:“已经熔断了,你别调用我了,过段时间再来试下吧。”
熔断有个原则: 一段时间内,统计失败的次数或者失败请求的占比超过一定阈值,就进行熔断。
详细的原理如下图所示:
熔断原理图
下面是原理介绍:
6.3 统计请求的算法
- 请求访问到后台服务后,首先判断熔断开关是否打开。
- 如果熔断开关已打开,则表明当前请求不能被处理。
- 如果熔断开关未打开,则判断时间窗口是否已满。
- 如果时间窗口未满,则请求桶中的请求数加 1。
- 如果返回的响应有异常,则失败桶的失败数加 1,如果返回的响应没有异常,则成功桶的成功数加 1。
- 如果时间窗口(判断统计错误率)已满,则开始判断是否需要熔断。
6.4 熔断的恢复算法
- 当熔断后,开关切换到
断开状态
。 - 过一段时间后,开关切换为
半断开状态
(Half-Open)。半断开状态下,允许对应用程序的一定数量的请求可以去调用服务,如果调用成功,则认为服务可以正常访问了,于是将开关切换为闭合状态
。 - 如果半断开状态下,还是有调用失败的情况,则认为服务还没有恢复,开关从半断开状态切换到
断开状态
。
6.5 统计失败率的时间窗口
时间窗口又分为固定窗口和滑动窗口。
固定时间窗口:
原理:固定时间内统计流量总量,超过阀值则限制流量。
缺陷:无法限制短时间之内的集中流量。
滑动窗口原理:
原理:统计的总时间固定,但时间段是滑动的。
缺陷:无法控制流量让它们更加平滑
时间窗口的原理图在这里:
统计失败率的时间窗口
- 时间窗口可以比喻为人坐在窗户边,看外面来往的车辆,一定时间内从窗户外经过的车辆。
- 每次请求,都会判断时间窗口是否已满(如5分钟),如果时间窗口已满,则重新开始计时,且清理请求数/成功数/失败数。
- 注意:第一次开始的起始时间默认为当前时间。
尝试恢复服务的时间窗口
尝试恢复服务的时间窗口
- 开关为断开的状态,经过一定时间后,比如 1 分钟,设置为半断开的状态,尝试发送请求检测服务是否恢复。
- 如果已恢复,则切换状态为关闭状态。如果未恢复,则切换状态为断开的状态,经过 1 分钟后,重复上面的步骤。
- 这里的时间窗口可以根据环境的运行状态进行动态调整,比如第一次是 1 分钟,第二次是 3 分钟,第三次是 10 分钟。
中间件:
比如阿里系的 Sentinel
(推荐),Netflix 的 Hystrix
(已停止更新,维护阶段)。
限流、主动降级
服务降级:服务压力剧增的时候根据当前业务情况及流量对一些服务和页面有策略的降级,以此缓解服务器的压力,以保证核心任务的进行。同时保证部分甚至大部分任务能得到正确的响应(如给一个默认的返回)
关闭边缘服务,以保证核心服务运行
对请求的流量进行控制, 只放行部分请求
,使服务能够承担不超过自己能力的流量压力。
常见限流算法有三种:时间窗口、漏桶算法、令牌桶算法。
漏桶算法
原理:按照一个固定的速率将流量露出到接收端。
缺陷:面对突发流量的时候,采用的解决方式是缓存在漏桶中,这样流量的响应时间就会增长,这就与互联网业务低延迟的要求不符。
令牌桶算法
原理:一秒内限制访问次数为 N 次。每隔 1/N 的时间,往桶内放入一个令牌。分布式环境下,用 Redis 作为令牌桶。原理图如下:
总结的思维导图在这里:
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
nnd又搞数学期望是吧
六种限流算法
服务限流,是指通过控制请求的速率或次数来达到保护服务的目的,在微服务中,我们通常会将它和熔断、降级搭配在一起使用,来避免瞬时的大量请求对系统造成负荷,来达到保护服务平稳运行的目的。下面就来看一看常见的6种限流方式,以及它们的实现与使用。
固定窗口算法
固定窗口算法通过在单位时间内维护一个计数器,能够限制在每个固定的时间段内请求通过的次数,以达到限流的效果。
算法实现起来也比较简单,可以通过构造方法中的参数指定时间窗口大小以及允许通过的请求数量,当请求进入时先比较当前时间是否超过窗口上边界,未越界且未超过计数器上限则可以放行请求。
@Slf4j
public class FixedWindowRateLimiter {
// 时间窗口大小,单位毫秒
private long windowSize;
// 允许通过请求数
private int maxRequestCount;
// 当前窗口通过的请求计数
private AtomicInteger count=new AtomicInteger(0);
// 窗口右边界
private long windowBorder;
public FixedWindowRateLimiter(long windowSize,int maxRequestCount){
this.windowSize = windowSize;
this.maxRequestCount = maxRequestCount;
windowBorder = System.currentTimeMillis()+windowSize;
}
public synchronized boolean tryAcquire(){
long currentTime = System.currentTimeMillis();
if (windowBorder < currentTime){
log.info("window reset");
do {
windowBorder += windowSize;
}while(windowBorder < currentTime);
count=new AtomicInteger(0);
}
if (count.intValue() < maxRequestCount){
count.incrementAndGet();
log.info("tryAcquire success");
return true;
}else {
log.info("tryAcquire fail");
return false;
}
}
}
进行测试,允许在1000毫秒内通过5个请求:
void test() throws InterruptedException {
FixedWindowRateLimiter fixedWindowRateLimiter
= new FixedWindowRateLimiter(1000, 5);
for (int i = 0; i < 10; i++) {
if (fixedWindowRateLimiter.tryAcquire()) {
System.out.println("执行任务");
}else{
System.out.println("被限流");
TimeUnit.MILLISECONDS.sleep(300);
}
}
}
运行结果:
图片
img
固定窗口算法的优点是实现简单,但是可能无法应对突发流量的情况,比如每秒允许放行100个请求,但是在0.9秒前都没有请求进来,这就造成了在0.9秒到1秒这段时间内要处理100个请求,而在1秒到1.1秒间可能会再进入100个请求,这就造成了要在0.2秒内处理200个请求,这种流量激增就可能导致后端服务出现异常。
滑动窗口算法
滑动窗口算法在固定窗口的基础上,进行了一定的升级改造。它的算法的核心在于将时间窗口进行了更精细的分片,将固定窗口分为多个小块,每次仅滑动一小块的时间。
并且在每个时间段内都维护了单独的计数器,每次滑动时,都减去前一个时间块内的请求数量,并再添加一个新的时间块到末尾,当时间窗口内所有小时间块的计数器之和超过了请求阈值时,就会触发限流操作。
看一下算法的实现,核心就是通过一个int类型的数组循环使用来维护每个时间片内独立的计数器:
@Slf4j
public class SlidingWindowRateLimiter {
// 时间窗口大小,单位毫秒
private long windowSize;
// 分片窗口数
private int shardNum;
// 允许通过请求数
private int maxRequestCount;
// 各个窗口内请求计数
private int[] shardRequestCount;
// 请求总数
private int totalCount;
// 当前窗口下标
private int shardId;
// 每个小窗口大小,毫秒
private long tinyWindowSize;
// 窗口右边界
private long windowBorder;
public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) {
this.windowSize = windowSize;
this.shardNum = shardNum;
this.maxRequestCount = maxRequestCount;
shardRequestCount = new int[shardNum];
tinyWindowSize = windowSize/ shardNum;
windowBorder=System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if (currentTime > windowBorder){
do {
shardId = (++shardId) % shardNum;
totalCount -= shardRequestCount[shardId];
shardRequestCount[shardId]=0;
windowBorder += tinyWindowSize;
}while (windowBorder < currentTime);
}
if (totalCount < maxRequestCount){
log.info("tryAcquire success,{}",shardId);
shardRequestCount[shardId]++;
totalCount++;
return true;
}else{
log.info("tryAcquire fail,{}",shardId);
return false;
}
}
}
进行一下测试,对第一个例子中的规则进行修改,每1秒允许100个请求通过不变,在此基础上再把每1秒等分为10个0.1秒的窗口。
void test() throws InterruptedException {
SlidingWindowRateLimiter slidingWindowRateLimiter
= new SlidingWindowRateLimiter(1000, 10, 10);
TimeUnit.MILLISECONDS.sleep(800);
for (int i = 0; i < 15; i++) {
boolean acquire = slidingWindowRateLimiter.tryAcquire();
if (acquire){
System.out.println("执行任务");
}else{
System.out.println("被限流");
}
TimeUnit.MILLISECONDS.sleep(10);
}
}
查看运行结果:
图片
img
程序启动后,在先休眠了一段时间后再发起请求,可以看到在0.9秒到1秒的时间窗口内放行了6个请求,在1秒到1.1秒内放行了4个请求,随后就进行了限流,解决了在固定窗口算法中相邻时间窗口内允许通过大量请求的问题。
滑动窗口算法通过将时间片进行分片,对流量的控制更加精细化,但是相应的也会浪费一些存储空间,用来维护每一块时间内的单独计数,并且还没有解决固定窗口中可能出现的流量激增问题。
漏桶算法
为了应对流量激增的问题,后续又衍生出了漏桶算法,用专业一点的词来说,漏桶算法能够进行流量整形和流量控制。
漏桶是一个很形象的比喻,外部请求就像是水一样不断注入水桶中,而水桶已经设置好了最大出水速率,漏桶会以这个速率匀速放行请求,而当水超过桶的最大容量后则被丢弃。
看一下代码实现:
@Slf4j
public class LeakyBucketRateLimiter {
// 桶的容量
private int capacity;
// 桶中现存水量
private AtomicInteger water=new AtomicInteger(0);
// 开始漏水时间
private long leakTimeStamp;
// 水流出的速率,即每秒允许通过的请求数
private int leakRate;
public LeakyBucketRateLimiter(int capacity,int leakRate){
this.capacity=capacity;
this.leakRate=leakRate;
}
public synchronized boolean tryAcquire(){
// 桶中没有水,重新开始计算
if (water.get()==0){
log.info("start leaking");
leakTimeStamp = System.currentTimeMillis();
water.incrementAndGet();
return water.get() < capacity;
}
// 先漏水,计算剩余水量
long currentTime = System.currentTimeMillis();
int leakedWater= (int) ((currentTime-leakTimeStamp)/1000 * leakRate);
log.info("lastTime:{}, currentTime:{}. LeakedWater:{}",leakTimeStamp,currentTime,leakedWater);
// 可能时间不足,则先不漏水
if (leakedWater != 0){
int leftWater = water.get() - leakedWater;
// 可能水已漏光,设为0
water.set(Math.max(0,leftWater));
leakTimeStamp=System.currentTimeMillis();
}
log.info("剩余容量:{}",capacity-water.get());
if (water.get() < capacity){
log.info("tryAcquire success");
water.incrementAndGet();
return true;
}else {
log.info("tryAcquire fail");
return false;
}
}
}
进行一下测试,先初始化一个漏桶,设置桶的容量为3,每秒放行1个请求,在代码中每500毫秒尝试请求1次:
void test() throws InterruptedException {
LeakyBucketRateLimiter leakyBucketRateLimiter
=new LeakyBucketRateLimiter(3,1);
for (int i = 0; i < 15; i++) {
if (leakyBucketRateLimiter.tryAcquire()) {
System.out.println(“执行任务”);
}else {
System.out.println(“被限流”);
}
TimeUnit.MILLISECONDS.sleep(500);
}
}
查看运行结果,按规则进行了放行:
但是,漏桶算法同样也有缺点,不管当前系统的负载压力如何,所有请求都得进行排队,即使此时服务器的负载处于相对空闲的状态,这样会造成系统资源的浪费。由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少。
令牌桶算法
令牌桶算法是基于漏桶算法的一种改进,主要在于令牌桶算法能够在限制服务调用的平均速率的同时,还能够允许一定程度内的突发调用。
它的主要思想是系统以恒定的速度生成令牌,并将令牌放入令牌桶中,当令牌桶中满了的时候,再向其中放入的令牌就会被丢弃。而每次请求进入时,必须从令牌桶中获取一个令牌,如果没有获取到令牌则被限流拒绝。
假设令牌的生成速度是每秒100个,并且第一秒内只使用了70个令牌,那么在第二秒可用的令牌数量就变成了130,在允许的请求范围上限内,扩大了请求的速率。当然,这里要设置桶容量的上限,避免超出系统能够承载的最大请求数量。
Guava中的RateLimiter就是基于令牌桶实现的,可以直接拿来使用,先引入依赖:
com.google.guava guava 29.0-jre 进行测试,设置每秒产生5个令牌:void acquireTest(){
RateLimiter rateLimiter=RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double time = rateLimiter.acquire();
log.info(“等待时间:{}s”,time);
}
}
运行结果:
可以看到,每200ms左右产生一个令牌并放行请求,也就是1秒放行5个请求,使用RateLimiter能够很好的实现单机的限流。
那么再回到我们前面提到的突发流量情况,令牌桶是怎么解决的呢?RateLimiter中引入了一个预消费的概念。在源码中,有这么一段注释:
-
It is important to note that the number of permits requested never affects the
- throttling of the request itself (an invocation to {@code acquire(1)} and an invocation to {@code
- acquire(1000)} will result in exactly the same throttling, if any), but it affects the throttling
- of the next request. I.e., if an expensive task arrives at an idle RateLimiter, it will be
- granted immediately, but it is the next request that will experience extra throttling,
- thus paying for the cost of the expensive task.
大意就是,申请令牌的数量不同不会影响这个申请令牌这个动作本身的响应时间,acquire(1)和acquire(1000)这两个请求会消耗同样的时间返回结果,但是会影响下一个请求的响应时间。
如果一个消耗大量令牌的任务到达空闲的RateLimiter,会被立即批准执行,但是当下一个请求进来时,将会额外等待一段时间,用来支付前一个请求的时间成本。
至于为什么要这么做,通过举例来引申一下。当一个系统处于空闲状态时,突然来了1个需要消耗100个令牌的任务,那么白白等待100秒是毫无意义的浪费资源行为,那么可以先允许它执行,并对后续请求进行限流时间上的延长,以此来达到一个应对突发流量的效果。
看一下具体的代码示例:
void acquireMultiTest(){
RateLimiter rateLimiter=RateLimiter.create(1);
for (int i = 0; i <3; i++) {
int num = 2 * i + 1;
log.info("获取{}个令牌", num);
double cost = rateLimiter.acquire(num);
log.info("获取{}个令牌结束,耗时{}ms",num,cost);
}
}
运行结果:
可以看到,在第二次请求时需要3个令牌,但是并没有等3秒后才获取成功,而是在等第一次的1个令牌所需要的1秒偿还后,立即获得了3个令牌得到了放行。同样,第三次获取5个令牌时等待的3秒是偿还的第二次获取令牌的时间,偿还完成后立即获取5个新令牌,而并没有等待全部重新生成完成。
除此之外RateLimiter还具有平滑预热功能,下面的代码就实现了在启动3秒内,平滑提高令牌发放速率到每秒5个的功能:
void acquireSmoothly(){
RateLimiter rateLimiter=RateLimiter.create(5,3, TimeUnit.SECONDS);
long startTimeStamp = System.currentTimeMillis();
for (int i = 0; i < 15; i++) {
double time = rateLimiter.acquire();
log.info(“等待时间:{}s, 总时间:{}ms”
,time,System.currentTimeMillis()-startTimeStamp);
}
}
查看运行结果:
可以看到,令牌发放时间从最开始的500ms多逐渐缩短,在3秒后达到了200ms左右的匀速发放。
总的来说,基于令牌桶实现的RateLimiter功能还是非常强大的,在限流的基础上还可以把请求平均分散在各个时间段内,因此在单机情况下它是使用比较广泛的限流组件。
中间件限流
前面讨论的四种方式都是针对单体架构,无法跨JVM进行限流,而在分布式、微服务架构下,可以借助一些中间件进行限。Sentinel是Spring Cloud Alibaba中常用的熔断限流组件,为我们提供了开箱即用的限流方法。
使用起来也非常简单,在service层的方法上添加@SentinelResource注解,通过value指定资源名称,blockHandler指定一个方法,该方法会在原方法被限流、降级、系统保护时被调用。
@Service
public class QueryService {
public static final String KEY=“query”;
@SentinelResource(value = KEY,
blockHandler ="blockHandlerMethod")
public String query(String name){
return "begin query,name="+name;
}
public String blockHandlerMethod(String name, BlockException e){
e.printStackTrace();
return "blockHandlerMethod for Query : " + name;
}
}
配置限流规则,这里使用直接编码方式配置,指定QPS到达1时进行限流:
@Component
public class SentinelConfig {
@PostConstruct
private void init(){
List rules = new ArrayList<>();
FlowRule rule = new FlowRule(QueryService.KEY);
rule.setCount(1);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp(“default”);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
在application.yml中配置sentinel的端口及dashboard地址:
spring:
application:
name: sentinel-test
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8088
启动项目后,启动sentinel-dashboard:
java -Dserver.port=8088 -jar sentinel-dashboard-1.8.0.jar
在浏览器打开dashboard就可以看见我们设置的流控规则:
进行接口测试,在超过QPS指定的限制后,则会执行blockHandler()方法中的逻辑:
Sentinel在微服务架构下得到了广泛的使用,能够提供可靠的集群流量控制、服务断路等功能。在使用中,限流可以结合熔断、降级一起使用,成为有效应对三高系统的三板斧,来保证服务的稳定性。
网关限流
网关限流也是目前比较流行的一种方式,这里我们介绍采用Spring Cloud的gateway组件进行限流的方式。
在项目中引入依赖,gateway的限流实际使用的是Redis加lua脚本的方式实现的令牌桶,因此还需要引入redis的相关依赖:
org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-data-redis-reactive 对gateway进行配置,主要就是配一下令牌的生成速率、令牌桶的存储量上限,以及用于限流的键的解析器。这里设置的桶上限为2,每秒填充1个令牌:spring:
application:
name: gateway-test
cloud:
gateway:
routes:
- id: limit_route
uri: lb://sentinel-test
predicates:
- Path=/sentinel-test/**
filters:
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶上限
redis-rate-limiter.burstCapacity: 2
# 指定解析器,使用spEl表达式按beanName从spring容器中获取
key-resolver: “#{@pathKeyResolver}”
- StripPrefix=1
redis:
host: 127.0.0.1
port: 6379
我们使用请求的路径作为限流的键,编写对应的解析器:
@Slf4j
@Component
public class PathKeyResolver implements KeyResolver {
public Mono resolve(ServerWebExchange exchange) {
String path = exchange.getRequest().getPath().toString();
log.info(“Request path: {}”,path);
return Mono.just(path);
}
}
启动gateway,使用jmeter进行测试,设置请求间隔为500ms,因为每秒生成一个令牌,所以后期达到了每两个请求放行1个的限流效果,在被限流的情况下,http请求会返回429状态码。
除了上面的根据请求路径限流外,我们还可以灵活设置各种限流的维度,例如根据请求header中携带的用户信息、或是携带的参数等等。当然,如果不想用gateway自带的这个Redis的限流器的话,我们也可以自己实现RateLimiter接口来实现一个自己的限流工具。
gateway实现限流的关键是spring-cloud-gateway-core包中的RedisRateLimiter类,以及META-INF/scripts中的request-rate-limiter.lua这个脚本,如果有兴趣可以看一下具体是如何实现的。
总结
总的来说,要保证系统的抗压能力,限流是一个必不可少的环节,虽然可能会造成某些用户的请求被丢弃,但相比于突发流量造成的系统宕机来说,这些损失一般都在可以接受的范围之内。前面也说过,限流可以结合熔断、降级一起使用,多管齐下,保证服务的可用性与健壮性。****
服务治理
分布式SOA环境下系统的依赖错综复杂。
服务治理主要分为两个方面:文章来源:https://www.toymoban.com/news/detail-605359.html
- 服务管理
- 服务上线下线、服务路由、限流和降级、归组、机房规则、服务授权
- 相关信息的收集展示:
- 服务基本信息、容量和水准、依赖、调用统计、提供的方法和参数、负责人、统计报表
- 运行状态加监控
依赖管理
服务间互相调用,可能形成复杂的网状依赖文章来源地址https://www.toymoban.com/news/detail-605359.html
调用日志分析
服务分级
到了这里,关于微服务基础理论的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!