当前位置: 首页 > 后端技术 > Java

ES+Redis+MySQL,这高可用架构设计太top了!

时间:2023-04-01 15:20:18 Java

文章来源:【公众号:同程艺龙技术中心】背景介绍会员制度是与公司各业务线下单主要流程密切相关的基础制度。如果会员系统出现故障,用户将无法下单,影响范围将遍及公司所有业务线。因此,会员系统必须保证高性能和高可用,并提供稳定高效的基础服务。随着同程与艺龙的合并,越来越多的系统需要打通同程APP、艺龙APP、同程微信小程序、艺龙微信小程序等多平台会员系统。比如在微信小程序的交叉营销中,用户买了火车票,此时想给他发酒店红包,这就需要查询用户的统一会员关系。因为火车票使用的是同程会员系统,而酒店使用的是艺龙会员系统,只有找到对应的艺龙会员卡号后,才能将红包挂载到会员账户中。除了上面提到的交叉营销,还有很多场景需要查询统一的会员关系,比如订单中心、会员等级、里程、红包、常出差、实名、各种营销活动等等。因此,会员系统的请求量越来越大,并发量越来越高。今年清明假期的第二次并发tps甚至超过了20000。在如此大流量的冲击下,会员系统如何做到高性能和高可用呢?这就是本文的重点。ES高可用解决方案|ES双中心主备集群架构同程与艺龙整合后,整个平台所有系统的成员总数超过10亿。这么大的数据量,业务线的查询维度也比较复杂。有的业务线是根据手机号,有的是根据微信unionid,有的是根据艺龙卡号等查询会员信息。基于如此庞大的数据量和如此多的查询维度,我们选择ES来存储统一的成员关系。ES集群在整个会员体系架构中非常重要,那么如何保证ES的高可用呢?首先我们知道ES集群本身是保证高可用的,如下图所示:当ES集群的一个节点宕机时,其他节点对应的ReplicaShard会升级为PrimaryShard继续提供服务服务。但这还不够。比如ES集群部署在A机房,现在A机房突然断电,怎么办?比如服务器硬件出现故障,ES集群大部分机器都宕机了,怎么办?或者突然有一个非常火爆的闪购活动,带来一波非常大的流量,直接秒杀ES集群,怎么办?面对这些情况,让运维师兄赶到机房去解决?这是很不现实的,因为会员系统直接影响到公司所有业务线的下单主流程,故障恢复的时间肯定很短。如果需要运维小哥人工干预,时间太长,绝对不能容忍。ES的高可用怎么样?我们的方案是ES双中心主备集群架构。我们有两个机房,分别是A机房和B机房,我们在A机房部署ES主集群,在B机房部署ES备集群,成员系统的读写都在ES中主集群,数据通过MQ同步到ES备集群。此时如果ES主集群崩溃,通过统一配置,将成员系统的读写切换到B机房的ES备集群,这样即使ES主集群出现故障,也可以实现故障转移短时间内。保障会员系统稳定运行。最后在主ES集群故障恢复后,打开开关将故障期间的数据同步到主ES集群。数据同步一致后,将成员系统的读写切换到主ES集群。|ES流量隔离三集群架构双中心ES主备集群做这一步,感觉应该没什么大问题,但是去年的一次恐怖流量冲击让我们改变了想法。那是一个假期,某商家发起了一场营销活动。在一次用户请求中,会员系统被调用了10多次,导致会员系统的tps暴涨,差点炸掉ES集群。这件事让我们心惊肉跳,也让我们意识到必须对调用者进行优先级排序,实施更细化的隔离、熔断、降级、限流策略。首先,我们对所有调用者进行了梳理,将其分为两类请求:第一类是与下单主流程密切相关的请求。这种类型的请求非常重要,应该得到高优先级的保证。第二类与营销活动有关。这种请求有一个特点。他们请求量大,tps高,但不影响下单主流程。基于此,我们又搭建了一个ES集群,专门用来处理高tps的营销秒杀请求,做到与ES主集群隔离,不会因为一个流量的影响影响用户的主单下单某些营销活动。过程。如下图所示:|ES集群深度优化提升说完ES的双中心主备集群的高可用架构,我们来深入讲解一下ES主集群的优化。有一段时间,我们很痛苦,就是每次吃饭的时候,ES集群就开始报警,搞得我们每次吃饭都很慌,生怕ES集群一个人扛不住,整个公司都会被炸毁。那为什么一到吃饭时间就报警呢?因为流量比较大,ES线程数量猛增,CPU直线上升,查询时间增加,传递给所有调用者,造成更大范围的延迟。那么如何解决这个问题呢?通过深入挖掘ES集群,我们发现了以下问题:ES负载不合理,热点问题严重。ES主集群有几十个节点。一些节点部署了过多的分片,而另一些节点部署了很少的分片。结果,一些服务器负载很重。当交通高峰时,会频繁发出警告。ES线程池大小设置过大,导致cpu暴涨。我们知道,在设置ES的线程池时,一般会设置线程数为服务器的CPU核数。即使ES的查询压力大,需要增加线程数,也最好不要超过“cpucore*3/2+1”。如果线程数设置太多,CPU会在多个线程上下文之间频繁来回切换,浪费大量CPU资源。分片分配的内存太大,100g,导致查询变慢。我们知道ES的索引要合理分配分片数量,将一个分片的内存大小控制在50g以内。如果一个分片分配过多的内存,会减慢查询速度,增加时间消耗,严重影响性能。string类型的字段设置了double字段,既是text又是keyword,存储量翻倍。查询会员信息不需要根据关联度打分,直接根据关键字查询即可,这样可以完全去掉textfield,可以节省很大一部分存储空间,提高性能。ES查询,使用过滤器,而不是查询。因为查询会计算搜索结果的相关度得分,比较消耗cpu,而会员信息的查询不需要计算得分,这部分性能损失是完全可以避免的。为了节省ES的算力,将ES在成员系统的jvm内存中的搜索结果进行排序。添加路由键。我们知道,一个ES查询会将请求分发到所有分片,在所有分片返回结果后聚合数据,最后将结果返回给调用者。如果我们提前知道数据分布在哪些分片上,就可以减少大量不必要的请求,提高查询性能。经过上面的优化,效果非常显着,ES集群的cpu大大降低,查询性能大大提升。ES集群cpu使用率:会员系统耗时接口:会员Redis缓存方案。长期以来,会员系统不做缓存。主要有两个原因。3万多,99行大概需要5毫秒,足以应对各种棘手的场景。第二,有些业务要求会员绑定关系实时一致,而会员是一个已经发展了10多年的老系统。它是由许多接口和许多系统组成的分布式系统。所以,只要有一个接口考虑不到位,缓存没有及时更新,就会导致脏数据,进而引发一系列问题。比如:用户在APP上看不到微信订单,APP和微信的会员等级和里程没有合并,微信和APP不能交叉营销等等,那为什么还要缓存呢?就是因为今年机票的盲盒活动,带来的瞬时并发量太高了。会员制虽然安然无恙,但还是心有余悸。为了安全起见,我们最终决定实施缓存解决方案。|ES近一秒延迟导致Redis缓存数据不一致问题的解决方案。我们知道ES运行数据是接近实时的。如果在ES中添加一个Document,立马可以查看,但是找不到。您需要等待1秒钟才能检查它。如下图:为什么ES的近实时机制会导致Redis缓存数据不一致?具体来说,假设用户已经退出APP账号,需要更新ES删除APP账号与微信账号的绑定关系。ES的数据更新是接近实时的,也就是说1秒后可以查询到更新后的数据。并且在这1秒内,有请求查询用户的会员绑定关系。它先查Redis缓存,没有,再查ES,找到了,但是查到的是update前的旧数据。最后,请求将查询到的旧数据更新到Redis缓存中并返回。这样1秒后,用户在ES中的成员资格数据更新了,但是Redis缓存的数据还是旧数据,导致Redis缓存中的数据和ES不一致。如下图所示:面对这个问题,如何解决?我们的思路是在更新ES数据的时候加一个2秒的Redis分布式并发锁,以保证缓存数据的一致性,然后删除成员在Redis中的缓存数据。如果此时有查询数据的请求,先获取分布式锁,发现成员ID已经被锁定,说明ES刚刚更新的数据还没有生效,那么此时查询完数据后,redis缓存不会更新,直接返回。这样就避免了缓存数据不一致的问题。如下图:上面的方案乍一看似乎没有问题,但仔细分析还是有可能导致缓存数据不一致。比如更新请求添加分布式锁之前,恰好有一个查询请求获取分布式锁,但是此时没有锁,所以可以继续更新缓存。但是就在他更新缓存之前,线程被阻塞了。这时候更新请求来了,加了分布式锁,删除了缓存。当更新请求完成操作后,查询请求的线程就活跃起来了。这时,它执行更新缓存,将脏数据写入缓存。你找到了吗?问题的主要症结在于“删除缓存”和“更新缓存”之间存在并发冲突。只要它们是互斥的,问题就可以解决。如下图所示:实施缓存方案后,统计显示缓存命中率在90%+,极大的缓解了ES的压力,会员系统的整体性能也有了很大的提升。|Redis双中心多集群架构接下来我们来看看如何保证Redis集群的高可用。如下图所示:关于Redis集群的高可用,我们采用了双中心多集群的模型。在A机房和B机房分别部署一套Redis集群。更新缓存数据时,双写,只有两个机房的Redis集群都写成功,才会返回success。查询缓存数据时,就近机房查询,减少延迟。这样即使A机房整体出现故障,B机房仍然可以提供完整的会员服务。上面提到的高可用会员主库方案,整个平台的会员绑定关系数据存在于ES中,会员的注册明细存在于关系型数据库中。最早会员使用的数据库是SqlServer,直到有一天,一位DBA来找我们说,单个SqlServer数据库存储了超过十亿的会员数据,服务器已经达到物理极限,无法再扩展了更多的。按照现在的增长趋势,用不了多久整个SqlServer数据库就会崩溃。想一想,那是一种怎样的灾难场景:会员数据库崩溃,会员系统崩溃;当会员体系崩溃时,公司的所有业务线都崩溃了。想想都让人不寒而栗,真是耳目一新,于是马上开始了迁移DB的工作。|MySQL双中心Partition集群方案经过调研,我们选择了分库分表的双中心MySQL集群方案,如下图:会员总共有十亿多条数据,我们对会员进行了分库主库分为1000多个分片,平均分到每个分片百万量级,足够使用。MySQL集群采用1主3从架构。主库放在A机房,从库放在B机房,两个机房之间通过专线同步数据,延时在1毫秒以内。成员系统通过DBRoute读写数据,写入数据路由到主节点所在机房A,读取数据路由到本地机房,就近访问,减少网络延迟。这样,采用双中心的MySQL集群架构,大大提高了可用性。即使A机房整体崩溃,B机房的Slave也可以升级为Master继续提供服务。双中心MySQL集群搭建完成后,我们进行了压力测试。经测试,秒并发可达200??00以上,平均耗时在10毫秒以内,性能达标。|会员主库平滑迁移计划接下来的工作是将会员系统的底层存储从SqlServer切换到MySQL,这是一个风险很大的工作。主要有以下难点:会员系统一刻也关不掉。在不关机的情况下完成从SqlServer到MySQL的切换,就像给高速行驶的汽车换轮子一样。会员系统由许多系统和接口组成。毕竟已经发展了10多年了。由于历史原因,遗留了大量旧接口,逻辑错综复杂。这么多系统必须一一梳理,DAL层代码一定要重写,不能出问题,否则后果不堪设想。数据的迁移要无缝,不仅要迁移超过10亿的存量数据,还要将实时产生的数据无缝同步到MySQL。另外,除了保证数据同步的实时性外,还需要保证数据的正确性以及SqlServer和MySQL之间数据的一致性。基于以上痛点,我们设计了“全同步、增量同步、实时流量灰度切换”的技术方案。首先,为了保证数据的无缝切换,采用了实时双写方案。由于业务逻辑的复杂性和SqlServer与MySQL的技术差异,在双写MySQL的过程中,可能会写入不成功,一旦写入失败,SqlServer与MySQL的数据就会不一致,这就是绝对不允许。所以,我们采用的策略是,试运行时,主要写入SqlServer,然后通过线程池异步写入MySQL。如果写入失败,请重试3次。如果仍然失败,记录日志,然后手动排查原因。继续双写,直到运行一段时间,没有双写失败。通过以上策略,在大多数情况下可以保证双写操作的正确性和稳定性。即使试运行时SqlServer和MySQL的数据不一致,也可以完全基于SqlServer重新构建MySQL的数据。因为我们在设计双写策略的时候,会保证SqlServer能够写入成功,也就是说SqlServer中的数据是所有数据中最完整、最正确的。如下图所示:说完双写,我们再来看看如何对“读取数据”进行灰度化。总体思路是通过A/B平台逐步灰度化流量。一开始100%流量读取SqlServer数据库,之后逐渐削减流量读取MySQL数据库。先是1%,如果没有问题,再逐步释放流量,最后100%的流量全部走MySQL数据库。在流量逐渐灰度化的过程中,需要验证机制。只有验证ok了才能进一步放大流量。那么这个验证机制是如何实现的呢?解决方法是在一个查询请求中使用异步线程比较SqlServer和MySQL的查询结果是否一致。如果不一致,记录日志,然后人工排查不一致的原因,直到彻底解决不一致问题,再逐步灰度流量。如下图所示:因此,整体的实现流程如下:首先,在一个黑风高的夜晚,流量最小的时候,完成SqlServer到MySQL数据库的全量数据同步。然后,启用双写。这时候如果有用户注册,会实时双写到两个数据库中。那么在全量同步和实时双写使能之间,这期间两个数据库的数据还是有差异的,所以需要再次增量同步来补齐数据,防止数据不一致。剩下的时间花在监控各种日志,看是否有双写问题,看数据对比是否一致,等等。这个时期耗时最长,也最容易出问题。如果有些问题比较严重,导致数据不一致,就需要从头开始,重新基于SqlServer搭建一个完整的MySQL数据库,然后再对流量进行灰度化。直到最后,100%的流量灰度到MySQL。在这一点上,你就完成了。灰度逻辑离线,读写全部切换到MySQL集群。|MySQL和ES主备集群方案都做到了这一步,感觉成员主库应该没问题,但是dal组件的一次严重故障改变了我们的想法。那次失败太可怕了。公司很多应用连不上数据库,订单创建量直线下降。这让我们意识到即使数据库是好的,但是dal组件出现异常,会员系统还是会挂掉。因此,我们再次对主成员数据库的数据源进行异构异构,将数据双写到ES,如下图:如果dal组件出现故障或者MySQL数据库挂了,可以切换读写到ES,并等待MySQL恢复,然后将数据同步写入MySQL,最后切换回MySQL数据库进行读写。如下图所示:会员关系管理异常会员系统不仅要保证系统的稳定性和高可用,还要保证数据的准确性和正确性。比如分布式并发故障导致用户的APP账号绑定了别人的微信小程序账号,会造成非常不好的影响。首先,两个账号绑定后,两个用户下的酒店、机票、火车票订单都可以互相看到。你想想,别人能看到你的酒店预订,你不受欢迎你会抱怨吗?除了可以看到别人的订单,还可以操作订单。比如用户在APP的订单中心看到别人订的机票订单。他认为订单不是他自己的,所以他取消了订单。这会带来非常严重的客户投诉。众所周知,机票的取消费是相当高的,不仅影响了用户的正常出行,还会造成比较大的经济损失,非常不好。对于这些异常的会员账号,我们进行了详细的梳理,通过非常复杂和烧脑的逻辑识别出这些账号,并对会员界面进行了深度优化和管理,在代码逻辑层堵住了相关漏洞,并完成异常会员账号。治理工作。如下图:展望:更细化的流控和降级策略任何系统都不能保证100%不出问题,所以我们必须要有面向故障的设计,即更细化的流控和降级策略Downgrade战略。|更精细的流控策略热点控制。对于刷单场景,同一个会员id会出现大量重复请求,形成热点账号。当这些账号的访问量超过设定的阈值时,就会实施限流策略。基于主叫账号的流控规则。这个策略主要是为了防止调用者代码bug导致的大流量。比如在一个用户请求中,调用方多次循环调用会员接口,导致会员系统多次流量骤增。因此,需要为每个呼叫账户设置流量控制规则,并在超过阈值时执行限流策略。全局流量控制规则。我们的会员系统可以承受每秒超过30,000tps的并发请求。如果这个时候,有一个可怕的流量过来,tps高达10万。与其让这一波流量把会员数据库和ES全部干掉,还不如把超过会员系统承受能力的流量Fastfail掉,至少30000tps以内的会员请求能正常响应,整个会员系统不会坍塌。|更细化的降级策略是基于平均响应时间的降级。成员接口还依赖于其他接口。当调用其他接口的平均响应时间超过阈值时,进入准降级状态。如果接下来1秒内传入请求的平均响应时间持续超过阈值,那么在下一个时间窗口内,熔断器将自动熔断。根据异常值的数量和异常值的比例进行降级。当成员接口所依赖的其他接口发生异常时,如果1分钟内异常次数超过阈值,或者异常总数与每秒吞吐量的比值超过阈值,则进入降级状态,在下一个时间窗口内自动融合。目前,我们最大的痛点是会员调用账户的管理。在公司,如果要调用会员接口,必须申请一个调用账号。我们会记录账户的使用场景,并设置流量控制和降级策略的规则。但是在实际使用过程中,申请账号的同事可能会换到其他部门。这个时候,他可能还会调用会员系统。为了省事,他不会重新申请会员账号,而是直接使用之前的账号。这使得我们无法判断会员账户的具体使用场景,无法实施更精细的流量控制和降级策略。因此,接下来,我们将对所有调用的账户进行一一梳理。这是一项非常庞大而繁琐的工作,但没有出路,必须做好。近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.厉害了!Java协程来了。..3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!