马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
1 、背景 Redis的出现确实大大地提高系统大并发能力支撑的可能性,转眼间Redis的最新版本已经是3.X版本了,但我们的系统依然继续跑着2.8,并很好地支撑着我们当前每天5亿访问量的应用系统。想当年Redis的单点单线程特性无法满足我们日益壮大的系统,只能硬着头皮把Redis“集群化”负载。且这套“集群化”方案良好地运行至今。虽难度不高,胜在简单和实用。无论简单还是很简单,记录这种经历是一件非常有趣的事情。 2 、问题 系统访问量日益倍增,当前的Redis单点服务确实客观存在连续可用性以及支撑瓶颈风险,这种主/备模式在服务故障突发的情况下就会被动停止服务进行Redis节点切换。针对单点问题,我们结合自身的业务应用场景对Redis“集群化”提出几个主要目标: 1、避免单点情况,确保服务高可用; 2、紧可能把数据分布式存储,降低故障影响范围,满足服务灵活伸缩; 3、控制“集群化”的复杂度,从而控制边际成本; 3 、过程 以上目标1和2就是所谓的分布式集群方案,把大问题分而治之。但最难把控的是目标3的“简化”实现。基于当时开源社区的那几种Redis集群方案,对于我们“简化”的要求来说相对略显臃肿。所以还是决定结合自身的业务应用等因素打造一个“合适”的Redis集群。 初始,我们凭借自己对分布式集群的认识勾结合应用场景勾勒出一个我们觉得足够“简化”的设计图,然后在这个“简化”架构的基础上继续击破我们各种应用场景所带来的缺陷。架构图如下: 不难看出,我们想尽量通过一个RedisManager类和配置文件就能管理整个集群,不需要而外的软件支持。单例使用的时候RedisManager和配置文件就已经存在,RedisManager有各种单例操作的API重写(如get、set等),现在我们还是想保持这种模式对业务处理提供集群API,保持整个服务化应用框架(类似于今天所倡导的“微服务”)的轻量级特性。如上图所示,数据根据hash实现分成不同块放在不同的hash节点上,而每个hash节点必须存在两个Redis实例做hash节点集群支撑。为什么会是两个而不是三个或可扩展多个?我们是这样考虑的: 1、任何可持续扩展或抽象是站在规范这个巨人的肩膀上,我们秉承了整个系统架构“约定远远大于配置”的原则,适当地限制了边界范围换取控制性而又不失灵活。 2、对于我们系统目前的服务器质量来说,宕机的概率较小,双机(双实例)同时宕机的概率更小。就算这个概率出现,我们众多的业务场景还是允许这种部分间接性故障。这就是成本与质量之间的平衡和取舍。 3、由于我们没有使用额外的软件辅助,这些额外的操作都依赖了线程额外性能去弥补,例如两个Redis实例负载之间的同步等,所以我们是用性能换取部分一致性。负载节点越多性能消耗越多,所以两个实例做负载是我们“适当约束”和衡量的决策。 4 、场景 4.1 场景 1 在RedisManager类中有两个最基本的API,那就是c_get和c_set,其中c代表cluster。这两个API的基本实现如下: 从这两个基本操作可以看出,我们利用了遍历hash节点的所有负载实例来实现高可用性,并通过“同步写”来满足Redis数据“弱一致性”问题。而这个“同步写”就是额外的性能消耗,是依赖于双写过程中只成功写入一个实例的概率。因为Redis的稳定性,这种概率不高,所以额外性能消耗的概率也不高。以上操作几乎适用所有缓存类集群支持。这类缓存数据的强一致性更多放在数据库。数据在库表变更后,只需要把缓存数据delete即可。具体场景如下: 1、数据的变更通过“后台”维护,变更后同步DELETE缓存的相互数据。 2、业务线程请求缓存数据为空(c_get),则查询书库并把数据同步至缓存(c_set)。 3、Redis的hash节点集群安装集群内部实现负责和数据同步问题(c_get和c_set的实现原来)。 我们大部分业务数据缓存都是基于以上流程实现,这个流程会存在一个脏数据问题,例如当UPDATE库表成功但DELETE缓存数据不成功,就会存在脏数据。为了能尽可能降低脏数据的可能性,我们会在缓存设置缓存数据一个有效期(setex),就算脏数据出现,也只会影响seconds时间段。另外在后台变更过程如果DELETE缓存失败我们会有适当的提示语提示,好让认为发现继续进一步处理(例如重新变更)。 4.1 场景 2 除了场景1的缓存用途外,还存在持久化场景。就是基于Redis做数据持久化集群,即所有操作都是基于Redis集群的。那么在数据一致性问题上就需要下点功夫了(c_set_sync),伪代码如下: - c_set_sync(key,value){
- if(c_del_sync(key)){
- c_set(key,value);
- }
- }
- c_del_sync(key){
- if(del(r1,key)&&del(r2,key)){
- return true;
- }
- return false;
- }
复制代码
通过以上伪代码可以看出,c_set_sync方法是先强制全部删除数据后再c_set,确保数据一致性。但这会出现一个数据丢失问题,就是c_del_sync后但set失败,那数据就会丢失,因为我们的数据几乎都是从后台操作的,如果出现这种数据丢失,简单的我们可以重新配置,复杂的我们可以通过日志恢复。
|