摘要:作为noSql中的kv数据库的王者,redis以其高性能,低时延,丰富的数据结构备受开发者青睐,但是由于redis在水平伸缩性上受限,如何做到能够水平扩容,同时对业务无侵入性是很多使用redis的开发人员都会面临的问题,而redis分布式解决方案的一个开源产品【codis】较好的弥补了这一弱势,本文主要讲解codis是如何做到对业务无感知,平滑迁移,迁移性能高,迁移异常处理,高可用以及常见的redis的避坑指南,虽然codis目前随着公司的nosql产品越来越成熟,生命周期也即将结束,不过鉴于还有很多同学对codis的原理比较感兴趣,于是将以前的分享的内容重新整理,当然codis在公司外应用目前依旧还是相对比较广泛。
目录
一、背景
二、Redis相关基础概览
2.1Redis简介
2.2Redis的特点
2.3Redis应用场景
三、Redis分布式解决方案公司内外比较
四、Codis的架构设计
4.1 Codis整体的架构设计
4.2Codisproxy的架构设计实现
五、数据可靠性 &高可用&容灾&故障转移&脑裂处理
5.1 数据可靠性
5.2 高可用&容灾&故障转移
六、codis水平扩容细节&迁移异常处理
6.1 Codis扩容迁移细节
6.2 迁移异常处理
七、Codis相关数据
八、运维手册及避坑指南
九、参考资料
随着直播元年开启,越来越多的直播产品如春笋般出现,在拉动营收的过程中,产品竭尽全力思考着各种活动来刺激用户的消费欲望,而这类活动的基础形式就是榜单,在2016年我们基于cmem及扫描流水表的方式来实现榜单排名,2017开始,我们对原有系统进行重构,使用redis作为我们的榜单基础存储,在重构的过程中接到调研redis分布式解决方案的任务之后,比对业内各种开源产品,最后定下Codis,并对其中细节做了一些研究,期间在与Codis作者交流的过程中,有幸知道增值产品部的simotang已经在部门引入codis近2年时间,遂加入到codis的运维工作中,目前在部门内部署运维codis集群15套,2T容量,总日访问量百亿+.支撑了互动视频产品部基础存储,运营活动,榜单类业务2年多,共计100多个活动,榜单上千个。同时在这里非常感谢codis作者spinlock在接入codis过程中给予的指导与帮助。见spinlock github 与 codis地址
二、Redis相关基础概览
2.1 Redis简介
redis是一个基于内存同时具备数据持久化能力的高性能,低时延的KV数据库,value的数据结构可以是string,hash表,list(列表),set(集合),sortedset(有序集合)。
Redis(RemoteDictionary Server) Redis is anopen source (BSD licensed), in-memory data structure store, used as adatabase, cache and message broker. It supports data structures suchas strings, hashes, lists, sets, sorted sets with rangequeries,Practice: http://try.redis.io/ |
2.2 Redis的特点
1. 单线程异步架构(单线程,收包,发包,解析,执行,多路io复用接收文件事件)
2. k-v结构,value支持丰富的数据结构(string,hash,list,set,sortset)
3. 高性能,低时延,基于内存操作,Get/Set10w+,高性能,基于RDB、AOF落地保证数据可靠性
4. 丰富的特性,可用于缓存,消息队列,TTL过期
5. 支持事务,操作是原子性,要么全部提交,要么全部不提交。
2.3 Redis应用场景
string | 计数器,用户信息(id)映射,唯一性(例如用户资格判断),bitmap |
hash | 常见场景:存储对象的属性信息(用户资料) |
list | 常见场景:评论存储,消息队列 |
set | 常见场景:资格判断(例如用户奖励领取判断),数据去重等 |
sorted set | 常见场景:排行榜,延时队列 |
其他 | 分布式锁设计 推荐2篇文章: 基于Redis的分布式锁到底安全吗(上) http://zhangtielei.com/posts/blog-redlock-reasoning.html 基于Redis的分布式锁到底安全吗(下) http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html |
【图codis架构图】
如上图所示,codis整体属于二层架构,proxy+存储,相对于ckv+无proxy的设计来说整体设计会相对简单,同时对于客户端连接数据逐渐增大的情况下,也不用去做数据层的副本扩容,而只需要做proxy层的扩容,从这一点上看,成本会低一些,但是对于连接数不大的情况下,还需要单独去部署proxy,从这一点上看,成本会高一些。
其中,开源的codisproxy的服务的注册发现是通过zk来实现,目前部门是基于l5来做.
从整体的架构设计图来看,codis整体的架构比较清晰,其中codisproxy是分布式解决方案设计中最核心的部分,存储路由,分片迁移均与codisproxy分不开,这块我们来看一下codisproxy的设计实现。
4.2Codisproxy的架构设计实现
codisproxy的架构实现分成2个部分,分别为4.2.1的路由映射的细节与4.2.2的proxy请求处理的细节
4.2.1 路由映射细节
如下图所示:该部分主要涉及到codis的路由细节,主要涉及到如何将一个key映射到具体的物理结点
【图】路由映射细节 |
如上图所示:该部分主要涉及到codis的路由细节
| 相关词汇说明
slot:分片信息,在redis当中仅仅表示一个数字,代表分片索引。每个分片会归属于具体的redis实例
group:主要是虚拟结点,由多台redis机器组成,形成一主多从的模式,是逻辑意义上的结点
为了帮助大家对proxy路由映射的细节有一个更深入的理解,我整理了几个常见的路由映射的相关问题来帮忙大家理解
问题一:proxy是如何把请求映射到具体的redis实例中?
Codis基于crc32的算法%1024得到对应的slot,slot就是所谓的逻辑分片,同时codis会将对应的逻辑分片映射到对应的虚拟结点上,每个虚拟结点是由1主多从的物理redis结点组成。至于为啥会用crc32,这个具体也没有细究,作者也是借鉴于rediscluster中的实现引入的。通过引入逻辑存储结点group,这样即使底层的主机机器实例变更,也不映射上层的映射数据,对上层映射透明,便于分片的管理。
问题二,proxy是如何做到读写分离
如上图所示,key映射到具体的虚拟结点时,能够感知到虚拟结点对应的主与备机实例,此时redisproxy层面能够识别到具体的redis命令得到对应的命令是读与写,再根据集群的配置是否支持读写分离的特性,如配置的是支持,则随机路由到主与从机实例,如配置的是不支持,则路由到主机补全
问题三,proxy目前支持哪些命令,是否支持批量命令,如何保证原子性
命令支持链接
不支持命令 | 半支持命令 |
命令支持部分:Prxoy支持的命令分为三种:不支持命令,半支持命令,支持命令,除了上表所示命令外,其他命令proxy均是支持的,其中不支持命令部分主要是因为这些命令参数中没有key,因此无法识别路由信息,不知道具体路由到哪台实例上,而半支持命令部分通常是会操作多个key,codis基于一种简单实现,以第一个key的路由为准,因此需要业务方自己来保持多个key路由到同一个slot,当然业务也是可以不保证,具体后果业务来承担,是一种弱校验的模式,而公司级产品ckv+对于多key操作是强校验,如果多key不在同一slot上,则以错误的形式返回。
多key操作&原子性部分:Redis本身对于多key的一些操作例如mset等命令是原子性的,而在分布式操作下,多key会分布到多个redis实例当中,涉及到分布式事务,所以在codis当中进行了简化处理,多key操作拆成多个单key命令操作,所以codis当中的mset多key操作不具备原子性的语义。
问题四,如何保证多个key在一个slot当中
有些场景下,我们希望使用到lua或者一些半支持命令来保证我们操作的原子性,因此我们需要在业务层面来去保证多key在一个slot当中,codis采用了和rediscluster一样的模式,基于hashtag,例如我想让七天的主播榜单都中路由在同一个slot的话,{anchor_rank}day1,{anchor_rank}day2,{anchor_rank}day3,即可支持,对就是采用大括号的模式,codis会识别大括号,只会取大括号中的字符串进行hash操作。
4.2.2Proxy请求处理细节
如下图所示:该部分主要涉及到proxy的处理细节,涉及到如何接受一个请求到响应回包的过程
【图】Proxy请求处理细节 |
如上图所示:该部分主要涉及到proxy的处理细节
Codisproxy主要基于go语言这种从语言层面天然支持协程的语言来实现的
1)proxy接收客户端的连接之后,新建一个session,同时启动session中reader与writer两个协程,reader主要用于接收客户端请求数据并解析,对多key的场景下进行命令的拆分,然后将请求通过router进行分发到具体的redis实例,并将redis处理的数据结果写到通道到中,writer从通道中接收对应的结果,将写回给客户端。
loop reader | loop writer |
2)Router层主要是通过crc命令得到key对应的路由信息,从源码可以看到hashtag的特性,codis其实也是支持的。
hash源码 |
至此,proxy相关的路由映射与请求处理细节已经结束,整体下来是不是很简单
作为存储层,数据可靠性与服务高可用是稳定性的核心指标,直接影响到上层核心服务的稳定性,本节将主要针对这两个指标来做一下阐述。
5.1 数据可靠性
作为codis的实现来讲,数据高可靠主要是redis本身的能力,通常存储层的数据高可靠,主要是单机数据高可靠+远程数据热备+定期冷备归档实现的
单机数据高可靠主要是借助于redis本身的持久化能力,rdb模式(定期dum)与aof模式(流水日志),这块可以参考前文所示的2本书来了解,其中aof模式的安全性更高,目前我们线上也是将aof开关打开,在文末也会详细描述一下。
远程数据热备主要是借助于redis自身具备主从同步的特性,全量同步与增量同步的实现,让redis具体远程热备的能力
定期冷备归档由于存储服务在运行的过程中可能存在人员误操作数据,机房网络故障,硬件问题导致数据丢失,因此我们需要一些兜底方案,目前主要是单机滚动备份备份最近48小时的数据以及sng的刘备系统来做冷备,以备非预期问题导致数据丢失,能够快速恢复。
5.2 高可用&容灾&故障转移
codis的架构本身分成proxy集群+redis集群,proxy集群的高可用,可以基于zk或者l5来做故障转移,而redis集群的高可用是借助于redis开源的哨兵集群来实现,那边codis作为非redis组件,需要解决的一个问题就是如何集成redis哨兵集群。本节将该问题分成三部分,介绍redis哨兵集群如何保证redis高可用,codisproxy如何感知redis哨兵集群的故障转移动作,redis集群如何降低“脑裂”的发生概率。
5.2.1 哨兵集群如何保证redis高可用
Sentinel(哨岗,哨兵)是Redis的高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统,可以监视任意多个主服务器,以及这些主服务器属下的所有的从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由主服务器代替已下线的主服务器继续处理命令请求。
通常来说要达到服务的高可用的效果需要做2个事情:故障探测与故障转移(即选主并做主从切换)
故障 探测 | Sentinel集群故障转移 1)选出一台Sentinel-leader,来进行故障转移操作(raft协议,过半选举) if (winner && (max_votes < voters_quorum || max_votes < master->quorum)) 2)领头sentinel在已下线的从服务器里面,挑选一个从服务器,并将其转换为主服务器 3)让已下线主服务器属下的所有从服务器改为复制新的主服务器 4)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器 注:从服务器中挑选新的主服务器的步骤 1)选举列表中剔除所有下线或者断线状态的从服务器 剔除所有最近五秒内没有回复过领头Sentinel的info命令的从服务器 剔除所有与已下线服务器超过down-after-millisenconds * 10(ms)的从服务器 2)根据从服务器优先级(取最高),复制偏移量(取最大),运行ID(取最小)1)每1秒,向主服务器,从服务器,其他sentinel实例发送ping命令 有效回复:+PONG, -Loading,+MASTERDOWN三种回复一种 无效回复:除以上三种回复之外的回复,或者在指定时限内没有返回的回复 Sentinel.conf -> Sentinel down-master-millsenconds master 50000 (当连续50秒,sentinel都接收到无效请求或者无回复时,就会将master标记为主观下线) 2)主观下线之后,向其他sentinel发送询问命令,如果达到配置中指定的数量时,则标记master为客观下线 Sentinel monitor master xx.xx.xx.xx 2 |
故障 转移 | Sentinel集群故障转移 1)选出一台Sentinel-leader,来进行故障转移操作(raft协议,过半选举) if (winner && (max_votes < voters_quorum || max_votes < master->quorum)) 2)领头sentinel在已下线的从服务器里面,挑选一个从服务器,并将其转换为主服务器 3)让已下线主服务器属下的所有从服务器改为复制新的主服务器 4)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器 注:从服务器中挑选新的主服务器的步骤 1)选举列表中剔除所有下线或者断线状态的从服务器 剔除所有最近五秒内没有回复过领头Sentinel的info命令的从服务器 剔除所有与已下线服务器超过down-after-millisenconds * 10(ms)的从服务器 2)根据从服务器优先级(取最高),复制偏移量(取最大),运行ID(取最小) |
5.2.2 codis如何感知哨兵集群的故障转移动作
codis的架构本身分成proxy集群+redis集群,redis集群的高可用是由哨兵集群来保证的,那么proxy是如何感知redis主机故障,然后切换新主保证服务高可用的呢?
如上图所示,proxy本身会监听sentinle集群的+switch-master事件,该事件发出,意味着redis集群主机出现问题,sentinel集群开始进行选举并切换主机,proxy监听了sentinel的主从切换事件,收到主从切换事件之后,proxy会做一个动作,就是把所有sentinel上的集群所感知的当前认为的主机拉取出来,选取过半sentinel认为的主机当作目前的集群主机。
讲到这里,大家可能会忽略一个问题,就是配置存储,配置中心的存储还是旧的主机,一旦proxy重起,那拉取的依旧是故障的主机,其实dashboard和proxy也做了一样的事情,收到主从切换事件之后,就会将新主持久化到storage中(目前为zk)
5.2.3 脑裂处理
脑裂(split-brain)集群的脑裂通常是发生在集群中部分节点之间不可达而引起的。如下述情况发生时,不同分裂的小集群会自主的选择出master节点,造成原本的集群会同时存在多个master节点。,结果会导致系统混乱,数据损坏。
在这个问题上,这里simotang同学已经讲解的非常完善了,大规模codis集群的治理与实践,这里简单说一下,由于redis集群不能单纯的依赖过半选举的模式,因为redismaster自身没有做检测自身健康状态而降级的动作,所以我们需要一种master健康状态辅助判断降级的方式。具体实现为
1)降级双主出现的概率,让Quorums判断更加严格,让主机下线判断时间更加严格,我们部署了5台sentinel机器覆盖各大运营商IDC,只有4台主观认为主机下线的时候才做下线。
2)被隔离的master降级,基于共享资源判断的方式,redis服务器上agent会定时持续检测zk是否通常,若连接不上,则向redis发送降级指令,不可读写,牺牲可用性,保证一致性。
由于codis是针对redis分布式的解决方案,必然会面临着redis单点容量不足的情况下水平扩容的问题,本节主要针对codis水平扩容与迁移异常的细节做一下说明,大家先带着两个问题来看,问题一,迁移过程中,正在迁移的key的读写请求怎么处理,问题二,迁移过程中的异常(例如失败,超时)怎么处理。
6.1 Codis扩容迁移细节
【图】迁移流程 |
子进程开销
监控与优化
不要和其他CPU密集型服务部署在一起,造成CPU过度竞争
如果部署多个Redis实例,尽量保证同一时刻只有一个子进程执行重写工作
1G内存fork时间约20ms
内存
背景:子进程通过fork操作产生,占用内存大小等同于父进程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write)。父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。
Fork耗费的内存相关日志:AOF rewrite: 53 MB of memory used by copy-on-write,RDB: 5 MB of memory used by copy-on-write
关闭巨页,开启之后,复制页单位从原来4KB变为2MB,增加fork的负担,会拖慢写操作的执行时间,导致大量写操作慢查询
“sudo echo never>/sys/kernel/mm/transparent_hugepage/enabled
硬盘
不要和其他高硬盘负载的服务部署在一起。如:存储服务、消息队列
8.6 AOF持久化细节
常用的同步硬盘的策略是everysec,用于平衡性能和数据安全性。对于这种方式,Redis使用另一条线程每秒执行fsync同步硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞。
1)主线程负责写入AOF缓冲区(源码:flushAppendOnlyFile) 2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。 3)主线程负责对比上次AOF同步时间: 如果距上次同步成功时间在2秒内,主线程直接返回。 如果距上次同步成功时间超过2秒,主线程将调用write(2)阻塞,直到同步操作完成 备注:打开AOF持久化功能后,Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer,如果此时write(2)被阻塞,Redis就不能处理下一个事件。Linux规定执行write(2)时,如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘, write(2)会被Block住,整个Redis被Block住。 通过对AOF阻塞流程可以发现两个问题: 1)everysec配置最多可能丢失2秒数据,不是1秒。 2)如果系统fsync缓慢,将会导致Redis主线程阻塞影响效率。 Redis提供了一个自救的方式,当发现文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被Block。但如果已经超过两秒都还是这个样子,则会硬着头皮执行write(2),即使redis会被Block住。 Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF buffer,without waiting for fsync to complete, this may slow down Redis |
8.7 不小心手抖执行了flushdb
如果配置appendonlyno,迅速调大rdb触发参数,然后备份rdb文件,若备份失败,赶紧跑路。配置了appedonlyyes, 办法调大AOF重写参数auto-aof-rewrite-percentage和auto-aof-rewrite-minsize,或者直接kill进程,让Redis不能产生AOF自动重写。·拒绝手动bgrewriteaof。备份aof文件,同时将备份的aof文件中写入的flushdb命令干掉,然后还原。若还原不了,则依赖于冷备。
8.8 线上redis想将rdb模式换成aof模式
切不可,直接修改conf,重启
正确方式:备份rdb文件,configset的方式打开aof,同时configrewrite写回配置,执行bgrewriteof,内存数据备份至文件
Redis开发与运维(付磊)
Redis设计与实践(黄健宏)
大规模codis集群的治理与实践
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。