【Java】字节二面:如何设计一个支持10万QPS的会员系统

一、问题分析

5.1.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 主集群。

5.1.2 ES 流量隔离三集群架构

双中心 ES 主备集群做到这一步,感觉应该没啥大问题了,但去年的一次恐怖流量冲击让我们改变了想法。

那是一个节假日,某个业务上线了一个营销活动,在用户的一次请求中,循环 10 多次调用了会员系统,导致会员系统的 tps 暴涨,差点把 ES 集群打爆。

这件事让我们后怕不已,它让我们意识到,一定要对调用方进行优先级分类,实施更精细的隔离、熔断、降级、限流策略。

首先,我们梳理了所有调用方,分出两大类请求类型:

第一类是跟用户的下单主流程密切相关的请求,这类请求非常重要,应该高优先级保障。

第二类是营销活动相关的,这类请求有个特点,他们的请求量很大,tps 很高,但不影响下单主流程。

基于此,我们又构建了一个 ES 集群,专门用来应对高 tps 的营销秒杀类请求,这样就跟 ES 主集群隔离开来,不会因为某个营销活动的流量冲击而影响用户的下单主流程。

如下图所示:

5.1.3 ES 集群深度优化提升

讲完了 ES 的双中心主备集群高可用架构,接下来我们深入讲解一下 ES 主集群的优化工作。

有一段时间,我们特别痛苦,就是每到饭点,ES 集群就开始报警,搞得每次吃饭都心慌慌的,生怕 ES 集群一个扛不住,就全公司炸锅了。

那为什么一到饭点就报警呢?因为流量比较大, 导致 ES 线程数飙高,cpu 直往上窜,查询耗时增加,并传导给所有调用方,导致更大范围的延时。那么如何解决这个问题呢?关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册

通过深入 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 使用率:

会员系统的接口耗时:

5.2 会员 Redis 缓存方案

一直以来,会员系统是不做缓存的,原因主要有两个:

第一个,前面讲的 ES 集群性能很好,秒并发 3 万多,99 线耗时 5 毫秒左右,已经足够应付各种棘手的场景。

第二个,有的业务对会员的绑定关系要求实时一致,而会员是一个发展了 10 多年的老系统,是一个由好多接口、好多系统组成的分布式系统。

所以,只要有一个接口没有考虑到位,没有及时去更新缓存,就会导致脏数据,进而引发一系列的问题。

例如:用户在 APP 上看不到微信订单、APP 和微信的会员等级、里程等没合并、微信和 APP 无法交叉营销等等。

那后来为什么又要做缓存呢?是因为今年机票的盲盒活动,它带来的瞬时并发太高了。虽然会员系统安然无恙,但还是有点心有余悸,稳妥起见,最终还是决定实施缓存方案。

5.2.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 了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执行更新缓存,就把脏数据写到缓存中了。

发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发生了并发冲突,只要将它们互斥,就能解决问题。

如下图所示:

实施了缓存方案后,经统计,缓存命中率 90%+,极大缓解了 ES 的压力,会员系统整体性能得到了很大提升。

5.2.2 Redis 双中心多集群架构

接下来,我们看一下如何保障 Redis 集群的高可用。

如下图所示:

关于 Redis 集群的高可用,我们采用了双中心多集群的模式。在机房 A 和机房 B 各部署一套 Redis 集群。

更新缓存数据时,双写,只有两个机房的 Redis 集群都写成功了,才返回成功。查询缓存数据时,机房内就近查询,降低延时。这样,即使机房 A 整体故障,机房 B 还能提供完整的会员服务。

二、粉丝福利

  • 我根据我从小白到架构师多年的学习经验整理出来了一份50W字面试解析文档、简历模板、学习路线图、java必看学习书籍 、 需要的小伙伴斯我一下,或者评论区扣“求分享
  • 20
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单副本支持5QPSJava demo可以通过以下步骤实现: 1. 数据库优化:首先,对于高QPS的应用程序,选择高性能的数据库非常重要。可以考虑使用高性能、可伸缩性强的数据库,如MySQL Cluster或Cassandra。另外,可以通过调整数据库的缓存大小、索引优化、分表等方式来提升数据库的性能。 2. 垂直扩展:通过增加服务器的处理能力来提高QPS。可以使用更高性能的硬件、使用多核处理器、增加内存容量等方式来提升单个服务器的处理能力。 3. 并发处理:使用多线程或线程池的方式来实现并发处理。通过将请求分发给多个线程来提高系统的并发能力。 4. 缓存机制:将查询结果缓存起来,避免重复查询数据库。可以使用内存数据库如Redis或使用缓存框架如Ehcache来实现缓存机制。 5. 异步处理:将耗时的操作(如网络请求、IO操作)采用异步方式进行处理,以提高系统的并发能力。 6. 代码优化:对关键代码进行优化,例如避免不必要的循环、减少对象创建和销毁等,以提高代码的执行效率和系统的响应速度。 7. 负载均衡:通过负载均衡器将请求分发到多个副本上,以提高系统的并发能力和可靠性。 通过以上的优化和调整,可以将单副本的Java demo的QPS提升到5。需要注意的是,实际的性能还受到许多因素的影响,如网络带宽、硬件设备状况、代码质量等,因此要全面考虑系统的各个方面,并综合优化才能达到目标QPS

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值