一、多级缓存架构
1、缓存原理和设计架构
缓存 VS 缓冲:
缓存:英文单词是Cache指位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为Cache,常见的缓存有,CPU的L1/L2/L3 cache、Linux文件系统的page cache、InnoDB buffer pool、Redis、Memcache
缓冲:英文单词是Buffer,指某个临时存储区域,保存将要从一个设备(系统)传输到另一个设备(系统)的数据,常见的Buffer有,Java IO BufferdInputStream等、磁盘控制器写缓存(Write cache)、MySQL log buffer、消息队列缓冲写请求、Innodb buffer pool。
缓存的技术本质:凡是位于性能相差较大的两种系统或硬件之间,用于协调两者性能差异结构,均可称之为cache,其技术本质就是空间换时间。
缓存设计使用3W1H设计即可,3W分别表示what(存什么)、when(存多久,什么时间过期)、where(存在哪里),1H表示how(如何存),what和when表示存什么和存多久,这个根据业务而定,where表示存在哪,可以是App缓存、Http缓存、CDN、Nginx缓存、Memcache、Redis等,how表示如何存,即更新机制,可以是过期更新、定期更新、主动更新等。
上面提到了更新机制有三种,过期更新是指缓存有效期内一直用缓存,超过有效期后去重新读取,例如Http缓存;定期更新是指定期更新缓存,例如后台每10分钟更新一次缓存;主动更新是指当数据修改后主动修改缓存,例如业务数据写完之后直接更新Redis,视频更新后通知CDN。
例如朋友圈动态缓存案例,一般人的微信好友都在200人以内,且发布之后基本不会修改,除非删除,因此将缓存存储在APP本地是最好的。
朋友圈广告的缓存案例,由于能看广告的人更多,因此虽然将朋友圈内存缓存道本地也是可以,但是最好的是缓存到CDN服务器。
2、多级缓存架构
(1)多级缓存架构模式一:五级缓存
如下图所示,在一个系统的整体链路中,有五级缓存,本地缓存(App缓存和浏览器缓存),CDN缓存、Web容器缓存(Tomcat、Nginx等),应用类缓存(进程内缓存、进程外缓存、SSD缓存等)、分布式缓存。其实数据库也有自己的缓存,为什么我们不说六级缓存而是说五级缓存呢,因为这里提到的五级缓存在架构设计时是可以取舍的,但是数据库的缓存我们是没有办法取舍的,要么用数据库系统,那么就会用数据库缓存,要么就不用数据库系统。
多级缓存架构设计时,需要考虑一些关键点:
应用多级缓存架构的时候,如果数据发生了变化,如何保证每一级及时更新数据?这种确实会存在这种问题,问题也会变得越复杂
多级缓存大大增加了架构复杂度,直接使用分布式缓存不是更简单么?这样的话性能会下降,可能会达不到性能要求。
是否所有的业务都需要按照这个多级架构来设计?要根据业务的性能要求和复杂度要求,使用合适原则涉及。
因此我们设计的时候需要考虑性能需求和架构复杂度
(2)多级缓存架构模式二:四级缓存
四级缓存对比五级缓存来说是去除了CDN,为什么首先去除的CDN呢,因为使用CDN是需要付费的,如果系统业务没有那么复杂、性能要求没有那么高,我们就可以不适用CDN,首先就会节省一部分成本。
(3)多级缓存架构模式三:三级缓存架构
三级缓存对比四级缓存去掉了应用内的缓存,这是因为应用内缓存的实现复杂度较高,因为编码实现复杂度比较高,需要判断本地缓存,没有的话在查询分布式缓存,同时还要考虑本地缓存和分布式缓存的一致性。另外也不建议去掉本地缓存和Web缓存,因为这两种缓存的复杂度很低,App缓存、浏览器缓存、Web容器缓存的技术成熟度已经很高了,基本上拿来配置即用,同时可以对性能带来很大的提升。因此涉及的时候,基本上都会是五级缓存、四级缓存、三级缓存,基本不会用二级缓存、一级缓存。
3、缓存技术概要介绍
(1)本地缓存
App本地缓存:App将数据缓存到本地;可以缓存任何想缓存的数据,主要是IOS、安卓等,常见的技术有SQLite缓存、本地文件缓存、图片缓存Picasso(Square)、Fresco(Facebook)、Glide(Google)
HTTP缓存:指HTTP标准协议缓存,主要用于HTTP资源的缓存,例如图片、CSS文件等,具体实现可以参考HTTP协议、Cache-Control、ETag/If-None-Match 等指令
举例:如果App使用HTTP接口获取业务数据,应该使用哪种方式缓存?我觉得应该是使用App本地缓存,一般情况下HTTP缓存使用在前端通过浏览器访问的数据
(2)CDN缓存
Content Delivery Network,即内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率,关键技术是内容存储和分发技术。
优点是功能强大,能够支撑超高流量,缺点是贵
典型的应用场景比如说直播、视频、资讯等
国内主要的供应商有阿里云、网宿、腾讯云、金山云、七牛云等
在做架构设计时,我们只需要考虑是否要用CDN,哪些数据需要用CDN,如果要用,就找对应的CDN服务提供商购买CDN服务,我们不需要考虑CDN服务的部署。
(3)Web容器缓存
Web 容器一般缓存静态资源,例如图片、JavaScript、CSS 等,配合 HTTP 协议实现缓存。下面是Nginx的缓存配置。
(4)应用缓存 + 分布式缓存
应用缓存:指应用在本地缓存记录,可以缓存任何想要缓存的记录,常见的缓存技术:开源的缓存方案(进程内缓存、ConcurrentHashmap、Ehcache、OSCache),还有进程外缓存、堆外缓存,进程外缓存和堆外缓存在Java技术中会用到,但是用的也不是很多,因为使用进程内缓存会更好控制;还有就是使用本地磁盘SSD缓存等,SSD缓存一般在巨头里面才会看到,因为基于SSD磁盘的缓存首先技术要求较高,另外一个是业务只有达到一定规模之后才会使用这么复杂的缓存技术。
分布式缓存:由分布式系统提供缓存功能,这也是工作中最经常接触到的缓存技术,可以缓存任何想要缓存的记录,目前只有两种可选方案:Redis、Memcached
这里说一下为什么SSD可以作为缓存,如下图所示,可以看到SSD随机读取速度和普通机械硬盘读取速度的差异。
Redis和Memecache怎么选择呢,区别如下图所示,在应用时,也需要根据两者的技术原理进行选择,例如根据Redis是单线程、Memcached是多线程的区别,那么在应用时,如果有较多大对象,就是Memcached、否则就用Redis;根据Redis支持复杂数据结构的特点,如果是数据库+分布式缓存这种组合就能满足需求,用Memcached会简单一点,如果业务上有数据库难以满足的场景,可以使用Redis复杂的数据结构来处理了;根据Redis支持持久化的特点来说,如果生成缓存的代价很高,丢失后可能引起严重的系统问题,则用Redis,如果短时间丢失部分缓存影响不大,则用Memcached。
但是如果没办法判断使用Redis还是Memcached,那么就优先选择Redis,因为Redis功能强大、应用也更灵活,没有明显的瓶颈,除了对大对象的支持。
二、分布式缓存架构设计
1、分布式缓存架构模式
分布式缓存架构主要分为数据缓存和结果缓存。
数据缓存是指查询数据库较慢,因此加一层缓存,先查询缓存,查询不到再查询数据库,一般这种是对于数据实时性要求较高、读多写少 的业务,例如微博浏览等,在设计的时候,需要考虑用什么缓存系统,如果保证数据一致性的问题。
结果缓存是指查询的是计算的结果,一般用于实时性要求不高,但是计算比较耗时的场景,例如推荐、热榜、排行榜、分页等,一般是使用定时任务在后台计算结果后,放入缓存,读取的时候直接从缓存读取。,在设计的时候,需要考虑用什么缓存系统,缓存有效期和结果新鲜度的平衡。
在我们设计系统时,首先是使用数据库,如果读性能不够,首先考虑数据库主从架构和读写分离,如果还是读性能不够,再考虑加缓存,如果是写性能不够,数据库就需要分片,然后再加缓存。
2、缓存一致性设计
数据缓存架构一致性复杂度:
读场景:先读取缓存,没有数据再读取数据库,这个没问题
写场景:有四种处理方式
(1)先写缓存后写存储:可能造成缓存写入成功单写存储失败,这样单个业务数据在缓存有效期内查询是没有问题的,但是需要连表查询的关联数据会有问题。
(2)先写存储后写缓存:可能造成存储写成功但是缓存写入失败,这样业务查询时查到的都是旧数据,除非原缓存失效。
(3)先删除缓存在写入存储系统(适合用户相关数据):正常情况下能保证数据一致性,但是缓存系统异常的时候,为了保证缓存系统不影响业务操作,还是需要继续写入,一般我们都会开启Redis的持久化,如果此时Redis恢复,并使用持久化数据恢复缓存,同样会造成数据不一致;这样的场景比较适合用户相关的数据,因为对于同一个用户来说,很少会在短时间内同时对同一份数据做操作。
(4)双删(适合全局数据,例如运营活动图片):先删除缓存,再写存储,再写缓存,这种情况仍然会存在数据不一致,例如删除缓存后,还没有将数据写入数据库,此时就有请求查询数据,那么此时又会将旧数据加载到缓存。
上面可以看到,各个场景都会出现缓存和存储数据不一致的情况,要实现完全的缓存一致性本质上是需要跨越缓存系统和存储系统实现分布式事务,这种实现方式比较复杂,一般也不会这么实现。
数据缓存一致性解决方案:
(1)容忍一致性:根据容忍度设定缓存的有效期,例如新闻资讯、微博、商品信息等,这种实现方案简单,但是有一定时期的数据不一致
(2)关系数据库本地表事务:正常采用先删除缓存再写入数据的策略;缓存系统异常时,通过事务记录一条消息到本地消息表,然后后台定时读取本地消息表,重试删除操作。缺点是实现复杂,优点是数据不一致的时间短,等于重试的时间。
(3)消息队列异步删除:正常情况下采用先删除缓存再写入数据的策略,缓存系统异常时,发送一条删除操作给消息队列,然后后台读取消息队列记录,重试删除操作。
在实际业务中,使用方案一的场景是最多的,因为使用缓存的核心场景就是提高读性能,缓存不一致在绝大多数情况下都不会对有很大的问题。
3、缓存架构存在的问题
(1)缓存穿透
指数据库没有数据,导致查询打入数据库,例如黑客使用不存在的key请求,导致每一次都查询数据库;
解决方案:可以使用null存入redis,设置过期时间;还可以使用布隆过滤器或布谷鸟过滤器做一道过滤,防止大量请求打入数据库;再或者缓存当前数据,即对数据做分离,最近的数据和历史数据分离,最近的数据做缓存,历史数据不做缓存,这种情况会存在历史数据查询很慢,但是这个是可以接受的,因为绝大多数场景只会查询最近的数据,历史数据很少查询。
(2)缓存击穿:数据库有,但是缓存中还没有,导致请求打入数据库;例如冷门数据或是老数据,没有加载到缓存,再或者系统刚起步,数据还没有加载到缓存,再或者缓存过期等
解决方案:缓存预热、随机失效;
缓存预热主要是应对运营活动、秒杀、大促等场景,常用的实现方式有:模拟请求触发系统生成缓存,这种方式实现比较复杂;还可以后台按照规则批量生成缓存,这种方式工作量比较大,因为需要生成的缓存比较多,规则也比较复杂;还可以从业务的角度做预热,使用灰度发布/预发布触发系统生成缓存,例如在大促或者秒杀的场景,可以先让用户提前预约、签到等,可以先把活动资源、图片等信息先缓存到用户本地,然后到真正秒杀或大促的时候,直接使用本地缓存即可,不需要再将所有的请求都打到服务中。
随机失效:缓存有效期设定一个时间范围内的随机值,例如3~5分钟内随机失效,应对后台批量生成的缓存,例如排行榜、推荐等。
(3)缓存雪崩:当缓存失效(过期)后引起存储系统或应用系统性能急剧下降的情况。上面提到缓存主要有存储缓存和计算缓存,对于存储缓存来说,如果我们的查询SQL较复杂或者比较慢,导致查询时间较长,然后造成存储系统的性能急剧下降,最终导致系统不可用;计算型缓存是因为计算时间和复杂度较高,导致应用系统性能下降是因为计算任务太耗时,最终把计算系统拖垮,例如计算排行榜。
这里思考一下缓存穿透和缓存血崩的一个区别,缓存穿透主要是因为有大量的缓存失效导致大量的请求打入数据库导致的数据库压力,从而造成系统的性能压力;而缓存雪崩指的是并不需要大量的缓存失效,有可能是只有一两个缓存失效就能把存储系统或者计算系统拖垮。
以下图为例说明一下缓存雪崩的场景:
首先线程一访问缓存,缓存中没有数据,然后线程一访问数据库,由于SQL比较复杂,是个慢查询,那么线程一就在此等待,同时还有其他的线程并发访问,同样是上述流程,假设有200各线程并发操作,这样整个系统性能就会急剧下降;虽然后续线程一执行完成会将数据写入缓存并返回调用端成功,但是此时为时已晚,因为存储系统和计算系统已经垮了。
所以缓存雪崩的本质主要有两方面:1. 生成缓存较慢(复杂的数据查询、大量的计算等);2. 缓存失效后并发请求量较大,例如50个以上。可以看到,缓存雪崩一般发生在多个用户读取同一份数据的场景,如果每个用户读取自己的数据,基本上不会发生缓存雪崩的问题。
解决方案:1、使用更新锁,对缓存更新进行加锁保护,保证只有一个线程能够更新缓存,未获取锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或默认值。这种方案优点是能保证只有一个线程更新缓存,缺点是要引入分布式锁。2、后台更新,有后台线程来更新缓存,而不是由业务线程更新,缓存有效期设置为永久,后台线程更新缓存,更新策略分为定时更新、事件触发更新,定时更新就是每隔一段时间进行更新,事件触发是指由业务上的一些逻辑或者事件判断来触发业务线程;业务线程只读取缓存,缓存不存在就返回空值或默认值。这种方案优点是实现简单,缺点是需要保证后台线程的高可用。
另外一些其他的点,虽然不是解决缓存雪崩的,但是从架构上也可以适当的做对应的设置:使用本地缓存 + mysql限流 + 熔断降级
(4)缓存热点
特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大,有可能撑不住。例如微博大V发的微博或者明星热点事件。
解决方案:多副本缓存。在写入缓存时,缓存的key加上编号,写入到多个缓存服务器,读取的时候,随机生成编号组装Key,然后读取。这种方式的挑战是不太好确定哪些key是热点数据,这也是微博经常挂的原因,需要动态决策或人工干预。
三、负载均衡架构
1、负载均衡整体架构
(1)四级负载均衡
一级负载均衡:基于机房级别的负载均衡。使用DNS或GSLB做机房的负载均衡,用户从APP或者浏览器发起请求,分发给不同的虚拟IP。
二级负载均衡:机房内部的负载均衡,使用F5/LV5 + Keeplived分发给Nginx集群。
三级均衡:在Nginx层面做负载均衡,分发给网关集群。
四级负载均衡:使用网关服务分发给具体的业务集群,这一步一般会依靠注册中心,然后使用轮询策略进行分发。
这种方式是性能最好、但是也是最复杂的负载均衡架构,在实际架构落地时,要考虑系统性能的需求以及维护的复杂度来做最终选择。例如下面三个问题:
问题一:多级级联增加了处理路径,性能应该会受到影响,为何还要这么设计?
在实际逻辑后可以观测到,多级级联是增加处理路径,但是对于性能的影响并不大, 因为负载均衡器的性能非常强大,其不会成为性能瓶颈。一般在业务调用时,瓶颈主要出现在业务逻辑处理和数据的存储与读取。
问题二:多级级联架构很复杂,看起来违背了架构的简单原则,直接用 F5 或者 LVS 负载均衡到服务网关不就可以了么?
这是因为F5和LVS一般作为机房内的负载均衡器,如果把服务网关挂到F5或LVS上,那么业务发生变更,就需要调整F5和LVS,如果出现问题,则会影响到整个机房的所有服务,影响面太大,也就是维护复杂度太高。
问题三:是否所有业务都要按照这个多级级联来设计负载均衡架构?
这个要根据自己业务性能的需要和维护的复杂度之间做平衡,在满足自己业务性能要求的前提下,尽可能的简单。
(2) 三级负载均衡
可以看到,三级负载均衡对比四级负载均衡去掉了F5/LVS,为什么首先去掉的是F5/LVS呢,这和上面提到在缓存中首先去掉CDN是一样的考虑,就是成本。
(3)二级负载均衡
可以看到,二级负载均衡对比三级负载均衡又去掉了Nginx,通过APP或浏览器直接访问网关集群。
2、负载均衡技术剖析
(1)DNS
主要用于地理位置和机房级别的负载均衡,例如直接使用IP,北方的用户访问北京机房,南方的用户访问杭州机房。
DNS的优点是使用了标准协议,在应用时,只需要购买DNS服务,将自己机房的IP配置到DNS服务器即可,无需再做其他的配置。
DNS的缺点是能力有限,不够灵活,容易产生DNS挟持和DNS缓存。不够灵活:DNS只能基于IP来做负载均衡;DNS挟持:如果DNS服务器出现问题或者被挟持,用户访问时就会被指向错误的地址;DNS缓存:浏览器、操作系统一般都会缓存DNS解析结果,短的可能几个小时,长的可能一天,如果其中一个机房发生故障,想让受影响的用户访问其他的机房,由于此时存在DNS缓存,在缓存期内无法切换。
(2)HTTP-DNS
由于DNS的缺点,在实际落地时,可以使用HTTP-DNS。其核心思想是提供一个SDK到App或者客户端,然后提供一个HTTP-DNS服务,SDK从HTTP-DNS服务器获取哪些服务器是可用的,读取之后才会发起真正的调用;另外提供了健康监控和智能调度两个服务,健康监控用来监控服务器集群的健康状态,如果有服务器不可用,则通知智能调度服务,智能调度服务去修改HTTP-DNS的接卸结果。
HTTP-DNS的优点是可以根据业务和团队技术灵活定制,例如使用Java实现,或者使用其他语言实现等。
缺点是使用的是非标准协议,不通用,不太适合Web业务,因为Web的DNS解析是通过浏览器内核解析和缓存的,不能通过SDK的方式去解析和缓存。
在设计时,有几个关键点:
智能调度模块可以独立,也可以嵌入到HTTP-DNS,一般独立成运维系统,因为智能调度系统有很多作用;
正常的时候走DNS,异常的时候走HTTP-DNS;这样可以提高性能,否则每一次都走HTTP-DNS,对性能会有影响。
SDK会缓存HTTP-DNS解析结果,但是可以自定义缓存时间,例如一分钟。
(3)GSLB
全称为Global Server Load Balancing,全局负载均衡,主要用于多个区域拥有自己服务器的站点,为了使全球用户只以一个IP地址或域名就能访问到离自己最近的服务器,从而获得最快的访问速度。这种适合超大规模业务,多地甚至全球部署的业务,例如Google、facebook、阿里、美团等。优点是功能强大,可以实现就近访问、容灾切换、流量调节。缺点是实现复杂。
基于DNS的GSLB:
核心流程:用户访问本地DNS服务器,然后本地DNS从上一级DNS服务器获取DNS解析信息,在DNS服务器中基于DNS转发到GSLB,然后GSLB根据自己监控的结果返回对应的服务器IP,最终用户访问根据DNS解析的IP地址。
优点是实现简单,容易实施,成本低;
缺点是可能判断不准确,例如用户手工指定了DNS服务器;但是这个在使用时也不能算一个缺点,很少有人会自己设置DNS服务器。
基于HTTP Redirct的GSLB:
这是基于HTTP重定向的GSLB,其实现原理是DNS解析时解析到的是GSLB的地址(这是一个标准的DNS解析),用户请求GSLB,GSLB将请求重定向到实际的服务器。
优点是能够拿到用户IP
判断准确;缺点是只适合HTTP业务,因为其本身就是基于HTTP的重定向实现的,如果使用的不是HTTP访问,则不能使用。
基于IP欺骗的GSLB
原理是DNS解析到的结果还是GSLB的地址,用户请求GSLB,GSLB再请求到具体的服务器,但是在发送请求时,篡改发送IP为用户IP,那么服务器响应结果直接给用户。
优点是适合所有业务(弥补了基于HTTP重定向实现的GSLB只能在会吃HTTP业务的缺点)
缺点是每次请求必须经过GSLB设备,性能低。
一般情况下会配合HTTP Redirct GSLB一起使用。
(4)F5
硬件负载,跟硬件配置有关。
高性能:
配置为(处理器:英特尔四核 Xeon 处理器(共8个超线程逻辑处理器内核)、内存:32GB、硬盘:400GB SSD)的F5,性能:
每秒 L7 请求数:1M(七层请求转发每秒100万)
每秒 L4 连接数:400K(四层连接数可以达到40万)
每秒 L4 HTTP 请求数:7M(四层请求转发每秒700万)
最大 L4 并发连接数:24M(四层最大连接数可达24万)
L4 吞吐量:40Gbps(四层吞吐量可达40G)
L7 吞吐量:18Gbps(七层吞吐量可达18G)
配置为(处理器:单 CPU;基本内存:8GB;硬盘:500GB;端口:8个千兆端口,4个可选千兆光纤端口)的F5,性能为吞吐量:4Gbps
贵:如下图所示,上面两种配置,一个为100W,一个为20W,一个机房中肯定不止一台F5,因此成本会很高。
(5)LVS
章文嵩博士1998年创建的开源项目,Linux 2.4 版本集成到内核;内核级别的负载均衡,基本能跑满千兆网卡带宽,性能量级10~100万请求。
LVS-NAT:
LVS绑定VIP,客户端向VIP发起请求连接,LVS在经过调度之后,选取RS,将本地端口与RS的端口做映射,然后RS返还数据给LVS,LVS将数据返还给客户端。
主要用于反向代理,类似于Nginx,Internet不知道内部服务器的任何信息。
LVS-DR
LVS绑定VIP,客户端向VIP发起请求连接,LVS修改目的的mac地址为某个服务器RS,RS服务器处理后直接返回结果给客户端。
主要用于LVS和服务器在同一企业网络。
LVS-TUN
LVS绑定VIP,客户端向 VIP 发起请求连接,LVS通过隧道技术转发给某个 RS 服务器,RS 服务器处理后直接返回结果给客户端。
主要用于LVS 和服务器在不同企业网络,企业网络分为多个子网。
(6)各种负载均衡技术对比
四、负载均衡技巧
1、通用负载均衡算法
(1)轮询 & 随机
轮询是将请求一次发给服务器,随机是将请求随机发给服务器,这两种都是适用于无状态的负载均衡,优点是实现简单,缺点是不会判断服务器状态,除非服务器连接丢失。
例如某个服务器当前因为触发了程序bug进入死循环导致CPU负载很高,负载均衡系统是无法感知的,还是会将请求源源不断发给他。再或者服务器的配置不一样,有新加入的32核机器,也有老的16核的机器,负载均衡器也不关注这些,新老机器分配的任务数是一样的。
(2)加权轮询
按照预先配置的权重,将请求按照权重比例发送给不同的服务器。适用于服务器的处理能力有差异,例如新老服务器搭配使用时。
这种方式实现复杂,按照权重计算,且不会判断服务状态,除非服务连接丢失;权重配置不合理可能导致过载,例如公司购入了一批新机器,CPU核数是老机器的两倍,运维直接给新机器配置了两倍的权重,结果导致新服务器全部过载(这是因为虽然服务器性能差两倍,但是有可能代码逻辑等其他因素导致性能并不会相差两倍,甚至出现性能相差不大的情况)。
加权轮询算法一:权重 = 请求数量
例如有三台服务器,给第一台发送40个请求,再给第二台发送40个请求,再给第三台发送40个请求。可以配置为[40,40,20],这样配置服务器资源利用不均衡,会出现毛刺现象(在请求时,先来的40个请求都发送给服务器一,而其他服务器则处于空闲状态),但是如果这种配置配置的好,反而会最好,例如配置[2,2,1]。
加权轮询算法二:权重概率
将所有的权重加起来,然后计算各个服务器的分配概率,用随机数区间来做分配。还是上面的例子,概率分配的结果是,服务器一分得0~39,服务器二分得40~79,服务器三分得80~99,然后对请求生成随机数,落入哪个区间就访问哪个服务器。
加权轮询算法三:权重动态调整
Nginx的实现,兼顾服务器故障后的慢启动;例如服务刚启动时,性能会比较低,分配请求数少一点,随着服务的运行,性能提高后,再动态调整分配的请求数。
(3)负载优先
负载均衡系统将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量,例如请求连接数、CPU使用率、IO情况等。
适用于LVS这种四层网络负载均衡,可以以连接数来判断服务器状态,服务连接数越大,表示服务器压力越大。还适用于Nginx这种七层网络负载系统,可以以http请求数来判断服务器状态(Nginx内置的负载均衡算法不支持这种方式,需要进行扩展)
该方式实现方式复杂,需要管理或者获取服务器状态;优点是可以根据服务器状态进行负载均衡。
虽然负载优先性能更好,但是在实际应用中,反而是轮询算法应用最广,因为轮询算法基本上可以满足业务需要,并且也简单。
(4)性能优先
负载均衡系统将任务分配给性能最好的服务器,主要是以响应时间作为性能衡量标准。
适用于Nginx这种七层网络负载系统,可以以HTTP响应时间来判断服务器状态(Nginx内置的负载均衡算法不支持这种方式,需要进行扩展)
这种方式实现复杂,需要统计请求时间,需要耗费一定的CPU运算资源,但是对于负载均衡器来说,性能本身是其立身之本,请求数本来就很多,如果还需要统计响应时间,那么性能就会有损耗;在实际落地时,一般会使用采样统计的方式,兼顾统计和性能。
优点是可以根据性能进行负载均衡,但是如果服务器响应不经过负载均衡器,则不能使用这种算法。
(5)Hash
基于某个参数计算Hash值,将其映射到具体的服务器。
适用于有状态服务,例如购物车;任务是分片的,例如某个用户的请求只能在某台服务器处理。
这种方式优点是实现简单,缺点是不会检测服务器状态,除非服务器连接丢失
常见的Hash键有用户IP(Session场景)、URL(缓存场景)
2、负载均衡算法举例
(1)Nginx
可以看到,Nginx实现了轮询、加权轮询、Hash、性能优先的轮询算法,但是没有实现基于连接数的负载优先的算法,这是因为Nginx一般作为反向代理服务器,用户的连接都是建立在Nginx上的而非建立在后端服务器上。
(2)LVS
3、业务级别负载均衡
通用负载均衡器算法是基于请求的,业务级别的负载均衡是基于业务内容的,如订单ID、用户ID等,会更灵活,例如蚂蚁金服的LDC架构、腾讯的SET单元化架构。
以蚂蚁金服的LDC架构来说,简单如下图所示,可以分为一次路由、两次路由、数据路由。(spanner:自研的业务级别的负载均衡器,其可以识别请求中的业务信息,如订单ID、用户ID等,然后可以根据业务信息做负载均衡;RZ:regional Zone,一个业务的处理单元)
首先请求根据DNS或者GSLB访问到具体一个机房的spanner:
一次路由:箭头一和箭头二,箭头一表示应该在本IDC处理的请求,直接映射到对应的RZ即可,箭头二表示如果不该本IDC处理的请求,spanner直接转发给对应IDC的spanner即可。
二次路由:对于有些场景,A用户的一个请求可能关联了B用户数据的访问,例如转账,这时候就涉及到箭头三和箭头四,分别跳转到本IDC的其他RZ和其他IDC的RZ。
数据路由:RZ想访问哪个数据库,是可以配置的,对应图中箭头五。
业务负载均衡应用案例:
(1)Cookie
第一次请求不带cookie,使用轮询或随机的方式访问一个服务器,该服务器处理后返回结果,并设置cookie,下次访问时,带上cookie,负载均衡器根据cookie进行负载均衡。
一般用于Session保持、购物车、下单等场景。
(2)Http Header
可以通过 X- 来自定义 HTTP Header,可以服务器下发,也可以直接带上本地信息。这种用法被 IETF 在2012年6月发布的 RFC5548 中明确不推荐,虽然 RFC 已经不推荐使用带X前缀的 http 头部,但是不推荐不代表禁止,目前应用广泛。
应用场景:一般用于精细化的地理位置、机房级别、版本、平台、渠道等负载均衡,例如:1. 如果客户端位于加利福尼亚州山景城,则负载平衡器会添加 Header:X-Client-Geo-Location:US,Mountain View. 2. X-Client-Version: 3.0.0, X-Client-Platform:iOS 11, X-Client-Channel:Huawei.
(3)HTTP Query String
query String包含路由信息,这种方案实现简单,但是对业务侵入很大,还不如用Cookie
做负载均衡时,还要评估需要的服务器数量,依照经验,可以从接口性能和服务器性能来评估:
接口性能:根据经验,线上业务服务器接口处理时间分布为20~100ms,平均大约50ms,访问存储或者其他系统接口时主要的性能消耗点
服务器性能:线上单个服务器(32核)性能大约为300~1000TPS/QPS。
服务器数量:服务器数量 = 总(TPS+QPS)/单个服务器性能。
这里需要注意一点,单纯的提升CPU数量并不会让性能线性提升,因为接口性能的提升并不是由CPU核数决定的,而是由编码、存储系统等来决定的。
五、接口高可用
接口高可用的挑战主要是出现雪崩效应和链式效应:
雪崩效应指的是请求量超过系统处理能力后导致系统性能螺旋快速下降;通常使用限流和排队的方式防止雪崩效应
链式效应是指某个故障引起后续一连串故障;通常使用熔断和降级的功能方式链式效应。
接口高可用架构本质上是丢车保帅策略,业务和用户体验会部分有损。
1、限流
(1)不同端限流
用户请求全流程的各个阶段都可以做限流。
请求端限流:
原理:发起请求的时候进行限流,被限流的请求实际上并没有发给后端服务器。
常用的手段:限制请求次数,例如按钮变灰等;嵌入简单业务逻辑,例如生成随机数,不再范围内就被限流
优缺点:优点是实现简单,流量在本地基本上已经被限制,缺点是防君子不防小人,使用脚本绕过前端的方式是不能被限流的。
接入端限流:
原理:接收到业务请求的时候限流,避免业务请求进入真正的业务处理流程。
常用的手段:限制同一用户请求频率,随机抛弃无状态请求,例如限流浏览请求,但是有状态请求通常不会被限流,例如下单请求
优缺点:优点是可以防刷,缺点是实现复杂,限流阈值可能需要人工判断,如果阈值设置的太小,就会丢到太多的请求,如果阈值设置太大,则限流不会有响应的作用
微服务限流:
单个服务的自我保护措施,处理能力不够的时候丢弃新的请求。例如使用Netty时,可以加一个Handler进行限流
优缺点:优点是实现简单,缺点是处理能力难以精确匹配,这个和接入端限流一个原理
(2)限流算法
固定窗口 & 滑动窗口:
固定窗口限流:统计固定时间周期内的请求量,超过阈值则限流;存在临界点问题,如图中的红蓝两点对应的时间范围;
滑动窗口限流:统计滑动时间周期内的请求量,超过阈值则限流,判断比较准确,但是实现稍微复杂。实际在代码开发中,滑动窗口限流也没有那么复杂,如果需要对一定时间内请求进行限流,建议使用滑动窗口。
漏桶算法:
请求放入桶(消息队列等)中,业务处理单元(线程/进程/服务)从桶里拿请求处理,桶满则丢弃请求。
其本质是对请求总量的控制,桶的大小是设计的关键。
这种算法优点是突发流量时丢弃的请求较少,缺点是无法准确控制流出速度,流出的速度是系统的处理速度,并且桶的大小动态调整比较困难,例如java的BlockingQueue
漏桶算法主要用于瞬时高并发流量,例如零点签到、秒杀等场景
Java限流漏桶算法:
如果我们是基于Netty做的访问和限流,其使用了Reactor模型,将业务线程和请求线程分离,通过队列传递请求。
设计的时候要适当的配置Blocking Queue的长度,如果配置的太长,限流就没有作用了,如果配置的太短,则会浪费。
如果使用的是Tmcat或者SpringBoot这类的框架时,一般都会有plugin或者hook钩子函数,可以在plugin或者hook函数中做限流。
漏桶算法变种:
例如写缓冲:原理是如果桶的大小没有限制,例如Kafka消息队列,则桶可以用来做写缓存,其技术本质是同步转异步,缓冲所有请求慢慢处理。这种方案主要用于高并发写入请求,例如热门微博的评论。
这里有个问题,为什么看微博请求可以丢弃,而评论请求却要全部缓冲起来?这是根据业务场景决定的。
令牌桶算法:
某个处理单元按照指定速率将令牌放入桶(消息队列等)中,业务处理单元收到请求后需要先获取令牌,如果获取不到则丢弃请求。
其本质是速率控制,令牌桶产生的速度是设计的关键。
这种算法优点是可以动态调整处理速度,缺点是遇到突发流量可能会丢弃很多请求,另外实现也比较复杂。
令牌桶算法主要用于控制访问第三方服务的速度,防止把下游压垮;也可以用于控制自己的处理速度,防止过载。
2、排队
收到请求后并不是同步处理,而是将请求放入队列,系统根据能力异步处理。
其本质是请求缓存 + 同步改异步 + 请求端轮询,主要用于抢购、秒杀等场景
排队的设计关键是如何设计异步处理流程,如何保证用户体验(前端、客户端交互)
排队的详细流程如下,首先是用户对排队服务器发起一个请求,排队服务器生成一个排队token,然后发送消息给消息队列,返回给用户排队token。实际的业务处理器按照自己的处理能力从消息队列中获取任务,然后生成实际业务处理的token,将排队token-处理token放入Redis,客户端采用轮询的方式,定时访问排队服务器,排队服务器访问redis中是否有排队token,如果没有说明还没有排到,如果有则返回对应的业务token,客户端返回可以操作了,客户端提交业务处理时带上业务操作token访问业务服务器,业务服务器再验证Redis中是否存在该处理Token,没有的话说明是伪造的请求,不予处理,如果有,说明确实排到了,就正常处理。
一号店双十一排队案例:
系统分为三个模块:排队模块、调度模块、服务模块
排队模块:负责接收用户的抢购请求,将请求以先入先出的方式保存下来。每一个参加秒杀活动的商品保存一个队列,队列的大小可以根据参与秒杀的商品数量(或加点余量)自行定义。
调度模块:负责排队模块到服务模块的动态调度,不断检查服务模块,一旦处理能力有空闲,就从排队队列头上把用户访问请求调入服务模块。
服务模块:是负责调用真正业务处理服务,并返回处理结果,并调用排队模块的接口回写业务处理结果。
同时也做了很好的用户交互:
3、降级
降级是直接停用某个接口或者URL,收到请求后直接返回错误(例如HTTP 503),主要用于故障应急,通常会把非核心业务降级,保住核心业务,例如降级日志服务、升级服务等。
常见的降级架构:运维或者管理员人工发送降级指令,运维系统的降级模块通知接入服务器降级。
在做降级设计的时候,独立系统操作降级,可以是独立的降级系统,也可以是嵌入到其他系统的降级功能;降级需要人工判断、人工执行,不要相信类似AIOps之类的噱头。
4、熔断
下游系统故障的时候,一定时期内不再调用,主要用于服务的自我保护,防止故障链式效应。
常见的熔断架构:可以通过配置中心或配置文件的方式配置熔断策略,熔断一般由框架或者SDK提供,例如Dubbo、Hystrix等,熔断策略一般按照失败次数、失败比例、响应时长来确定。
六、微博计算架构实战
1、微博业务场景计算性能估算
之前说过,性能估算主要分为用户量预估、用户行为建模、性能需求计算三个步骤,在微博业务场景中,分析如下:
用户量预估:
用户量:2020.9月月活5.11亿,日活2.24亿(参考《微博2020用户发展报告》)。 这个在实际做架构设计时,是需要根据自己的业务进行预估的。
用户行为:发微博、看微博、评论微博
用户行为建模:
发微博:考虑到微博是一个看得多发的少的业务,假设平均每天每人发1条微博(只考虑文字微博),则微博每天的发送量约为2.5亿条。大部分的人发微博集中在早上8:00~9:00点,中午12:00~13:00,晚上20:00~22:00,假设这几个时间段发微博总量占比为60%,则这4个小时的平均发微博的 TPS 计算如下:2.5亿 * 60% / (4 * 3600) ≈ 10 K/s
看微博:由于绝大部分微博用户看微博的对象是大V和明星,因此我们假设平均一条微博观看人数有100次,则观看微博的次数为:2.5亿 * 100 = 250亿。大部分人看微博的时间段和发微博的时间段基本重合,因此看微博的平均 QPS 计算如下:250亿 * 60% / (4*3600) = 1000K/s。
2、微博高性能计算架构设计
发微博:
业务特征分析:发微博是一个典型的写操作,因此不能用缓存,可以用负载均衡。
架构分析:用户量过亿,应该要用多级负载均衡架构,覆盖 DNS -> F5 -> Nginx -> 网关的多级负载均衡。
架构设计:负载均衡算法选择和业务服务器数量预估
负载均衡算法算则:发微博的时候依赖登录状态,登录状态一般都是保存在分布式缓存中的,因此发微博的时候,将请求发送给任意服务器都可以,这里选择“轮询”或者“随机”算法。
业务服务器数量预估:发微博涉及几个关键的处理:内容审核(依赖审核系统)、数据写入存储(依赖存储系统)、数据写入缓存(依赖缓存系统),因此按照一个服务每秒处理500来估算,完成10K/s的 TPS,需要20台服务器,加上一定的预留量,25台服务器差不多了。
根据上面轮询算法选择和服务器数量预估,发微博的多级负载均衡架构:
看微博:
业务特征分析:看微博是一个典型的读场景,由于微博发了后不能修改,因此非常适合用缓存架构,同时由于请求量很大,负载均衡架构也需要。
架构分析:用户量过亿,应该要用多级负载均衡架构;请求量达到250亿,应该要用多级缓存架构,尤其是 CDN 缓存,是缓存设计的核心。
架构设计:负载均衡算法和业务服务器数量预估
负载均衡算法:游客都可以直接看微博,因此将请求发送给任意服务器都可以,这里选择“轮询”或者“随机”算法。
业务服务器数量预估:假设 CDN 能够承载90%的用户流量,那么剩下10%的读微博的请求进入系统,则请求 QPS 为1000K/s * 10% = 100K/s,由于读取微博的处理逻辑比较简单,主要是读缓存系统,因此假设单台业务服务器处理能力是1000/s,则机器数量为100台,按照20%的预留量,最终机器数量为120台。这里为什么说CDN可以承载90%的用户流量,是因为我们使用CDN的本心就是要让大量的请求走CDN,就像在使用Redis和Memcached时,我们也希望所有的缓存都能命中,因此使用CDN也是同样的,如果CDN只承载了50%的流量,那么说明CDN配置的不够合理。
看微博的多级负载均衡架构:
看微博的多级缓存架构
可以看到在所及缓存时用到了进程缓存,在之前描述中,一般不太建议用进程内缓存,是因为进程内缓存特别容易出问题,例如如何保证进程内缓存一致性的这个问题。但是看微博场景,一旦用户发了微博之后就不能修改了,除非删除,所以基本上就不需要考虑缓存一致性的问题,另外一个原因是,看微博一般发生在微博发出的一个小时内,属于时间局部性的场景,使用进程内缓存可以很好的解决这个问题。
时间局部性和空间局部性:时间局部性指数据被访问后,在接下来的时间中,该数据仍然可能会被访问;空间局部性指的是数据被访问后,在接下来的时间内,其相邻的数据被访问。
将发微博和看微博进行合并,微博业务主要用到任务分配和任务分解,任务分配是使用双机房或三机房,任务分解是指将发微博和看微博差分到不同的服务中。
像之前的发红包和抢红包没有拆分服务,为什么发微博和看微博需要拆分服务?这里面主要有两个原因:性能差异和时间跨度
性能差异:在发红包和拆红包的场景,发红包后,可能也就一二十个人抢红包,量级相差不大;但是在发微博和看微博的场景中,一条微博发出后,可能上千上万的人会去看着一条微博,量级相差很大。
时间跨度:在发红包和拆红白的场景中,发完就要拆,实时性要求很强,时间跨度很小;在发微博和看微博的场景中,时间跨度可以很大,例如几天后再看都是有可能的。
基于以上两点,可以把发微博和拆微博做服务拆分。
由于发微博和看微博的负载均衡架构都是四级负载均衡,因此合并后的多级负载均衡架构如下图所示:
由于发微博是写操作,不需要缓存,因此合并后的多级缓存架构实际上就是看微博的多级缓存架构, 如下图所示:
3、微博高可用计算架构设计
微博的高可用主要是微博热点事件下是否可以保证服务可用,做设计仍然是从用户量预估、用户行为建模、性能需求计算三个步骤进行设计。
用户量预估:热点事件指某个大V或者明星爆料或者官宣,虽然只有一两条微博,但引起大量用户在短时间内访问,给系统造成很大压力。而造成热点事件的微博自己只有1~2条,但是用户围观后会有很多转发,假设有10%的围观用户会在事件发生后60分钟内转发。看微博很难预估,和事件的影响力和影响范围有关;其实我们主要考虑的就是转发微博和看微博。
业务特征:转发微博:转发微博的业务逻辑基本等同于发微博,但是业务上可以区分是“原创”还是“转发”,转发的微博重要性和影响力不如原微博;看微博:热点事件发生后,绝大部分请求都落在了导致热点事件发生的那一条微博上面。
架构分析:
因为上面提到对于看微博的人数很难预估,既然无法预估,那就做好预防。
转发微博:转发的微博重要性和影响力不如原微博,可以考虑对“转发微博”限流,由于转发能带来更好的传播,因此尽量少丢弃请求,考虑用“漏桶算法”。
看微博:很明显,热点事件微博存在缓存热点问题,可以考虑“多副本缓存”,由于原有的缓存架构已经采用了“应用内的缓存,总体上来看,缓存热点问题其实不一定很突出。
架构设计:
微博热点事件计算高可用架构示意图如下所示: