乐趣区

关于jav:ESRedisMySQL这个高可用架构设计太顶了

会员零碎是一种根底零碎,跟公司所有业务线的下单主流程密切相关。如果会员零碎出故障,会导致用户无奈下单,影响范畴是全公司所有业务线。所以,会员零碎必须保障高性能、高可用,提供稳固、高效的根底服务。

关注公众号:码猿技术专栏,回复关键词:1111 获取阿里外部 Java 性能调优

随着同程和艺龙两家公司的合并,越来越多的零碎须要买通同程 APP、艺龙 APP、同程微信小程序、艺龙微信小程序等多平台会员体系。

例如微信小程序的穿插营销,用户买了一张火车票,此时想给他发酒店红包,这就须要查问该用户的对立会员关系。

因为火车票用的是同程会员体系,酒店用的是艺龙会员体系,只有查到对应的艺龙会员卡号后,能力将红包挂载到该会员账号。

除了上述讲的穿插营销,还有许多场景须要查问对立会员关系,例如订单核心、会员等级、里程、红包、常旅、实名,以及各类营销流动等等。

所以,会员零碎的申请量越来越大,并发量越来越高,往年清明小长假的秒并发 tps 甚至超过 2 万多。

在如此大流量的冲击下,会员零碎是如何做到高性能和高可用的呢?这就是本文着重要讲述的内容。

文章首发公众号:码猿技术专栏

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 还能提供残缺的会员服务。

高可用会员主库计划

上述讲到,全平台会员的绑定关系数据存在 ES,而会员的注册明细数据存在关系型数据库。

最早,会员应用的数据库是 SqlServer,直到有一天,DBA 找到咱们说,单台 SqlServer 数据库曾经存储了十多亿的会员数据,服务器已达到物理极限,不能再扩大了。依照当初的增长趋势,过不了多久,整个 SqlServer 数据库就崩了。

你想想,那是一种什么样的劫难场景:会员数据库崩了,会员零碎就崩了;会员零碎崩了,全公司所有业务线就崩了。想想就不寒而栗,酸爽无比,为此咱们立即开启了迁徙 DB 的工作。

MySQL 双核心 Partition 集群计划

通过调研,咱们抉择了双核心分库分表的 MySQL 集群计划,如下图所示:

会员一共有十多亿的数据,咱们把会员主库分了 1000 多个分片,平分到每个分片大略百万的量级,足够应用了。

MySQL 集群采纳 1 主 3 从的架构,主库放在机房 A,从库放在机房 B,两个机房之间通过专线同步数据,提早在 1 毫秒内。

会员零碎通过 DBRoute 读写数据,写数据都路由到 master 节点所在的机房 A,读数据都路由到本地机房,就近拜访,缩小网络提早

这样,采纳双核心的 MySQL 集群架构,极大进步了可用性,即便机房 A 整体都崩了,还能够将机房 B 的 Slave 降级为 Master,持续提供服务。

双核心 MySQL 集群搭建好后,咱们进行了压测,测试下来,秒并发能达到 2 万多,均匀耗时在 10 毫秒内,性能达标。

会员主库平滑迁徙计划

接下来的工作,就是把会员零碎的底层存储从 SqlServer 切到 MySQL 上,这是个危险极高的工作。

次要有以下几个难点:

  • 会员零碎是一刻都不能停机的,要在不停机的状况下实现 SqlServer 到 MySQL 的切换,就像是在给高速行驶的汽车换轮子。
  • 会员零碎是由很多个零碎和接口组成的,毕竟倒退了 10 多年,因为历史起因,遗留了大量老接口,逻辑盘根错节。这么多零碎,必须一个不落的全副梳理分明,DAL 层代码必须重写,而且不能出任何问题,否则将是灾难性的。
  • 数据的迁徙要做到无缝迁徙,不仅是存量 10 多亿数据的迁徙,实时产生的数据也要无缝同步到 MySQL。另外,除了要保障数据同步的实时性,还要保证数据的正确性,以及 SqlServer 和 MySQL 数据的一致性。

基于以上痛点,咱们设计了“全量同步、增量同步、实时流量灰度切换”的技术计划。

首先,为了保证数据的无缝切换,采纳实时双写的计划。因为业务逻辑的简单,以及 SqlServer 和 MySQL 的技术差异性,在双写 MySQL 的过程中,不肯定会写胜利,而一旦写失败,就会导致 SqlServer 和 MySQL 的数据不统一,这是绝不允许的。

所以,咱们采取的策略是,在试运行期间,主写 SqlServer,而后通过线程池异步写 MySQL,如果写失败了,重试三次,如果仍然失败,则记日志,而后人工排查起因,解决后,持续双写,直到运行一段时间,没有双写失败的状况。

通过上述策略,能够确保在绝大部分状况下,双写操作的正确性和稳定性,即便在试运行期间呈现了 SqlServer 和 MySQL 的数据不统一的状况,也能够基于 SqlServer 再次全量构建出 MySQL 的数据。

因为咱们在设计双写策略时,会确保 SqlServer 肯定能写胜利,也就是说,SqlServer 中的数据是全量最残缺、最正确的。

如下图所示:

讲完了双写,接下来咱们看一下“读数据”如何灰度。整体思路是,通过 A/B 平台逐渐灰度流量,刚开始 100% 的流量读取 SqlServer 数据库,而后逐渐切流量读取 MySQL 数据库,先 1%,如果没有问题,再逐渐放流量,最终 100% 的流量都走 MySQL 数据库。

在逐渐灰度流量的过程中,须要有验证机制,只有验证没问题了,能力进一步放大流量。

那么这个验证机制如何施行呢?计划是,在一次查问申请里,通过异步线程,比拟 SqlServer 和 MySQL 的查问后果是否统一,如果不统一,记日志,再人工查看不统一的起因,直到彻底解决不统一的问题后,再逐渐灰度流量。

如下图所示:

所以,整体的施行流程如下:

首先,在一个夜黑风高的深夜,流量最小的时候,实现 SqlServer 到 MySQL 数据库的全量数据同步。

接着,开启双写,此时,如果有用户注册,就会实时双写到两个数据库。那么,在全量同步和实时双写开启之间,两个数据库还相差这段时间的数据,所以须要再次增量同步,把数据补充残缺,以防数据的不统一。

剩下的工夫,就是各种日志监控,看双写是否有问题,看数据比对是否统一等等。

这段时间是耗时最长的,也是最容易产生问题的,如果有的问题比较严重,导致数据不统一了,就须要从头再来,再次基于 SqlServer 全量构建 MySQL 数据库,而后从新灰度流量。

直到最初,100% 的流量全副灰度到 MySQL,此时就功败垂成了,下线灰度逻辑,所有读写都切到 MySQL 集群。

MySQL 和 ES 主备集群计划

做到这一步,感觉会员主库应该没问题了,可 dal 组件的一次重大故障扭转了咱们的想法。

那次故障很恐怖,公司很多利用连贯不上数据库了,创单量直线往下掉,这让咱们意识到,即便数据库是好的,但 dal 组件异样,仍然能让会员零碎挂掉。

所以,咱们再次异构了会员主库的数据源,双写数据到 ES,如下所示:

如果 dal 组件故障或 MySQL 数据库挂了,能够把读写切到 ES,等 MySQL 复原了,再把数据同步到 MySQL,最初把读写再切回到 MySQL 数据库。

如下图所示:

异样会员关系治理

会员零碎不仅仅要保证系统的稳固和高可用,数据的精准和正确也同样重要。

举个例子,一个分布式并发故障,导致一名用户的 APP 账户绑定了他人的微信小程序账户,这将会带来十分顽劣的影响。

首先,一旦这两个账号绑定了,那么这两个用户下的酒店、机票、火车票订单是相互能够看到的。

你想想,他人能看到你订的酒店订单,你火不火,会不会投诉?除了能看到他人的订单,你还能操作订单。

例如,一个用户在 APP 的订单核心,看到了他人订的机票订单,他感觉不是本人的订单,就把订单勾销了。

这将会带来十分重大的客诉,大家晓得,机票退订费用是挺高的,这不仅影响了该用户的失常出行,还导致了比拟大的经济损失,十分蹩脚。

针对这些异样会员账号,咱们进行了具体的梳理,通过非常复杂烧脑的逻辑辨认出这些账号,并对会员接口进行了深度优化治理,在代码逻辑层堵住了相干破绽,实现了异样会员的治理工作。

如下图所示:

瞻望:更精细化的流控和降级策略

任何一个零碎,都不能保障百分之一百不出问题,所以咱们要有面向失败的设计,那就是更精细化的流控和降级策略。

更精细化的流控策略

热点管制。针对黑产刷单的场景,同一个会员 id 会有大量反复的申请,造成热点账号,当这些账号的拜访超过设定阈值时,施行限流策略。

基于调用账号的流控规定。这个策略次要是避免调用方的代码 bug 导致的大流量。例如,调用方在一次用户申请中,循环很屡次来调用会员接口,导致会员零碎流量暴增很多倍。所以,要针对每个调用账号设置流控规定,当超过阈值时,施行限流策略。

全局流控规定。 咱们会员零碎能抗下 tps 3 万多的秒并发申请量,如果此时,有个很恐怖的流量打过去,tps 高达 10 万,与其让这波流量把会员数据库、ES 全副打死,还不如把超过会员零碎接受范畴之外的流量疾速失败,至多 tps 3 万内的会员申请能失常响应,不会让整个会员零碎全副解体。

更精细化的降级策略

基于均匀响应工夫的降级。 会员接口也有依赖其余接口,当调用其余接口的均匀响应工夫超过阈值,进入准降级状态。

如果接下来 1s 内进入的申请,它们的均匀响应工夫都继续超过阈值,那么在接下的工夫窗口内,主动地熔断。

基于异样数和异样比例的降级。当会员接口依赖的其余接口产生异样,如果 1 分钟内的异样数超过阈值,或者每秒异样总数占通过量的比值超过阈值,进入降级状态,在接下的工夫窗口之内,主动熔断。

目前,咱们最大的痛点是会员调用账号的治理。公司内,想要调用会员接口,必须申请一个调用账号,咱们会记录该账号的应用场景,并设置流控、降级策略的规定。

但在理论应用的过程中,申请了该账号的共事,可能异动到其余部门了,此时他可能也会调用会员零碎,为了省事,他不会再次申请会员账号,而是间接沿用以前的账号过去调用,这导致咱们无奈判断一个会员账号的具体应用场景是什么,也就无奈施行更精密的流控和降级策略。

退出移动版