Getting Start
高性能
- 性能优势的体现
- C语言实现的内存管理
- epoll的I/O多路复用技术+IO连接/关闭/读写通过事件实现异步的非阻塞IO
- TCP协议
- 单线程架构,不会因为高并发对服务器造成太多压力
- Redis内部不支持序列化
- 上面几个特性保证了Redis的高并发性能
- 性能指标
- 单点并发量:压力测试
- 命令处理速度:每秒数万次操作
- key的数量对性能的影响
- 内存大小对性能的影响
- 单线程架构
- Redis提供了很多API,单线程架构不需要担心一些API对数据操作的一致性问题
- 不会因为高并发对服务器造成太多压力,从而使得Redis可以拥有更复杂更丰富的API
- Redis被设计为面向快速执行场景的缓存数据库应用,内存的读写性能本身够快,多线程带来的提升在线程切换的影响下并不是那么明显,单线程避免了线程切换的消耗
- epoll的I/O多路复用技术+IO连接/读写/关闭通过事件实现异步的非阻塞IO,也是性能的保障之一
- 综上所述,选择单线程已经够快了(每秒数万次操作),不需要使用多线程来提速,效果不明显,复杂度增加
- 单线程的弊端
- 某个耗时操作会阻塞其它命令的执行
- 所以redis使用必须注意单个操作的快速执行特点
- 要特别注意耗时API的使用,否则丰富的API反而会成为Redis的弊端
- 单线程架构的优化
- 可以通过大规模服务集群+耗时优先的负载均衡策略来减小单线程架构带来的问题
- 对服务器内存的消耗会很大,要适度使用
- 可以在同一台服务器上面部署多个实例,发挥多核CPU的优势
- 建议用户直接通过客户端访问Redis,不要通过中间服务
- 发挥Redis高并发优势, 除非中间服务能够支持高并发,并且服务资源足够
- 客户端能够发挥更多的Redis丰富的API优势,除非中间服务完全支持这些API的定制使用要求
- 如果用户为单个服务定制Redis中间服务,也是好的架构方式,这个与EhCache/Liberator等客户端服务设计理念一样
对外数据结构和内部编码实现
- 改进或添加内部实现而不影响外部使用
- 通过配置选项启用不同内部实现
使用场景
- 作为高可用二级缓存使用,与其他数据库配合
- 作为小型高性能数据库使用
特点
- 只能做分布式存储,不能做全量数据节点的负载均衡
- 集群节点通过维护其他所有节点信息来实现服务发现与故障转移
下载安装
基本操作API
启停命令
- redis-cli -v :检查版本
- redis-cli -h host -p port save : 持久化
- 指定参数运行redis
- 指定port
- src/redis-server --port 8888
- 指定持久化目录
- dir
- 默认./,可能会遇到写权限问题导致退出失败
- 指定日志文件
- logfile
- 指定配置文件: 配置文件一般又自己集中管理
- src/redis-server /path/redis.conf
- 指定参数停止redis
- redis-cli -h host -p port shutdown
- 所做的事情
- 断开客户端连接
- 生成持久化文件
- 通过kill -9起不到上述作用
- 连接,操作Redis
- 交互式
- redis-cli -h host -p port
- set key value
- get key
- 命令式
- redis-cli -h host -p port set key value
- 开启redis-cli客户端自动重定向
- redis-cli -c
Key管理命令
- rename/renamenx key newname
- renamenx 只在newname不存在的时候
- value过大可能造成堵塞
- randomkey
- exists key1
- del key1 key2
- type key1 : 查看对外数据结构
- object encoding key1 : 查看内部编码实现
过期
- expire key seconds: 设置过期时间
- pexpire key millisenconds : 毫秒后过期
- expireat key timestamp : 某个时间后过期
- pexpireat key millisenconds-timestamp : 毫秒级timestamp后过期
- ttl/pttl key1 : 查看剩余存活时间
- persist : 清除过期时间
- 字符串类型的key在set后会清除过期时间
- 二级数据结构,不支持内部value的过期时间
- setex 是原子执行
数据迁移
- 在同一个Redis的不同db间进行数据迁移
- move key db :
- 在不同Redis实例中进行数据迁移
- dump key + restore key ttl value
- dump将数据序列化
- restore反序列化, ttl为过期时间,value为序列化后的值
- migrate host port key destination-db timeout [COPY] [REPLACE] [keys]
- destination-db : 数据库索引,比如0
- copy 迁移后不删除原来数据
- replace 目标存在也会覆盖
- keys 迁移多个key,需要设置key为""
遍历
- keys pattern
- *,?
- [] : [1,4],[1-4]
- \x : 转义
- xargs
- redis-cli keys key1* | xargs redis-cli del
- 不支持*的全量?
- 不支持交互式?
- scan cursor [MATCH pattern] [COUNT count]
- cursor:游标,每次scan返回当前游标,下次从当前开始,返回0说明完成了
- COUNT : 每次的个数,默认10
- hscan->hgetall, sscan->smenbers, zscan->zrange
- scan过程中如果存在改动,会影响scan结果的完整性
数据库管理
- Redis一个实例可以存在多个独立不相关的数据库,但是建议不使用这种方式,用多个实例的方式来代替多数库
- 单线程架构使得多数据库使用单一CPU,发挥不了多核的优势
- 不同数据库的查询会相互阻塞,而且问题难以定位
- select dbIndex
- flushdb/flushall
- 清除数据库
- dbsize, 复杂度是O(1)
配置
- config set key value
- config get key
- config rewite
可能会造成阻塞的命令 : 可以在副本上面执行阻塞命令
- keys
- hgetall key
- lrange start end
- smembers key
- flushdb/flushall
5种数据结构以及API
字符串
- 特点
- 包括字符串,数字,JSON,序列化信息,二进制等
- 最大size 512MB
- 直接使用字符串
- 简单直观,能做最细粒度的操作
- key的数量庞大,内存开销大
- 信息内聚性差
- JSON或者序列化
- 只供显示用的时候性能好,最方便
- 转换过程造成开销
- 操作粒度太粗
- API
- set/setnx/setex/get
- mset/mget : 批量存取
- 自增长/自减
- incr/decr/incrby/decrby/incrbyfloat
- append key value
- strlen key
- getset 返回上一个值
- setrange key offest value : 替换指定字符
- getrange key start end : 获取部分字符
- 内部编码
- int : 8字节
- embstr : <= 39字节
- war : > 39字节
- 根据长度和值自动选择内部编码
- 使用场景
- 计数功能 : incr
- 限速功能 : 1分钟5次
- set key 1 EX 60 NX
- incr key <= 5
Hash : 传统关系型数据库的替代方案,打破关系
- 特点
- key:field:value形式,相当于两级Map
- 相比于使用key:json的形式,更加直观,操作更方便高效
- 相比于使用key-field:value的形式,key的数量更少,内存消耗小
- 相比于关系型数据库
- 非结构化的数据类型,每个key可以有不同的field,更灵活
- 关系型数据库可以做复杂的关联查询,Redis实现则很困难
- 要充分利用hash/json的优点,做出最优选择
- API
- hset key field value
- 内部编码
- ziplist
- 连续存储,从而节省内存,读写比hashtable满
- field个数<512,所有的field值小于64字节
- hashtable
- 读写复杂度为O(1)
List : 与顺序相关的存储
- 特点
- 有序
- 支持很多list操作
- API
- lpust.rpush key value
- lrang key start end
- 内部编码
- ziplist
- linkedlist
- quicklist : linkedlist 的value 是 ziplist
- 使用场景
- 简单的消息队列功能
- lpust + brpop
- 可启用多个消费者
- 分页功能
- lrang
- 按序号查找/修改/删除
Set : 集合间操作/随即选择/去重复
- 特点
- 无序
- 无重复
- API
- sadd key
- smembers
- 支持集合的交集/并集/差集,并可保存结果
- 内部编码
- intset
- hashtable
有序集合 : 按分数排序
- 特点
- 按分数排序
- API
- zadd key score member
- zrange key start end
- 支持集合的交集/并集/差集
- 支持sum/min/max, 默认sum
- 可保存结果
- 内部编码
- ziplist
- skiplist
- 使用场景
- 排行榜
特殊功能API
慢查询
- 配置
- slowlig-log-slower-than : 默认10000
- slowlog-max-len : 默认128
- API
- slowlog get/len/reset
发布订阅
- API
- publish channel message
- subscribe/unsubscribe channels
- psubscribe/punsubscribe channel-pattern
- 支持*等通配符订阅
- pubsub
- channels [pattern] : 查看有订阅的消息通道
- numsub channel : 查看订阅数量
- numpat : 查看通过psubscribe模式订阅的数量
- 特性 : 简单
- 不支持消费分区
- 不支持消费组,无法做分布式消费
- 发布的消息不做持久化
Pipeline
- 一次组装的命令个数要适量
- 只能操作一个Redis实例
GEO
- geoadd key longitude latitude member
- geopos key member
- GEODIST key member1 member2 [unit]
- 相同key的地理位置的距离
- unit : m/km/mi/ft
- GEORADIUS/GEORADIUSBYMEMBER key member radius [unit]
- 以member 或者经纬度为中心获取指定半径内的member
- geohash
- zrem key member
- 删除
事务与Lua
- 简单的事务
- multi/exec/discard
- 不提供运行时错误的回滚,只支持命令语法错误检查后不执行
- watch key : 在multi之前执行,保证被改动的情况下事务不执行key相关操作
- Lua
- 通过Lua脚本语言批量执行Redis API
Bitmaps
HyperLogLog
Redis Shell
Redis监控API
客户端管理
- CLIENT SETNAME name
- CLIENT KILL host:port
- CLIENT PAUSE 10000
- 阻塞毫秒数
- 不阻塞主从复制,所以可以用于让主从复制更上节奏,从而保持一致
- client list
- 获取客户端信息,包括 id,name,db
- age(存活时间),idle(最近一次空闲时间)
- age与idle相近书面client一直空闲但是没有关闭连接
- qbuf/qbuf-free
- 输入缓冲区的大小与free
- obl/ollomem
- 输出缓冲区的 obl(固定缓冲区的字节长度
- oll(动态缓冲区的对象数)
- omem(输出缓冲区使用的大小)
- flag :
- 客户端类型 N(Nomel)/M(Master)/S(Slave)等等
- sub/psub
- 订阅的消息通道数量
- cmd
- 最后一次执行的命令
info信息查询
- info 统计所有信息
- info clients
- client连接数/输出输入缓冲区最大值/阻塞的client数
- info stats
- latest_fork_usec : 最近一个fork操作的耗时
- info Persistence 持久化信息
- rdb_last_save_time : 最后一次生成RDB的时间,等同于lastsave API
- info replication : 主从复制相关信息
monitor
- 用于监控其他所有客户端的操作
- 会因为输出缓冲过大的内存问题
2种持久化
RDB
- bgsave
- 主进程fork一个新的子进程来save数据
- 不会阻塞Redis,阻塞只发生在fock阶段,fock操作一般耗时在每GB 20毫秒左右,建议每个Redis实例控制在10GB
- 触发gbsave
- 配置save m n
- shutdown
- 主从全量复制
- debug reload重新加载redis
- 优点
- 压缩的二进制文件,对文件做替换(AOF是做追加),适合全量复制
- Load数据速度比AOF快很多
- 缺点
- fork操作消耗比较大,无法做到秒级别的实时持久化
- RDB配置
- 触发bgsave配置
- save 900 1 : 900秒内有一次数据更新触发bgsave
- save 300 10
- save 60 10000
- dir :持久化文件目录
- dbfilename : 持久化文件名字
- rdbcompression yes
- 是否开启LZF算法对持久化文件进行压缩
- 如果想减轻CPU压力,可以设置为no
- rdbchecksum yes
- save和load数据时是否对文件进行校验
- 会有10%左右的性能消耗,设置no开启最高性能
AOF
- 开启AOF
- appendonly yes : 开启AOF持久化方式
- appendfilename "appendonly.aof" : AOF文件
- aof-load-truncated yes : 重启加载时兼容结尾不完整的AOF文件
- 写入命令
- 以独立日志的方式记录每次命令,通过重新执行所有命令来load数据
- 以Redis协议格式写入文本,避免二次处理开销
- 命令写入AOF缓冲,提高性能
- 文件同步
- write() : 将命令写入AOF缓冲aof_buf,然后返回,系统决定什么时候写入硬盘,系统故障会造成数据丢失
- fsync() : 阻塞,做强制的硬盘写入,和fsync/write由操作系统提供支持
- appendfsync
- always : 每次都调用fsync(),最安全,性能最低,一般不推荐
- no : 每次只调用write(),不安全,但性能最高,同步周期可长达30秒左右
- everysec : 每次只调用write(),每秒钟调用一次fsync(),组合安全性和性能,默认配置
- everysec虽然不是像always那样每次都阻塞主线程,但也不像no那样每次都直接返回
- 他会开启另一个线程进行fsync(),Redis并非完全单线程
- 主线程会分析fsync线程,如果距离上次成功小于2秒,直接返回,否则阻塞
- 所有everysec最多丢失2秒数据,而不是1秒
- 重写机制
- 触发重写
- 手动调用bgrewriteaof命令
- 自动触发重写
- auto-aof-rewrite-min-size 64mb : AOF文件(aof_current_size)大于64M才会触发
- auto-aof-rewrite-percentage 100 : (aof_current_size - aof_base_size(上一次重写后的size)) / aof_base_size > 100%
- 重写过程
- bgrewriteaof,主进程fork子进程进行重新,fork消耗于bgsave一样
- 重写操作原理
- 过滤超时的数据
- 使用内存中的数据重新写入AOF文件,多余的旧命令就不存在了
- 合并操作,为防止客户端缓冲区溢出,以64个元素为界进行合并
- aof-rewrite-incremental-fsync yes : AOF重写每次批量写入32M数据到AOF文件,防止磁盘阻塞
- 主进程依然响应client请求,原来的AOF流程依然进行,并保存在旧的AOF文件里
- no-appendfsync-on-rewrite yes :在rewrite过程中。不会调用fsync(),以保证fsync不会被阻塞,但是可能会丢失数据
- aof_buf的数据也会保存在到aof_rewrite_buf(AOF重写缓冲区)
- 在子进程完工后主进程将aof_rewrite_buf写入文件,使用新AOF文件替换老文件
- 如果重写失败,旧文件还存在,只要no-appendfsync-on-rewrite no,那就不会有数据丢失
- 重启加载
- 优先load AOF文件,如果文件不存在,load RDB文件进行数据恢复
- aof-load-truncated yes : 兼容结尾不完整的AOF文件
- 文件校验与修理
- redis-check-aof --fix xxx.aof
- diff -u
- 手动添加丢失数据
内存与持久化的key管理
- 惰性删除+定时删除
- 维护每个键精准的过期删除会消耗大量CPU,惰性删除采用客户端读取键时才分析过期信息进行删除
- 定时删除防止长久不访问的的键也能被删除
- 慢速模式默认每秒运行10次,每次检查20个key,如果超过25%的过期,循环执行,直到不超过25%或者25毫秒超时
- 超时后Redis每次事件都会执行快速模式,采用1毫秒超时的,并且2秒只能运行1次
- 内存溢出策略
- noeviction : 默认策略,不删除数据,只响应读操作,拒绝写操作并报错
- volatile-lru/allkeys-lru : 用LRU算法(空闲时间)删除带有超时设置的key/所有key,直到腾出足够的空间,如果没有,回退到noeviction
- volatile-random/allkeys-random: 随机删除带有超时设置的key/所有key,直到腾出足够的空间
- volatile-ttl : 更具剩余存活时间ttl删除key,没有回退到noeviction的策略
持久化是性能瓶颈
- fork操作耗时与内存大小有关系,建议控制在10G,要尽量减少fork
- fock的子进程的性能影响,主要是对服务器本身的消耗,对Redis主进程的影响可能不大
- Redis是CPU密集型服务,避免与计算密集服务公用,避免邦定单核
- Linux的写时复制功能帮助Redis子进程节约一半内存消耗
- Redis的瓶颈可能会在硬盘IO方面,避免与存储服务,消息队列一同部署,部署多个实例时可以做分盘,减轻磁盘IO
- AOF的always绝对要慎用,即使everysec也是高并发下的Redis住线程阻塞元凶,磁盘IO能力不行啊
- 可以通过iotop工具定位
主从复制
相关配置
- slaveof
- 是配置也是动态API
- slave no one : 断开主从连接,从slave变成master,数据不丢失
- 切换master, 会删除之前的数据,重新从新master复制数据
- masterauth
- 如果master配置了requirepass ,需要验证密码
- slave-read-only yes
- 从节点因该不允许修改,因为主节点无法获得这些修改
- repl-disable-tcp-nodelay no
- 默认no,开启实时传输,任何命令都会实时发送到从节点,适合同机房等网络条件好的情况
- yes,开启延时传输,会合并一定时间内的tcp数据包,减少网络带宽压力,延时时间根据Linux内核,一般40毫秒
- 主从部署一般不同机器但因尽量相同区域
- repl-ping-slave-period 10
- 主节点ping从节点的心跳频率
- repl-timeout 60
- 主从复制的超时
- slave-serve-stale-data yes
- 从节清除数据有加载RDB文件的过程中,是否响应client
- 默认yes,可能造成拿不到数据或者数据与其他节点不一致的情况
- 如果为no,client的请求会返回SYNC with master in progress
三种主从拓扑
- 一主一丛
- 适合主节点写并发量比较大的情况
- 主节点使用RBD,保证主节点性能,从节点使用AOF,保证数据的安全性
- 如果主节点没有持久化,或者RBD,要避免使用自动重启,会造成从节点因为复制主节点丢失数据,正确的做法是主从调换
- 一主多从
- 可用于但主节点读压力,做读写分离
- 可以将阻塞命令在其中一台上进行
- 不适合写并发高的情况
- 树状结构
- 对一主多从结构的优化,降低主节点的复制压力
复制原理
- 复制偏移量
- 主从节点都会维护自己当前的总数据长度,就是偏移量,记录在info replication
- master_repl_offset/slave_repl_pffset
- slave每秒报告自己的offset给主节点
- slave0:....offset=....
- 复制积压缓冲区
- 主节点的命令不但会发送给从节点,还会放入缓冲区
- 默认大小1M的先进先出的队列
- 用于部分复制的命令丢失补救措施
- 运行ID
- Redis每次启动默认运行ID都会改变,保存在info server里的run_id
- 运行ID改变后,会做主从全量复制,以保证对持久化配置/文件的修改不会造成复制的数据遗失
- 用debug reload重启Redis,run_id不变,从而避免全量复制
- 全量复制
- sync/psync
- psync ? -1 : 进行全量复制,返回+FULLRESYNC
- slave保存主节点run_id和offset
- master执行bgsave,保存RDB文件(所以即使Master市AOF,也会存在RDB文件)
- master发送RDB文件给slave
- 在千兆网卡下,带宽最大100M/s,默认60s超时时间可以传输6GB的RDB文件
- 传输RDB文件的过程中master仍然工作,接受的命令会存放在slave对应的client输出缓冲区
- 默认设置client-output-buffer-limit slave 256mb 64mb 60,60s超出64m或者总量超出256m立即关闭连接
- slave接受RDB文件后,清空数据,开始加载RDB文件
- slave响应client请求见slave-serve-stale-data
- 如果slave开启了AOF,会立即调用bgrewriteaof来生成AOF文件
- 全量复制6GB数据耗时大约2分钟
- master生成RDB时间
- 传输RDB时间
- slave清空数据时间
- slave加载RDB时间
- AOF时间
- 部分复制
- 建立连接后,从节点更具自己的保存的run_id和offset发送psync run_id offset请求
- 主节点查看run_id和offset,对比复制缓冲区
- 如果run_id一致,复制缓冲区有这些数据,返回+CONTINUE进行部分复制
- 否则进行全量复制
- 异步复制
- 在建议复制连接后,master会异步的发送受到的命令给slave
- 异步导致slave会存在数据延迟,延迟受repl-disable-tcp-nodelay影响
- 通过计算主从偏移量的差值可以知道当前延迟的字节数,一般延迟在1s内,也可以看出是否存在网络延迟或命令阻塞等情况
- 维护心跳
- master默认每10秒对ping slave,判断连接状态,如果断开超过repl-timeout,则断开复制连接
- repl-ping-slave-period 10
- 从节点默认每秒发送replconf ack offset,上报自己的offset
- 通过对比offset检查数据是否丢失,如果丢失,发送psync做部分复制
- 主节点通过replconf判断从节点状态与阈值min-slaves-to-write/min-slaves-max-lag
- 比如slave个数小于3个或者slave延迟大于10s,则master不响应write请求
可能造成全量复制的情况
- Redis每次启动默认run_ID都会改变,然后就会产生全量复制
- 重新选举master后,run_id改变造成全量复制
- 主从复制的复制积压缓冲区太小,容易产生全量复制
- psync ? -1手动全量复制
Redis Sentinel 哨兵
实现原理
- 三个定时任务
- 每10s向master/slave发送info命令,用于获取master和slave的拓扑以及变化
- 每2s向master的频道_sentinel_:hello发布和订阅对master的判断
- 用于发现其他sentinel信息
- 用于交换对节点的判断
- 每1s对master/slave/sentinel发送ping命令,来判断是否存活
- 主观下线/客观下线
- 如果sentinel对master的ping命令在down-after-milliseconds之后没ping通,会认为master主观下线
- sentinel在认为master下线的时候会通过发送命令相互交换对master的状态:sentinel is-master-down-by-addr ip port current_epoch runid
- current_epoch : 当前配置纪元
- runid : 如果是*,代表交换意见,如果是当前sentinel的run_id,代表申请成为sentinel leader
- sentinel收到>=quorum个数sentinel的主观下线之后,就能确认master已经客观下线
- sentinel申请leader
- 确认master客观下线后,sentinel会向其他sentinel申请成为leader进行故障转移
- 当得到超过max(quorum, num/2+1)票后成为leader
- leader选举很快,基本上谁先完成客观下线判断,谁就是leader
- 故障转移,选举slave的过程
- 健康度过滤:5秒内没有ping通的, 与master失联超过down-after-milliseconds*10的时间的会被过滤
- slave-priority里最高的会被选出,如果没有,才会继续下一步骤
- 可以通过slave-priority参数来自定义slave的优先级
- 选择offset最大的slave,如果没有选出,才会继续下一步骤
- 选择runid最小的
配置详解
- sentinel monitor master-name host port quorum
- sentinel从master获取master,slave和其他sentinel信息进行监控
- quorum代表哨兵决策者的数量
- 需要几个哨兵确定发生故障,建议设置成sentinels/2+1
- 需要几个哨兵进行选举,去quorum和sentinels/2+1中比较大的
- sentinel down-after-milliseconds master-9961 30000
- sentinel发送ping给redis节点和其他sentinel节点
- 超过多少时间没有ping通,说明节点发生故障
- 超时时间越小,哨兵工作越及时,但是误判率越高
- sentinel parallel-syncs master-name 1
- 重新选举master后,slave会向新的master发起复制请求,这时候run_id改变造成全量复制
- 设置一次有多少个slave同时进行复制,如果设置成1,所以slave会轮训进行复制
- 越大对master的消耗越大
- sentinel failover-timeout master-name 180000
- 故障转移任何阶段的操作超时,故障转移失败
- 失败后再次启动的时间是超时的2倍
- sentinel auth-pass master-name admin
部署技巧
- 部署在不同的物理机,实现更好的高可用
- 部署至少3个奇数个节点
- 相同业务线的redis用一套sentinel,不同的业务线用多套
API
- sentinel set master-nam key value
- 修改配置文件, 会立即更新配置文件
- 建议所以sentinel配置保持一致,便于故障转移达成一致
- sentinel master/slaves/sentinels master-name
- sentinel ckquorum master-name
- 检查取当前sentinel总数是否达到quorum的个数
- sentinel remove master-name
- sentinel monitor master-name host port quorum
- 取消监控/重新监控
- sentinel failover master-name
- 强行故障转移
- 与自动故障转移一样,都会改变配置文件
- 恢复后会让之前的master变成slave,也会改变配置文件
- sentinel flushconfig
- 用当前运行配置重置配置文件
Sentinel发布订阅频道 : 可以通过psubscribe *订阅全部
- +reset-master -- 当master被重置时.
- +slave -- 当检测到一个slave并添加进slave列表时.
- +failover-state-reconf-slaves -- Failover状态变为reconf-slaves状态时
- +failover-detected -- 当failover发生时
- +slave-reconf-sent -- sentinel发送SLAVEOF命令把它重新配置时
- +slave-reconf-inprog -- slave被重新配置为另外一个master的slave,但数据复制还未发生时。
- +slave-reconf-done -- slave被重新配置为另外一个master的slave并且数据复制已经与master同步时。
- -dup-sentinel -- 删除指定master上的冗余sentinel时 (当一个sentinel重新启动时,可能会发生这个事件).
- +sentinel -- 当master增加了一个sentinel时。
- +sdown -- 进入SDOWN状态时;
- -sdown -- 离开SDOWN状态时。
- +odown -- 进入ODOWN状态时。
- -odown -- 离开ODOWN状态时。
- +new-epoch -- 当前配置版本被更新时。
- +try-failover -- 达到failover条件,正等待其他sentinel的选举。
- +elected-leader -- 被选举为去执行failover的时候。
- +failover-state-select-slave -- 开始要选择一个slave当选新master时。
- no-good-slave -- 没有合适的slave来担当新master
- selected-slave -- 找到了一个适合的slave来担当新master
- failover-state-send-slaveof-noone -- 当把选择为新master的slave的身份进行切换的时候。
- failover-end-for-timeout -- failover由于超时而失败时。
- failover-end -- failover成功完成时。
- switch-master -- 当master的地址发生变化时。通常这是客户端最感兴趣的消息了。
高可用的读写分离
- 设计思路是通过Sentinel发布订阅频道来获取从节点的状态变化
- 需要自己实现从节点的资源池子维护和切换,好傻,sentinel就不能cover这部分工作么
集群
数据分区
- 分布式存储算法
- 节点取余分区
- 用hash(key)%N的余数算出分区顺序
- 优点是数据分布均衡,算法简单,但是节点数N变化时需要重新计算和分区,大规模数据迁移
- 一般使用较大的提前量以便适应数据的增长,并且采用翻倍扩容,防止分区增加过渡频繁
- 一致性哈希分区
- hash(key)>=节点的token,则分布在此节点
- 增删节点只影响相邻的节点,如果做数据迁移,那么只需要迁移两个节点
- 如果不迁移可能造成部分数据无法命中,适合不需要100%命中的缓存策略
- 如果节点少,影响命中的比例就大,所以不迁移的方案只适合节点数多的情况
- 如果要保证数据的均衡,则token的范围与hash要对应,并且token要均衡分布
- 增删节点也需要*2或/2来保证token能均衡分布,但是这样又造成了50%命中的问题或者大规模迁移数据问题
- 要么数据迁移,要么忽略数据命中,要么大规模迁移或不命中,要么忽略数据均衡分布,鱼与熊掌不能兼得
- 虚拟槽分区 : Redis采用的方法
- Redis槽的范围是0-16383,所有槽平均分配给节点
- CRC16(key,keylen)计算出散列值
- 如果key包含{hash_tag},则使用hash_tag计算散列值,否则用完整key计算
- 散列值对16383取余,使得所有key都能映射到槽
- hash_tag的作用
- 使得不同key能够具有相同槽,从而分配到一个节点
- 给集群下的批处理/Pipeline操作提供可能性
- 用于优化Redis IO
- 虚拟槽方便了数据迁移,支持节点,槽和key的映射查询
- 数据分区带来的限制
- mset/mget等批量操作只支持key在同区的情况
- 事务操作也只支持key在同区的情况
- 只支持一个数据库
- 不支持树状结构
搭建集群
- 启动所有节点
- cluster-enabled yes
- 启动后会自动生成并维护集群配置文件, cluster-config-flie node-port.conf, 里面有每个节点的nodeId
- 目录结构建议: conf,data,log
- 手动搭建集群
- 节点握手 : 在某个节点执行命令 cluster meet host port, 来连接所有节点
- 槽分配 : cluster addslots {0...5461},将所有槽均分给所有master
- 从节点 : cluster replicate nodeId
- redis-trib.rb搭建集群
- 安装ruby
- 安装rubygem redis依赖
- 安装redis-trib.rb
- redis-trib.rb create --replicas 1 host:port host:port host:port host:port host:port host:port
- trib会尽可能保证主从节点不分配在同一机器下
- 集群验证与查询
- cluster nodes : 查看节点信息
- cluster info
- cluster slots:查看槽分布情况
- 所有节点会分享信息,任何节点都能查询所有槽的分布情况
- redis-trib.rb check host:port
- redis-trib.rb info host:port
集群通讯
- 消息类型
- meet : 通知接受方节点加入当前集群
- ping : 集群内的每个节点每秒向其他节点发送ping,用于请求获取其他节点的状态信息
- pong : 受到meet/ping后返回pong,内部封装了节点的状态数据,除了请求响应模式,也可以广播pong消息给其他节点
- fail : 当节点判断某个节点异常后,会广播fail消息通知其他节点
- 所有消息都包含head/body
- head包含发送方节点的自身信息:id,槽,主从角色,是否下线等
- body包含发送方节点发送的部分其他节点的信息
- 通讯成本
- 节点每秒随机选择5个节点,选出未通讯时间最长的进行ping通讯,保证Gossip的随机性
- 消息的发送频率
- 节点每100毫秒(每秒10次)扫描本地列表,满足条件就会发送ping消息
- node.pong_received > cluster_node_timeout/2
- 所以cluster_node_timeout越大,通讯频率越低,默然15s
- 太大会影响故障转移,槽信息更新,新节点加入
- 消息数据量
- head固定大小约2kb
- body的大小和节点数量成正比
集群伸缩
- 使用redis-trib.rb进行集群扩容
- redis-trib.rb add-node host:port cluster_host:port
- 内部使用cluster meet实现添加master
- 内部会执行新节点状态检查,看是否已经是另一个集群的成员,或者已经包含数据,如果是则放弃集群操作报错
- 手动cluster meet如果已经是另一个集群的成员,那么将原来集群合并到现在的集群,造成数据丢失和错乱,避免使用
- redis-trib.rb reshard host:port --from --to --slots --yes --timeout --pipeline
- 手动流程解释
- cluster setslot 4096 importing from_id : 让目标节点准备导入某个节点的4096个槽
- cluster nodes : 确认目标节点开启准备导入
- cluster setsolt 4096 migrating to_id : 让源节点准备导出4096个槽到to_id节点
- cluster nodes : 确认源节点开启准备导出
- cluster getkeysinslot 1 100 : 获取100个源节点槽1的key
- mget key1 key2...key100 : 确认keys属于这个节点
- migrate将所有keys进行迁移
- 使用mget/cluster nodes进行验证,太他妈烦了
- 参数解释
- host:port:这个是必传参数,用来从一个节点获取整个集群信息,相当于获取集群信息的入口。
- --from :需要从哪些源节点上迁移slot,可从多个源节点完成迁移,以逗号隔开,传递的是节点的node id
- --from all 这样源节点就是集群的所有节点,不传递该参数的话,则会在迁移过程中提示用户输入。
- --to :slot需要迁移的目的节点的node id,目的节点只能填写一个,不传递该参数的话,则会在迁移过程中提示用户输入。
- --slots :需要迁移的slot数量,不传递该参数的话,则会在迁移过程中提示用户输入。
- --yes:设置该参数,可以在打印执行reshard计划的时候,提示用户输入yes确认后再执行reshard。
- --timeout :设置migrate命令的超时时间。
- --pipeline :定义cluster getkeysinslot命令一次取出的key数量,不传的话使用默认值为10。
- redis-trib.rb rebalance host:port
- 检查数据分布均衡性,如果不均衡会平衡集群节点slot数量
- redis-trib.rb add-node host:port cluster_host:port --slave --master-id id
- 内部使用cluster replicaate添加slave
- redis-trib.rb check/info
- 检查集群
- 使用redis-trib.rb进行集群收缩
- redis-trib.rb reshard进行槽和数据的迁移
- redis-trib.rb del-node host:port downNodeId
- cluster forget忘记节点,有效期60s
- shutdown节点,如果有从节点,会将从节点指向其他主节点
- 如果要把从节点下线,需要先下线从节点,以避免全量复制带来的开销
故障转移
- 与Sentinel不同,Cluster的故障转移由节点自己的相互通讯机制完成
- 整个故障转移策略和Sentinel类似,包括主观下线/客观下线/选举slave
- 主观下线:
- cluster-note-timeout时间内某个节点没有ping通,则当前节点认为此节点主观下线
- 节点维护了一个有时限的下线报告列表,过期时间是cluster-note-timeout*2
- 客观下线
- 当节点发现下线列表的主观下线数量大于槽节点(Master)总数的一半时,就会进行客观下线广播
- 广播会通知所有master和salve,触发故障转移
- 注意事项
- cluster-note-timeout太小会造成客观下线判断跟不上下线报告过期时间
- 不同区域的小集群可能在一定时间无法收到其他区域的大集群的下线报告,如果大集群的slave在小集群,就无法完成转移,所以尽量master/slave在同区域
- 故障恢复,选举slave的过程
- 与master断线时间超过cluster-note-timeout*cluster-slave-validity-factor(默认也是10)的没有资格
- offset最大的slave优先,通过不同的延迟竞选政策,比如offset大的延迟最小(1s)
- 竞选slave更新配置纪元 configEpoch
- master configEpoch都不同,slave复制master
- 每次集群有重大事件,都回更新配置纪元,configEpoch越大说明节点维护了最新的事件
- 集群出现slots等关键信息不一致,以configEpoch大的为准
- slave通过广播竞选来保证一个配置纪元内只能竞选一次
- master进行投票,当N/2+1票后完成选举
- master不要在同一台物理机,否则服务器故障可能造成不够票
- 故障转移的灵敏度
- failover-time <= cluster-note-timeout + cluster-note-timeout/2 +1
- cluster-note-timeout应该根据对故障转移的容忍度设置,默认15s
集群运维 : 见10.7
Java客户端 Jedis
缓冲区
- 输入缓冲区
- 用于保存客户端发送的命令
- 缓冲区大小动态调整,不可以通过参数设定
- 每个客户端最大1G,超过会断开连接
- 不受maxmemory限制,从而可能会超出限制
- 输入缓冲区出现异常的情况比较小
- 输出缓冲区
- 用于保存需要返回给客户端的数据
- 可以通过client-output-buffer-limit设置大小
- 分为normal普通客户/slave副本/pubsub订阅客户
- hard limit : 超出限制立即关闭连接
- soft limit/soft seconds : 超出一定时间关闭
- 分为固定缓冲区(存放小的结果)和动态缓冲区(存放大的结果)
- 不受maxmemory限制,从而可能会超出限制
- 输出缓冲区出现异常的情况会比较大,可能由于redis的处理能力比IO能力强大造成的
- 如果master节点输入大,Slave的输出压力会比较大,应该适度增大
- 发现内存抖动频繁,很有可能是输出缓冲区过大造成
Jedis对象
- Maven Dependency
- redis.clients : jedis
- Jedis对API的使用与Redis Shell基本一致
- jedis.close() : 关闭或者归还连接
- Client
- jedis.getClient()
- setSoTimeout(soTimeout) : 设置读写超时
- setConnectionTimeout(connectionTimeout) : 设置连接超时
Pipeline对象
- jedis.pipelined();
- sync/syncAndReturnAll
- Pipeline对API的使用与Redis Shell基本一致
Jedis连接池 JedisPool / GenericObjectPoolConfig
- 连接池连接数量
- maxTotal : 最大连接数,默认8
- maxIdle : 最大空闲连接数, 默认8个,超出的会被移出
- minIdle : 最小空闲连接数, 默认0个,保持一定的最小空闲对性能可能有积极影响
- 空闲检查
- timeBetweenEvictionRunsMillis : 空闲检测时间间隔(毫秒), 默认-1不开启检测
- numTestsPerEvictionRun :一次检查几个连接的空闲状态, 默认3
- minEvictableIdleTimeMillis : 空闲的最小时间,超出的空闲连接将被移出,默认30分钟
- 有效性检查
- testOnBorrow : 在获取连接的时候检查有效性, 无效会被移出,默认false
- testOnReturn : 向连接池归还时检查有效性, 无效会被移出,默认false
- testWhileIdle : 在获取连接的时候做空闲检测, 超时会被移出,默认false
- 连接等待
- blockWhenExhausted : 是否等待连接池,默认true
- maxWaitMillis : 等待连接池资源的时间,默认-1,表示不超时,blockWhenExhausted=true才生效
- jxmEnabled : 开启JMX,用于监控连接池的使用情况,默认true
Jedis使用Sentinel
- JedisSentinelPool实现原理
- sentinels.add("host:port");将所有节点设置在sentinels集合里Set
- 遍历sentinel的host:port节点集合,选择第一个可用sentinel
- 通过sentinel get-master-addr-by-name master-name获取master节点
- 为每个sentinel节点启动一个线程,用于订阅sentinel节点的故障转移频道+switch-master
- pool.getResource();会返回故障转移完成后新的master节点
- 故障转移期间的操作会跑出异常,需要try/cache
Jedis使用Claster
- JedisCluster使用细节
- HostAndPort可以是全部,也可以是部分,因为Jedis通过cluster slots发现所有槽
- JedisCluster内部包含了所有节点的JedisPool,所以应该是单例
- JedisCluster不需要关系连接池的获取和归还,由内部实现,所以它的close()方法不是归还,而是销毁所有JedisPool
- 多节点操作
- ...
- JedisCluster方法
- get/set
- getClusterNodes : 返回JedisPool的Map
- Jedis j = jedisPool.getValue().getResource() : 获取到的可能是master也可能是slave节点
- JedisClusterCRC16.getSlot(hashtag)
- JedisCluster实现原理
- Dummy客户端
- 客户端可以随机连接任意节点请求Cache数据,如果数据的槽不在当前节点,就会返回Moved信息
- 客户端通过Moved信息进行重定向访问正确的节点请求Cache数据
- JedisCluster避免了这种带来额外IO开销的傀儡客户端,而使用了Smart客户端
- Smart客户端
- JedisCluster初始化时发送cluster slots从任一节点获取所有槽的分布情况
- 槽映射信息存储在JedisClusterInfoCache对象中,实现任意客户端都维护了所有槽的映射关系
- 这样就有效的避免了Redis Moved重定向问题带来的IO消耗,偶尔的重定向会触发cluster slots更新客户端维护的槽映射关系
- JedisCluster请求数据
- 计算key的sort并根据槽映射关系找到对应节点发送请求
- 如果连接出错(指抛出JedisConnectionException),返回第一步重试,attempts-1, 默认5
- 如果attempts=1或者任意一次重试捕获到Moved重定向(MovedDataException),使用renewSlotCache方法发送cluster slots更新槽映射缓存
- 更新槽映射缓存会加锁
- 同时只会有一个线程进行cluster slots更新槽映射缓存,减少并发调用和锁等待
- 然后继续1,2,3知道正确返回或者redirections<=0抛出异常
- Ask重定向
- 槽迁移期间发送请求不会得到Moved重定向,而是Ask重定向
- 客户端会发送Ask请求打开新节点的连接,来获取数据,Ask连接是一次性的,每次都要这个过程
- Ask重定向不会更新槽映射关系,所以知道迁移完成,都可能发生Ask
- 源节点和目标节点维护槽信息来支持客户端Ask
- 不支持批量操作的Ask重定向
- 不支持Pipeline的自动Ask,但是可以通过手动分析Ask返回进行手动Ask重定向
- 集群对性能的消耗
- 大规模集群
- 由于Jedis为每个节点创建一个独立的JedisPool,会维护非常多的连接
- 每个客户端维护的槽与节点的映射带来内存消耗
- 客户端发送cluster slots时带来更多的数据传输
- 异常与重试机制
- 抛出JedisConnectionException的情况
- Jedis与节点发生socket错误
- Read Time Out
- JedisPool获取对象超时抛出JedisException,不会引起重试
- 可以看出节点故障转移期间,所有请求都回抛出JedisConnectionException异常
- 每5次重试会有一次cluster slots操作,并加锁,会阻塞所有请求,造成请求积压
- 高并发
- 当高并发遇到异常重试,会造成大规模请求积压在客户端
- 集群规模大也会使这个问题更加凸显
- 所以高并发不仅要做好Redis集群数量的平衡,也要做要客户端集群和负载均衡
Jedis使用Lua脚本
Redis配置 redis.conf
- 启动相关配置
- port 9961
- bind 127.0.0.1 : 注释bind,将redis公开到网络
- bind将redis邦定到指定网卡
- daemonize yes
- logfile "/opt/citimkts/rates/redis/logs/9961/redis.log"
- dir "/opt/citimkts/rates/redis/dir/9961"
- 安全配置
- protected-mode no:
- no : 非保护模式建议设置密码
- yes : 如果没有设置密码和bind,默认只能通过回环地址(127.0.0.1)访问,也就是本机
- requirepass pwd
- 内存配置
- maxmemory byte : 设置数据使用的最大内存
- maxmemory-policy : 内存溢出策略
- 慢查询配置
- slowlog-log-slower-than 2000
- slowlog-max-len 128
- client相关配置
- maxclients : 最大client数量,默认10000
- timeout : client空闲多久关闭连接,默认0,表示不限制
- tcp-keepalive : tcp连接有效性检测周期,默认300
- tcp-backlog : tcp连接握手后放入队列的大小,连接成功后会从队列移出
性能分析与优化
阻塞
- 客户端输出缓冲区出现问题的几率比较大,发现内存抖动频繁,很有可能是输出缓冲区过大造成
- 集群规模
- 当高并发遇到异常重试,会造成大规模请求积压在客户端
- 集群规模大也会使这个问题更加凸显
- 所以高并发不仅要做好Redis集群数量的平衡,也要做要客户端集群和负载均衡
- 慢查询
- Redis的问题可能会出在客户端使用不当上面,造成阻塞, slowlog发现慢查询
- 见可能会造成阻塞的命令
- 避免大对象 : --bigkeys
- 持久化
- Redis的瓶颈可能会在硬盘IO方面,避免与存储服务,消息队列一同部署,部署多个实例时可以做分盘,减轻磁盘IO
- AOF追加阻塞 :
- AOF的always绝对要慎用,即使everysec也是高并发下的Redis住线程阻塞元凶,磁盘IO能力不行啊
- info persistence的aof_delayed_fsync是所有AOF追加阻塞的时间
- fock阻塞:
- fock的耗时与数据量相关
- info stats察看latest_fork_usec,超过1s就得注意
- HugePage写操作阻塞
- CPU饱和
- Redis是CPU密集型服务,避免与计算密集服务公用
- 如果没有开启持久化,建议邦定CPU减少CPU切换,但是如果开启了持久化,fork的子进程消耗90%以上CPU,影响主进程,所有不能邦定
- --stat察看OPS,如果在5W+基本就会饱和了,top,sar等察看CPU
- 单机多实例的部署方式可以发挥多核CPU的优势,弥补单线程的不足
- 内存交换
- cat /proc/皮带/samps | grep Swap
- 如果内存交换过大,会使得Redis被磁盘速度拖累
- 防止
- maxmemory设置最大可用内存,并保证服务器内存充足
- 降低系统swap优先级: echo 10 > /proc/sys/vm/swappiness
- 网络问题
- 连接拒绝
- 网络闪段:sar -n DEV查看历史流量情况/Ganglia等监控工具
- Redis连接拒绝,maxclient 10000 : info Stats | grep rejected_connections
- 连接溢出:
- 操作系统文件句柄个数,ulimit -n,默认1024
- 操作系统backlog,默认128: echo 511 > /proc/sys/net/core/somaxconn
- tcp-backlog,默认511
- 网络延迟
- 同物理机>同机架>跨机架>同机房>同城机房>异地机房
- 带宽瓶颈:机器网卡带宽,机架交换机带宽,机房专线带宽
- redis-cli -h host -p port : --latency/--latency-history/--latency-dist
- 网卡软中断
内存优化
- info memory
- used_memory_human: 数据占用内存
- used_memory_rss_human: 进程占用内存
- used_memory_peak_human: 数据占用内存的峰值
- mem_fragmentation_ratio:
- used_memory_rss/used_memory : 表示内存碎片率
- 如果小于1,说明出现内存交换Swap
- 如果大于1比较多,说明碎片率严重
- 缓冲区
- 输入缓冲区和AOF缓冲区一般不会有问题
- 输出缓冲区
- client-output-buffer-limit设置大小和策略
- 当客户端数量过大,网络传输有问题的时候,可能造成输出缓冲区过大,默认不限制大小
- 当从节点数量过大,网络传输有问题的时候,可能造成输出缓冲区溢出,默认256mb
- 当订阅客户端的消费能力差的时候,可能造成输出缓冲区溢出,默认32mb
- 复制积压缓冲区
- 所有从节点公用一个缓冲区
- 默认1M太小了,可以设置100MB,减少全量复制的发生
- 内存碎片
- 频繁的对字符串的修改操作,过期健的删除会提高碎片率
- 可以通过定时安全重启做内存重新分配
- 更加高成本的优化是做数据对齐
- 子进程内存消耗
- 子进程内存消耗一般比父进程小,实际消耗根据写入命令量决定
- sysctl vm.overcommit_memory=1允许内核分配所有内存
- 建议关闭THP
- 内存溢出消耗
- 建议Redis一直工作在maxmemory>used_memory的状态下(也就是不太适合作为缓存?),避免内存回收带来的开销
- 设置过期会带来一定的额外CPU开销
- value对象长度
- 可以采用更高效的序列化工具,比如protostuff,kryo
- 可以对json等进行压缩,比如GZIP算法,或者更快速的Snappy
- 开启整数共享对象池
- 字符串优化
- 频繁的对字符串的修改操作不但提高碎片率,还增加字符串的预分配大小,要避免大量append,setrange等操作,用set代替
- 用hash代替json能减少内存开销
- 内部编码优化 : 见8.3.5
- intset尽量做数据对齐,防止个别大整数使得整个set触发升级操作
- hash+ziplist优化:见8.3.6
Linux优化
- vm.overcommit_memory
- 0 : 表示如果有足够内存,申请内存通过,否则申请失败
- 1 : 表示允许超量申请,因为申请后并不会马上使用,直到用完内存
- 2 : 决不过量
- Redis建议设置1,保证fork操作能在低内存下也能执行成功
- swappiness
- 1-100,数字越大,用swap的概率越高
- 分布式redis,宁愿死掉也不要swap
- THP
- Redis建议关闭THP功能
- OOM killer
- 当服务器内存不足时,优先杀死oom_adj大的进程
- 分布式redis,宁愿死掉
- ulimit
- 客户端与Redis的tcp连接都是文件句柄,建议open files和maxclients吻合
- tcp-backlog
- Redis和服务器的设置建议吻合
热点key优化
如何用好系列
策略选择
- 持久化策略 :数据安全性考虑
- RDB
- 全量数据存储,服务故障可能会有几分钟的数据没有持久化
- 对性能影响小,数据加载快速,支持文件压缩
- AOF
- 更安全的持久化策略,最低级别也就30s左右的数据丢失
- everysec 数据最多丢失2s,安全性也性能的综合使用方法,推荐
- 一般涉及到客户操作的非再生数据都要用AOF,RDB会有BUG的
- no-appendfsync-on-rewrite:
- yes : 数据不安全,不阻塞
- no : 数据安全,阻塞
- 可以主节点使用RDB,从节点使用AOF
- 主从复制策略 :数据安全性考虑
- repl-disable-tcp-nodelay
- 数据安全性高的使用默认no
- 否则yes进行合并40毫秒左右的tcp数据包降低网络带宽压力,增加吞吐量
- 故障转移的灵敏度
- sentinel down-after-milliseconds master-9961
- 超时时间越小,哨兵工作越及时,但是误判率越高,默认30s
- cluster-note-timeout
- 应该根据对故障转移的容忍度设置,默认15s
- failover-time <= cluster-note-timeout + cluster-note-timeout/2 +1
- 当Redis作为数据库
- 数据安全性
- 持久化文件安全性
- 主从复制安全性
- 会造成持久化文件重写的情况
运维数据
- RDB持久化建议每个Redis实例控制在10GB
- sentinel 部署至少3个奇数个节点
- 从节点一般不要多于2个
- 复制积压缓冲区设置为100MB,减少全量复制
- 正常的碎片率在1.03左右
- 相同业务线的redis用一套sentinel,不同的业务线用多套
- slowlog-log-slower-than
- 默认10000,注意是10毫秒,建议设置成1毫秒左右,以便发现慢查询,保证OPS至少1000
- 值的长度如果能控制在39个字节内,可以减少内存分配次数
- ziplist建议长度不超过1000,每个元素大小控制在512字节
- 1M以上就可以认为是bigkey
数据丢失
- 能够造成持久化文件丢失的情况
- flushdb/flushall清楚缓存后,产生了RDB bgsave/save覆盖持久化文件,AOF重写覆盖持久化文件
- AOF重写失败
- 从节点复制了有问题的主节点
- 措施
- 使用预定义脚本恢复flushdb/flushall后的AOF文件
- 定期备份数据
- 使用复杂密码(64位)/使用非root权限启动:防止暴力破解进行攻击