配景
会员零碎是一种根底零碎,跟公司一切业务线的下单主流程亲密相干。假如会员零碎出毛病,会致使用户无奈下单,影响规模是全公司一切业务线。所以,会员零碎必需包管高机能、高可用,提供不乱、高效的根底办事。
跟着同程和艺龙两家公司的合并,愈来愈多的零碎需求买通同程 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 缓存了,间接前往,这样就防止了缓存数据的纷歧致问题。
如下图所示:
对于 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 数据库。
如下图所示: