Redis开发与运维

Redis概述

Redis是基于键值对的NoSQL数据库,值可以是由string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyberLogLog、GEO等多种数据结构和算法组成,可以满足多种应用场景。

特性

主要特性有:速度快,数据全部放在内存中、基于C语言实现(距离操作系统更近,执行速度相对更快)、采用单线程架构预防了多线程可能产生的竞争问题;基于键值对结构,全称是remote dictionary server;丰富的功能,键过期功能可以实现缓存功能、发布订阅功能可以用来实现消息系统,支持Lua脚本可以创造出新的redis命令,提供了简单的事务功能可在一定程度上保证事务特性,提供了流水线(Pipeline)功能支持批量操作减少了网络开销;简单稳定,代码行数相对少,使用单线程模型,是的服务端处理模型和客户端开发变得简单,且redis不依赖于操作系统中的类库,自己实现了事务处理的功能;客户端语言多,几乎涵盖了主流的编程语言;持久化,支持两种持久化方式,保证了数据的可持久化特性;主从复制,复制功能是分布式redis的基础;高可用和分布式,提供了高可用实现redis sentinel保证节点的故障发现和故障自动转移功能,分布式实现redis cluster是redis的真正分布式实现,提供了高可用、读写和容量的可扩展性。

使用场景

缓存,键过期机制和内存淘汰策略;排行榜系统,redis提供了列表和有序集合数据结构;计数器应用,天然支持高性能的计数器功能;社交网络,redis提供的数据结构比较容易实现赞/踩、粉丝、推送等功能;消息队列系统,提供了发布订阅和阻塞队列的功能,可以满足一般消息队列的功能。

API理解和使用

命令手册

数据结构和编码

                                   

每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。这样做的目的在于,可以改进内部编码而对外的数据结构和命令没有影响,这样一旦开发开发出优秀的内部编码,无需改动外部数据结构和命令。第二,多种内部编码实现可以在不同场景下发挥各自的优势。例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。

单线程架构

redis使用了单线程架构I/O多路复用模型来实现高性能的内存数据库服务。单线程可以简化数据结构和算法的实现,避免了线程切换和竟态产生的消耗,对于服务端来说,锁和线程切换通常是性能杀手。但是单线程对于每个命令的执行时间是有要求的(如果某个命令执行时间过长,就会造成其他命令的阻塞,对于redis这种高性能服务来说是致命的,所以redis是面向快速执行场景的数据库)。

为什么单线程还这么快?

为什么redis使用单线程模型还会达到每秒万级的处理能力,大致分为如下三点:

(1)redis是基于内存来存储的,然而内存的读取/响应市场大约为100纳秒,这一点也就是redis能打到每秒万级的重要基础;

(2)非阻塞I/O,redis使用epoll作为I/O多路复用技术的实现,再加上redis的自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件不在网络I/O上浪费时间;

(3)单线程避免了线程切换和竟态产生的消耗

字符串

键都是字符串类型,值实际可以是字符串(简单字符串、复杂字符串Json、Xml等)、数字(整数、浮点)、甚至是二进制(图像、音频、视频),但是值最大不能超过512MB。

set命令选项

  • NX :键必须不存在,才可以设置成功,用于添加。NX 为 "Not eXists"的缩写

  • XX :与XX相反,键必须存在,才可以设置成功,用于更新

字符串类型的内部编码有3种,int是8个字节的长整型;embstr是小于等于39个字节的字符串;raw是大于39个字节的字符串,Redis会根据当前值的类型和长度决定使用内部编码实现。

哈希

哈希对比关系型数据库是稀疏的数据结构,每个键可以有不同的field,在Redis中,哈希类型是指键值本身又是一个键值对结构,形如:value={{field1,value1},{field2,value2},{fieldN,valueN}}

                                ae31caca7a3130f273e5d4bccbd4c234124.jpg

                                                       图-hash结构示意图

哈希类型的内部编码有两种,ziplist(压缩列表),当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。hashtable(哈希表),当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现。因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)

列表

队列存储多个有序的可重复字符串,可以充当栈和队列的角色。

列表类型的内部编码有两种,ziplist(压缩列表),当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现,linkedlist(链表),当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。3.2版本之后提供quickList内部编码 ,是zipList 和linkedList的混合体,它将linkedList按段切分,每一段使用zipList来紧凑存储,多个zipList之间使用双向指针串接起来,结合二者优势。

集合

集合类型的内部编码有两种,intset(整数集合),当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合内部实现,从而减少内存的使用。hashtable(哈希表),当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

有序集合

有序集合元素不能重复,给每个元素设置一个分数score作为排序依据。

7b2798e5d8c36aebe9eb89c7b859c0af0d7.jpg

                                       表-列表、集合、有序集合区别

有序集合类型的内部编码有两种,ziplist(压缩列表),当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值小于zset-max-ziplist-value配置(默认64个字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存使用skiplist(跳跃表),当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时zip的读写效率会下降。

键管理

单个键管理,type、del、object、exists、expire,键重命名rename key newkey、随机返回一个键randomkey、键过期(除了expire、ttl命令外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令)、迁移键move(不推荐)、dump+restore、migrate(推荐使用,原子操作、支持多键,是Redis Cluster实现水平扩容的重要工具

遍历键,由于单线程架构keys在大量键时容易造成Redis阻塞,推荐使用渐进式遍历scan(也会出现统计漏掉或者重复的极端情况,不能保证完整遍历),多次执行,除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似。

数据库管理,切换数据库select,清除数据库flushdb/flushall命令

小功能大用处

慢查询分析

redis命令执行分为4个步骤,发送命令,命令排队,执行命令,返回结果,这里的慢查询只统计执行命令的时间(不含命令排队和网络传输时间所以没有慢查询并不代表客户端没有超时时间)。

预设阀值如何设置(线上建议调大慢查询列表,根据Redis并发量进行调整),慢查询的记录在哪里(慢查询日志是一种先进先出的队列,可以将它持久化到其他存储中(mysql),然后使用可视化工具进行查看)。

Redis Shell

redis-cli

redis-server

redis-benchmark做基准性能测试,它提供了很多选项帮助开发和运行人员测试Redis的性能。

Pipeline

Redis命令真正执行的时间通常在微秒级别,所以才会有Redis性能瓶颈是网络这样的说法。为了解决这一的问题,Pipeline流水线机制就出现了,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组命令的结果按照顺序返回给客户端,但是它不是原子性的。虽然Pipeline好用,但是每次组装还需要节制,否则一次组装的数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞。

事物与Lua

Redis提供了简单的事务功能,将一组需要放在一起执行的命令放到multi和exec两个命令之间,分别代表事务的开始和结束,它们之间的命令是原子顺序执行的。Redis事务不支持回滚功能,开发人员需要自己修复这类问题。

命令说明
mutli代表事物开始
exec代表事物结束
discard命令表示停止事物。
watch监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

                                                    表-redis事务命令

redis事务命令出现错误,处理机制不尽相同:

  • 语法错误则整个事物无法执行
  • 非语法错误则会部分执行,并且不支持回滚, 只会将错误包含在事务的结果中, 这不会引起事务中断或整个失败,不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令

Lua脚本

Lua语言是C语言开发的脚本语言,广泛用于游戏领域。Redis就可以通过eval命令或evalsha命令(利用脚本SHA1校验和作为参数执行对应脚本,避免每次发送脚本的开销,实现脚本常驻服务端,使得脚本功能得到复用执行Lua脚本,Lua脚本在Redis中的执行是原子性执行的,执行过程中不会插入其他命令;可以利用Lua脚本定制命令,并将这些命令常驻内存实现复用的效果;Lua脚本可以将多条命令一次性打包,有效减少网络开销;

Bitmaps

实现对位的操作,本身不是数据结构,实际上是可以对位进行操作的字符串。可以把bitmaps想象成以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。Bitmaps基于最小的单位bit进行存储,所以非常省空间;设置时候时间复杂度O(1)、读取时候时间复杂度O(n),操作是非常快的;二进制数据的存储,进行相关计算的时候非常快。redis中bit映射被限制在512MB之内,所以最大是2^32位。建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越多。

HyberLogLog

并非一种新的数据结构,而是一种基数算法,可以利用极小的内存空间完成独立总数的统计。使用场景,只为了计算独立总数,不需要获取单条数据,可以容忍一定的误差率,毕竟其在内存的占用量上有很大的优势。

发布订阅

redis提供了简单的发布订阅功能,胜在足够简单,不支持消息的持久化存储(意味着新开启的客户端无法接受该频道之前的消息)和消息传输保障机制。

 

                                    f42394bb7dfe40c450ab5e7fe00cd380a02.jpg

                                                     图-redis发布订阅示意图

GEO

提供地理信息定位的功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。其实现主要包含了以下两项技术:使用geohash保存地理位置的坐标,使用有序集合(zset)保存地理位置的集合。

客户端

Redis是用单线程来处理多个客户端的访问。

客户端通信协议

几乎所有的主流编程语言都有Redis的客户端,不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:客户端与服务端之间的通信协议是在TCP 协议之上构建的,客户端和服务器通过TCP连接来进行数据交互,服务器默认的端口号为6379。客户端和服务器发送的命令或数据一律以rn(CRLF)结尾,Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

Java客户端Jedis

Java有很多优秀的Redis客户端,其中使用较广泛的是Jedis。

下图直连方式每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式,同时资源无法控制,极端情况下会出现连接泄露,生产环境考虑使用连接池的方式对Jedis连接进行管理。

                                    f3bcdc80f75714c5361c8632335986978c9.jpg

                                                           图-Jedis直连Redis

Jedis本身没有提供序列化工具,也就是说开发者需要自己引入序列化工具,Jedis支持Pipeline特性,支持Lua脚本的执行。

客户端管理

Redis提供了客户端相关API对其状态进行监控和管理。

client list命令能列出与Redis服务端相连的所有客户端连接信息,client setName用于给客户端设置名字,client kill(client kill ip:port)命令用于杀掉指定IP地址和端口的客户端,client pause命令用于阻塞客户端timeout毫秒数(在此期间客户端连接将被阻塞),monitor命令用于监控Redis正在执行的命令(一旦Redis的并发量过大monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存)。

客户端相关配置,Redis提供了maxclients参数来限制最大客户端连接数,timeout(单位为秒)参数来限制连接的最大空闲时间,tcp-keepalive检测TCP连接活性的周期,tcp-backlog,TCP三次握手后,会将接受的连接放入队列中,tcpbacklog就是队列的大小。

客户端统计片段info clients命令,info stats命令。

客户端常见异常

无法从连接池获取到连接

客户端读写超时

客户端连接超时

客户端缓冲区异常

Lua脚本正在执行

Redis正在加载持久化文件

Redis使用的内存超过maxmemory配置

客户端连接数过大

客户端案例分析

客户端执行monitor命令使得缓冲区暴增造成Redis主节点内存陡增

hgetall慢查询引起客户端周期性超时(解决办法)

持久化

Redis支持RDB和AOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。

RDB

RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发

手动触发采用save命令(阻塞当前redis服务器,对于内存比较大的实例会造成长时间阻塞,已废弃,线上环境不建议使用)bgsave命令(redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,阻塞只发生在fork阶段,一般而言时间很短,是主流的触发RDB持久化方式

流程如下:

                                    28921ee86328ba836a034a863f958b687bf.jpg

                                                  图-bgsave命令的运作流程

RDB文件的处理保存、压缩、校验。

RDB的优点

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份、全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复,适合数据冷备和复制传输
  • Redis加载RDB恢复数据远远快于AOF的方式

RDB的缺点

  • RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作频繁执行成本过高
  • RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式 的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。

AOF

AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

                                                    097e682a10072870a900191f304438360a5.jpg

                                                       图-AOF工作流程

工作流程

  • 所有的写入命令会追加到aof_buf(缓冲区)中

AOF为什么直接采用文本协议格式?可能的理由如下:

  • 文本协议具有很好的兼容性。
  • 开启AOF后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销。
  • 文本协议具有可读性,方便直接修改和处理。

AOF为什么把命令追加到aof_buf中?
Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负 载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。

  • AOF缓冲区根据对应的策略向硬盘做同步操作;

可配置值    说明
always       命令写入aof_buf后调用系统fsync操作同步到AOF文件,fsync完成后线程返回
everysec    命令写入aof_buf后调用系统write操作,write完成后线程返回.fsync同步文件操作由专门线程每秒调用一次
no             命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒

write操作会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机缓冲区内数据将丢失。
fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化。

Redis提供了多种AOF缓冲区同步文件策略,由参数appendfsync控制

  • 配置为always时,每次写入都要同步AOF文件,在一般的SATA硬盘 上,Redis只能支持大约几百TPS写入,显然跟Redis高性能特性背道而驰,不建议配置。
  • 配置为no,由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
  • 配置为everysec,是建议的同步策略,也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失1秒的数据(严格说来不太准确)。
  • 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的;

重写后的AOF文件为什么可以变小?有如下原因:
1)进程内已经超时的数据不再写入文件。
2)旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
3)多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。
AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF文件可以更快地被Redis加载,AOF重写过程可以手动触发和自动触发。手动触发,直接调用bgrewriteaof命令。自动触发,根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。

  • 当Redis服务器重启时,可以加载AOF文件进行数据恢复。

AOF和RDB文件都可以用于服务器重启时的数据恢复,下图表示Redis持久化文件加载流程:

                                            c57d4f2a31d7d150ca029cf1ea2b9538a2c.jpg

                                                              图-Redis持久化文件加载流程

文件校验
加载损坏的AOF文件时会拒绝启动,AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启。加载AOF时,当遇到此问题时会忽略并继续启动。

复制

在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提供了复制功能,实现了相同数据的多个Redis副本。复制功能是高可用Redis的基础,哨兵和集群都是在复制的基础上实现高可用的。

配置

参与复制的Redis实例划分为主节点(master)和从节点(slave)。默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点。配置复制方式采用salveof命令,需要考虑到安全性传输延迟

拓扑

Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构。

一主一从

一主一从结构用于主节点出现宕机时从节点提供故障转移支持,当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。

一主多从

一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的读命令,如:keys、sort等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了主节点的负载影响服务稳定性。

树状主从结构

树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。如下图所示,数据写入节点A后会同步到B和C节点,B节点再把数据同步到D和E节点,数据实现了一层一层的向下复制。当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用树状主从结构降低主节点压力

                                        5e4b6d93d068ae454340080031329705aa6.jpg

                                                             图-树状主从结构

原理

                                        de1bb1f65e23c92de70e1420fef546bfa45.jpg

                                                         图-主从节点建立复制流程图

Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。
全量复制:一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。
部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。
部分复制是对老版复制的重大优化,有效避免了不必要的全量复制操作。因此当使用复制功能时,尽量采用2.8以上版本的Redis。

主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令replconf命令主要用于实时监测主从节点网络状态,上报自身复制偏移量,实现保证从节点的数量和延迟性功能。

                                        889a3b186f8f85d00b57eba45dcc0ee1214.jpg

                                                            图-主从心跳检测

异步复制,主节点不但负责数据读写,还负责把写命令同步给从节点。写命令的发送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户 端,并不等待从节点复制完成。因此从节点数据集会有延迟情况

开发与运维中注意的问题

通过复制机制,数据集可以存在多个副本(从节点)。这些副本可以应用于读写分离、故障转移(failover)、实时备份等场景。但是在实际应用复制功能时,依然有一些坑需要跳过。

读写分离

当使用从节点响应读请求时,业务端可能会遇到如下问题:复制数据延迟,读到过期数据,从节点故障。

                                            44f2372c604b8b2613ec6fb7f3dc7684cc3.jpg

                                                                   图-Redis读写分离示意图

数据延迟

Redis复制数据的延迟由于异步复制特性是无法避免的,延迟取决于网络带宽和命令阻塞情况,比如刚在主节点写入数据后立刻在从节点上读取可能获取不到。需要业务场景允许短时间内的数据延迟。对于无法容忍大量延迟场景,可以编写外部监控程序监听主从节点的复制偏移量,当延迟较大时触发报警或者通知客户端避免读取延迟过高的从节点,实现逻辑如下图所示。

                                                06b8a08bb6e3a6b6543a2c66af702bfeb6b.jpg

                                                            图-监控程序监控主从节点偏移量

这种方案的成本比较高,需要单独修改适配Redis的客户端类库。如果涉及多种语言成本将会扩大。客户端逻辑需要识别出读写请求并自动路由,还需要维护故障和恢复的通知。采用此方案视具体的业务而定,如果允许不一致性或对延迟不敏感的业务可以忽略,也可以采用Redis集群方案做水平扩展。

读到过期数据

当主节点存储大量设置超时的数据时,如缓存数据,Redis内部需要维护过期数据删除策略,删除策略主要有两种:惰性删除和定时删除

惰性删除:主节点每次处理读取命令时,都会检查键是否超时,如果超时则执行del命令删除键对象,之后del命令也会异步发送给从节点。需要注意的是为了保证复制的一致性,从节点自身永远不会主动删除超时数据。

                                            9d895a521b42c060f8a31d2844848aa5e0e.jpg

                                                图-主节点惰性删除过期键同步给从节点

定时删除:Redis主节点在内部定时任务会循环采样一定数量的键,当发现采样的键过期时执行del命令,之后再同步给从节点

                                                84752e19916e1bf12ac4ec838819ae2ccac.jpg

                                                图-主节点定时删除同步给从节点

如果此时数据大量超时,主节点采样速度跟不上过期速度且主节点没有读取过期键的操作,那么从节点将无法收到del命令。这时在从节点上可以读取到已经超时的数据。Redis在3.2版本解决了这个问题,从节点读取数据之前会检查键的过期时间来决定是否返回数据,可以升级到3.2版本来规避这个问题。

从节点故障问题

对于从节点的故障问题,需要在客户端维护可用从节点列表,当从节点故障时立刻切换到其他从节点或主节点上。这个过程类似上文提到的针对延迟过高的监控处理,需要开发人员改造客户端类库。

综上所出,使用Redis做读写分离存在一定的成本。Redis本身的性能非常高,开发人员在使用额外的从节点提升读性能之前,尽量在主节点上做充分优化,比如解决慢查询,持久化阻塞,合理应用数据结构等,当主节点优化空间不大时再考虑扩展。建议在做读写分离之前,可以考虑使用Redis Cluster分布式解决方案,这样不止扩展了读性能还可以扩展写性能和可支撑数据规模,并且一致性和故障转移也可以得到保证,对于客户端的维护逻辑也相对容易。

主从配置不一致

对于有些配置主从之间是可以不一致,比如:主节点关闭AOF在从节点开启。但对于内存相关的配置必须要一致,比如maxmemory,hash-max-ziplist-entries等参数。当配置的 maxmemory从节点小于主节点,如果复制的数据量超过从节点maxmemory 时,它会根据maxmemory-policy策略进行内存溢出控制,此时从节点数据已经丢失,但主从复制流程依然正常进行,复制偏移量也正常。修复这类问题也只能手动进行全量复制。当压缩列表相关参数不一致时,虽然主从节点存储的数据一致但实际内存占用情况差异会比较大。

规避全量复制

全量复制是一个非常消耗资源的操作,因此如何规避全量复制是需要重点关注的运维点。

第一次建立复制:由于是第一次建立复制,从节点不包含任何主节点数据,因此必须进行全量复制才能完成数据同步。对于这种情况全量复制无法避免。当对数据量较大且流量较高的主节点添加从节点时,建议在低峰时进行操作,或者尽量规避使用大数据量的Redis节点。

此外还要避免节点运行ID不匹配、复制积压缓冲区不足等情况造成的全量复制。

规避复制风暴

复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程。复制风暴对发起复制的主节点或者机器造成大量开销,导致CPU、内存、带宽消耗。

单主节点复制风暴

单主节点复制风暴一般发生在主节点挂载多个从节点的场景。当主节点重启恢复后,从节点会发起全量复制流程,这时主节点就会为从节点创建RDB快照,如果在快照创建完毕之前,有多个从节点都尝试与主节点进行全量同步,那么其他从节点将共享这份RDB快照。这点Redis做了优化,有效避免了创建多个快照。但是,同时向多个从节点发送RDB快照,可能使主节点的网络带宽消耗严重,造成主节点的延迟变大,极端情况会发生主从节点连接断开,导致复制失败。解决方案首先可以减少主节点(master)挂载从节点(slave)的数量, 或者采用树状复制结构,加入中间层从节点用来保护主节点,如下图所示:

                                               00bce6a6ddabff4dde418b55bd844c68865.jpg

                                               图-采用树状结构降低多个从节点对主节点的消耗
从节点采用树状树非常有用,网络开销交给位于中间层的从节点,而不必消耗顶层的主节点。但是这种树状结构也带来了运维的复杂性,增加了手动和自动处理故障转移的难度。

单机器复制风暴

由于Redis的单线程架构,通常单台机器会部署多个Redis实例。当一台 机器(machine)上同时部署多个主节点(master)时,如果这台机器出现故障或网络长时间中断,当它重启恢复后,会有大量从节点(slave)针对这台机器的主节点进行全量复制,会造成当前机器网络带宽耗尽。如何避免?应该把主节点尽量分散在多台机器上,避免在单台机器上部署过多的主节点。当主节点所在机器故障后提供故障转移机制,避免机器恢复后进行密集的全量复制。

Redis的噩梦:阻塞

Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。Redis用于高并发的场景时,这条线程就变成了它的生命线。如果出现阻塞,哪怕是很短时间,对于应用来说,都是噩梦。导致阻塞问题的场景分为内在原因和外在原因。

当Redis阻塞时,线上应用服务应该最先感知到,这时应用方会收到大量Redis超时异常。常规做法是在应用方加入异常统计并通过邮件/微信/短信报警,以便及时发现通知问题。开发人员需要处理如何统计异常以及触发报警的时机。何时报警根据应用的并发量决定。由于Redis调用API会分散在项目的多个地方,每个地方都监听异常并加入监控代码必然难以维护。这可以借助于日志系统,使用logback或者log4j。当异常发生时,异常信息最终会被日志系统收集到Appender,默认的Appender一般是具体的日志文件,开发人员可以自定义一个Appender,用于专门统计异常和触发报警逻辑。

                                    7b8a47624239c3497f07cc0032c6f4166d6.jpg

                                       图-自定义Appender收集Redis异常

内在原因

内在原因主要包括,不合理使用API或数据结构、CPU饱和、持久化阻塞

通常Redis执行命令速度非常快,但也存在例外,如对一个包含上万个元素的hash结构执行hgetall操作,由于数据量比较大且命令算法复杂度是O(n),这条命令执行速度必然很慢,这个问题就是典型的不合理使用API和数据结构,在高并发场景下对于时间复杂度超过O(n)的命令应该尽量避免在大对象上执行

慢查询

Redis原生提供慢查询统计功能,执行slowlog get{n}命令可以获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。

发现慢查询后,按照以下两个方向去调整:

  • 修改低算法度的命令,例如hgetall改为hmget等,禁用keys、sort等命令
  • 调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。大对象拆分过程需要视具体的业务决定,如用户好友集合存储在Redis中,有些热点用户会关注大量好友,这时可以按时间或其他纬度拆分到多个集合中。如何发现大对象,redis-cli --bigkeys命令,内部原理采用分段进行scan操作,把历史扫描过的最大对象统计出来便于分析优化。

CPU饱和

单线程的Redis处理命令只能使用一个CPU。而CPU饱和是指Redis把CPU使用率跑到接近100%。使用top命令很容易识别出对应Redis进程的CPU使用率redis-cli --stat获取当前redis使用情况,该命令每秒输出一行统计信息。

对于接近饱和的Redis实例,垂直层面的命令优化很难达到效果,这时就需要水平扩展来分摊OPS压力。如果只有几百或者几千OPS的Redis实例就接近CPU饱和是很不正常的,有可能使用了高算法复杂度的命令,还有一种情况是过度的内存优化(如ziplist的过度内存使用优化

持久化阻塞

开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞。

外在原因

外在原因主要包括,CPU竞争、内存交换、网络问题等。

CPU竞争

进程竞争:Redis是典型的CPU密集型应用,不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时候,将严重影响Redis吞吐量。可以通过top/sar等命令定位到CPU消耗的时间点和具体进程,这个问题比较容易发现,需要调整服务之间部署结构。

绑定CPU:部署Redis时候为了充分利用多核CPU,通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定CPU上,用于降低CPU频繁上下文切花的开销。当Redis父进程创建子进程进行RDB/AOF重写时,子进程在重写时对单核CPU使用率通常在90%以上,父子进程共享CPU将会存在激烈CPU竞争,极大影响Redis的稳定性。

内存交换

内存交换对于Redis来说是非常致命的,Redis保证高性能的一个重要前提是所有的数据都在内存中。如果操作系统把Redis使用的部分内存换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降。预防内存交换的方法有:保证机器充足的可用内存;确保所有Redis实例设置最大可用内存,防止极端情况下Redis内存不可控的增长;降低系统使用swap优先级。

网络问题

连接拒绝,当出现网络闪断或者连接数溢出时,客户端会出现无法连接Redis的情况,具体可以分为网络闪断、Redis连接拒绝、连接溢出(指操作系统或者Redis客户端在连接时的问题);

网络延迟,网络延迟取决于客户端到Redis服务器之间的网络环境,主要包括它们之间的物理拓扑和带宽占用情况;

网卡软中断,网卡软中断指由于单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况,一般出现在网络高流量吞吐的场景

理解内存

高效利用Redis内存首先需要理解Redis内存消耗在哪里如何管理内存,最后才能考虑如何优化内存,能够实现用更少的内存存储更多的数据,从而降低成本。当Redis内存不足时,首先考虑的问题不是加机器做水平扩展,应该先尝试做内存优化,当遇到瓶颈时,再去考虑水平扩展。即使对于集群化方案,垂直层面优化也同样重要,避免不必要的资源浪费和集群化后的管理成本。内存优化首先要掌握Redis内存存储特性比如字符串、压缩编码、整数集合等,再根据规模和所用命令需求去调整,从而达到空间和效率的最佳平衡

内存消耗

内存消耗可以分为进程自身消耗子进程消耗。

可以使用info memory 查看内存消耗。

自身内存非常少,一个空的Redis进程消耗内存可以忽略不计。

对象内存是Redis内存占用最大的一块,存储着所有的用户数据。每次创建键值对时,至少创建两个类型对象,key对象和value对象。对象内存消耗可以简单理解为 = sizeof(key) + sizeof(value)。键对象都是字符串,在使用时很容易忽略键对内存消耗的影响,应当避免使用过长的键。value主要包含前述的5种数据类型,其它数据类型都是在这5种数据结构之上实现的,如Bitmaps和HyperLogLog使用字符串实现,GEO使用有序集合实现等。

缓存内存主要包括客户端缓存(所有接入到Redis服务器TCP连接的输入输出缓冲)、复制积压缓冲区(可重用的固定大小缓冲区用于实现部分复制功能,整个主节点只有一个,所有从节点共享此缓冲区,可有效避免全量复制)、AOF缓冲区(用于在Redis重写期间保存最近的写入命令)

内存碎片,默认内存分配器采用jemalloc,可选还有glibc、tcmalloc。频繁的更新操作、大量过期键删除将会导致高内存碎片。主要的解决办法有数据对齐、安全重启进行碎片重新整理。

                                    15d07d3fa51b2be56809048094941e91d55.jpg               

                                              图-Redis内存消耗划分        

子进程内存消耗:AOF/RDB重写时Redis创建的子进程消耗的内存。

内存管理

Redis主要通过设置内存上限回收策略实现内存管理。

设置内存上限

Redis使用maxmemory参数限制最大可用内存,主要目的有:用于缓存场景,当超出上限时使用LRU等删除释放空间;防止所有内存超过服务器物理内存。maxmemory参数限制的事Redis实际使用的内存量,由于内存碎片的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。得益于Redis单线程架构和内存限制机制,即使没有采用虚拟化,不同的Redis进程之间可以很好的实现CPU和内存的隔离性,如下图所示。Redis的内存上限可以通过config set maxmemory进行动态修改,从而达到自由伸缩内存的目的。

                                                     3360162f5e53a1d8ea208f09e25b10e6c11.jpg

                                            图-服务器分配4个4GB的Redis进程

内存回收策略

主要体现在以下几个方面:

  • 删除达到过期时间的键对象;

由于进程内保存大量的键,维护每个键精准的过期删除机制会消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除定时任务删除机制实现过期键的内存回收。

惰性删除:用于客户端读取带有超时属性的键时,如果已经超出键设置的过期时间,会执行删除操作并返回空,这种策略是由于节省CPU成本考虑,单独使用这种方式存在内存泄露的问题。

定时任务删除:内部维护定时任务,采用自适应算法,根据键的过期比例、使用快慢两种速率模式回收键,如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或者运行超时为止,快慢模式内部删除逻辑相同,只是执行的超时时间不同,慢模式为25秒。

                                            c92e3df7dba20c23e0931a7f25e42cf1d9d.jpg

                                                         图-定时任务删除过期键逻辑

  • 内存使用达到maxmemory上限时触发内存溢出控制策略。

                                   42e7cd8c50e271956063bbb4518e8158cf4.jpg

                                                                      图-内存溢出的控制策略

当Redis一直工作在内存溢出状态下且设置非noeviction策略时,会频繁触发回收内存操作,频繁执行内存回收成本很高,主要包括查找可回收键和删除键的开销,如果当前Redis有从节点,会进行同步操作,导致写放大的问题。

                                             2ea23907b4e4328b9fcfe218add242f736b.jpg

                                                                 图-写入操作触发内存回收操作

内存优化

redisObject对象

                                    6485f5a1f1ed3d80162751c6a409f2623a0.jpg

                                              图-redisObject

缩减键值对象

降低内存使用最直接的方式就是缩减键和值的长度。在设计键时,在完整描述业务情况下,键值越短越好。值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数据放入Redis,首先需要在业务上精简业务对象,其次在序列化工具使用上,应该选取更高效的序列化工具来降低字节数组的大小。格式存储数据如json和xml作为值对象便于调试和跨语言,但是同样的数据对比字节数组所需的空间更大,在内存紧张的情况下可以采用一定压缩算法,如Google Snappy、GZIP。

共享对象池

Redis内部维护0-9999的整数对象池以节省对象内存,需要注意共享对象池与LRU+maxmemory策略冲突。

字符串优化

Redis自身实现字符串,存在预分配机制,所以尽量减少字符串频繁修改操作(append、setrange),直接使用set,降低预分配带来的内存浪费和内存碎片化。

                                        52b3dbd233303a875e4e31a7b5a91adff7e.jpg

                                                    图-字符串结构SDS

编码优化

Redis通过编码实现空间和时间的平衡。如ziplist编码采用线性连续的内存结构以节约内存,可以作为hash、list、zset类型的底层数据结构实现,适合小对象和长度有限的数据,内部表现为数据紧凑排列的一块连续内存数组。如inset编码是集合类型编码的一种,内部表现为存储有序、不重复的整数集,inset编码适合整数集合。

                                    81f7cf783c20e2009783120e0b5a1879baf.jpg

                                                        表-redis内部编码

控制键的数量

过多的键同样会消耗内存。通过在客户端预估键规模,把大量键分组映射到多个hash结构中降低键的数量(客户端需要预估键的规模并设计hash分组规则,加重了客户端开发成本)。

                                            190a8504e1eda705c9cee6b8cc4a495448f.jpg

                                                 图-客户端维护哈希分组降低键规模

哨兵

Redis的高可用方案。

基本概念

主从复制的问题:主节点故障后的恢复需要人工干预,故障转移实时性和准确性无法得到保障,且主节点的写能力和存储能力受到单机的限制。即使将故障转移流程自动化,仍然存在以下问题:判断节点不可达的机制是否健全和准确;如果存在多个从节点,怎么保证只有一个被晋升为主节点;通知客户端新的主节点机制是否足够健壮。Redis Sentinel正是用于解决这些问题。

当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。使用Redis Sentinel,建议使用2.8以上版本。

Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。整个过程完全是自动的,不需要人工来介入,所以这套方案很有效地解决了Redis的高可用问题。

                                                0045486a1b25eec1cf4ccfb41e354d1c42c.jpg

                                                   图-Redis主从复制与Redis Sentinel架构的区别

整个故障转移的处理逻辑可以参考,可以看出 Redis Sentinel具有以下几个功能:

  • 监控:Sentinel节点会定期检测Redis数据节点、其余Sentinel节点是否可达。
  • 通知:Sentinel节点会将故障转移的结果通知给应用方。
  • 主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系。
  • 配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息。

同时看到,Redis Sentinel包含了若个Sentinel节点,这样做也带来了两个好处:

  • 对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判。
  • Sentinel节点集合是由若干个Sentinel节点组成的,这样即使个别Sentinel 节点不可用,整个Sentinel节点集合依然是健壮的。

但是Sentinel节点本身就是独立的Redis节点,只不过它们有一些特殊, 它们不存储数据,只支持部分命令。

安装和部署

Redis哨兵的部署可以参考,实际部署时需要注意,Sentinel节点不应该部署在一台物理“机器”上,部署至少三个且奇数个的Sentinel节点,只有一套Sentinel,还是每个主节点配置一套Sentinel?(一套还是多套需要权衡)。

API

Sentinel节点是一个特殊的Redis节点,它有自己专属的API

客户端连接

Sentinel节点集合具备了监控、通知、自动故障转移、配置提供者若干功能,也就是说实际上最了解主节点信息的就是Sentinel节点集合,而各个主节点可以通过<master-name>进行标识的,所以,无论是哪种编程语言的客户端,如果需要正确地连接Redis Sentinel,必须有Sentinel节点集合和 masterName两个参数。客户端初始化连接的是Sentinel节点集合,不再是具体的Redis节点,但Sentinel只是配置中心不是代理。

Redis Sentinel客户端基本实现原理。

实现原理

三个定时监控任务

一套合理的监控机制是Sentinel节点判定节点不可达的重要保证,Redis Sentinel通过三个定时监控任务完成对主节点、从节点、其余Sentinel节点的监控。

                                           3bd14f775b254588b73031bbd08bfd08cbb.jpg

                                                图-Sentinel节点定时执行info命令

每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构;每隔2秒,每个Sentinel节点会向Redis数据节点的__sentinel__:hello 频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息(如下图所示),同时每个Sentinel节点也会订阅该频道,来了解其它Sentinel节点以及它们对主节点的判断;每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。

                                        c4286985951a97740239617e8c0223e45be.jpg

 

                                      图-Sentinel节点发布和订阅__sentinel__hello频道

                                       f51cc8f118df9870d8824b7e8c9648c3427.jpg

                                           图-Sentinel节点向其余节点发送ping命令

主观下线和客观下线

上一小节介绍的第三个定时任务,每个Sentinel节点会每隔1秒对主节 点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过 down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。从字面意思也可以很容易看出主观下线是当前Sentinel节点的一家之言,存在误判的可能,如下图所示。

                                       dc34da47198075045622ee7c2637097abfc.jpg

                                                图-Sentinel节点主观下线检测

当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel ismaster-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过 <quorum>个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分 Sentinel节点都对主节点的下线做了同意的判定,那么这个判定就是客观的,如下图所示。

                                        61dd9a204f7f7ffced297d379e25f96a1d0.jpg

                                             图-Sentinel节点对主节点做客观下线

领导者Sentinel节点选举

假如Sentinel节点对于主节点已经做了客观下线,那么是不是就可以立即进行故障转移了?当然不是,实际上故障转移的工作只需要一个Sentinel 节点来完成即可,所以Sentinel节点之间会做一个领导者选举的工作,选出 一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举,具体可以参考

故障转移

领导者选举出的Sentinel节点负责故障转移,具体步骤如下:

  • 在从节点列表中选出一个节点作为新的主节点,选择方法如下:过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节点ping响应、与主节点失联超过down-after-milliseconds*10秒。选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。选择runid最小的从节点。

                                    9fb405f0196df58f44703e7dbbf5516d0e1.jpg

                                                   图-选出最好的从节点

  • Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点。
  • Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。
  • Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。

高可用读写分离

从节点可以实现主节点的故障转移并且可以扩展主节点的读能力,通常模型如下图所示,但是从节点不是高可用的,如果slave-1节点出现故障,客户端client-1将与其失联,其次Sentinel只会对该节点做主观下线因为Sentinel的故障转移是针对主节点的,所以很多时候,从节点仅仅是作为主节点的一个热备,不让它参与客户端的读操作,就是为了保证整体的高可用性,存在一些浪费在设计Redis Sentinel的从节点高可用时,借助Sentinel节点的消息通知机制,实时掌握所有节点的状态,把所有从节点看做一个资源池(如下图所示Redis Sentinel下的读写分离架构图),无论是上线还是下线从节点,客户端都能及时感知到(将其从资源池中添加或者删除),这样从节点的高可用目标就达到了。

                                0f1e6e7cc9aa2f26ad610566d63330a0582.jpg

                                            图-一般的读写分离模型

                                   2ab05d8c2000021e4e85b2259d14d193228.jpg

                                            图-Redis Sentinel下的读写分离架构图

集群

当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。

数据分布

分布式数据首先要解决把整个数据集按照分区规则映射到多个节点的问题。需要重点关注的事数据分区规则,常见的分区规则有哈希分区顺序分区两种。

哈希分区,离散度好、数据分布业务无关、无法顺序访问,代表产品有Redis Cluster、Cassandra;

顺序分区,离散度易倾斜、数据分布业务相关、可顺序访问,代表产品Bigtable、Hbase。

哈希分区

常见的哈希分区有这几种:

  • 节点取余分区

使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N计算出哈希值,用来决定数据映射在哪一个节点,其优点就是简单。缺点:添加或者移除服务器时,几乎所有缓存都要重建,还要考虑雪崩式崩溃问题。

如果使用多倍扩容,可以使得迁移降到50%,这个迁移会存在一个问题,数据进行迁移后第一次是无法从缓存中获取到数据的,要在数据库中去取数据,然后进行回写,写到新的节点,大量的回写也会对系统性能带来问题。

  • 一致性哈希分区

具体可以参考

好处:在于加入和删除节点只影响哈希中相邻节点,对其他节点无影响,对于节点比较多的情况下,影响范围特别小。

缺点:加减节点会造成哈希环中部分数据无法命中,需要手动处理或者过略这部分数据,因此一致性哈希常用于缓存场景;当使用少量节点,节点变化将大范围影响哈希环中数据映射,因此不适合少量数据节点的分布式方案;普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。

  • 虚拟槽哈希分区

虚拟槽分区巧妙使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,槽是集群内数据管理和迁移的基本单位。目的就是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽。虚拟槽分区解耦了数据和节点之间的关系,简化了节点扩容和收缩难度,节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据,支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。

                                                5d7f623e8286cc1648913d1bb2bac68d62a.jpg

                                                    图-槽集合与节点关系

                                        a9f4adc81a39cf470e50a84dcc05c813b8b.jpg

                                               图-使用哈希函数将键映射到槽上

集群功能对比单机在功能上存在一些限制,批量操作、事务操作支持有限,不支持多数据库空间、复制结构只支持一层等。

搭建集群

三步:准备节点、节点握手(节点通过Gossip协议彼此通信,达到感知对方过程)、分配槽。也可以用Redis官方工具redis-trib.rb搭建集群。

节点通信

分布式部署需要提供维护节点元数据信息的机制(元数据是指:节点负责哪些数据,是否出现故障等状态信息)。常见的元数据维护方式:集中式和p2p方式。Redis集群采用P2P的Gossip(流言)协议。Gossip协议工作原理就是节点彼此不断通信交换信息,一般时间后所有节点都会知道集群完整的信息,类似流言传播。通信过程:集群中的每个节点会单独开启一个TCP通道,用于节点之间彼此通信,通信端口都在基础端口加10000;每个节点在周期内选择几个节点发送ping消息;收到ping消息的节点通过pong消息作为响应。

有些节点可能知道全部也有可能知道部分节点,只要这些节点彼此可以正常通信,当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发送通过不断ping/pong消息通信,经过一段时间后所有节点都会知道集群全部节点的最新状态,达到集群状态同步。

Gossip协议主要职责就是信息交换。常用的消息可分为ping、pong、meet消息、fail消息等。meet,通知新节点加入,之后在集群中进行周期性的ping、pong消息交换(用于检测节点是否在线和交换彼此状态信息)。fail消息,当节点预判集群内另一个节点下线时,会向集群内广播一个fail消息。

                                        7dc315053a786bf984835ff9ab165eec7c9.jpg

                                                        图-不同消息通信模式

节点选择:因为ping/pong消息会携带当前节点和部分其他节点的状态信息,势必加重带宽和计算负担。redis集群节点通信采用固定频率。通信节点选择过多虽然可以做到信息及时交换但是成本过高,如果过少降低交换频率影响故障判断、新节点发现等需求的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销。具体选择规则如下图所示,信息交换的成本主要体现在单位时间发送消息的节点数量和每个消息携带的数据量。具体规则参考

                                    a493566413b37b777967fb6cf28dafe2662.jpg

                                              图-选择通信节点的规则和消息携带的数据量

集群伸缩

Redis集群可以实现对节点的灵活上下线控制,其中原理可以抽象为槽和对应数据在不同节点之间的灵活移动。集群伸缩=槽和数据在节点之间的移动

扩容集群

三步骤,准备新节点、加入集群、迁移槽和数据。迁移过程是集群扩容最核心的环节。槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。槽迁移计划确定后开始逐个把槽内数据从源节点迁移到目标节点,数据迁移过程是逐个槽进行的,每个槽数据迁移的流程如下图所示。

                                    1fbc172e4b279738a8e4724ac757e610dff.jpg

                                                   图-新节点加入的槽迁移计划

                                    1a95e0b04b4a6db53f3282b16a96d54cac7.jpg

                                                图-槽和数据迁移流程

收缩集群

收缩集群意味着缩减规模,需要从现有集群中安全下线部分节点,安全下线节点流程如下图所示。具体分为下线迁移槽、忘记节点两步。迁移下线节点中的槽和数据,方向正好和扩容迁移方向相反。集群内的节点不停地通过Gossip消息彼此交互节点状态,通过cluster forget {down Nodeld}实现让集群内所有节点忘记下线的节点。

                                        5de457f47ca1cd710592c270748d59be5c2.jpg

                                                图-节点安全下线流程

请求路由

在集群模式下,Redis接受任何键相关命令时首先计算键对应的槽(根据键有效部分使用CRC16计算出散列值,再取余16383,使每个键都可以映射到0~16383槽范围内),再根据槽找出所对应的节点,如果节点是自身,则处理键命令,否则回复MOVED重定向错误,通知客户端请求正确的节点。每次执行键命令前都要到Redis上进行重定向才能找到执行命令的节点,额外增加了IO开销,这不是Redis集群集群高效的使用方式,这种客户端又叫作Dummy(傀儡)客户端,集群客户端都采用另外一种实现:Smart(智能)客户端。Smart客户端通过在内部维护slot-node的映射关系,本地就可以实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot-node映射。Jedis( Smart客户端)操作流程如下图。

                                              8042e589ebb0bf7cf0ec88a0dc9f0c41756.jpg

                                                          图-MOVED重定向执行流程

                                        16a54b3f6d93922be36444028545e8f8711.jpg

                                                        图-Jedis客户端命令执行流程

ASK重定向,Redis集群支持在线迁移槽和数据来完成水平伸缩,当cao对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。

                                                f11c6b1f9eeae3940dcb6c7b70b6a73c90f.jpg

                                                        图-slot迁移中的部分键场景

                                        131f1e65d12190e1fbcd869b0251e7db68b.jpg

                                                        图-ASK重定向流程

故障转移

Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其它状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线和客观下线。主观下线流程如下图。当某个节点判断另一个节点主观下线后,相应的节点状态会跟消息在集群内传播。通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告,当半数以上持有槽的主节点都标记某个节点是主观下线时,触发客观下线流程。

                                        86e65270a0a030b15f4ed1040ca4810fa73.jpg

                                                            图-主观下线识别流程

 

故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。

                                            63d26bee0e1081daf78e8dad5f9f03f1c63.jpg

                                                图-故障恢复流程

故障转移时间

故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。配置可以根据业务容忍度做出适当调整。

集群运维

集群完整性:默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态。建议修改 cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行。

带宽消耗:集群内Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000内。集群内所有节点通过ping/pong消息彼此交换信息, 集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗。节点间消息通信对带宽的消耗主要体现在:消息发送频率、消息数据量和节点部署的机器规模。在满足业务需要的情况下尽量避免大集群,适当提高cluster-node-timeout降低消息发送频率,如果条件允许集群尽量均匀部署在更多机器上避免集中部署。

Pub/Sub(发布/订阅)广播问题:用于针对频道实现消息的发布和订阅,在集群模式下内部实现对所有的publish命令都会向所有节点进行广播一次,加重带宽负担。当频繁应用pub/sub功能应该避免在大量节点的集群内使用,负责严重消耗集群内网络带宽。建议使用sentinel结构专门用于Pub/Sub功能,从而规避这个问题。

集群倾斜:数据倾斜,节点和槽分配严重不均 、不同槽对应键数量差异过大、集合对象包含大量元素、内存相关配置不一致;请求倾斜:(常出现在热点键场景)避免方式:合理设计键,热点集合对象做拆分或使用hmget代替hgetall避免整体读取,不要使用热键作为hash_tag,避免映射到同一槽,对于一致性要求不高的场景,可使用本地缓存减少热点调用。

集群读写分离:集群模式下的读写分离会遇到复制延迟、读取过期数据,从节点故障等问题,集群模式下读写分离成本比较高可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离。集群读写分离有时用于特殊业务场景:利用复制的最终一致性使用多个从节点做跨机房部署降低读命令网络延迟,主节点故障转移时间过长,业务端可以把读请求路由给从节点保证读操作可用。

手动故障转移:指定从节点发起转移流程,主从角色进行转换,从节点变为新的主节点对外提供服务,旧的主节点变为它从节点。

数据迁移:用于数据从单机向集群环境迁移的场景,redis-trib.rb提供导入功能,还有社区开源的迁移工具如唯品会开发的redis-migrate-tool。

开发与运维的陷阱

处理bigkey的方案

bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB。非字符串类型:体现在单个value值很大,一般认为超过10kB就是bigkey;非字符串类型:体现在元素个数过多。bigkey危害主要体现在,内存空间不均匀、超时阻塞、网络阻塞,如果bigkey是一个热点key,那么带来的危害将会放大。

如何发现bigkey?被动收集,修改Redis客户端当抛出异常时打印出所有key,方便排查bigkey问题。主动监测,scan+debug object key,结合pipeline机制或者在从节点完成,避免阻塞Redis。

如何删除bigkey?string类型用del, 其他类型用hscan命令获取部分field-value,再利用hdel删除每个field(为了快速可以使用pipeline)。Redis4.0版本支持lazy delete free模式以无阻塞方式删除bigkey。

热点key寻找与解决

极端情况下热点key会超过Redis本身能承受的OPS,寻找热点key对开发和运维人员非常重要。

可以从以下几个方面进行收集热点key:

客户端设置全局字典统计,需要考虑内存泄露问题、无侵入设计;

代理端实现,像Twemproxy、Codis等基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,代理是Redis客户端和服务端的桥梁,基本过程如下图所示。

                                            2d133eca2511b9deb8b46d801b42d3bbf98.jpg

                                                        图-基于代理的热点key统计

Redis服务端,monitor命令可以监控到Redis执行的所有命令,Facebook开源的redis-faina正是采用这样原理基于Python语言实现的,但是monitor命令在高并发条件下,会存在内存暴增和影响Redis性能的隐患,所有此种方法适合在短时间内执行且只能统计单个Redis节点。

机器抓包,Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是REST。如果站在机器的角度,可以通过对机器上所有的Redis端口的TCP数据包进行抓取完成热点key的统计。ELK体系下的packetbeat插件可以对Redis、Mysql等众多主流服务的数据包抓取、分析、报表展示,但是是以机器为单位进行统计,集群规模下需要后期汇总。

如何解决热点key?

  • 拆分复杂数据结构

如果当前key类型是二级数据结构,可以考虑将热点key拆分为若干个新的key分布到不同Redis节点上,从而减轻压力

  • 迁移热点key

以Redis Cluster为例,可以将热点key所在的slot单独迁移到一个新的Redis节点上,会增加运维成本。

  • 本地缓存

将热点key放在业务端的本地缓存中,数据更新时使用发布订阅机制解决业务端和Redis数据不一致的问题。

转载于:https://my.oschina.net/u/2939155/blog/3039395

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值