首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

深入浅出百亿请求高可用Redis(codis)分布式集群揭秘

摘要:作为 NoSQL中的kv数据库的王者,redis 以其高性能,低时延,丰富的数据结构备受开发者青睐,但是由于 redis 在水平伸缩性上受限,如何做到能够水平扩容,同时对业务无侵入性是很多使用redis的开发人员都会面临的问题,而redis分布式解决方案的一个开源产品【codis】较好的弥补了这一弱势。

本文主要讲解codis是如何做到对业务无感知,平滑迁移,迁移性能高,迁移异常处理,高可用以及常见的redis的避坑指南,虽然codis目前随着公司的NoSQL产品越来越成熟,生命周期也即将结束,不过鉴于还有很多同学对codis的原理比较感兴趣,于是将以前的分享的内容重新整理,当然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应用场景

2.4 写在前面:codis与redis的关系

codis与redis之间关系就是codis是基于多个redis实例做了一层路由层来进行数据的路由,每个redis实例承担一定的数据分片。

2.5 Redis学习资料

由于本文重点在于redis分布式解决方案,对于redis相关的基础部分,大家可以参考两本书及相关源码分析文章

  1. Redis开发与运维(付磊)
  2. Redis设计与实践(黄健宏)(值得多看两遍)

三、Redis分布式解决方案公司内外比较

在比较方案之前,我们先根据我们的经验输出了我们期望的解决方案应该具备的能力,以此来衡量我们的选择标准。

基于此我们对公司内外做了一个如下的比较

【公司内组件对比】

【公司外组件对比】

基于以上比较,codis作为开源产品,可以很直观的展示出codis运维成本低,扩容平滑最核心的优势.

对于数据安全目前我们基于机器本机48小时滚动备份加上公司刘备备份(每天定时目录备份的系统)的兜底备份,对于监控,目前接入monitor单机备份和米格监控告警)

四、Codis的架构设计

4.1 Codis整体的架构设计

【图codis架构图】

如上图所示,codis整体属于二层架构,proxy+存储,相对于ckv+无proxy的设计来说整体设计会相对简单,同时对于客户端连接数据逐渐增大的情况下,也不用去做数据层的副本扩容,而只需要做proxy层的扩容,从这一点上看,成本会低一些,但是对于连接数不大的情况下,还需要单独去部署proxy,从这一点上看,成本会高一些。

其中,开源的codisproxy的服务的注册发现是通过zk来实现,目前部门是基于l5来做.

从整体的架构设计图来看,codis整体的架构比较清晰,其中codisproxy是分布式解决方案设计中最核心的部分,存储路由,分片迁移均与codisproxy分不开,这块我们来看一下codisproxy的设计实现。

4.2 Codisproxy的架构设计实现

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的处理细节

Codisproxy主要基于go语言这种从语言层面天然支持协程的语言来实现的

1)proxy接收客户端的连接之后,新建一个session,同时启动session中reader与writer两个协程,reader主要用于接收客户端请求数据并解析,对多key的场景下进行命令的拆分,然后将请求通过router进行分发到具体的redis实例,并将redis处理的数据结果写到通道到中,writer从通道中接收对应的结果,将写回给客户端。

2)Router层主要是通过crc命令得到key对应的路由信息,从源码可以看到hashtag的特性,codis其实也是支持的。

至此,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个事情:故障探测与故障转移(即选主并做主从切换)。

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水平扩容细节&迁移异常处理

由于codis是针对redis分布式的解决方案,必然会面临着redis单点容量不足的情况下水平扩容的问题,本节主要针对codis水平扩容与迁移异常的细节做一下说明,大家先带着两个问题来看,问题一,迁移过程中,正在迁移的key的读写请求怎么处理,问题二,迁移过程中的异常(例如失败,超时)怎么处理。

6.1 Codis扩容迁移细节

影响面: 一阶段期间的影响:通知到通知成功结束期间,proxy读写请求阻塞,不丢失,延时增高(时间极短,并行通知,仅仅修改状态,使proxy中slot状态达到一致) 迁移过程:可读,正在迁移批次的不可写,迁移完成的批次涉及到两次网络io

如上图所示,其实redis平滑迁移过程,主要是实现了3个点,迁移准备,迁移动作,迁移性能保证。

迁移准备 主要是在迁移动作执行前,所有的请求都能够感知到路由的变化,所以有了一阶段的处理流程,此处实现是通过并行发送给所有的proxy,proxy会对相应的slot加写锁,所以的请求在队列中排队,直到所有的proxy都通知dashboard之后,proxy的锁才放开,此时请求的延时会有轻微增高,但由于是并行响应,影响时间很短,视图会轻微抖动。

迁移动作 主要由dashboard按批次触发直到所有的key都迁移ok,迁移的过程,slot上的key可能存在2种情况,一种在新的redis实例上A,一种在旧的redis实例上B,所以对于有迁移状态的slot,所有向这个slot发送的命令都通过在redis中定制的命令SLOTSMGRT-EXEC-WRAPPER来处理,该命令是基于3.2的分支新增的,该命令主要做这几个事情:

1)判断key是否存在,如果存在,但不在迁移批次,则直接对key调用真实方法,如果存在,但在迁移批次,则允许读操作,不允许写操作;

2)如果key不存大,则key可能已经被迁移到新实例,也可能key不存在,则通知proxy前往新的实例进行操作。

迁移性能 Codis的迁移其实之前2.x版本的迁移性能并不高,3.x之前性能提升了非常之大,千万级别的zset结构迁移只需要10多秒,而在原来的模式需要50多秒,具体原因在于

6.2 迁移异常处理

另外,看到这里,不知道大家有没有什么问题,不过这里我准备了一些问题,来看看codis是如何来处理的,特别在网络环境复杂,不稳定的情况下怎么操作

问题一:把大key拆分成小批次进行迁移,如果批次迁移失败,超时,怎么做?

我们知道分布场景下网络调用有三态,成功,失败,超时,对于失败还好一点,超时的情况,我们能否盲目进行重试,这里显然不行,通常对于数据层面的重试,我们需要保证一个非常重要的原则,幂等性,但是在redis结构中除了zset,set,hash,string结构重试理论不会受影响,对于list怎么办?所以codis用了一种比较暴力的方式,批次迁移成功重试时,会先带上一个del命令,让目标结点先将key删掉,再进行重试。

问题二,带过期时间key迁移过程中,先在目标结点上设置过期时间再传数据,还是先传数据在最后再设置过期时间?

先看一下在目标结点上设置过期时间再传数据的问题:传输一半B机器的key过期,后续key就没有过期时间。不符合我们的期望

再看一下先传数据在最后再设置过期时间的问题:如果传输一半Acrash重启,而此时key过期,则数据落在B机器上成僵尸数据,也不符合我们的期望。那codis如何来做呢?

为了保证迁移过程中的分片在迁移异常时能自动销毁,所以每次分片传输的时候,都重置一下key过期时间为90秒(大于超时时间30秒),在key迁移完成之后再重置为真实的过期时间,这样即使迁移过程中Acrash,key过期或者其他的异常,分片数据也只会在目标结点上存活90秒就销毁。

问题三:迁移过程中Acrash, 此时对应分片的数据一半在A,一半在B,怎么办了?

常在河边走,哪有不挨刀,我们就碰到过codis的一个因expire迁移实现不当造成的血案,不过幸好发生在测试环境,此时千万千万不要拉起A,因为A上可能有旧数据,此时会导致已经迁移完成的key重新迁移,造成B的数据丢失,正确的姿势是A的备机顶上去,继续迁移,因为A的备机虽然是异步复制,但基本接近于A的全量数据,所以问题不太大。不过所有的迁移过程中,都最好把数据和分片信息备份,以防数据丢失。此时也千万千万不能反向将B的数据迁移回A,因为B上可能残留有部分迁移的数据,会覆盖掉A的全量数据。

问题四:为了性能问题,可否A不做备机,不开启AOF和RDB

这个也是万万不可,因为A如果crash之后,被织云拉起,则相当于一个空实例,会清掉备机的数据,造成数据丢失。

七、Codis 相关数据

其中压测环境:压测服务器(v4-8-100)+proxy(v4-8-100) +  redis( B5(4 -32-100) )

从上图中可以看出,当单次获取的数据量越来越大时,proxy的性能下降会非常快,例如ZRANGE_500的直连的性能是proxy的2倍

八、运维手册及避坑指南

操作注意项:

8.1 主从切换

每次主从切换之后,都确认一下被切的主或者备机上的conf文件都已经rewriteok。

代码语言:javascript
复制
grep "Generatedby CONFIG REWRITE" -C 10 {redis_conf路径}/*.conf

8.2 迁移数据

关键操作前,备份数据,若涉及切片信息,备份切片信息。

A迁移B时间过长的命令查看:连上Acodisserver,命令行中执行slotsmgrt-async-status查看正在迁移的分片信息(尤其是大key),做到心中有数。千万级别的key约20秒左右可以迁移完成。

8.3 异常处理

redis宕机后重启,重启之后加载key快加载完时,页面上报error

8.4 客户端出现大量超时

  1. 网络原因,联系“连线NOC智能助手”,确认链路网络是否出现拥塞
  2. 观察视图,查看监听队列是否溢出 全连接队列的大小取决于:min(backlog,    somaxconn) ,backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数,基于命令ss    -lnt,观察监听队列目前的长度是否与预期一致, 调整参数:vim    /etc/sysctl.conf net.core.somaxconn=1024   sysctl -p
  3. 慢查询,slowlogget,确认是否有耗时操作执行,现网默认是10ms

slowlog-log-slower-than和slowlog-max-len

其中注意:慢查询不包含请求排队时间,只包含请求执行时间,所以有可能是redis 本身排队导致的问题,但通过慢查询可能查不出来。

8.5 fork耗时高

原因: 1)当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创建子进程,虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表,可以在info    stats统计中查latest_fork_usec指标获取最近一次fork操作耗时,单位(微秒)。

改善:

  1. 优先使用物理机或者高效支持fork操作的虚拟化技术。
  2. 控制redis单实例的内存大小。 fork耗时跟内存量成正比,线上建议每个Redis实例内存控制在10GB以内。
  3. 适度放宽AOF rewrite触发时机,目前线上配置: auto-aof-rewrite-percentage增长100%

子进程开销&监控与优化

CPU

  • 不要和其他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主线程阻塞。

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集群的治理与实践
  • 发表于:
  • 原文链接http://news.51cto.com/art/201911/606392.htm
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券