陈爱云,2010年毕业于天津大学,2013年加入爱奇艺,现任爱奇艺搜索架构组高级经理,致力于提高搜索系统的性能和高可用性。
打造坚如磐石的搜索架构
对于一个在线系统而言,性能和稳定性是永远要追求的两个方向,如果是分布式系统,性能不够可以用机器来凑(当然这不是最好的方法,性能的提升不是本文的关注点,所以这里不对提升性能的方法赘述),但是稳定性不能靠机器来堆,并且机器越来越多可能会带来更多的稳定性的问题。做在线系统的同学应该会对墨菲定理感触特别深,如果系统中的某个模块可能会出错,那么它一定会出错。或许可以尝试把奶酪面包和猫绑一起,制作一个永动机:)
下面介绍爱奇艺搜索架构高可用之路的演进。爱奇艺搜索始于2011年,距今已有六个年头,在这六年间爱奇艺搜索的用户量和qps都呈指数增长,这也对稳定性提出了很高的挑战。前面我们说,任何一个系统都不可能做的完美,一定会出错,我们需要做的是出错后,把对用户的影响降到最低,让用户感知不到系统的故障。把系统做稳定,多活、降级、限流、扩容都必不可少,下面分别说说我们是怎么做的。
异地多活
曾有人开玩笑说挖掘机是互联网行业最恐怖的武器,挖掘机一铲子下去,可能就把机房的光缆电缆挖断,此时如果没有备份机房,那再完美的系统都无法对外服务了。
备份可以分为两种,一种是备用的机房平时是不接线上流量的,主机房出问题后再把流量切到备用机房上。这样做需要注意几点,一是备用机房的数据和程序版本要保持与主机房一致,二是备用机房平时没有接线上流量,在线上真出问题时,是否敢把线上流量切到备用机房上去,备用机房此时是否可以正常服务,三是备用机房的机器平时不在线上服务,也在一定程度上带来资源的浪费。另外一种备份方式是备用机房平时也在线上服务,一个机房出问题后可以把流量打向其余的机房。假设说有三个机房,那每个机房冗余1/3的机器即可,这样可以接受任意一个机房挂掉。爱奇艺采用的是后一种方式,采取离线数据多写的方式,在线读本机房的数据。
异地多活主要还是在数据一致性上比较难处理,搜索在线系统对数据一致性要求不是非常非常高,所以在线方面在实现异地多活困难并不多。
降级
降级可以通过各种手段降低系统的负载,去掉一些锦上添花的功能,保证基本的服务质量。基本的降级可以让用户几乎无感知的情况下降低系统的负载。
1. 延长缓存过期时间
众所周知,增加缓存可以大大降低后端模块的负载,增加缓存过期时间可以提高缓存命中率,同样可以降低打向后端模块的流量。但是如果缓存时间固定不变且比较长的话,后端数据更新后就不能及时体现在前端,如果缓存时间固定不变且比较短的话,缓存命中率比较低,对后端模块起不到太大的降低流量的作用。所以缓存过期时间要随着后端模块的压力而动态变化,那么,如何实现动态控制缓存过期时间的目的呢?
首先在写缓存时,把写缓存的时间戳也加进去,假设这个时间戳为t1,从缓存系统中取出缓存后,同时取得t1,假设当前时间为t2, t = t2 - t1,同时系统中维护一个过期时间的阈值,让t与这个阈值比较,如果t小于阈值,则认为缓存足够新,反之则认为缓存过期。这个阈值不是固定不变的,随着后端压力和后端的成功率而变化。
如果取到的缓存已过期,那么去请求后端模块,如果请求后端模块失败了,可以返回过期的缓存的内容。因为对于视频搜索来说,返回一个稍微旧点的结果,要远远好于不返回结果。
2. 降低计算复杂度
先举一个上学考试的例子,大部分同学考到80分相对比较容易,但是如果想要考到100分,需要为提高的20分花费大量的精力。可能花20%的精力可以达到80分,但是要再花80%的精力才能再提高20分。搜索质量的提高也如此。
可以通过缩小索引的数据范围来降低计算的复杂度。在100亿个doc中进行检索与在100万个doc中进行检索,所消耗的时间都不是一个量级的,所以可以通过缩小索引的数据范围来减少搜索消耗的CPU和时间。这里的范围也不是随便缩小的,需要保证重点数据依旧在索引范围内,可以通过文档的点击数、上线时间等来确定是否属于重点数据。
还可以通过省略部分大量消耗CPU的步骤来达到降低计算复杂度的目的,这些步骤就相当于我们前边所说的为了最后的20分而额外消耗了80%的精力,对于爱奇艺搜索来说,这部分是重排序,由于之前已经有粗排序了,去掉重排序依旧可以达到一个基本满意的搜索结果,同时消耗的CPU也很低。
这里所说的缩小数据集的范围与去掉重排序的步骤,可以是人为操作的,也可以是自动触发的。比如根据该进程所消耗的CPU,一旦达到某一阈值,则自动进行降级。
3. LIFO
很多系统都有一个任务队列,有一个IO线程负责往任务队列里push任务,工作线程从队列里pull任务,默认情况下,这个队列是FIFO的,但是设想这种情况,当工作线程处理不及时时,队列里的任务会越积越多,导致每一个任务都需要在队列里等待很长时间才会被得到处理,但是由于任务在队列里等待了过长的时间,当任务处理完后,可能client已经认为本次请求超时了,server做了无用功,极端情况下可能会导致server一直在做无用功。
所以在需要降级时,可以改变任务队列取任务的方式,改为LIFO,起码可以保证新任务得以处理,旧任务如果过期则丢弃掉,有舍才有得嘛。因为任务队列中的任务是按照时间顺序放进去的,所以在LIFO时,一旦取出来一个任务已经过期,则意味着接下来取出的任务也是过期的,此时可以直接把队列清空,不需要挨个取出来再丢弃。
4. 缩短任务队列过期时间
为什么要给队列中的任务设置过期时间?因为防止任务已经在队列中等待了很长时间,防止取出执行完后任务已经超时。比如说从队列中取出任务后,得知任务已经在队列中等待了90ms,而此时处理一个任务平均需要消耗20ms,client设置的超时时间是100ms,那么就可以大概率的得知这个任务处理完后,client已经认为该任务超时了,那么就没必要再消耗CPU继续处理。所以设置一个任务队列的过期时间是必要的。
为什么要动态调整队列中任务的过期时间呢?正如我们前边所说,任务的过期时间是跟当前处理任务的平均耗时相关的,而当前处理任务的平均时间不是一成不变的,会随着当前机器的各种资源的情况发生变化。当任务平均处理时间比较短时,可以容忍任务在队列中多等一会儿,而当任务平均处理时间比较长时,只能允许任务在队列中停留较短的时间。
限流
相比降级,限流会直接让部分用户的请求不会被处理,会在一定程度上影响用户体验,是有损的。但是如果经过降级,还不能保证系统的负载处于一个安全的范围内,就需要限流了。限流属于舍小为大,拒绝一部分用户的请求,保证整个系统可用,而不是不顾自身实力蛮力处理所有请求,导致系统挂掉,最终一个请求都处理不了。
限流又可以分为两种,分为server根据自身处理能力主动丢弃部分请求,和client根据server的平均响应时间和server的成功率减少给server发的请求的数量。
限流的维度可以有多个,根据程序本身的指标:连接数、总的qps、分类的qps,server一般要对接不同的端,不能因为某一个端的流量上涨而影响到其他端的流量。也可以根据机器的总体指标来进行限流,比如说网络流量、CPU、内存等。
扩容
如果经过降级、限流后,系统的负载依旧不能维持在一个相对安全的范围内,此时说明我们现有的资源已经不足以满足用户热情的需求了,此时就需要扩容了。
扩容可以使用docker自动完成,根据请求的平均响应时间、请求的成功率、机器的负载等自动判断和扩容,也可以根据过去一天、一周、一个月、一年的流量,来提前预估接下来的流量趋势,提前做出扩容和缩容。
经过上述异地多活、降级、限流、扩容等措施,可以保证系统不出大问题,但是优化的道路任重道远,要持续不断地优化才能保证系统越来越稳定。也希望我们的经验教训能帮到大家,让大家再也不用担心各种异常各种突然状况。(全文完)
来源:中生代技术