一、ES高可用方案
1. ES双中心主备集群架构
全平台所有体系的会员总量是十多亿。在这么大的数据体量下,业务线的查询维度也比较复杂。有的业务线基于手机号,有的基于微信unionid,也有的基于卡号等查询会员信息。这么大的数据量,又有这么多的查询维度,基于此,选择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主集群。如下图所示:
2. ES流量隔离三集群架构
双中心ES主备集群升级版一定要对调用方进行优先级分类,实施更精细的隔离、熔断、降级、限流策略。首先,梳理了所有调用方,分出两大类请求类型。第一类是跟用户的下单主流程密切相关的请求,这类请求非常重要,应该高优先级保障。第二类是营销活动相关的,这类请求有个特点,请求量很大,TPS很高,但不影响下单主流程。
基于此,构建了一个ES集群,专门用来应对高TPS的营销秒杀类请求,这样就跟ES主集群隔离开来,不会因为某个营销活动的流量冲击而影响用户的下单主流程。如下图所示:
3. 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上,那么就可以减少大量不必要的请求,提升查询性能。
二、会员Redis缓存方案
在更新ES数据时,加一个2秒的Redis分布式并发锁,为了保证缓存数据的一致性,接着再删除Redis中该会员的缓存数据。如果此时有请求来查询数据,先获取分布式锁,发现该会员ID已经上锁了,说明ES刚刚更新的数据尚未生效,那么此时查询完数据后就不更新Redis缓存了,直接返回,这样就避免了缓存数据的不一致问题。如下图所示:
上述方案,乍一看似乎没什么问题了,但仔细分析,还是有可能导致缓存数据的不一致。例如,在更新请求加分布式锁之前,恰好有一个查询请求获取分布式锁,而此时是没有锁的,所以它可以继续更新缓存。
但就在他更新缓存之前,线程block了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执行更新缓存,就把脏数据写到缓存中了。发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发生了并发冲突,只要将它们互斥,就能解决问题。如下图所示:
1. Redis双中心多集群架构
关于Redis集群的高可用,我们采用了双中心多集群的模式。在机房A和机房B各部署一套Redis集群。更新缓存数据时,双写,只有两个机房的Redis集群都写成功了,才返回成功。查询缓存数据时,机房内就近查询,降低延时。这样,即使机房A整体故障,机房B还能提供完整的会员服务。
三、MySQL和ES主备集群方案
会员主库的数据源,双写数据到ES,如下所示:
如果dal组件故障或MySQL数据库挂了,可以把读写切到ES,等MySQL恢复了,再把数据同步到MySQL,最后把读写再切回到MySQL数据库。如下图所示: