折800缓存服务的构建

发表于2016-10-29
评论0 2.3k浏览
1、背景
       zhe800公司内大量使用redis,且用途多样:缓存、队列、数据库,都有功能。同时公司内对redis的使用比较随意,每个应用按自己的需求要求运维部署redis-server,导致对redis的使用很混乱,运维部门都无法精确得知redis-server的部署情况。经过长时间的调查、搜集资料才整理出公司内redis的使用情况,目前部署数百多个redis-server实例、版本分为:2.8.9、2.8.11、2.8.17、2.8.19、2.8.23 多种。

2、 需求的演变
       鉴于公司redis使用、管理混乱的情况,公司要求改善对redis的使用和管理,由成都“基础架构部”负责此项改进。此时产生了第一阶段需求:
       1、通过一个统一管理系统管理公司内所有的redis进程
       2、可以通过这个管理系统监控redis的运行状态
       3、redis资源通过管理系统来分配       
       针对第一阶段的需求,我们基础架构部和用户、运维部、技术委员会进行了多次讨论,认为目前redis使用的问题:
       没有实现跨机房高可用,这点很重要,公司内所有应用要求必须跨机房高可用。
       没有考虑横向扩展问题。
       一部分队列、持久化等功能不适合使用redis,应该使用更“专业”的队列系统和数据库解决。
       此时对第一阶段需求进行了修改,产生了第二阶段需求:
       1、redis仅作为缓存使用。
       2、实现横向扩展。
       3、实现跨机房高可用,双机房至少要实现“温备”。
       4、由一个后台系统管理、分配、监控redis资源。
       其中第3个需求:实现跨机房高可用最为困难,因为缓存的典型场景:缓冲数据库查询,如果主机房失效后,切换到备机房的缓存服务,此时备机房缓存没有数据(是”冷“的),会导致对数据库(MySQL)的查询量爆增;因此要求备机房的缓存必须至少有主机房的部分数据,在主机房失效的情况下才能防止数据库查询量爆增,而且在缓存的持续使用下,备机房缓存会很快达到主机房的状态。
       因此跨机房数据同步是必须的。

3、选型
       在最终需求的指导下,已经明确我们基础架构部需要打造一套缓存服务系统,包含redis-server和管理、监控系统。
       此时我们有两种方案可选择:

1、基于代理的横向扩展方案
       以codis为代表的基于代理的集群方案,通过在redis-client和redis-server之间增加一个代理层,通过代理层sharding链接到正确的redis-server进行操作。

2、基于redis官方集群的方案
       redis从3.0版开始提供了很受期待的集群功能,按照CAP理论,redis cluster属于保证AP,放弃C,仅能提供最终一致性。按照redis cluster的协议,client需要在本地sharding,选择正确的redis结点,然后进行操作,否则会返回MOVE错误。
       调研结论是:采用redis官方集群方案
       有以下原因促使我们选择redis cluster方案:
       1、基于代理的方案会导致性能下降,对于一个缓存服务来说性能很重要。
       2、redis cluster方案较简单,不需要部署额外的进程,其本身就能实现高可用了,如果是基于代理的方案,代理本身也需要高可用,这增加了复杂度。
       3、redis cluster是官方支持的自带功能,比起第三方开发的代理进程,可能更稳定可靠。
       4、redis cluster能实现读扩展、读写分离功能;如果使用代理,那么读写扩展时必须同时考虑redis-server和代理程序。
       redis cluster仍然不能实现跨机房容灾,跨机房高可用的功能最后决定由基础架构部自行实现

4、 redis cluster简介
       其基本思想是将数据放置于槽(slots)中,slot有0x3fff(16384)个。为了实现数据分片,这些槽分布在多个redis master结点中,为了实现高可用,每个master结点拥有零到多个slave结点,这些slave同步保存master的数据,一旦master crash以后,会选举他的slave结点成为新的master。


       仅master结点可写入,然后数据会异步同步到slave,slave可读不可写。
       因此理想情况下:结点永不crash、只读写master结点,redis cluster可保证强一致。 但因为现实使用中,结点可能crash,且可读slave结点,因此redis cluster仅能保证最终一致性。
       redis cluster通过谣言传播同步结点的状态,每一个结点都保存了所有结点的状态信息,其中最重要的是:结点的ip+端口、每个节点包含的slots。

client 集群模式流程:
       1、从任意一个集群的结点获取所有结点的状态(使用CLUSTER NODES命令),同时获知了每一个slot所在的结点。结点有两个角色:master/slave。
       2、在get/set/hget/hset等命令时,对key进行计算:crc16(key) & 0x3fff,所得的结果就是slot编号。
       3、通过slot编号获取结点的ip+port,然后发送此结点。
       4、此时如果返回-ASK ip:port错误,表明slot临时发生了变化,此时应往ASK指定的结点先发送ASKING命令,再发送实际的命令。
       5、此时如果返回-MOVED slot-number ip:port错误,表明集群发生了变化(扩容、缩减、槽迁移过),则先将命令发送到MOVED错误指明的结点后,重新获取集群结点状态,以同步最新的结点状态。

5、管理系统的设计与实现
       zhe800基于redis cluster的缓存服务被命令为z(he800-r)edis,包含一个管理系统对所有缓存集群进行管理,以及双机房数据同步机制,保证主备两个机房之间部分数据同步,实现双机房容灾。
对于zedis,我们有几个指导原则:
       1、简单:复杂的方案容易出错,且成本高,因此在简单和复杂之中选择简单的方案。
       2、用户透明:不要要求用户了解大量的细节才能使用。
       3、可用:可正常使用,可从错误中恢复。


5.1 管理系统的功能设计
       缓存集群的操作:
       容量扩容/读写扩容:增加1个到多个分片,同时扩展了读写能力。
       高可用/读扩容:仅增加slave结点数,增强高可用性,同时扩展了读能力。
       1、分配:按应用的需求分配一个全新的redis cluster,如果配置了跨机房容灾选项,还会按需求在备机房启动一个灾备集群。
       2、下线:将一个运行中的redis cluster删除,并进行清理。
       3、扩容:
       4、缩减:扩容的逆操作,支持读写缩减和读缩减。

监控:
       1、物理机资源监控:监控物理机当前的资源,为缓存集群的操作提供依据。
       2、集群监控:对每个节点到集群本身进行监控,提供给运维查看。
       3、报警:对接公司报警系统,遇到结点crash、集群不可用等情况及时发出报警
       缓存集群的操作需要以物理机资源数据为依据,这部分数据通过zabbix获取。同时集群的每个节点需要使用cgroup进行资源隔离,这些操作都由管理后台自动进行。
       对物理机的操作:启动redis-server进程、创建执行文件夹、生成redis.conf文件等操作通过salt进行,因为公司内每台服务器都安装了salt-minion,因此可直接利用。

5.2 事务支持
       对redis cluster的各种操作都是通过redis client向redis-server发送命令实现,但是这些操作必须是原子操作,长时间的操作必须可中途放弃并会滚到初始状态,否则对redis cluster的操作风险会比较高,运维人员可能不敢使用管理系统。
       事务系统采取模仿数据库的一种实现方式:记录日志。数据库日志分为:undo、redo、undo/redo三种形式。
       我们采取undo日志的形式,对于管理系统四大操作:
       分配
       下线
       扩容
       缩减
       都通过事务系统执行。
       模仿数据库,事务管理器提供三个原语:
       1、begin: 开始一个事务,并返回一个事务id(tid)       
       2、abort-rollback: 中途放弃,会滚到begin时候的状态
       3、commit: 提交事务,标记事务已经完成。
       四大操作每一个步骤都会在操作成功后,记录undo日志到文件系统中,因此还需要确保每个“步骤”是原子操作,如果遇到abort或者错误时,逆向执行undo日志中命令就能恢复到初始状态了。如果rollback仍然失败,则说明遇到了管理系统无法处理的故障(如机器断电、网络中断等)此时需要暂停会滚,通知运维人员恢复故障,再进行回滚。

6. 高可用的实现
6.1 redis cluster的高可用
        redis很早就可以通过master-slave机制实现双机热备,到了cluster时代,仍然保留了master-slave机制。
        在cluster模式中,每个master结点保存了一部分slots,同时每个master结点可设置0~多个slave结点,master的数据会异步同步到slave结点(redis的特点:所有IO操作皆异步),这样一个master-slave的组合可称之为一个partition;一个partition是高可用的:
        slave结点crash,对集群本身无影响,对partition无影响;slave结点重新连接上master后会检查自己与master的数据差别,如果差距太大,会先进行一次全量同步,然后开始增量同步。
        master结点crash,对集群本身无影响,对partition有影响;集群会选举一个slave作为新的master,因为slave结点几乎有master结点的所有数据,因此数据仅有小概率丢失。
        partition整体crash,集群会整体不可用,因为集群认为slots不连续了,保存在这个partition中的slots无法访问。如果没开启持久化,会导致这部分数据永久丢失。
        可见,redis cluster做到了一定的高可用,我们使用jedis测试结果来看,在一个1主1备的redis cluster中,随机kill一个结点不会导致任何错误,但kill一个master结点和它所有的slave结点就会导致集群不可用的错误,此时无法get/set数据。

6.2 对于高可用的思考
        对于redis cluster高可用的研究和实验发现,其应对单机房高可用是可以胜任的,已有的客户端:jedis新版本也能很好的处理高可用问题。
        但对于跨机房高可用/容灾,仍然没有发现现成的方案可供使用,对此有以下方案被提出:       
        1、客户端双写:客户端需要连接到主、备两个集群,同步写主、异步写备;平时读主集群的数据,主集群不可用后马上切换为同步写备、读备。
        2、集群间数据同步:开启持久化,然后同步redis的数据库文件。
        3、服务端双写:redis-server将set/hset等写命令再写一份到备集群。
        最终我们选择了第三个方案:服务端双写。原因为:
        按照用户透明原则,如果要求客户端双写,客户端需要做的事太多,而服务端双写方案客户端仅需处理主备集群切换就可以了。
        集群间数据同步方案,要求数据先写到文件中,在同步到灾备集群,这其实是不必要的,因为如果做到双机房容灾,持久化都是可以关闭的,因为partition整体crash的集群大幅度下降了(按平方下降,如果单个partition crash的几率为p,主备parttion同时crash的几率仅有p^2,且p随着slave结点的数量指数级的变小),退一步说:因为本身为缓存服务,可接受少量数据丢失。
        服务端双写足够简单,仅需要将客户单对redis-server的写入命令复制一份发送到灾备集群即可。
        服务端双写并不需要完全双写,仅保证灾备集群拥有主集群的部分数据即可,因为对于缓存服务来说,只要发生灾备时,灾备集群不“冷”就可以接受了;同时这也节省了机房间光纤带宽。

6.3 跨机房高可用
        跨机房高可用的基本思想为:在主备两个两个机房各启动一个redis cluster,主机房集群在写入数据的时候,同时写入一分到备机房。
        要达到以上设计目的,必须对官方redis进行修改,我们选择了版本3.2.0为基础进行修改。 redis的一个重要设计思想是所有IO操作皆异步非阻塞进行,为此redis封装了ae一个事件处理库封装了各个操作系统提供的事件处理模型:
        OS X/Darwin/BSD:kqueue
        Linux:epoll
        Sun OS:evport      
        Others:select
        在3大操作系统中并不支持Windows的IO Complete Port,原因是IOCP是真异步模型,在收到事件通知时,数据已经接收到/发送完毕,而不像epoll,kqueue等模型,在收到事件通知时,还需要再调用read/write,通知仅仅是告诉用户态可读/可写了,而远不同于IOCP,通知是通知用户态读/写已经完成了。由此可见IOCP现在比较难融入redis现有的IO层中。
        我们先开始研究redis的代码,可得知它的IO层设计:

redis执行一个命令的时序:

       最终我们决定在processInputBuffer中,解析redis协议时,将写操作按配置的百分比过滤后,复制一份发送到备机房;此时有新的考虑:
       备机房的接收者也是集群,如果需要发送给集群,那么势必会增加对redis-server代码的修改程度,因为还必须处理目标集群的高可用、sharding问题,风险增加。我们希望对redis-server的修改尽可能的小。
       如果机房间光纤中断,会丢失一部分数据;虽然作为缓存集群,数据可以丢失一小部分,但能保证数据不丢失尽量不丢。
       只能写入另一个redis cluster中,不灵活。
       因此我们增加了一个名为zedis-gateway的代理程序,redis-server中复制出来的写操作先发送到zedis-gateway,zedis-gateway负责对这些写操作进行处理,发送到备份集群。增加zedis-gateway的好处:
       zedis-gateway可灵活处理接收到的写操作,比如:写入备份集群、写入MySQL数据库中、等等。
       zedis-gateway负责处理较复杂的,写入备份集群的failover、sharding问题,简化了对redis-server的修改。
       如果机房间光纤中断,zedis-gateway可将写操作暂时写入文件;等待通信恢复后在从文件中发送暂时保存的写操作;数据可以不丢失。
       首先,我们为redis增加了两个配置项:



       同时zedis-gateway也需要高可用,我们简单实现:
       每个redis-server可配置多个zedis-gateway地址,从中随机选择一个连接发送写命令。
       如果某个zedis-gateway地址连接不上,或连接错误,则再随机选一个,直到能连上一个正确的或者一个也不能连上。
       如果没有一个zedis-gateway地址连接成功,则写入错误日志;但不影响其他操作。
       最后将官方版redis-server修改为:


双机房容灾zedis集群结构为:


实现结果:
      我们实现了主备机房间集群数据0~100%同步,但为了节省机房间光纤带宽,一般不允许开启100%同步。
      性能损失:在100%双写的配置下,性能损失<10%。
      动态调整:所有关于双写的配置都可以在运行中随时调整。
      可维护:zedis-gateway可以随时更新版本,而不会影响双写。

7、问题与展望
      目前的zedis还存在一些问题,需要我们持续已经,以达到完善。
      1、redis client中,只有jedis对集群支持最好,其他语言版本,如ruby、node.js、golang版本的客户端还需要进行一定改造才能支持redis cluster。
      2、zedis-gateway目前还是单进程、单线程模型,如果未来它成为瓶颈,我们还需要增加进程数;后续根据需求也许需要将它改造为类似nginx的多进程模型。
      3、修改版的redis还需要优化。
      4、目前只能从主到备双写数据,如果从主切换到备,再从备切换到主,会丢失一部分数据;因为目前定位为缓存集群,因此可接受这个损失,如果今后需要升级作为内存数据库使用,我们就还需要处理这个问题。
      5、只有部分命令支持双写,剩余的还在添加中。
  看了上面的文章 热爱游戏创作的你是不是已经开始热血沸腾了呢?是不是迫不及待的想加入游戏团队成为里面的一员呢?
  福利来啦~赶快加入腾讯GAD交流群,人满封群!每天分享游戏开发内部干货、教学视频、福利活动、和有相同梦想的人在一起,更有腾讯游戏专家手把手教你做游戏!
腾讯GAD游戏程序交流群:484290331     

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引