如何构建一个高可靠系统(上)

·  阅读 4468

本篇博客参考:余春龙的《架构设计2.0》

什么是高可靠系统?站在使用者的角度,高可靠系统就是靠谱的系统,值得信赖的系统,不容易出现莫名其妙的问题,使用非常丝滑。本篇博客分为上下两部分,会从多个维度聊聊如何构建一个高可靠的系统。

过载保护:限流与熔断

系统上线前,我们会对流量进行估算,申请对应的服务器,为了不背锅,我们往往会多申请几台服务器或者申请配置更高的服务器,即使这样,也没办法保证我们的系统一定能承受所有的用户请求。这个时候,系统就需要有过载保护:拒绝一部分用户的请求,确保大多数用户可以正常的使用系统。

过载保护有两种常用的方式:限流与熔断。

限流

限流,在生活中也非常常见,比如:

  • 进入火车站候车室需要安检,如果进入安检区域的人太多,那会变得一团糟,所以在人多的时候,管理人员会控制进入安检区域的人数,慢慢的将人放进去;
  • 去景点游玩,如果景点人太多,会严重影响用户体验,也不太安全,所以管理人员有时候会在景点外设置拦截,控制入园人数,等有游客从景点出来了,再放一部分游客进入景点。

限流的两种维度

限流通常有两种限制维度:并发数,频率。

Nginx中的限流

Nginx就提供了限制并发数,频率这两种维度的模块:limit_conn(限制并发数),limit_req(限制频率)。 在秒杀系统中,有一个商品的库存是1000,现在有几万人抢购,那意味着大部分人都是没办法抢购到商品的,将所有用户请求全部放到后面的应用服务器,应用服务器再将所有用户请求全部放到后面的数据库,这是没有任何意义的,而且会造成应用服务器、数据库的压力激增,甚至可能将应用服务器、数据库打垮。基于这两点原因,可以在Nginx进行如下的限流:

  • 并发连接数的控制:只放2000个人进到后面的应用服务器,剩下的直接响应“已售完”,那应用服务器、数据库的压力就会小的多;
  • 频率的控制:每个IP,每秒只能进行一次请求,避免刷子流量。

RPC中的整体限流

某些基于TCP的RPC服务提供方大致工作原理如下图所示: image.png 客户端向服务端发起请求,服务端有一个监听线程、多个Work线程,监听线程将请求暂存到Request缓冲区,Work线程从Request缓冲区取出Request,进行处理。这个Request缓冲区往往是有界的内存队列,为什么是有界的?

  • 有些Request很大,如果Request缓冲区是无界的,很容易撑爆内存;
  • 客户端往往会设置超时时间(没有手动设置,也会有默认的),如果Request缓冲区是无界的,积压了太多的请求,那后面的请求即使处理了也没有任何意义,因为客户端已经超时,发起重试或者抛出异常了,还不如直接将Request进行丢弃。

Hystrix中的限流

大部分开发小伙伴,第一次听说限流,应该就是在Hystrix了,Hystrix默认使用线程隔离模式,可以通过线程数+队列大小进行限流。

限流的另类用法

限流就是拒绝一部分用户的请求,确保大多数用户可以正常的使用系统,但是限流偶尔会有另类的用法,我在开发中,就另类的使用过限流。 当时,我负责的系统是面向商家的,接收商家的下单请求:

  • 如果某一时刻,没有触发限流,我接收到商家的下单请求后,就及时处理下单请求,及时响应商家;
  • 如果某一时刻,触发了限流,我接收到商家的下单请求后,就将请求放入MQ或者放入消息表中,后续慢慢处理,处理完成后,以站内通知的方式或者邮件的方式告知商家。

可以看到,即使触发了限流,也没有拒绝用户的请求,还是能正常处理用户的请求,这算是限流的另类用法吧。

RateLimiter

RateLimiter是Guava库中的一个限流器,是基于令牌桶限流算法实现的,使用起来非常方便:

//创建一个限流器,每秒钟产生100个令牌
RateLimiter rateLimiter = RateLimiter.create(100);
//获取一个令牌,如果可以获得到,马上返回;不能获得到,则阻塞,等待新的令牌产生
rateLimiter.acquire(1);
//不管有没有获得到令牌,都马上返回,根据返回的布尔值,判断是否获得到令牌
rateLimiter.tryAcquire(1);
复制代码

如果是我来实现这个限流器,肯定会在后台启动一个线程,这个线程的任务就是每隔一段时间,往“桶”里丢一定数量的令牌,但是神一般的Guava不是这么做的,它会记录最后一次获得令牌的时间,拿令牌的时候,计算最后一次获得令牌的时间与现在的时间差,在这个时间差内,可以获得多少令牌,然后进行令牌的增加与扣减。

中央限流

在大部分场景中,我们使用的都是单机限流,但是单机限流有局限性,比如我们的应用有3台服务器,我们应用依赖的服务最多只能支撑100个并发,那我们只需要控制每台服务器最多可以同时“流”下去30个请求就可以了(303=90<10030*3=90<100),但是如果有一天,我们增加了服务器,却没有通知依赖服务,那情况可就不妙了(304=120>10030*4=120>100),有什么办法可以解决问题呢?可以使用中央限流。

我们来看下,使用中央限流的处理流程:

image.png

  1. 客户端请求服务端;
  2. 服务端请求中央限流模块,询问是否可以处理此请求;
  3. 中央限流模块响应服务端,是否可以处理此请求。

服务端处理完成后,可能还需要上报中央限流模块:请求已经处理完成。当然也可以不上报,中央限流模块默认一定时间后,请求被服务端处理完成。

可以看出,使用中央限流有如下的弊端:

  • 单点问题:中央限流模块是单点的,如果中央限流模块出现故障,应该怎么办?当然,我们可以采用ZooKeeper、ETCD等方式,使中央限流模块高可用,这就非常复杂了;
  • 性能问题:每个请求都要先经过服务端,服务端再请求中央限流模块,中央限流模块再响应服务端,虽然都在内网,但是损耗也必然是有的;
  • 中央限流模块压力问题:众多服务端请求中央限流模块,中央限流模块的压力剧增,中央限流模块是否可以扛得住?
  • 超出服务端承受能力问题:因为此方案中没有单机限流的存在,只是询问中央限流模块,当中央限流模块响应“可以处理请求”,可能对于服务端而言,已经超出了自身的承受能力。

那有没有办法解决这些问题呢?很遗憾,目前没有办法完全解决这些问题,但是我们可以利用单机限流+中央限流的方式最大程度的去缓解这些问题:

  • 当中央限流模块出现故障,就采用单机限流,这样就可以避免中央限流模块的单点问题;
  • 不必每次都去请求中央限流模块,可以从中央限流模块申请一批额度,当这批额度用完之后,再去申请新的额度,这样就可以缓解中央限流模块的压力和中央限流模块带来的性能问题。

针对于第二点,我们还可以再次进行优化:最开始,我们并不需要请求中央限流模块,可以完全采用单机限流,当单机限流到达一定的阈值后,再去中央限流模块申请一批额度。

“从中央限流模块申请一批额度”这种方式很美好,但还是存在问题:

  • 服务端的某台机器向中央限流模块申请了最后一批额度后,服务端的其他机器就申请不到额度了,就限流了,此时只有申请了最后一批额度的机器可以正常处理客户端请求;
  • 服务端申请的额度没有及时用完,应该如何处理?

超时

只要发生了网络交互:比如Http请求、发送消息到MQ、RPC调用等等,都需要设置超时时间,这是构建高可靠系统最简单的,但也是最容易被忽视的一个点,在发起Http请求、发送消息到MQ、RPC调用的时候,请合理设置超时时间。

客户端发起请求,需要设置超时时间,那是不是意味着服务端就没有超时时间了,非也,有时候在服务端也有超时时间的概念,而且这超时时间也尤为重要,如下图所示: image.png 小伙伴一定很熟悉这张图,在介绍限流的那一节,已经出现过了,这里又出现了,为了增强大家的记忆,我再介绍下工作流程:客户端向服务端发起请求,服务端有一个监听线程、多个Work线程,监听线程将请求暂存到Request缓冲区,Work线程从Request缓冲区取出Request,进行处理。这个Request缓冲区往往是有界的内存队列。 那超时体现在哪里呢?Work线程从Request缓冲区取出Request,可以先检查下这个Request是否已经超时了,如果已经超时了,那Work线程处理这个请求是没有任何意义的,还不如直接丢弃。 在这种情况之下,客户端和服务端可能都会有一个超时时间,此时就需要满足公式:

客户端超时时间>=服务端超时时间

如果客户端的超时时间设置为3秒,服务端的超时时间设置为5秒,那服务端多出来的2秒超时时间是没有任何意义的:服务端辛辛苦苦处理完成请求后,客户端已经超时,发起重试或者抛出异常了。

重试

进行网络交互,网络有时会发生抖动,服务端可能也有可能正在GC,没有及时处理客户端请求,这个时候,客户端请求就会失败,我们可以进行重试,但是有一个前提:服务端必须幂等。 重试也并非是无脑的,会有两种常见的策略:

  • 延迟策略:延迟一定的时间再发起重试,甚至可以逐步提高延迟时间,比如第一次重试间隔50ms,第二次重试间隔100ms,第三次重试间隔200ms;
  • 退避策略:向某台服务器发起请求失败,发起重试就避开这台服务器,向其他服务器发起重试。

重试的时候,还有一个小的优化点:客户端设置的总超时时间(包括重试)是3秒,服务端处理请求需要2秒,因为网络原因,客户端第一次请求经过1.5s后失败了,那还要必要进行重试吗?

本篇博客主要介绍了构建一个高可靠系统的三个要素:限流、超时、重试,下一篇博客将会介绍隔离、降级、熔断、监控、灰度、告警。

分类:
后端
收藏成功!
已添加到「」, 点击更改