redis-005主从复制和哨兵配置

阅读量: zyh 2018-01-13 15:15:09
Categories: > Tags:

基本配置

构建三个节点,一主两从。

之所以是三节点,是因为哨兵模式(一种高可用)需要最少三个才稳健。

✨这里的三个指的是哨兵进程,而不是redis进程。redis可以只有主从两个。

服务类型 是否是主服务器 IP地址 端口
Redis 10.200.16.51 6379
Redis 10.200.16.52 6379
Redis 10.200.16.53 6379
Sentinel - 10.200.16.51 26379
Sentinel - 10.200.16.52 26379
Sentinel - 10.200.16.53 26379

哨兵模式下,不应该使用的东西:

💥不应该使用主机名,因为当前哨兵模式下支持度不行(6.2版本有提到相关配置,但默认是禁止)。

💥不应该使用docker部署,除非你采用主机网络模式。原因在于:

✨你可以使用docker主机网络模式,因为这种网络模式下不存在端口映射。

主配置

# Base
bind 0.0.0.0
protected-mode yes
port 6379
daemonize yes
pidfile "/export/redis_6379/redis.pid"
loglevel warning
logfile "/export/redis_6379/logs/redis.log"
dir "/export/redis_6379/data/"
# Auth
requirepass 123456
# Replica master
## 必须有一个从服务,才允许写入
min-replicas-to-write 1
## 从服务有10秒时间来发送异步确认,若超过则认为从不可用,进而因为 min-replicas-to-write 1 导致不可写入
min-replicas-max-lag 10
# Replica slave
## master 认证
#masterauth <master-password>
## 指定 master 地址
#replicaof <masterip> <masterport>
replica-read-only yes
replica-priority 100
# Memory
maxmemory 1g
maxmemory-policy allkeys-lru
# RDB
save 900 1
save 300 10
save 60 10000
dbfilename redis_6379.rdb
# AOF
appendonly yes
appendfilename appendonly_6379.aof
appendfsync everysec
no-appendfsync-on-rewrite no
aof-use-rdb-preamble yes
# rewrite command
rename-command FLUSHDB GOD_FLUSHDB
rename-command FLUSHALL GOD_FLUSHALL
#rename-command CONFIG GOD_CONFIG
rename-command KEYS GOD_KEYS

从节点

将主配置里略加修改即可

## master 认证
masterauth <master-password>
## 指定 master 地址
replicaof <masterip> <masterport>

命令行方式:

-> bin/redis-cli -h 127.0.0.1 -p 6379 -a 123456 replicaof <master_ip> 6379

链接状态

主节点:

127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=10.200.16.52,port=6379,state=online,offset=309,lag=1
……

从节点:

127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:10.200.16.51
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:365
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

从升级主

当主不可用的时候,从执行命令可升级为主

-> bin/redis-cli -h 127.0.0.1 -p 6379 -a 123456 SLAVEOF NO ONE
OK

健康检测

-> bin/redis-cli -h 127.0.0.1 -p 6379 -a 123456 monitor
OK
1448515184.249169 [0 10.86.255.166:6379] "PING"

可以通过ping指令来确认节点是否正常。

-> bin/redis-cli -h 127.0.0.1 -p 6379 -a 123456 ping
PONG

如何实现主从自动切换

  1. 通过编写keepalived脚本来实现健康检测,从而实现VIP漂移。
    1. keepalived方式不适合云环境,因为没法实现vip。
  2. 通过官方sentinel哨兵来进行检测。
    1. 哨兵方式因为没有vip,所以需要客户端库实现及时获取正确的主节点

哨兵模式

https://redis.io/topics/sentinel

哨兵模式,通过哨兵来监控master节点,并通过调用master节点info命令中发现slave节点。

当故障切换的时候,哨兵需要执行config指令来修改配置信息。

💥因此,info和config两个指令不可自定义别名。

哨兵分为主观下线(sdown)和客观下线(odown)。主观下线就是自己监控master down掉,但此时还不会切换,客观下线就是其它哨兵也发现有问题,则此时满足客观下线条件。

当客观下线也满足的时候,就会开始选出执行故障转移的哨兵领导者(一个哨兵不会在两次连续的故障中被选为领导者)。

领导者选出一个从redis,并发送slaveof no one指令将其转为主。

之后,发送修改配置指令给其它节点,变更配置指向新主。

最后执行主切换指令。在此之后,通过哨兵查询主节点就会返回新的主地址给客户端。

配置

redis-sentinel.conf

protected-mode no
port 26379
daemonize yes
pidfile "/export/redis_6379/redis-sentinel.pid"
logfile "/export/redis_6379/logs/redis-sentinel.log"
dir "/export/redis_6379/data"

# 配置监听的主服务器
## mymaster 自定义一个主服务器的昵称
## 代表监控的主服务器
## 6379代表端口
## 2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster <主redis的ip> 6379 2

# 访问master所需的认证账户密码
## mymaster 自定义的主服务器昵称
## 123456 master 配置的密码
sentinel auth-pass mymaster <主redis的密码>

# 客户端访问哨兵所需的auth认证
requirepass <哨兵的密码>

sentinel down-after-milliseconds mymaster 10000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

✨这个配置里的信息会在哨兵启动后自动变更,不要在随意修改:

启动

先启动主服务,然后启动2个从服务器,最后启动三个哨兵

# 启动Redis服务器进程
## 先启动主服务,然后启动从服务器
systemctl start redis
# 启动哨兵进程
## 最后启动哨兵
systemctl start redis-sentinel

状态

$ redis-cli -p 26379
127.0.0.1:26379> sentinel master mymaster
 1) "name"
 2) "mymaster"
 3) "ip"
 4) "127.0.0.1"
 5) "port"
 6) "6379"
 7) "runid"
 8) "953ae6a589449c13ddefaee3538d356d287f509b"
 9) "flags"          # 如果master没了,这里会变成 s_down(主观认为) 或者 o_down(客观认为)
10) "master"          
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "735"
19) "last-ping-reply"
20) "735"
21) "down-after-milliseconds"
22) "5000"
23) "info-refresh"
24) "126"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "532439"
29) "config-epoch"
30) "1"
31) "num-slaves"        # 通过master发现的副本数
32) "1"
33) "num-other-sentinels"  # 发现的其它哨兵数
34) "2"
35) "quorum"
36) "2"
37) "failover-timeout"
38) "60000"
39) "parallel-syncs"
40) "1"

其它状态命令

SENTINEL replicas mymaster  # 获取副本信息
SENTINEL sentinels mymaster # 获取其它哨兵信息

客户端需要实时的获取master的地址,否则无法使用,哨兵有API可以拿到。命令行里获取master地址

SENTINEL get-master-addr-by-name mymaster

模拟主故障

bin/redis-cli -p 6379 -a 123456 DEBUG sleep 120

故障信息日志

被选为执行故障转移的领导者哨兵日志如下:

30977:X 15 Mar 2022 18:25:46.361 # +sdown master mymaster 10.200.16.51 6379    # 10.200.16.51 主观下线
30977:X 15 Mar 2022 18:25:46.419 # +odown master mymaster 10.200.16.51 6379 #quorum 2/2  # 10.200.16.51 满足客观下线
30977:X 15 Mar 2022 18:25:46.420 # +new-epoch 3  # 第三次出现问题
30977:X 15 Mar 2022 18:25:46.420 # +try-failover master mymaster 10.200.16.51 6379   
30977:X 15 Mar 2022 18:25:46.422 # +vote-for-leader c4f6768af1f0f82e2b094d10e0cdaeba030a5412 3   # 选出哨兵领导,字符串是哨兵ID
30977:X 15 Mar 2022 18:25:46.424 # 2c0702024e54b96dfc750455d4fb927add2ca3fe voted for c4f6768af1f0f82e2b094d10e0cdaeba030a5412 3
30977:X 15 Mar 2022 18:25:46.427 # 52ac0f8bc13ff4ddd3a0557008664742a06db10f voted for c4f6768af1f0f82e2b094d10e0cdaeba030a5412 3
30977:X 15 Mar 2022 18:25:46.523 # +elected-leader master mymaster 10.200.16.51 6379
30977:X 15 Mar 2022 18:25:46.523 # +failover-state-select-slave master mymaster 10.200.16.51 6379
30977:X 15 Mar 2022 18:25:46.585 # +selected-slave slave 10.200.16.52:6379 10.200.16.52 6379 @ mymaster 10.200.16.51 6379 # 选定 10.200.16.52 从作为备选主
30977:X 15 Mar 2022 18:25:46.585 * +failover-state-send-slaveof-noone slave 10.200.16.52:6379 10.200.16.52 6379 @ mymaster 10.200.16.51 6379 # 发送 slaveof no one ,将 10.200.16.52 升级为主
30977:X 15 Mar 2022 18:25:46.675 * +failover-state-wait-promotion slave 10.200.16.52:6379 10.200.16.52 6379 @ mymaster 10.200.16.51 6379
30977:X 15 Mar 2022 18:25:47.486 # +promoted-slave slave 10.200.16.52:6379 10.200.16.52 6379 @ mymaster 10.200.16.51 6379
30977:X 15 Mar 2022 18:25:47.486 # +failover-state-reconf-slaves master mymaster 10.200.16.51 6379  # 重写 10.200.16.51 配置为从
30977:X 15 Mar 2022 18:25:47.540 * +slave-reconf-sent slave 10.200.16.53:6379 10.200.16.53 6379 @ mymaster 10.200.16.51 6379 # 重写 10.200.16.53 从配置
30977:X 15 Mar 2022 18:25:48.490 * +slave-reconf-inprog slave 10.200.16.53:6379 10.200.16.53 6379 @ mymaster 10.200.16.51 6379
30977:X 15 Mar 2022 18:25:48.491 * +slave-reconf-done slave 10.200.16.53:6379 10.200.16.53 6379 @ mymaster 10.200.16.51 6379
30977:X 15 Mar 2022 18:25:48.542 # -odown master mymaster 10.200.16.51 6379 # 10.200.16.51 客观下线事件结束
30977:X 15 Mar 2022 18:25:48.542 # +failover-end master mymaster 10.200.16.51 6379  # 10.200.16.51 故障转移结束
30977:X 15 Mar 2022 18:25:48.542 # +switch-master mymaster 10.200.16.51 6379 10.200.16.52 6379  # 切换主
30977:X 15 Mar 2022 18:25:48.543 * +slave slave 10.200.16.53:6379 10.200.16.53 6379 @ mymaster 10.200.16.52 6379
30977:X 15 Mar 2022 18:25:48.543 * +slave slave 10.200.16.51:6379 10.200.16.51 6379 @ mymaster 10.200.16.52 6379 # 51成为从
30977:X 15 Mar 2022 18:25:58.554 # +sdown slave 10.200.16.51:6379 10.200.16.51 6379 @ mymaster 10.200.16.52 6379 # 51从主观下线(因为debug指令还未结束)
30977:X 15 Mar 2022 18:30:36.203 # -sdown slave 10.200.16.51:6379 10.200.16.51 6379 @ mymaster 10.200.16.52 6379 # 51从恢复(debug指令结束)
30977:X 15 Mar 2022 18:30:46.159 * +convert-to-slave slave 10.200.16.51:6379 10.200.16.51 6379 @ mymaster 10.200.16.52 6379

监控主节点

-> watch bin/redis-cli -h 127.0.0.1 -a 123456 -p 26379 SENTINEL get-master-addr-by-name mymaster
===
10.200.16.52
6379

添加一个哨兵

只需要正常配置一个哨兵并启动即可

删除一个哨兵

  1. 停止要删除的哨兵
  2. 在所有剩余哨兵里执行重置命令
SENTINEL RESET <master昵称>
  1. 检查哨兵,确认其它哨兵数量是否一致
SENTINEL MASTER <master昵称>

删除一个节点

在你删除一个旧的主节点或者不可用的节点后,应在所有哨兵里执行重置命令

SENTINEL RESET <master昵称>

客户端(JAVA)

网上找的一个JAVA例子=。=

jedis通过配置哨兵列表从而满足从哨兵中动态的获取主节点信息。

💥如果哨兵里配置了requirepass “your_password_here”,则需要jedis支持。

import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;

public class RedisSentinel {

    private static Logger log = Logger.getLogger(RedisSentinel.class);
    
    public static void redisSentinel() {
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("192.168.170.128:26379");//192.168.170.11
        sentinels.add("192.168.170.129:26379");//192.168.170.12
        sentinels.add("192.168.170.130:26379");//192.168.170.13
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("mymaster", sentinels);
        Jedis jedis = jedisSentinelPool.getResource();
        String a = jedis.get("a");
        String b = jedis.get("b");
        System.out.println("a=" + a);
        System.out.println("b=" + b);
        jedis.close();
        jedisSentinelPool.close();
    }
    
    
    /**
     * 测试哨兵转移
     */
    public static void redisSentinelTransfer() {
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("192.168.170.11:26379");
        sentinels.add("192.168.170.12:26379");
        sentinels.add("192.168.170.13:26379");
        @SuppressWarnings("resource")
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("mymaster", sentinels);
        Jedis jedis = null;
        
        while(true) {
            try {
                //不能放在while里面,这样连接池在8次后会被用完,不执行redis方法,也不报错。
                //如果放在while,必须回收。
                jedis = jedisSentinelPool.getResource();
                int randInt = new Random().nextInt(10000);
                String key  = "k_" + randInt;
                String value  = "v_" + randInt;
                jedis.set(key, value);
                
                log.info(key + " = " + value);
                TimeUnit.SECONDS.sleep(2);
                
            } catch (Exception e) {
                log.info(e);
                
            } finally {
                if(jedis != null) {
                    jedis.close();
                }
            }
            
        }
        
    }
    
    
    public static void main(String[] args) {
        //redisSentinel();
        redisSentinelTransfer();
    }  
}