笔记:Redis开发运维实战

Redis的特性

速度快、持久化、多种数据结构、支持多种编程语言、功能丰富、简单、主从复制、高可用和分布式

速度快

官方给出的数据是10W OPS(每秒10W次读写)

Redis的数据是存储在内存中的、使用C语言编写、使用单线程模型

持久化

会异步将内存数据同步到磁盘中,有RDB、AOF以及混合模式等三种模式。

memcache不支持持久化

数据结构

redis是以key-value形式存储的,支持多种数据结构。而memcache只支持string一种数据结构

image-20200606135637786

支持多种客户端语言

比如 Go、Python、Java等

功能丰富

除了用做缓存外,还提供了如:发布订阅、支持Lua脚本、简单的事务、pipeline

简单

初始版本约23000行代码,后续迭代功能后约50000行代码

不依赖外部库

单线程模型

主从复制

可以在主服务器与从服务器之间复制同步代码

高可用和分布式

image-20200606141041287

Redis典型应用场景

redis应用场景有:缓存系统、计数器、消息队列系统、排行榜、社交网络、实时系统

Redis初识

常用配置

image-20200606152232148

Redis API的使用和理解

通用命令

  • 添加元素 SET key value (SETNX key value key不存在时才进行设置)

  • 遍历所有key KEYS *(时间复杂度为O(n)一般不在生产环境中使用,替代方案:SCAN或热备从节点)

  • 查看数据库key的数目 DBSIZE

  • 判断一个key是否存在 EXISTS key

  • 查询一个Key的值 GET key

  • 删除一个key DEL key

  • 给key设置n秒后过期 EXPIRE key n

  • 查询key的过期时间 TTL key (返回值大于0表示还有多少秒过期,-1表示没有设置过期时间,-2表示已过期或数据库中不存在)

  • 取消key的过期时间 PERSIST key

  • 查看key的类型 TYPE key (如果key不存在则返回的类型为none)

image-20200606165841739

数据结构和内部编码

image-20200606170058428

单线程架构

我们在使用开发语言编程程序的时候通常会使用多线程来提升并发效率,为什么redis使用单线程还这么快?原因是redis是基于内存的,响应非常快,避免线程切换和竞态消耗;具有类似epoll的非阻塞IO机制

redis一次只运行一条命令,拒绝长(慢)命令

string

字符串常用命令总结

image-20200606202447266

hash(如Python之dict,Go之map)

image-20200606204241390

List 列表(时间上是一个双端队列)

Set 集合

向集合中添加元素 SADD,向集合中删除元素SREM

交集SINTER,差集SDIFF,并集SUNION

image-20200606211614013

SCAED 集合中的元素个数

SISMEMBER 判断元素是否存在集合中

SRANDMEMBER 从集合中随机取出n个元素

SPOP 从集合中随机剔除n个元素

SMEMBERS 从集合取出所有元素

有序集合

image-20200606212002726
image-20200606213132556

Redis客户端

Go客户端 redigo

瑞士军刀Redis

慢查询

生命周期

慢查询发生在执行命令阶段,客户端超时不一定是慢查询造成的,慢查询只是客户端超时的一个因素

两个配置

  1. slowlog-max-len: 慢查询会进入一个固定长度的队列(FIFO),也是保存在内存中的

  2. slowlog-log-slower-than:设置慢查询阈值(单位:微秒),其值为0时将记录所有命令(所有命令都会被认为是慢查询),其值小于0时将不记录任何命令

image-20200607003256536

三个命令

image-20200607003439293

运维经验

image-20200607003710582

pipeline(流水线)

什么是流水线

对命令进行打包,批量提交到服务端处理

image-20200607004211185

注意:redis命令执行时间是微秒级别的,网络传输时间消耗相对较大

pipeline客户端

pipeline与原生的M操作对比

原生M操作是原子的

pipeline是非原子的,会在命令队列中拆分成多个pipeline子命令,但返回结果是有序的

使用建议

image-20200607005255556

发布订阅

角色

发布者、订阅者以及通道(channel)

重要的API

发布消息(publish)、订阅(subscribe)、取消订阅(unsubscribes)

发布订阅和消息队列的区别

对于发布订阅模式:发布者发布的消息,订阅者都可以收到

对于消息队列模式:发布者(生产者)发布的消息,只有一个订阅者(消费者)可以收到

bitmap(bitset)

HyperLogLog

基于HyperLogLog算法,使用极小空间完成独立数量统计,本质还是字符串

一般用于统计独立用户数,有约0.81的错误率,不支持去除单个数据

image-20200607200034699

GEO(地理信息定位)

存储经纬度,计算两地距离,范围计算等

3.2版本开始支持,是基于zset实现的

没有删除元素的API

127.0.0.1:6379> GEOADD mygeo 116.28 39.55 beijing
(integer) 1
127.0.0.1:6379> GEOADD mygeo 116.28 39.55 beijing
(integer) 0
127.0.0.1:6379> GEOADD mygeo 117.12 39.08 tianjin
(integer) 1
127.0.0.1:6379> GEOPOS mygeo beijing
1) 1) "116.28000229597091675"
   2) "39.5500007245470826"
127.0.0.1:6379> GEODIST mygeo beijing tianjin km
"89.2061"

Redis持久化

什么是持久化

redis的数据是存在内存当中的,持久化就是 异步将内存中的数据落到磁盘上

持久化的方式

image-20200607231840891

RDB

启动方式

image-20200607232629559

AOF

开发运维常见问题

fork操作

fork操作是一个同步操作(会阻塞)

与内存量息息相关(内存越大,耗时越长)

查看上一次执行fork的耗时,info: latest_fork_usec

如何改善fork

优先使用物理机或者高效支持fork操作的虚拟化技术

控制Redis实例最大可用内存:maxmemory

合理配置Linux内存分配策略:vm.overcommit_memory=1

降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制

进程外开销

CPU

开销:RDB和AOF文件生成,属于CPU密集型

优化:不做CPU绑定,不和CPU密集型部署

内存

开销:父子进程会共用内存,但是在父进程有大量写入操作时,子进程会拷贝一份内存副本(copy-on-write)

优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled

硬盘

开销:AOF和RDB文件写入,可以结合iostat,iotop分析

优化:

  1. 不要和高硬盘负载服务器部署在一起:比如存储服务、消息队列等

  2. no-appendfsync-on-rewrite = yes

  3. 根据写入量决定磁盘类型

  4. 在单机多实例持久化文件目录可以考虑分盘

AOF追加阻塞

image-20200613133637770

单机多实例部署

不要进行大量的bgsave操作

Redis复制的原理和优化

单机有什么问题

机器故障:可用性问题

瓶颈容量:扩容时内存中数据的同步问题

QPS瓶颈:redis单机号称10W QPS,如果项目中需要支持100W QPS如何去做

什么是主从复制

从节点备份主节点数据

一个主节点可以有多个slave

一个slave只能有一个master

数据流向必须是单向的,从master到slave

作用:

  1. 数据备份

  2. 扩展读性能

复制的配置

通过slaveof命令设置,如果需要取消作为其他节点的从节点(执行slaveof on one)

通过修改配置文件设置:

  1. 添加 slaveof ip port

  2. slave-read-only yes (设置为只读)

如果一个redis节点被新指定为另一个节点的从节点,则这个节点当前的数据将被清除

尽管我们禁用持久化,但是主从复制还是会启用持久化

run_id以及偏移量

run_id是在一个redis服务启动时随机分配的唯一ID

偏移量master_repl_offset标识当前服务中数据的大小,是校验主从复制是否成功的依据

全部复制和部分复制

全量复制

image-20200613143940208

全量复制的开销

  1. bgsave的开销

  2. RDB文件网络传输时间

  3. 从节点清空数据时间

  4. 从节点加载RDB的时间

  5. 可能的AOF重写时间

部分复制

image-20200613144350504

最终能够达到全量复制的功能,又能减少全量复制带来的开销

故障处理

故障不可避免,要做自动故障转移

主从结构故障转移

slave宕掉:将指向该slave的连接均摊到其他slave上

master宕掉:从其slave节点中选举一个节点上升为master节点

主从复制故障转移问题

  1. 读写分离

    读流量分摊到从节点(互联网业务大部分都是读多写少的情况)

    但是存在复制数据延迟(比如发生阻塞),可能出现读写不一致的情况,可以对主从节点之间的偏移量进行监控,会有一定成本

    可能读到过期数据(在3.2版本已解决),原因是redis3.2版本以前的从节点没有删除数据的权限,因此在数据量很大时,主节点的数据过期了而从节点存在同步延迟而导致出现在从节点中读到过期数据

  2. 主从配置不一致

    maxmemory不一致:当主节点配置大于从节点时,会出现数据丢失

    设置一些数据结构优化参数时不一致(比如hash-max-ziplist-entries):会造成内存不一致

  3. 规避全量复制

    第一次全量复制是不可避免的,减少全量复制带来的危害可以在流量低峰时进行处理(如夜间),其次可以控制主节点的maxmemory。

    节点的run_id不一致:在主节点重启后run_id发生变化,从节点(认为此时备份的数据时不安全的)就会进行一次全量复制(更新当前数据),可以尝试将从节点晋升为主节点来规避这种情况

    复制积压缓冲区不足时:网络中断,部分复制无法满足就会造成积压,可以通过配置rel_backlog_size增大复制缓冲区

  4. 规避复制风暴

    当主节点复制风暴:主节点重启,多从节点复制时可能会造成复制风暴,可以更换复制拓扑以减少复制压力

    image-20200613152213900

    需要注意:当更换拓扑结构后, 假如slave1节点出现故障该如何处理??

    单机器复制风暴:多个主节点在同一台机器上,该机器宕机后重启,出现大量的全量复制

    image-20200613152542885

    应该尽量将主节点分散到多个机器上,或者选举其中一个从节点晋升为主节点

redis sentinel

主重复制高可用存在的问题

手动故障转移

写能力和存储能力受到限制

sentinel架构

实现了故障发现、通知以及自动转移

安装与配置

img

三个任务

主观下线与客观下线

集群节点数一般配置为奇数(N)个,下线阈值配置为N/2

领导者选举

只有一个sentinel节点完成故障转移

通过命令sentinel is-master-down-by-addr命令表达自己希望成为领导者(拉票)

  1. 每个主观下线的sentinel节点向其他sentinel节点发送命令,要求将它设置为领导者

  2. 收到命令的sentinel节点如果没有同意过其他sentinel节点发送的命令请求,则会同意该节点请求,否则拒绝

  3. 如果该sentinel节点发现自己的票数已经超过sentinel集合半数且超过quorum,那么它将成为领导者

  4. 如果此过程有多个sentinel节点成为了领导者,那么将等待一段时间重新进行选举

raft选举算法

故障转移(sentinel领导者节点完成)

  1. 从slave节点中选出一个“合适的”节点作为新的master节点

  2. 对上面的slave节点执行slave of one命令让其成为master节点

  3. 向剩余的slave节点发送命令,让他们成为新master节点的slave节点,复制规则和parallel-syncs参数有关

  4. 更新对原来的master节点配置为slave,并保持着对其“关注”,当其恢复后命令它去复制新的master节点

选择“合适的”slave节点:选择slave-priority(slave节点优先级)最高的slave节点,如果存在则返回,不存在则继续;选择复制偏移量最大的slave节点(复制得最完整的),如果存在则返回,不存在则继续;选择runID最小的slave节点。

Redis Sentinel运维

节点运维

节点(主节点、从节点和sentinel节点)的上线与下线

下线原因

机器下线:比如过保等情况

机器性能不足:比如CPU、内存、硬盘、网络等

节点本身的故障:比如服务不稳定等

下线节点

下线主节点,使用命令sentinel failover <masterName>下线并手动执行故障转移

从节点、Sentinel节点下线:临时下线还是永久下线

上线节点

主节点:sentinel failover进行替换

从节点:slaveof; sentinel节点可以感知

sentinel节点:参考其他sentinel节点启动即可

高可用读写分离

从节点:是副本,高可用的基础;扩展读能力

client感知的三条消息:1. 从节点升级为主节点;2.主节点降级为从节点;3.节点发送主观下线

redis Sentinel 回顾

sentinel是redis的高可用的实现方案:故障发现,故障自动转移,配置中心以及客户端通知

sentinel从redis2.8版本开始才正式生产可用

尽可能在不同的物理机上部署sentinel所有节点(可以尽量部署在一个网络中)

sentinel的节点数量应该大于等于3,且最好为奇数(便于选举)

sentinel中的数据节点和普通数据节点没有区别

客户端初始化时连接的是sentinel节点集合,不再是具体的redis节点,但sentinel只是配置中心(只在第一次启动时通过sentinel获取信息)而不是代理

sentinel通过三个定时任务实现了sentinel节点对于主节点、从节点、其余sentinel节点的监控

sentinel在对节点做失败判定时分为主观下线和客观下线

sentinel故障转移日志对于排查问题非常有帮助

sentinel实现读写分离高可用依赖sentinel节点的消息通知,获取redis数据节点的状态变化

redis cluster(集群)

在集群中只有0号DB

呼唤集群

并发量

官方给出redis单机性能为10W QPS

当业务需要100W QPS时如何解决

数据量

机器内存16~256G

当业务需要500G时如何解决

网络

如果单机配置为千兆网卡而业务需要万兆如何解决

数据分布

当数据过于庞大时,单机不能对全量数据进行存储,则通过一定的分区策略将全量数据分成不同的子集分配到不同的节点上

顺序分区

哈希分区

  1. 节点取余分区

    在增加节点时,需要进行rehash,需要降低迁移量可以通过多倍扩容方式完成

    优点:简单(哈希+取余)

    缺点:节点伸缩会导致大量数据迁移

  2. 一致性Hash分区(哈希环)

    image-20200628221113992

    在节点1、2之间加入节点5只会使原来节点1、2之间在节点5之前的数据进行迁移,其他节点数据保持不变,大大减少了数据迁移量

    客户端分片:哈希+顺时针

    节点伸缩:只影响邻近节点,但是还会有数据迁移

    翻倍伸缩:保证最小迁移数据和负载均衡

  3. 虚拟槽分区

    Redis Cluster选择的分区方式

    预设虚拟槽:每个槽映射一个数据子集,一般比节点数大

    良好的哈希函数:例如CRC16

    服务端管理节点、槽、数据:例如Redi Clusterimage-20200628221929249

搭建集群

集群:集群中每个节点(主节点)都负责读和写,彼此之间了解节点所负责的槽

meet

通信方式,用于交换信息,主要是交换彼此之间所负责的槽信息

指派槽

每个节点只有被指派了负责的槽才会工作

复制

对于集群中的每个节点也都配有从节点

特点

复制、高可用、分片

在生产环节中一般不使用Redis原生命令安装,步骤繁琐,在集群节点较多的时候产生错误的概率越大

Redis官方有提供安装工具

自研的可视化安装方式

集群伸缩

集群伸缩原理

对节点的上下线,是槽和数据在节点只之间的移动

扩容集群

  1. 准备新节点

    集群模式;配置和其他节点统一;启动后是孤儿节点

  2. 加入集群

    使用meet命令将节点加入到集群中

    使用cluster nodes确认是否加入成功

    作用:为它迁移槽和数据实现扩容;作为从节点负责故障转移

    加入集群操作建议命令redis-trib.rb add-node`可以避免新节点已经加入其他集群(检测该节点是否为孤儿节点)而造成故障

  3. 迁移槽和数据

    1. 制定槽迁移计划

    2. 迁移数据

      image-20200719145017495

    3. 添加从节点

缩容集群

image-20200719145645196
  1. 下线迁移槽

  2. 忘记节点

  3. 关闭节点

客户端路由

moved重定向:槽已经确定迁移

ask重定向:槽在迁移过程中

smart重定向

集群中的批量操作方案

对集群进行批量操作(mget、mset)必须在一个槽内,通常有以下几种方案实现批量操作。

  1. 串行mget:在程序中通过循环遍历(n次网络时间)key依次与redis服务器通信进行查找

    image-20200719220354923

  2. 串行IO:在客户端本地进行内聚(比如在本地通过crc16计算槽位置,按照槽进行分组),然后依次批量查询(node次网络时间)

    image-20200719220709085

  3. 并行IO:在客户端本地进行内聚(比如在本地通过crc16计算槽位置,按照槽进行分组),然后并行(比如通过多线程执行)进行批量查询(node次网络请求,1次网络等待时间)

    image-20200719220916007

  4. hash_tag:对于每个key都有一个前缀(或者其他公共部分)作为tag进行hash计算,这样这些key就会被分配到同一个节点上,从而达到支持批量查询的功能

image-20200719215613941

集群原理(重点)

Redis Cluster中的故障转移相关问题

  1. 故障发现:基于节点之间的ping/pong消息实现(可以传播主从状态),不需要sentinel。也分为主观下线和客观下线。

    主观下线:某个节点认为另一个节点不可用

    客观下线:当半数以上持有槽的主节点都标记某节点主观下线

  2. 故障恢复

    • 资格检查:对故障节点的从节点进行资格审查(如果从节点和故障节点断线时间超过一定时长则取消选择资格,默认是150秒)

    • 准备选举时间:是针对使偏移量最大的从节点成为主节点的优先级更高(选举是在一定时间段内完成的,对于偏移量较大的准备选举的时间越短,计算偏移量等信息需要的时间越长)

    • 选举投票:对多个参与选举的从节点进行投票,选举出主节点(准备选举时间越短的,获得投票的机率越大,因为它会优先去询问其他节点,而节点投票逻辑是将票投给第一个询问的)

    • 替换主节点:当前从节点取消复制变为主节点(slaveof no one);执行clusterDelSlot撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽分配给自己;向集群广播自己的pong消息,表明已经替换了故障节点

  3. 故障转移演练

    • 模拟宕机:比如kill命令杀死节点

    • 观察客户端故障恢复时间

    • 观察各个节点的日志

Redis Cluster开发运维中常见问题

集群完整性

默认打开完整性检查,在实际业务场景中一般将此配置关闭

集群完整性:集群中的16384个槽空间全部可用

如果节点故障或者正在故障转移时对集群进行操作则会报错:(error) CLUSTERDOWN THE cluster is down。对于大多数业务是无法容忍的,因为其中一个节点发生故障就会导致整个集群不可用,因此建议配置cluster-require-full-coverage=no

带宽消耗

redis官方建议redis cluster中节点数不要超过1000个

节点之间通信会进行数据交换,带宽消耗不可避免

带宽消耗主要有三个方面:

  1. 消息发送频率:节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息

  2. 消息数据量:slots槽数组(2KB空间)和整个集群1/10的状态数据(10个节点状态数据约1KB)

  3. 节点部署的机器规模:集群分布的机器越多且每台机器划分的节点数约均匀,则集群内整体的可用宽带越高

优化点注意

  1. 避免使用“大”集群:避免多业务使用一个集群,大业务可以多集群

  2. 配置cluster-node-timeout:带宽和故障转移速度的均衡

  3. 尽量均匀分配到多机器上:保证高可用和带宽

pub/sub广播

在集群中对一个节点publish(发布消息),publish会在集群每个节点广播,会增大带宽开销;如果需要pub/sub功能,请单独使用一套redis sentinel

集群倾斜

集群倾斜产生的问题:

  1. 数据倾斜:内存不均

  2. 请求倾斜:某个节点出现了热点key

数据倾斜可能原因:

  1. 节点和槽分配不均匀

  2. 不同槽对应键值数量差异较大

  3. 包含bigkey(比如一个非常大的Hash或者非常大的ziplist)

  4. 内存相关配置不一致

不同槽对应键值数量差异较大可能存在hash_tag

定期检查配置的一致性

请求倾斜可能原因:

热点key:重要的key或者bigkey。

优化:避免bigkey;热键不要用hash_tag;当一致性不高时,可以用本地缓存+MQ

读写分离

集群模式下不建议使用读写分离(读写分离存在复制延迟、读取过期数据、从节点故障等问题)

数据迁移

官方迁移工具:redis-trib.rb

  1. 只能从单机迁移到集群

  2. 不支持在线迁移

  3. 不支持断点续传

  4. 单线程迁移

在线迁移工具:大致是通过一个slave节点作为中转

唯品会redid-migrate-tool

豌豆荚redis-port

集群和单机的对比

集群限制:

  1. key批量操作支持有限:例如mget、mset必须在一个slot

  2. key事务和lua支持有限:操作的key必须在一个节点

  3. key是数据分区的最小粒度:不支持bigkey分区

  4. 不支持多个数据库:集群模式下只有一个DB 0

  5. 复制只支持一层:不支持树形复制结构

分布式Redis不一定好

  1. redis cluster:满足容量和可扩展性,很多业务场景可能没有这么大的需求

  2. 很多场景redis sentinel已经足够好了

缓存的使用与设计

缓存的受益与成本

受益

  1. 加速读写

  2. 降低后端负载

成本

  1. 数据不一致:缓存层和数据层有时间窗口不一致(总是存在的,可以通过更新策略根据可容忍范围进行调整),和更新策略有关

  2. 代码维护成本:多了一层缓存逻辑

  3. 运维成本:需要维护缓存系统

应用场景

  1. 降低后端负载

  2. 加速请求响应

  3. 大量写合并为批量写

缓存更新策略

  1. LRU/LFU/FIFO等策略:在达到最大内存时通过算法策略进行数据淘汰

  2. 超时剔除:设置过期时间等

  3. 主动更新:开发控制生命周期

建议:

  1. 低一致性:最大内存和淘汰策略

  2. 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底

缓存粒度控制

三个角度:

  1. 通用性:全量属性更好

  2. 占用空间:部分属性更好

  3. 代码维护:表面上全量属性更好,实际开发中通常只需要缓存使用到的字段信息

缓存穿透优化

缓存穿透:大量请求不命中(未命中缓存,请求将打到存储层)

产生原因:

  1. 业务代码逻辑问题

  2. 恶意攻击、爬虫等

如何发现:

  1. 业务的相应时间

  2. 监控相关指标:总调用数、缓存层命中数、存储层命中数

解决方法:

缓存空对象:缓存查询存储层失败的请求(这种方法需要更多的空间,我们应当设置过期时间;缓存层和存储层数据“短期不一致”)

布隆过滤器:会有极小的误判,可以设置理想误差率(要求误差率越低,需要的空间越大)

无底洞问题优化

无底洞问题描述:“加”机器性能没能提升反而下降

问题关键点:1. 更多的机器 != 更高的性能;2. 批量接口查询次数(及网络通信)会随着节点数增加而增加;3. 数据增长产生水平扩展需求

几种优化方法:

  1. 优化查询命令(如减少使用慢查询keys、hgetall bigkey)

  2. 减少网络通信次数

  3. 降低接入成本(客户端长连接/连接池、NIO等)

缓存雪崩优化

热点key重建优化

热点key在重建时间较长的情况下,会有多个线程进行重建工作

  1. 减少重建缓存的次数

  2. 数据尽可能一致

  3. 减少潜在危险

解决方案:

  1. 互斥锁(需要等待,思路简单,保持数据一致性;代码复杂度增加,存在死锁的风险)

  2. 设置用不过期(基本杜绝热点key重建问题;不保持数据一致性,逻辑过期时间增加维护成本和内存成本)

最后更新于

这有帮助吗?