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

超过十亿用户为中心的系统,ES+Redis+MySQL架构游刃有余!

时间:2023-04-01 21:11:02 Java

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