说在前面
在尼恩的(50+)读者社区中,经常遇到一个 非常、非常高频的一个面试题,但是很不好回答,类似如下:
千万级数据,如何做系统架构?
亿级数据,如何做系统架构?
千万级流量,如何做系统架构?
亿级流量,如何做系统架构?
高并发系统,如何架构?
最近,有个小伙伴字节二面,又遇到了这个问题。
其实,尼恩一直想梳理一个教科书式的答案,
咱们一直心心念念的 “千万级数据,如何做性能优化?” 的教科书式的答案,其实就藏着在这个行业案例里边。
这里有一个新的行业案例《同程艺龙会员系统的高并发架构》,尼恩从 面试维度,对这个方案,进行二次重构和梳理,现在把其作为参考答案,收入咱们的《尼恩Java面试宝典 PDF》 V96版本
下面的内容,是尼恩是结合自己的3高架构笔记,以及尼恩的3高架构知识体系(3高架构宇宙)做的二次分析。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】取
文章目录
同程艺龙会员系统的业务场景
同程艺龙是同程旅游集团旗下的同程网络与艺龙旅行网在2017年12月29日共同成立的公司,新公司将整合双方的交通、酒店等优势资源,打造更为领先的旅行服务平台。
2018年6月21日,同程艺龙在港交所提交招股书,联席保荐人为摩根士丹利、摩根大通、招银国际。
2018年11月26日,同程艺龙正式在港交所挂牌。
同程艺龙为机场、酒店、目的地等旅行行业产业链上下游提供技术赋能,加速产业数智化进程。终端用户应用包括:
- 同程APP
- 艺龙APP
- 同程微信小程序
- 艺龙微信小程序
2021年第三季度,同程艺龙平均月活达到2.77亿人,同比增长12.7%;平均月付费用户达到3360万人,同比增长12.8%。
截至2021年9月30日的12个月里,同程艺龙付费用户同比增加29.6%至1.96亿人。
在同程艺龙内部,会员系统是一种基础系统。
这种基础系统,跟公司所有业务线的下单主流程密切相关。
在同程艺龙平台的内部,如果会员系统出故障,会导致用户无法下单。
也就是说,如果会员系统出故障,影响范围不仅仅是会员系统本身,而是全公司所有业务线。
所以,会员系统必须保证高性能、高可用、高并发,为大的业务平台,提供稳定、高效的基础服务。
随着同程和艺龙两家公司的合并,越来越多的系统需要打通同程APP、艺龙APP、同程微信小程序、艺龙微信小程序等多平台会员体系。
例如微信小程序的交叉营销,用户买了一张火车票,此时想给他发酒店红包,这就需要查询该用户的统一会员关系。
因为火车票用的是同程会员体系,酒店用的是艺龙会员体系,只有查到对应的艺龙会员卡号后,才能将红包挂载到该会员账号。
除了了 交叉营销 场景之外,还有许多、许多的业务场景需要查询统一会员关系。
例如订单中心、会员等级、里程、红包、常旅、实名,以及各类营销活动等等。
所以,会员系统的请求量越来越大,并发量越来越高。
2022年五一小长假的秒并发TPS甚至超过2万多。
在如此大流量的冲击下,会员系统是如何做到高性能和高可用的呢?
会员数据异构存储架构方案
同程艺龙内部,会员系统采用的是 mysql集群+ES集群结合的 异构存储架构
具体如下图所示
-
为什么使用mysql
目前最火的关系型数据库,支持事务, 支持B+树结构高性能索引,数据低延迟(写入即可查)但是有两大缺点:
-
(1)不宜全文搜索,如果非索引全文搜索,会出现全表搜索, 性能低
-
(2)使用mysql 需要分库分表,这种时候,没法做全表关联
-
-
为什么使用elsaticsearch
es在搜索方面天生具有优势,-
(1)倒排索引,天生的全文检索
-
(2)支持大表、宽表、非结构数据。不像关系型数据库那样一个简单的搜索就需要跨表联表、跨库关联
-
-
mysql+elasticsearch的好处
以mysql存取数据,es作为搜索引擎,保证性能
MySQL会员主库的:高可用+高并发架构
上述讲到,全平台会员的绑定关系数据存在ES,而会员的注册明细数据存在关系型数据库。
最早,会员使用的数据库是SQL Server,
直到有一天,单台SQL Server数据库已经存储了十多亿的会员数据,服务器已达到物理极限,不能再扩展了。
按照当然的增长趋势,过不了多久,整个SQL Server数据库就崩了。
大家想想,SQL Server数据库崩了,那是一种什么样的灾难场景:
会员数据库崩了,会员系统就崩了;
会员系统崩了,全公司所有业务线就崩了。
想想就不寒而栗,酸爽无比,同城艺龙团队 立刻开启了迁移DB的工作。
MySQL双中心Partition集群方案
经过调研,团队选择了双中心分库分表的MySQL集群方案,
mysql分表架构
会员一共有十多亿的数据,团队把会员主库分了1000多个分片。
从单个分片的维度来说,平分到每个分片大概百万的量级,单个分片不到千万级,足够使用了。
mysql主从架构
整个MySQL集群采用1主3从的架构。
主库放在机房A,从库放在机房B,两个机房之间通过专线同步数据,延迟在1毫秒内。
mysql流量路由架构
-
写入的路由:写数据都路由到master节点所在的机房A,
-
读取的路由:读数据都路由到本地机房,就近访问,减少网络延迟。
会员系统通过DBRoute读写数据,DBRoute 是一个统一的数据库访问sdk 组件,可以根据流量开关,进行流量的切流。
具体的架构图,如下图所示:
这样,采用双中心的MySQL集群架构,极大提高了可用性,即使机房A整体都崩了,还可以将机房B的Slave升级为Master,继续提供服务。
双中心MySQL集群搭建好后,团队进行了压测,测试下来,秒并发能达到2万多,平均耗时在10毫秒内,性能达标。
会员主库平滑迁移方案
接下来的工作,就是把会员系统的底层存储从SQL Server切到MySQL上,这是个风险极高的工作,主要有以下几个难点:
- 会员系统是一刻都不能停机的,要在不停机的情况下完成SQL Server到MySQL的切换,就像是在给高速行驶的汽车换轮子。
- 会员系统是由很多个系统和接口组成的,毕竟发展了10多年,由于历史原因,遗留了大量老接口,逻辑错综复杂。这么多系统,必须一个不落的全部梳理清楚,DAL层代码必须重写,而且不能出任何问题,否则将是灾难性的。
- 数据的迁移要做到无缝迁移,不仅是存量10多亿数据的迁移,实时产生的数据也要无缝同步到MySQL。另外,除了要保障数据同步的实时性,还要保证数据的正确性,以及SQL Server和MySQL数据的一致性。
基于以上痛点,团队设计了“全量同步、增量同步、实时流量灰度切换”的技术方案。
首先,为了保证数据的无缝切换,采用实时双写的方案。
因为业务逻辑的复杂,以及SQL Server和MySQL的技术差异性,在双写MySQL的过程中,不一定会写成功,而一旦写失败,就会导致SQL Server和MySQL的数据不一致,这是绝不允许的。
所以,团队采取的策略是,在试运行期间,主写SQL Server,然后通过线程池异步写MySQL,
如果写失败了,重试三次,如果依然失败,则记日志,然后人工排查原因,解决后,继续双写,直到运行一段时间,没有双写失败的情况。
通过上述策略,可以确保在绝大部分情况下,双写操作的正确性和稳定性,即使在试运行期间出现了SQL Server和MySQL的数据不一致的情况,也可以基于SQL Server再次全量构建出MySQL的数据,因为团队在设计双写策略时,会确保SQL Server一定能写成功,也就是说,SQL Server中的数据是全量最完整、最正确的。
如下图所示:
讲完了双写,接下来团队看一下“读数据”如何灰度。
整体思路是,通过A/B平台逐步灰度流量,刚开始100%的流量读取SQL Server数据库,然后逐步切流量读取MySQL数据库,先1%,如果没有问题,再逐步放流量,最终100%的流量都走MySQL数据库。
在逐步灰度流量的过程中,需要有验证机制,只有验证没问题了,才能进一步放大流量。
那么这个验证机制如何实施呢?
方案是,在一次查询请求里,通过异步线程,比较SQL Server和MySQL的查询结果是否一致,如果不一致,记日志,再人工检查不一致的原因,直到彻底解决不一致的问题后,再逐步灰度流量。
如下图所示:
所以,整体的实施流程如下:
首先,在一个夜黑风高的深夜,流量最小的时候,完成SQL Server到MySQL数据库的全量数据同步。
接着,开启双写,
此时,如果有用户注册,就会实时双写到两个数据库。
那么,在全量同步和实时双写开启之间,两个数据库还相差这段时间的数据,所以需要再次增量同步,把数据补充完整,以防数据的不一致。
剩下的时间,就是各种日志监控,看双写是否有问题,看数据比对是否一致等等。
这段时间是耗时最长的,也是最容易发生问题的,
如果有的问题比较严重,导致数据不一致了,就需要从头再来,
再次基于SQL Server全量构建MySQL数据库,然后重新灰度流量,直到最后,100%的流量全部灰度到MySQL,此时就大功告成了,下线灰度逻辑,所有读写都切到MySQL集群。
MySQL和ES主备集群方案
做到这一步,感觉会员主库应该没问题了,可dal组件的一次严重故障改变了团队的想法。
那次故障很恐怖,公司很多应用连接不上数据库了,创单量直线往下掉,
这让团队意识到,即使数据库是好的,但dal组件异常,依然能让会员系统挂掉。
所以,团队再次异构了会员主库的数据源,双写数据到ES,如下所示:
如果dal组件故障或MySQL数据库挂了,可以把读写切到ES,
等MySQL恢复了,再把数据同步到MySQL,最后把读写再切回到MySQL数据库。
如下图所示:
ES高可用方案
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主集群。
如下图所示:
ES流量隔离三集群架构
双中心ES主备集群做到这一步,感觉应该没啥大问题了,但去年的一次恐怖流量冲击让团队改变了想法。
那是一个节假日,某个业务上线了一个营销活动,
在用户的一次请求中,循环10多次调用了会员系统,导致会员系统的TPS暴涨,差点把ES集群打爆。
这件事让团队后怕不已,它让团队意识到,一定要对调用方进行优先级分类,实施更精细的隔离、熔断、降级、限流策略。
首先,团队梳理了所有调用方,分出两大类请求类型。
第一类是跟用户的下单主流程密切相关的请求,这类请求非常重要,应该高优先级保障。
第二类是营销活动相关的,这类请求有个特点,他们的请求量很大,TPS很高,但不影响下单主流程。
基于此,团队又构建了一个ES集群,专门用来应对高TPS的营销秒杀类请求,这样就跟ES主集群隔离开来,不会因为某个营销活动的流量冲击而影响用户的下单主流程。如下图所示:
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使用率:
会员系统的接口耗时:
会员Redis缓存方案
一直以来,会员系统是不做缓存的,原因主要有两个:
- 第一个,前面讲的ES集群性能很好,秒并发3万多,99百分位耗时5毫秒左右,已经足够应付各种棘手的场景。
- 第二个,有的业务对会员的绑定关系要求实时一致,而会员是一个发展了10多年的老系统,是一个由好多接口、好多系统组成的分布式系统。
所以,只要有一个接口没有考虑到位,没有及时去更新缓存,就会导致脏数据,进而引发数据不一致的问题,
例如:
- 用户在APP上看不到微信订单
- APP和微信的会员等级、里程等没合并
- 微信和APP无法交叉营销等等。
那后来为什么又要做缓存呢?
是因为今年机票的盲盒活动,它带来的瞬时并发太高了。
虽然会员系统安然无恙,但还是有点心有余悸,稳妥起见,最终还是决定实施缓存方案。
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了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执行更新缓存,就把脏数据写到缓存中了。
发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发生了并发冲突,只要将它们互斥,就能解决问题。
如下图所示:
实施了缓存方案后,经统计,缓存命中率90%+,极大缓解了ES的压力,会员系统整体性能得到了很大提升。
Redis双中心多集群架构
接下来,团队看一下如何保障Redis集群的高可用。
如下图所示:
关于Redis集群的高可用,团队采用了双中心多集群的模式。
在机房A和机房B各部署一套Redis集群。
更新缓存数据时,双写,只有两个机房的Redis集群都写成功了,才返回成功。
查询缓存数据时,机房内就近查询,降低延时。
这样,即使机房A整体故障,机房B还能提供完整的会员服务。
展望:更精细化的流控和降级策略
任何一个系统,都不能保证百分之一百不出问题,所以团队要有面向失败的设计,那就是更精细化的流控和降级策略。
更精细化的流控策略
- 热点控制。针对黑产刷单的场景,同一个会员id会有大量重复的请求,形成热点账号,当这些账号的访问超过设定阈值时,实施限流策略。
- 基于调用账号的流控规则。这个策略主要是防止调用方的代码bug导致的大流量。例如,调用方在一次用户请求中,循环很多次来调用会员接口,导致会员系统流量暴增很多倍。所以,要针对每个调用账号设置流控规则,当超过阈值时,实施限流策略。
- 全局流控规则。团队会员系统能抗下TPS 3万多的秒并发请求量,如果此时,有个很恐怖的流量打过来,TPS高达10万,与其让这波流量把会员数据库、ES全部打死,还不如把超过会员系统承受范围之外的流量快速失败,至少TPS 3万内的会员请求能正常响应,不会让整个会员系统全部崩溃。
更精细化的降级策略
- 基于平均响应时间的降级。会员接口也有依赖其他接口,当调用其他接口的平均响应时间超过阈值,进入准降级状态。如果接下来1s内进入的请求,它们的平均响应时间都持续超过阈值,那么在接下的时间窗口内,自动地熔断。
- 基于异常数和异常比例的降级。当会员接口依赖的其他接口发生异常,如果1分钟内的异常数超过阈值,或者每秒异常总数占通过量的比值超过阈值,进入降级状态,在接下的时间窗口之内,自动熔断。
目前,团队最大的痛点是会员调用账号的治理。公司内,想要调用会员接口,必须申请一个调用账号,团队会记录该账号的使用场景,并设置流控、降级策略的规则。
但在实际使用的过程中,申请了该账号的同事,可能异动到其他部门了,此时他可能也会调用会员系统,为了省事,他不会再次申请会员账号,而是直接沿用以前的账号过来调用,这导致团队无法判断一个会员账号的具体使用场景是什么,也就无法实施更精细的流控和降级策略。所以,接下来,团队将会对所有调用账号进行一个个的梳理,这是个非常庞大且繁琐的工作,但无路如何,硬着头皮也要做好。
33章视频:10Wqps 基础用户平台 架构与实操
本文内容,尼恩会放到《第33章视频:10Wqps 基础用户平台 架构与实操》 项目介绍。
并且提供配套的简历模板, 帮助大家进行简历的亮点重建和升级,最终帮助大家进大厂、做架构、拿高薪。
所以,以上才是“教科书式” 答案
结合 B站的方案,大家回到前面的面试题:
-
千万级数据,如何做系统架构?
-
亿级数据,如何做做系统架构?
-
千万级流量,如何做系统架构?
-
亿级流量,如何做做系统架构?
-
高并发系统,如何架构?
以上的方案,才是完美的答案,才是“教科书式” 答案。
后续,尼恩会给大家结合行业案例,分析出更多,更加劲爆的答案。
当然,如果遇到这类问题,可以找尼恩求助。
推荐阅读
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓