本文写于 2014 年。
当时 Redis 官方高可用方案 Redis Sentine,处于 beta 阶段。

需求说明

在传统的非云化的IT环境中,Redis 高可用主要使用 Keepalived 搭建,Keepalived 向 Redis 客户端提供 VIP,监控主备机工作状态,VIP 自动漂移,实现容灾切换。

在商业公有云环境中,用户购买虚拟机,云平台会自动分配内网 IP,网络管理完全由平台控制,普通用户无法自由配置 IP 地址,VIP 作为 Keepalived 方案中重要的条件,在商业公有云中无法满足。

在阿里云、腾讯云搭建环境中,我们就碰到上面的问题。

解决方案是,利用 shell 脚本实现 Keepalived 的功能。

需求实现

架构图

工作流程

1.使用启动脚本启动 Redis 主机;
2.使用启动脚本启动 Redis 备机,向当前主机同步;
3.Hyperic 定时调用 Redis 检查脚本判断主备机状态;
4.主机异常时,Redis 备机自动切换为主机,触发告警;
5.主机恢复时,使用启动脚本重新启动主机,向当前主机(原备机)同步,同步完成后,再切换为主机;
6.备机异常时,触发告警;
7.备机恢复时,使用启动脚本重新启动备机,向当前主机同步;

实现原理

1.SLB 负载两台 Ha 服务器
SLB 为何不直接负载两台 Redis 服务器?
Redis 双机具备主从关系,一般 SLB 服务负载均衡策略较为简单,仅支持轮询或最小连接数策略,不支持主备模式,无法满足 Redis 双机负载均衡要求。
2.Ha 服务器负载两台Redis服务器
Ha 服务器运行开源负载均衡组件 Haproxy,通过配置文件可实现灵活的负载均衡策略,
针对 Redis 服务,使用主备模式。

具体配置

1.SLB,轮询模式,前端服务端口 6379,后端转发端口 6379,开启会话保持;

2.Haproxy,主备模式,前端服务端口 6379,后端转发端口 6379,监控端口 16379;

在 Redis 本地存在大量持久化数据的情况下,重新启动时,数据加载的时间耗时较长,在这个过程中,无法正常对外提供服务。

如果使用 Redis 服务端口 6379 作为监控端口,当组件启动时,Ha 会认为 Redis 可正常提供服务,实际上,只有组件启动且数据全部加载完成,才能正常对外提供服务,数据加载时间同数据量正相关,无法准确估计,所以监控端口使用 16379,当数据加载完成时,由脚本控制,实现对该端口的监听。

Haproxy 配置

listen  redis
bind 0.0.0.0:6379
mode tcp
balance roundrobin
server redis1 10.161.58.215:6379 weight 1 maxconn 10240 check port 16379 check inter 10s
server redis2 10.161.69.45:6379 weight 1 maxconn 10240 check port 16379 check inter 10s backup

3.Redis,主备机角色预置

主机

备机

主机启动脚本

#!/bin/bash

#判断redis进程是否存在,若进程不存在,则启动redis进程
if test $( pgrep -f "redis-server" | wc -l ) -eq 0
then
echo "redis 进程不存在,开始启动进程"

/usr/local/redis/src/redis-server /etc/redis.conf
echo "redis启动命令执行完成"

else
echo "redis进程已存在"
fi

#判断redis是否启动成功并预热完成
i=0
while [ "$i" -lt 60 ]
do

ALIVE=`/usr/local/redis/src/redis-cli ping`

if [ "$ALIVE" == "PONG" ];
then
echo "redis启动和预热状态检查,进度:$i/60"
i=`expr $i + 1`
sleep 1

else
echo “redis启动失败,请检查...”
sleep 1
fi

done

#判断slave状态,如果slave为启动状态,则master开始从slvae同步
if test $( nc -w 0 10.161.69.45 6379 && echo 0 || echo 1 ) -eq 0

then
echo "slave当前是启动状态"
echo "master开始从slave开始同步"

SYNC=`/usr/local/redis/src/redis-cli SLAVEOF 10.161.69.45 6379`

if [ "$SYNC" == "OK" ]
then
echo "master向slave发送同步指令成功"

elif [ "$SYNC" == "OK Already connected to specified master" ]
then
echo "master同slave已经是同步状态"
else
echo "master向slave发送同步指令失败"
echo "$SYNC"
fi

else
echo "slave当前不是启动状态"
echo "master无需同步"
echo "shell进程退出"

#启动监听端口
echo "启动监听端口"
nohup ./port_redis.sh > /dev/null &

exit
fi

#master从slave同步,判断同步的进度,当同步状态为up时,进行下一步

while [ "$SYNCTAG" != "1" ]
do

SYNCINFO=`/usr/local/redis/src/redis-cli info`
SYNCTAG=`echo "$SYNCINFO"|grep "master_link_status:up"|wc -l`
done

echo "master从slave同步成功"

#mster从slave同步成功,mater切换状态
MASTER=`/usr/local/redis/src/redis-cli SLAVEOF NO ONE`
if [ "$MASTER" == "OK" ]
then

#启动监听端口
echo "启动监听端口"
nohup ./port_redis.sh > /dev/null &

fi

主机检查脚本:

#!/bin/bash

#检查自身服务状态
if test $( nc -w 0 127.0.0.1 6379 && echo 0 || echo 1 ) -eq 0
then
#master检查自身正常
echo 0
else

#master服务异常,slave不再向其同步
/usr/local/redis/src/redis-cli -h 10.161.69.45 -p 6379 SLAVEOF NO ONE > /dev/null
#关闭监听端口

if test $( pgrep -f "port_redis" | wc -l ) -eq 1
then
kill -9 `ps -ef|grep "port_redis"|grep -v "grep" |awk '{print $2}'`
fi

if test $( pgrep -f "nc -l 16379" | wc -l ) -eq 1
then
kill -9 `ps -ef|grep "nc -l 16379"|grep -v "grep" |awk '{print $2}'`
fi

echo 1
fi

备机启动脚本

#!/bin/bash

#判断redis进程是否存在,若进程不存在,则启动redis进程
if test $( pgrep -f "redis-server" | wc -l ) -eq 0
then
echo "redis 进程不存在,开始启动进程"

/usr/local/redis/src/redis-server /etc/redis.conf
echo "redis启动命令执行完成"

else
echo "redis进程已存在"
fi

#判断redis是否启动成功并预热完成
i=0
while [ "$i" -lt 60 ]
do

ALIVE=`/usr/local/redis/src/redis-cli ping`

if [ "$ALIVE" == "PONG" ];
then
echo "redis启动和预热状态检查,进度:$i/60"
i=`expr $i + 1`
sleep 1

else
echo “redis启动失败,请检查...”
sleep 1
fi

done

#判断master状态,如果master为启动状态,则slave开始从master同步
if test $( nc -w 0 10.161.58.215 6379 && echo 0 || echo 1 ) -eq 0

then
echo "master当前是启动状态"
echo "slave开始从master开始同步"

SYNC=`/usr/local/redis/src/redis-cli SLAVEOF 10.161.58.215 6379`

if [ "$SYNC" == "OK" ]
then
echo "slave向master发送同步指令成功"

elif [ "$SYNC" == "OK Already connected to specified master" ]
then
echo "slave同master已经是同步状态"
else
echo "slave向master发送同步指令失败"
echo "$SYNC"
fi

else
echo "master当前不是启动状态"
echo "slave无需同步"
echo "shell进程退出"

#启动监听端口
echo "启动监听端口"
nohup ./port_redis.sh > /dev/null &

exit
fi

#slave从master同步,判断同步的进度,当同步状态为up时,进行下一步

while [ "$SYNCTAG" != "1" ]
do

SYNCINFO=`/usr/local/redis/src/redis-cli info`
SYNCTAG=`echo "$SYNCINFO"|grep "master_link_status:up"|wc -l`
done

echo "slave从master同步成功"

#启动监听端口
echo "启动监听端口"
nohup ./port_redis.sh > /dev/null &

备机检查脚本

#!/bin/bash

MASTERTAG=`nc -w 0 10.161.58.215 6379 && echo 0 || echo 1`
SLAVETAG=`nc -w 0 127.0.0.1 6379 && echo 0 || echo 1`


#检查自身服务状态
if [ $MASTERTAG -eq 0 ] && [ $SLAVETAG -eq 0 ]
then

#master和Slave测试正常
echo 0

elif [ $MASTERTAG -eq 1 ] && [ $SLAVETAG -eq 0 ]
then

#master服务异常,slave不再向其同步
/usr/local/redis/src/redis-cli -h 127.0.0.1 -p 6379 SLAVEOF NO ONE > /dev/null

echo 0

elif [ $MASTERTAG -eq 0 ] && [ $SLAVETAG -eq 1 ]
then

if test $( pgrep -f "port_redis" | wc -l ) -eq 1
then
kill -9 `ps -ef|grep "port_redis"|grep -v "grep" |awk '{print $2}'`
fi

if test $( pgrep -f "nc -l 16379" | wc -l ) -eq 1
then
kill -9 `ps -ef|grep "nc -l 16379"|grep -v "grep" |awk '{print $2}'`
fi

echo 1

else

if test $( pgrep -f "port_redis" | wc -l ) -eq 1
then
kill -9 `ps -ef|grep "port_redis"|grep -v "grep" |awk '{print $2}'`
fi

if test $( pgrep -f "nc -l 16379" | wc -l ) -eq 1
then
kill -9 `ps -ef|grep "nc -l 16379"|grep -v "grep" |awk '{print $2}'`
fi

echo 1

fi

监控端口启动脚本

#!/bin/bash
while [ 1 ]
do
if test $( nc -w 0 127.0.0.1 16379 && echo 0 || echo 1 ) -eq 0
then
exit
else
nc -l 16379
fi
done