Redis开发与运维

Redis概述

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

特性

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

使用场景

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

API理解和使用

命令手册

数据结构和编码

                                   

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

单线程架构

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

为何单线程还这么快?

为何redis使用单线程模型还会达到每秒万级的处理能力,大体分为以下三点:算法

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

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

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

字符串

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

set命令选项

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

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

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

哈希

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

                                

                                                       图-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做为排序依据。

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

有序集合类型的内部编码有两种,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提供了简单的发布订阅功能,胜在足够简单,不支持消息的持久化存储(意味着新开启的客户端没法接受该频道以前的消息)和消息传输保障机制。

 

                                    

                                                     图-redis发布订阅示意图

GEO

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

深刻分析参考:占小狼的博客-Redis的3个高级数据结构

客户端

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

客户端通讯协议

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

Java客户端Jedis

Java有不少优秀的Redis客户端,其中使用较普遍的是Jedis。

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

                                    

                                                           图-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持久化方式

流程以下:

                                    

                                                  图-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持久化的主流方式。

                                                    

                                                       图-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 key一、hdel key二、srem keys、set a1十一、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持久化文件加载流程:

                                            

                                                              图-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节点,数据实现了一层一层的向下复制。当主节点须要挂载多个从节点时为了避免对主节点的性能干扰,能够采用树状主从结构下降主节点压力

                                        

                                                             图-树状主从结构

原理

                                        

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

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

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

                                        

                                                            图-主从心跳检测

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

开发与运维中注意的问题

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

读写分离

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

                                            

                                                                   图-Redis读写分离示意图

数据延迟

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

                                                

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

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

读到过时数据

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

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

                                            

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

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

                                                

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

若是此时数据大量超时,主节点采样速度跟不上过时速度且主节点没有读取过时键的操做,那么从节点将没法收到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)的数量, 或者采用树状复制结构,加入中间层从节点用来保护主节点,以下图所示:

                                               

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

单机器复制风暴

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

Redis的噩梦:阻塞

Redis是典型的单线程架构,全部的读写操做都是在一条主线程中完成的。Redis用于高并发的场景时,这条线程就变成了它的生命线。若是出现阻塞,哪怕是很短期,对于应用来讲,都是噩梦。致使阻塞问题的场景分为内在缘由和外在缘由。

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

                                    

                                       图-自定义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。频繁的更新操做、大量过时键删除将会致使高内存碎片。主要的解决办法有数据对齐、安全重启进行碎片从新整理。

                                                   

                                              图-Redis内存消耗划分        

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

内存管理

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

设置内存上限

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

                                                     

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

内存回收策略

主要体如今如下几个方面:

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

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

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

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

                                            

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

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

                                   

                                                                      图-内存溢出的控制策略

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

                                             

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

内存优化

redisObject对象

                                    

                                              图-redisObject

缩减键值对象

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

共享对象池

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

字符串优化

Redis自身实现字符串,存在预分配机制,因此尽可能减小字符串频繁修改操做(append、setrange),直接使用set,下降预分配带来的内存浪费和内存碎片化。

                                        

                                                    图-字符串结构SDS

编码优化

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

                                    

                                                        表-redis内部编码

控制键的数量

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

                                            

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

哨兵

Redis的高可用方案。

基本概念

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

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

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

                                                

                                                   图-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节点的监控。

                                           

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

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

                                        

 

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

                                       

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

主观下线和客观下线

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

                                       

                                                图-Sentinel节点主观下线检测

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

                                        

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

领导者Sentinel节点选举

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

故障转移

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

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

                                    

                                                   图-选出最好的从节点

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

高可用读写分离

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

                                

                                            图-通常的读写分离模型

                                   

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

集群

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

数据分布

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

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

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

哈希分区

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

  • 节点取余分区

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

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

  • 一致性哈希分区

具体能够参考

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

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

  • 虚拟槽哈希分区

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

                                                

                                                    图-槽集合与节点关系

                                        

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

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

搭建集群

三步:准备节点、节点握手(节点经过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消息。

                                        

                                                        图-不一样消息通讯模式

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

                                    

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

集群伸缩

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

扩容集群

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

                                    

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

                                    

                                                图-槽和数据迁移流程

收缩集群

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

                                        

                                                图-节点安全下线流程

请求路由

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

                                              

                                                          图-MOVED重定向执行流程

                                        

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

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

                                                

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

                                        

                                                        图-ASK重定向流程

故障转移

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

                                        

                                                            图-主观下线识别流程

 

故障恢复

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

                                            

                                                图-故障恢复流程

故障转移时间

故障转移时间跟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客户端和服务端的桥梁,基本过程以下图所示。

                                            

                                                        图-基于代理的热点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数据不一致的问题。

相关文章
相关标签/搜索