【中间件】Redis 实战之主从复制、高可用、分布式

时间:2022-06-17 07:25:57

目录

  • 简介
  • 持久化
  • 主从复制
  • 高可用 Redis-Sentinel
    • .NET Core开发
  • 分布式 Redis-Cluster
  • 配置说明
  • 常见问题

简介

本节内容基于 CentOS 7.4.1708,Redis 3.2.12 环境实验。

Redis 是一个开源的高性能键值对数据库。

安装:yum install -y redis

特性:

  • 高性能 Key-Value 服务器
  • 多种数据结构
  • 丰富功能
    • 缓存(get|set)
    • 计数器(incre)
    • 消息队列(publish|subcrib)
  • 高可用(v2.8 redis-sentinel)
  • 分布式(v3.0 redis-cluster)

可执行文件:

  • redis-server:服务端
  • redis-cli:客户端
  • redis-benchmark:性能测试工具
  • redis-check-aof:aof修复工具
  • redis-check-dump:rdb修复工具
  • redis-sentinel:sentinel服务端

启动方式:

  • 最简启动:默认配置直接启动redis-server
  • 动态参数启动:命令行指定配置启动redis-server
  • 配置文件启动(推荐):指定配置文件启动redis-server

启动验证:

  • ps -ef|grep redis
  • redis-cli -h locahost -p 6379 ping

由于 redis 是单线程的,推荐在一台多核CPU机器上部署多个 redis 实例充分发挥。

持久化

redis 持久化支持2种:

  1. RDB:快照方式,相当于 MySQL 中的 dump
  2. AOF:写日志方式,相当于 MySQL 中的 binlog,推荐使用

注意:

  1. 当同时开启 RDB 和 AOF 的时候,redis启动的时候会读取 AOF 还原数据。
  2. 推荐:关闭 RDB 持久化机制,开启 AOF

RDB

RDB是什么:

  1. RDB方式的持久化是通过快照(snapshortting)完成的,当符合一定条件时Redis会自动将内存中所有数据完整的压缩存储到硬盘上。
  2. RDB开启条件由2个参数 时间 和 改动次数构成。如:save 900 1
  3. RDB文件由2个参数 dir 和 dbfilename 分别指定目录 和 文件名
  4. RDB方式是Redis默认的持久化方式。

触发命令:

  1. save 命令(阻塞)
  2. bgsave 命令(fork过程阻塞)

主要触发方式:

  1. 自动触发规则(内部调用bgsave,不推荐开启)
  2. 全量复制(内部调用bgsave)

过程:

  1. 执行 save 或 bgsave 命令
  2. 生成新的 rdb 文件,如:temp-36985.rdb
  3. 覆盖 rdb 文件,如:dump-6379.rdb

优点:

  1. 启动速度快
  2. 占用空间小

缺点:

  1. 容易丢失数据
  2. 时间复杂度O(n)

关闭RDB方式:

redis-cli config set save ""

注意:

RDB并不能真正的关闭,在主从复制时主从都会生成RDB文件

AOF

AOF是什么:

  1. AOF是纯文本文件,会记录 Redis 的每次改动命令(不记录查询)。
  2. AOF开启条件:appendonly yes
  3. AOF文件由2个参数 dir 和 appendfilename 分别指定目录 和 文件名
  4. AOF方式 默认情况下Redis并没有开启。

由于每次改动都会记录,产生2个问题:

  1. 每次改动都写入硬盘,普通硬盘只能承受几百次qps。通过写入策略来调整
  2. 对同1个key执行几次操作就记录几次,冗余量特别大。通过 aof 文件重写来调整

AOF文件有3种写入策略:

  1. always(每次写入都会fsync同步到硬盘)
  2. everysec(默认,1s写1次)
  3. no(并非不写,交给系统控制预计30s写1次)

AOF重写:

  1. 重写方式

    1. 手动执行 bgrewriteaof 命令
    2. 自动触发规则(通过指定最小aof文件和aof增长率来自动内部调用 bgrewriteaof)
  2. 过程

    1. fork 出子进程
    2. 子进程执行 bgrewriteaof 命令
    3. 父进程将新接收的命令,同时写到 aof 文件和 aof_rewrite_buffer文件中。(在 aof 重写时,可配置关闭aof写入)
    4. 子进程将 aof_rewrite_buffer 文件追加到新 aof 文件中。
    5. 覆盖旧的 aof 文件

注意:

  1. 采用 everysec 方式,最多可能丢失 2s 的数据。

主从复制

为什么需要主从复制:

通过持久化保证 Redis 在服务器重启的情况下数据也不会丢失。但数据在一台服务器上,如果服务器的硬盘坏了,也会导致数据丢失。为了避免单点故障,Redis 提供了主从复制高可用方案。

主从复制结构:

  1. 1个 master 可以有多个 slave
  2. 1个 slave 只能有1个 master
  3. 数据流向单向 master -> slave

开启复制:

  1. 命令:--slaveof ip port
  2. 配置:slaveof ip port(默认配置都是master)

关闭复制:

slaveof no one

复制类型:

  1. 全量复制(首次 或者 网络断开时间比较长)
  2. 部分复制(在网络抖动一定范围的情况下,v2.8以上可配置复制缓存区repl-backlog-size)

全量复制过程:

  1. slave 节点 发起 psync runid offset:psync ? -1
  2. master 节点 返回 fullresync runid offset
  3. master 节点 bgsave 保存当前数据到 rdb
  4. master 节点 在此期间接收到新的数据存储到 buffer 中
  5. master 节点 send RDB、send buffer
  6. slave 节点 flush old data
  7. slave 节点 load RDB、load buffer

在master重启(master 的run_id更新)和slave重启(slave 的run_id丢失)时都会发生全量复制,通过 info server 可以查看run_id。

部分复制过程:

  1. slave 节点 发起 psync runid offset
  2. master 节点 确认 runid 和 offset没问题后,发送增量数据
  3. slave 节点 接收同步数据。

当全量复制完成 或 网络抖动一定范围 时,master 相当于 slave 的 client 进行增量更新数据。

Redis Sentinel

Redis-Sentinel是什么?

  1. Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案
  2. Redis-Sentinel本身也是一个独立运行的进程,它能监控多个 master-slave 集群,发现 master宕机 后能进行自动故障转移。

sentinel工作原理:

  1. 准备多个Redis Sentinel节点(建议至少3个节点,避免单点故障)
  2. 多个 Sentinel 节点发现并确认 master 主观下线
  3. 超过 quorum 个 sentinel 判定确认 客观下线
  4. 选出 1个 sentinel 节点作为领导
  5. 选出 1个 slave 节点作为 master
  6. 切换 slave 节点的 master 为新的 master
  7. 通知 client 主从变化
  8. 等待故障的 master 复活成为新的 slave
  9. Client 不直接连接 Redis 节点,应该连接 Sentinel 节点获取 Redis Info

2种下线判定:

  1. sdown(subjectively down,主观下线):每个 sentinel 判定 redis 节点下线。
  2. odown(objectively down,客观下线):超过 quorum 个 sentinel 判定 redis 节点下线。

启动方式:

  1. redis-sentinel /path/to/sentinel.conf
  2. redis-server /path/to/sentinel.conf --sentinel

三个定时任务:

  1. 每个 Sentinel 节点每秒通过 redis 的 __sentinel__:hello 发布一条消息,宣布自己的存在。同时也订阅来确定其他的 Sentinel 节点。
  2. 每个 Sentinel 节点每秒对其他 redis 节点执行 ping。确定是否下线。
  3. 每个 Sentinel 节点每10秒 对 master 和 slave 执行 info,确定 slaves。

配置模拟:

  1. 配置 Redis 开启主从复制
  2. 配置 Sentinel 监控主节点
echo "停止当前所有redis-server + redis-sentinel";
ps -x | grep redis | grep -v grep | awk '{print $1}' | xargs -r kill
echo "生成并启动3个 redis 配置";
for port in 6379 6380 6381 ;do
echo -e "daemonize yes\nport $port\npidfile /var/run/redis-$port.pid\nlogfile /var/log/redis/redis-$port.log\n" > /etc/redis/redis-$port.conf
if [ $port != 6379 ];then
echo "slaveof 127.0.0.1 6379" >> /etc/redis/redis-$port.conf
fi
redis-server /etc/redis/redis-$port.conf
done
echo "生成并启动3个 redis-sentinel 配置";
for port in 26379 26380 26381 ;do
echo -e "daemonize yes\nport $port\ndir /tmp\nsentinel monitor mymaster 127.0.0.1 6379 2\nsentinel down-after-milliseconds mymaster 3000\nsentinel parallel-syncs mymaster 1\nsentinel failover-timeout mymaster 60000\nlogfile /var/log/redis/sentinel-$port.log\n" > /etc/redis/redis-sentinel-$port.conf
redis-sentinel /etc/redis/redis-sentinel-$port.conf
done
echo "结束";

常用的channel:

  • +switch-master:切换主节点
  • +convert-to-slave:切换从节点
  • +sdown:主观下线

.NET Core环境开发:

dotnet add package StackExchange.Redis

var options = new ConfigurationOptions()
{
CommandMap = CommandMap.Sentinel,
EndPoints = { { "192.168.0.51", 26379}, {"192.168.0.51", 26381}, {"192.168.0.51", 26380} },
AllowAdmin = true,
TieBreaker = "",
ServiceName = "mymaster",
SyncTimeout = 5000
}; var sentinelConn = ConnectionMultiplexer.Connect(options);
var master = sentinelConn.GetServer("192.168.0.51",26381).SentinelGetMasterAddressByName("mymaster");
// ...
var conn = ConnectionMultiplexer.Connect(master);
sentinelConn.GetSubscriber().Subscribe("+switch-master", (channel, message) =>
{
// mymaster 192.168.0.51 6380 192.168.0.51 6381
Console.WriteLine((string)message);
// ...
conn = ConnectionMultiplexer.Connect(ip);
conn.GetDatabase().StringSet("hello","故障切换后值");
});
sentinelConn.GetSubscriber().Subscribe("+convert-to-slave", (channel, message) =>
{
// slave 192.168.0.51:6379 192.168.0.51 6379 @ mymaster 192.168.0.51 6380
Console.WriteLine((string)message);
}); conn.GetDatabase().StringSet("hello","原始值");

注意:

  1. 所有Sentinel和Redis不能在同一个节点

Redis Cluster

实际上大部分场景下,Redis Sentinel已经足够好。请根据实际情况采用 Redis Cluster。

Redis Cluster 采用虚拟槽分区方式(16384个虚拟槽)。

原因:

  • 需要更高的qps(超过 10w/s)
  • 需要更高的数据量(超过 500G)
  • 需要更高的带宽(超过 1000M)

常用命令:

  1. redis-cli -h localhost -p 6382 cluster info:查看集群基本信息
  2. redis-cli -h localhost -p 6382 cluster slots:查看集群slot信息
  3. redis-cli -h localhost -p 6382 cluster nodes:查看集群node信息
  4. redis-cli -c:move自动跳转执行
  5. yum install -y redis-trib:官方提供了基于 ruby 的工具方便部署

搭建 Cluster 过程:

  1. 配置

    cluster-enabled:yes

    cluster-node-timeout 15000

    cluster-require-full-coverage no

    cluster-config-file node-${port}.conf
  2. meet

    redis-cli cluster meet ip port
  3. 分配槽(0-16383)

    redis-cli cluster addslots {0....5461}
  4. 分配主从(node-id)

    redis-cli cluster replicate {nodeid}

redis-cli 搭建:

echo "停止当前所有redis-server + redis-sentinel";
mkdir /etc/redis
ps -x | grep redis | grep -v grep | awk '{print $1}' | xargs -r kill
sleep 1
echo "启动6个 redis + meet";
for port in 7000 7001 7002 7003 7004 7005;do
echo -e "daemonize yes\nport $port\npidfile /var/run/redis-$port.pid\nlogfile /var/log/redis/redis-$port.log\ncluster-enabled yes\ncluster-config-file nodes-$port.conf\ncluster-require-full-coverage no" > /etc/redis/redis-$port.conf
redis-server /etc/redis/redis-$port.conf
done for port in 7000 7001 7002 7003 7004 7005;do
redis-cli -p $port FLUSHALL
redis-cli -p $port cluster reset soft
if [ $port != 7000 ];then
redis-cli -p 7000 cluster meet 127.0.0.1 $port
fi
done
sleep 1 echo "分配 16383 槽";
redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10922..16383} echo "配置 replication"
redis-cli -p 7003 cluster replicate `redis-cli -p 7000 cluster nodes | grep myself | awk '{print $1}'`
redis-cli -p 7004 cluster replicate `redis-cli -p 7001 cluster nodes | grep myself | awk '{print $1}'`
redis-cli -p 7005 cluster replicate `redis-cli -p 7002 cluster nodes | grep myself | awk '{print $1}'`

redis-trib搭建:

  1. 准备节点
  2. 使用redis-trib搭建

    redis-trib create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

redis-trib create 会自动meet、addslots、replicate。

配置

查看去除注释的配置:cat /etc/redis.conf | grep -v '^#' | grep -v '^$'

设置配置:config set key value

查询所有配置:config get *

基础配置

配置项 默认值 推荐值 说明
daemonize no yes(docker环境例外) 是否以守护进程方式启动
port 6379 - redis服务监听端口
pidfile /var/run/redis.pid /var/run/redis-{port}.pid pid文件
logfile /var/log/redis/redis.log /var/log/redis/redis-{port}.log 日志文件名:redis工作时产生的日志。
dir /var/lib/redis - rdb文件和aof文件目录。推荐使用大文件目录。(不指定则为当前目录)
protected-mode yes - 限制为127.0.0.1访问。启用条件:没有bindIP 和 没有设置密码

RDB配置

配置项 默认值 推荐值 说明
dbfilename dump.rdb dump-{port}.rdb rdb文件名
rdbcompression yes yes 压缩格式
stop-writes-on-bgsave-error yes yes 出现错误时,停止新的写入
rdbchecksum yes yes 数据完整性校验

AOF配置

配置项 默认值 推荐值 说明
appendonly no yes 是否开启 aof 模式
appendfilename "appendonly.aof" "appendonly-{port}.aof" aof文件名
appendfsync everysec everysec fsync方式
no-appendfsync-on-rewrite no(安全) yes(高性能) 在 aof 重写时,是否停止fsync
auto-aof-rewrite-min-size 64mb - aof文件重写的最小大小
auto-aof-rewrite-percentage 100 - aof文件增长率
aof-load-truncated yes yes 当 aof 文件不完整的时候,将完整的部分加载

主从复制配置

配置项 默认值 推荐值 说明
slowlog-max-len 128 1000 慢查询队列长度
slowlog-log-slow-than 10000 1000(qps1w) 慢查询阈值(单位:微秒)
slaveof ip port - 主从复制配置
slave-read-only yes yes 从节点只读
repl-backlog-size 1048576 10M 复制缓存区,可以再原有基础上稍微增加

Sentinel配置

配置项 默认值 推荐值 说明
daemonize no yes 是否以守护进程方式启动
port 26379 {port} sentinel监听端口
dir /tmp - 工作目录
sentinel monitor mymaster 127.0.0.1 6379 2 - odown(objectively down,客观下线)规则:masterName ip port quorum
sentinel down-after-milliseconds mymaster 30000 - sdown(subjectively down,主观下线)规则:masterName timeout(单位:毫秒)
sentinel parallel-syncs mymaster 1 - 并发同步数量
sentinel failover-timeout mymaster 180000 - 多长时间内不再故障转移(单位:毫秒)
logfile /var/log/redis/sentinel.log /var/log/redis/sentinel-{port}.log 日志文件

Cluster配置

配置项 默认值 推荐值 说明
cluster-enabled no yes 开启cluster模式
cluster-node-timeout 15000 - 故障转移时间,主观下线超时时间
cluster-config-file nodes-{port}.conf cluster配置
cluster-require-full-coverage yes no cluster 所有节点全部在线才提供服务

常见问题

redis是单线程吗?为什么这么快?

redis其实不是单线程(fsync,bgsave),一次只能执行一条命令。

慢查询

查询慢查询队列:slowlog get

客户端请求的生命周期:

  1. 发送命令
  2. 排队
  3. 执行命令
  4. 返回结果

慢查询发送在第三个阶段(执行命令),客户端超时不一定是慢查询。

fork

  1. fork本身是同步操作
  2. 内存越大耗时越长
  3. info:latest_fork_usec

规避全量复制

  • 首次全量复制:不可避免
  • runid 不匹配:故障转移
  • 复制缓冲区不足:配置repl_backlog_size调整大

常用命令

  • KEYS pattern :查询keys
  • DBSIZE :查询所有键的数量
  • EXISTS key :查询指定key是否存在
  • TYPE key :查询key的类型
  • DEL key :删除指定key
  • INFO :查看server 信息如:INFO memory

INFO 信息:

  • used_memory redis 当前使用的内存总量
  • used_memory_rss redis 当前使用的内存总量(包含内存碎片)
  • used_memory_peak redis 使用的内存总量峰值