ES+Redis+MySQL,这个⾼可⽤架构设计太顶了
⼀、背景
会员系统是⼀种基础系统,跟公司所有业务线的下单主流程密切相关。如果会员系统出故障,会导致⽤户⽆法下单,影响范围是全公司所有业务线。所以,会员系统必须保证⾼性能、⾼可⽤,提供稳定、⾼效的基础服务。
随着同程和艺龙两家公司的合并,越来越多的系统需要打通同程APP、艺龙APP、同程⼩程序、艺龙⼩程序等多平台会员体系。例如⼩程序的交叉营销,⽤户买了⼀张⽕车票,此时想给他发酒店红包,这就需要查询该⽤户的统⼀会员关系。因为⽕车票⽤的是同程会员体系,酒店⽤的是艺龙会员体系,只有查到对应的艺龙会员卡号后,才能将红包挂载到该会员账号。除了上述讲的交叉营销,还有许多场景需要查询统⼀会员关系,例如订单中⼼、会员等级、⾥程、红包、常旅、实名,以及各类营销活动等等。所以,会员系统的请求量越来越⼤,并发量越来越⾼,今年五⼀⼩长假的秒并发tps甚⾄超过2万多。在如此⼤流量的冲击下,会员系统是如何做到⾼性能和⾼可⽤的呢?这就是本⽂着重要讲述的内容。
⼆、ES⾼可⽤⽅案
1. ES双中⼼主备集架构
同程和艺龙两家公司融合后,全平台所有体系的会员总量是⼗多亿。在这么⼤的数据体量下,业务线的查询维度也⽐较复杂。有的业务线基于⼿机号,有的基于unionid,也有的基于艺龙卡号等查询会员信息。这么⼤的数据量,⼜有这么多的查询维度,基于此,我们选择ES ⽤来存储统⼀会员关系。ES集在整个会员系统架构中⾮常重要,那么如何保证ES的⾼可⽤呢?
⾸先我们知道,ES集本⾝就是保证⾼可⽤的,如下图所⽰:
当ES集有⼀个节点宕机了,会将其他节点对应的Replica Shard升级为Primary Shard,继续提供服务。但即使是这样,还远远不够。例如ES集都部署在机房A,现在机房A突然断电了,怎么办?例如服务器硬件故障,ES集⼤部分机器宕机了,怎么办?或者突然有个⾮常
热门的抢购秒杀活动,带来了⼀波⾮常⼤的流量,直接把ES集打死了,怎么办?⾯对这些情况,让运维兄弟冲到机房去解决?这个⾮常不现实,因为会员系统直接影响全公司所有业务线的下单主流程,故障恢复的时间必须⾮常短,如果需要运维兄弟⼈⼯介⼊,那这个时间就太长了,是绝对不能容忍的。那ES的⾼可⽤如何做呢?我们的⽅案是ES双中⼼主备集架构。
我们有两个机房,分别是机房A和机房B。我们把ES主集部署在机房A,把ES备集部署在机房B。会员系统的读写都在ES主集,通过MQ将数据同步到ES备集。此时,如果ES主集崩了,通过统⼀配置,将会员系统的读写切到机房B的ES备集上,这样即使ES主集挂了,也能在很短的时间内实现故障转移,确保会员系统的稳定运⾏。最后,等ES主集故障恢复后,打开开关,将故障期间的数据同步到ES主集,等数据同步⼀致后,再将会员系统的读写切到ES主集。
2. ES流量隔离三集架构
双中⼼ES主备集做到这⼀步,感觉应该没啥⼤问题了,但去年的⼀次恐怖流量冲击让我们改变了想法。那是⼀个节假⽇,某个业务上线了⼀个营销活动,在⽤户的⼀次请求中,循环10多次调⽤了会员系统,导致会员系统的tps暴涨,差点把ES集打爆。这件事让我们后怕不已,它让我们意识到,⼀定要对调⽤⽅进⾏优先级分类,实施更精细的隔离、熔断、降级、限流策略。⾸先,我们梳理了所有调⽤⽅,分出两⼤类请求类型。第⼀类是跟⽤户的下单主流程密切相关的请求,这类请求⾮常重要,应该⾼优先级保障。第⼆类是营销活动相关的,这类请求有个特点,他们的请求量很⼤,tps很⾼,但不影响下单主流程。基于此,我们⼜构建了⼀个ES集,专门⽤来应对⾼tps的营销秒杀类请求,这样就跟ES主集隔离开来,不会因为某个营销活动的流量冲击⽽影响⽤户的下单主流程。如下图所⽰:
3. ES集深度优化提升
讲完了ES的双中⼼主备集⾼可⽤架构,接下来我们深⼊讲解⼀下ES主集的优化⼯作。有⼀段时间,我们特别痛苦,就是每到饭点,ES 集就开始报警,搞得每次吃饭都⼼慌慌的,⽣怕ES集⼀个扛不住,就全公司炸锅了。那为什么⼀到饭点就报警呢?因为流量⽐较⼤,导致ES线程数飙⾼,cpu直往上窜,查询耗时增加,并传导给所有调⽤⽅,导致更⼤范围的延时。那么如何解决这个问题呢?
通过深⼊ES 集,我们发现了以下⼏个问题:
ES负载不合理,热点问题严重。ES主集⼀共有⼏⼗个节点,有的节点上部署的shard数偏多,有的节点部署的shard数很少,导致某些服务器的负载很⾼,每到流量⾼峰期,就经常预警。
ES线程池的⼤⼩设置得太⾼,导致cpu飙⾼。我们知道,设置ES的threadpool,⼀般将线程数设置为服务器的cpu核数,即使ES的查询压⼒很⼤,需要增加线程数,那最好也不要超过“cpu core * 3 / 2 + 1”。如果设置的线程数过多,会导致cpu在多个线程上下⽂之间频繁来回切换,浪费⼤量cpu资源。
shard分配的内存太⼤,100g,导致查询变慢。我们知道,ES的索引要合理分配shard数,要控制⼀个shard的内存⼤⼩在50g以内。
如果⼀个shard分配的内存过⼤,会导致查询变慢,耗时增加,严重拖累性能。
string类型的字段设置了双字段,既是text,⼜是keyword,导致存储容量增⼤了⼀倍。会员信息的查询不需要关联度打分,直接根据keyword查询就⾏,所以完全可以将text字段去掉,这样就能节省很⼤⼀部分存储空间,提升性能。
ES查询,使⽤filter,不使⽤query。因为query会对搜索结果进⾏相关度算分,⽐较耗cpu,⽽会员信息的查询是不需要算分的,这部分的性能损耗完全可以避免。
节约ES算⼒,将ES的搜索结果排序放在会员系统的jvm内存中进⾏。
增加routing key。我们知道,⼀次ES查询,会将请求分发给所有shard,等所有shard返回结果后再聚合数据,最后将结果返回给调⽤⽅。如果我们事先已经知道数据分布在哪些shard上,那么就可以减少⼤量不必要的请求,提升查询性能。
经过以上优化,成果⾮常显著,ES集的cpu⼤幅下降,查询性能⼤幅提升。ES集的cpu使⽤率:
mysql下载不了怎么办会员系统的接⼝耗时:
三、会员Redis缓存⽅案
⼀直以来,会员系统是不做缓存的,原因主要有两个:第⼀个,前⾯讲的ES集性能很好,秒并发3万多,99线耗时5毫秒左右,已经⾜够应付各种棘⼿的场景。第⼆个,有的业务对会员的绑定关系要求
实时⼀致,⽽会员是⼀个发展了10多年的⽼系统,是⼀个由好多接⼝、好多系统组成的分布式系统。所以,只要有⼀个接⼝没有考虑到位,没有及时去更新缓存,就会导致脏数据,进⽽引发⼀系列的问题,例如:⽤户在APP上看不到订单、APP和的会员等级、⾥程等没合并、和APP⽆法交叉营销等等。那后来为什么⼜要做缓存呢?是因为今年机票的盲盒活动,它带来的瞬时并发太⾼了。虽然会员系统安然⽆恙,但还是有点⼼有余悸,稳妥起见,最终还是决定实施缓存⽅案。
1. ES近⼀秒延时导致的Redis缓存数据不⼀致问题的解决⽅案
在做会员缓存⽅案的过程中,遇到⼀个ES引发的问题,该问题会导致缓存数据的不⼀致。我们知道,ES操作数据是近实时的,往ES新增⼀个Document,此时⽴即去查,是查不到的,需要等待1秒后才能查询到。如下图所⽰:
ES的近实时机制为什么会导致redis缓存数据不⼀致呢?具体来讲,假设⼀个⽤户注销了⾃⼰的APP账号,此时需要更新ES,删除APP账号和账号的绑定关系。⽽ES的数据更新是近实时的,也就是说,1秒后你才能查询到更新后的数据。⽽就在这1秒内,有个请求来查询该⽤户的会员绑定关系,它先到redis缓存中查,发现没有,然后到ES查,查到了,但查到的是更新前的旧数据。最后,该请求把查询到的旧数据更新到redis缓存并返回。就这样,1秒后,ES中该⽤户的会员数据更新了,但redis缓存的数据还是旧数据,导致了redis缓存跟ES的数据不⼀致。如下图所⽰:
⾯对该问题,如何解决呢?我们的思路是,在更新ES数据时,加⼀个2秒的redis分布式并发锁,为了
保证缓存数据的⼀致性,接着再删除redis中该会员的缓存数据。如果此时有请求来查询数据,先获取分布式锁,发现该会员ID已经上锁了,说明ES刚刚更新的数据尚未⽣效,那么此时查询完数据后就不更新redis缓存了,直接返回,这样就避免了缓存数据的不⼀致问题。如下图所⽰
上述⽅案,乍⼀看似乎没什么问题了,但仔细分析,还是有可能导致缓存数据的不⼀致。例如,在更新请求加分布式锁之前,恰好有⼀个查询请求获取分布式锁,⽽此时是没有锁的,所以它可以继续更新缓存。但就在他更新缓存之前,线程block了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执⾏更新缓存,就把脏数据写到缓存中了。发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发⽣了并发冲突,只要将它们互斥,就能解决问题。如下图所⽰:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论