Implementing Automatic Master-Slave Failover for Redis High Availability on Two Servers
Redis Deployment Options
When deploying Redis, several common approaches are available:
| Approach | Advantages | Disadvantages |
|---|---|---|
| Single Server | Simple deployment, only one server required | Service unavailable if the server fails |
| Sentinel Mode | Can continue service when less than half of nodes are unreachable | Requires at least 3 servers |
| Cluster Mode | Combines sentinel benefits with load balancing capabilities | Inherits sentinel limitations, lacks online scaling, more complex deployment |
This article presents a mutual master-slave approach using only two servers, similar to MySQL's replication, without requiring keepalived by using crontab and iptables for automation.
System Requirements
Server Configuration
Server Resources
| Server | IP Address | OS Version |
|---|---|---|
| Server A | 192.168.88.102 | Ubuntu 22 |
| Server B | 192.168.88.103 | Ubuntu 22 |
Software Requirements
Software Resources
| Name | Version | Download URL | Package Type |
|---|---|---|---|
| Redis | 7.0.5 | http://download.redis.io/releasees/redis-7.0.5.tar.gz | Source |
| Nginx | 1.22.1 | https://nginx.org/download/nginx-1.22.1.tar.gz | Source |
Redis Installation
The following steps should be performed on both Server A and Server B:
Extract Package
# Upload the package to your server and navigate to the directory containing it
# Extract the archive
tar -zxvf redis-7.0.5.tar.gz
Install Compilation Environment
# For Ubuntu
apt-get install make gcc pkg-config libc6-dev
# For CentOS
# yum -y install gcc automake autoconf libtool make
Compile and Install
# Navigate to the source directory
cd redis-7.0.5
# Compile
make
# Install
make PREFIX=/usr/local/redis install
# Copy the default configuration file to the installation directory
cp redis.conf /usr/local/redis/
# If compilation fails, clean residual files before retrying
# For Ubuntu
# make distclean
# For CentOS
# make clean
Modify Configuraton File
# Navigate to the installation directory
cd /usr/local/redis/
# Edit the configuration file
vi redis.conf
# Make the following changes:
# Allow remote connections to Redis
bind 0.0.0.0
# Enable daemon mode
daemonize yes
# Set log directory (disable logging by setting to "/dev/null")
logfile "/var/log/redis_6379.log"
# Data persistence directory
dir /usr/local/redis/
# Set password (example):
requirepass D83544E45CA39C7653BF21612FAD0FD1
Start and Test
# Start Redis
./bin/redis-server redis.conf
# Connect to Redis (replace password with your own)
./bin/redis-cli -a D83544E45CA39C7653BF21612FAD0FD1
# Set a variable 'a' with value 1
set a 1
# Get the value of variable 'a'
get a
# Exit
quit
Stop Redis
# D83544E45CA39C7653BF21612FAD0FD1 is the password
./bin/redis-cli -a D83544E45CA39C7653BF21612FAD0FD1 shutdown
Configure Firewall
Ubuntu 22
# If not enabled, start the firewall
ufw enable
# Allow TCP connections to port 6379 from all sources
ufw allow 6379/tcp
CentOS 7
# If not enabled, start the firewall
systemctl start firewalld
# Allow TCP connections to port 6379 from all sources
sudo firewall-cmd --zone=public --add-port=6379/tcp --permanent
# Reload firewall rules
firewall-cmd --reload
Master-Slave Configuration
Create two copies of the redis.conf file, named slave.conf and master.conf:
cp redis.conf slave.conf
cp redis.conf master.conf
Configure Slave Settings
Modify the slave.conf files on both servers as follows:
For Server A (slave.conf):
# Set Server B as the master
replicaof 192.168.88.103 6379
# Set the master's authentication password
masterauth D83544E45CA39C7653BF21612FAD0FD1
For Server B (slave.conf):
# Set Server A as the master
replicaof 192.168.88.102 6379
# Set the master's authentication password
masterauth D83544E45CA39C7653BF21612FAD0FD1
Start and Verify
Start Server A as master:
./bin/redis-server master.conf
Start Server B as slave:
./bin/redis-server slave.conf
Connect to Redis on both servers:
# Connect to Server A
./bin/redis-cli -h 192.168.88.102 -a D83544E45CA39C7653BF21612FAD0FD1
# Connect to Server B
./bin/redis-cli -h 192.168.88.103 -a D83544E45CA39C7653BF21612FAD0FD1
On Server A, set variable 'y' to 3:
set y 3
On Server B, query variable 'y' and verify it returns 3:
get y
This confirms the master-slave setup is working. Note that the master provides both read and write operations, while the slave only provides read operations.
Implementing Master-Slave Failover
The following steps should be performed on both Server A and Server B:
Create Redis Management Script
Create a redis.sh file in the Redis root directory:
vi /usr/local/redis/redis.sh
Add the following content:
#!/bin/bash
# Redis Management Script
# Get current directory
PRG="$0"
while [ -h "$PRG" ]; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`/"$link"
fi
done
PRGDIR=`dirname "$PRG"`
SH_DIR=`cd "$PRGDIR" >/dev/null; pwd`
APP_NAME="redis-server"
APP_PATH=$SH_DIR
BIN_PATH=$APP_PATH/bin
CONF_PATH=$APP_PATH/
CONF_FILE_PATH=$CONF_PATH/redis.conf
MASTER_CONF_FILE_PATH=$CONF_PATH/master.conf
SLAVE_CONF_FILE_PATH=$CONF_PATH/slave.conf
APP_PORT=6379
APP_PASS="D83544E45CA39C7653BF21612FAD0FD1"
# Read port and password from config file
PORT_STR=$(cat $CONF_FILE_PATH | grep -E '^port'| sed 's/port \([0-9]*\).*/\1/g' )
PASS_STR=$(cat $CONF_FILE_PATH | grep -E '^requirepass' | sed 's/.*requirepass \(.*\)/\1/g' | sed 's/"//g')
if [ -n "$PORT_STR" ]; then
APP_PORT=$PORT_STR;
fi
if [ -n "$PASS_STR" ]; then
APP_PASS=$PASS_STR;
fi
# Handle password connection string
PASS_KEY=""
if [ -n "$APP_PASS" ]; then
PASS_KEY="-a $APP_PASS"
fi
echo -e "\e[35m======================================================================\e[0m"
# Display usage information
usage(){
echo -e "\e[33mAPP_PATH:\e[0m \e[36m $APP_PATH \e[0m"
echo -e "\e[33mUsage:\e[0m \e[36m $0 [start|stop|restart] \e[0m"
echo -e "\e[33mUsage:\e[0m \e[36m $0 [status|running|role|slaveup] \e[0m"
echo -e "\e[33mUsage:\e[0m \e[36m $0 [tomaster|toslave|startmaster|startslave|startauto] \e[0m"
echo -e "\e[35m----------------------------------------------------------------------\e[0m"
echo -e " -\e[32m start \e[0m: Start Redis normally, $BIN_PATH/redis-server $CONF_FILE_PATH"
echo -e " -\e[32m stop \e[0m: Stop Redis, $BIN_PATH/redis-cli shutdown"
echo -e " -\e[32m restart \e[0m: Restart Redis, stop && start"
echo -e "\e[35m----------------------------------------------------------------------\e[0m"
echo -e " -\e[32m status \e[0m: Current running status and role type"
echo -e " -\e[32m running \e[0m: Current running status, returns 0 if running, 1 otherwise"
echo -e " -\e[32m role \e[0m: Current role type, returns 0 for master, 1 for slave, 2 for others, 9 if not running"
echo -e " -\e[32m slaveup \e[0m: When running as slave, check if master_link_status is up (returns 0 if yes, 1 if no)"
echo -e " -\e[32m masterup \e[0m: Check the role of the other node (returns 0 for master, 1 for slave)"
echo -e "\e[35m----------------------------------------------------------------------\e[0m"
echo -e " -\e[32m tomaster \e[0m: Convert current role to [master]"
echo -e " -\e[32m toslave \e[0m: Convert current role to [slave]"
echo -e " -\e[32m startmaster\e[0m: Start as [master] if not running"
echo -e " -\e[32m startslave \e[0m: Start as [slave] if not running"
echo -e " -\e[32m startauto \e[0m: Auto-determine role (master/slave) based on slave.conf master node status when starting"
echo ""
exit 1
}
# Check if Redis is running
is_running(){
# Get process pid by application name
pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}'
# Return 1 if not running, 0 if running
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}
# Start Redis
start(){
is_running
if [ $? -eq "0" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] is already running. pid=${pid}"
else
$BIN_PATH/redis-server $CONF_FILE_PATH
sleep 1
status
if [ $? -eq "0" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] started successfully. pid=${pid}"
else
echo "$APP_NAME - [PORT: $APP_PORT] failed to start."
fi
fi
}
# Start with master configuration
startmaster(){
echo "Preparing to start in [master] mode, replacing redis.conf..."
cp -f $MASTER_CONF_FILE_PATH $CONF_FILE_PATH
sleep 1
start
}
# Start with slave configuration
startslave(){
echo "Preparing to start in [slave] mode, replacing redis.conf..."
cp -f $SLAVE_CONF_FILE_PATH $CONF_FILE_PATH
sleep 1
start
}
# Auto-determine master or slave mode
startauto(){
is_running
if [ $? -eq "0" ]; then
echo "${APP_NAME} - [PORT: ${APP_PORT}] is already running. pid=${pid}"
exit 1
fi
# Read master configuration from slave.conf
arr=(`cat ${SLAVE_CONF_FILE_PATH} | grep ^replicaof`)
master_host=${arr[1]}
master_port=${arr[2]}
authcmd=$(cat $SLAVE_CONF_FILE_PATH | grep ^masterauth | sed 's/masterauth //g' | sed 's/"//g')
auth_key=""
if [ -n "$authcmd" ]; then
auth_key="-a $authcmd"
fi
echo "Checking master -h $master_host -p $master_port $auth_key"
# Determine the role of the configured master
roleName=$($BIN_PATH/redis-cli -h $master_host -p $master_port $auth_key info | grep role: | sed 's/.$//g')
echo "Configured master's current role: $roleName"
if [ "$roleName" = "role:master" ]; then
startslave
else
startmaster
fi
}
# Check if the other node is in master mode
masterup(){
# Read master configuration from slave.conf
arr=(`cat ${SLAVE_CONF_FILE_PATH} | grep ^replicaof`)
master_host=${arr[1]}
master_port=${arr[2]}
authcmd=$(cat $SLAVE_CONF_FILE_PATH | grep ^masterauth | sed 's/masterauth //g' | sed 's/"//g')
auth_key=""
if [ -n "$authcmd" ]; then
auth_key="-a $authcmd"
fi
echo "Checking master -h $master_host -p $master_port $auth_key"
# Determine the role of the configured master
roleName=$($BIN_PATH/redis-cli -h $master_host -p $master_port $auth_key info | grep role: | sed 's/.$//g')
echo "Configured master's current role: $roleName"
if [ "$roleName" = "role:master" ]; then
return 0
else
return 1
fi
}
# Stop Redis
stop(){
is_running
if [ $? -eq "0" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] is running. pid=${pid}. Stopping..."
# Execute shutdown
$BIN_PATH/redis-cli $PASS_KEY shutdown
sleep 1
is_running
if [ $? -eq "0" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] failed to stop. pid=${pid}"
else
echo "$APP_NAME - [PORT: $APP_PORT] stopped successfully."
fi
else
echo "$APP_NAME - [PORT: $APP_PORT] is not running."
fi
}
# Restart Redis
restart(){
stop
start
}
# Determine the current role [master, slave]
# Returns 0 for master, 1 for slave, 2 for others, 9 if not running
role(){
is_running
if [ $? -eq "0" ]; then
roleName=$($BIN_PATH/redis-cli $PASS_KEY info | grep role: | sed 's/.$//g')
if [ "$roleName" = "role:master" ]; then
return 0
elif [ "$roleName" = "role:slave" ]; then
return 1
else
echo "ERROR: Unknown role: [$roleName] !!!"
return 2
fi
else
echo "$APP_NAME - [PORT: $APP_PORT] is not running."
return 9
fi
}
# Check if current instance is slave and properly connected to master
# Simplified logic: checks for master_link_status:up in info (returns 0 if normal, 1 if not)
# Should be called only when the node is known to be running and a slave
slaveup(){
linkStatus=$($BIN_PATH/redis-cli $PASS_KEY info | grep master_link_status:up)
if [ -z "$linkStatus" ]; then
return 1
else
return 0
fi
}
# Change Redis to master role
tomaster(){
role
mode=$?
if [ $mode -eq "0" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] is already [master]. No switch needed."
elif [ $mode -eq "1" ]; then
# Switch configuration file to avoid changes after restart
cp -f $MASTER_CONF_FILE_PATH $CONF_FILE_PATH
# Execute switch to master
$BIN_PATH/redis-cli $PASS_KEY replicaof no one
sleep 1
status
else
echo "$APP_NAME - [PORT: $APP_PORT] cannot handle current mode [$mode]."
fi
}
# Change Redis to slave role
toslave(){
role
mode=$?
if [ $mode -eq "1" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] is already [slave]. No switch needed."
elif [ $mode -eq "0" ]; then
# Switch configuration file to avoid changes after restart
cp -f $SLAVE_CONF_FILE_PATH $CONF_FILE_PATH
authcmd=$(cat $CONF_FILE_PATH | grep ^masterauth | sed 's/"//g')
echo "Setting master connection password. $authcmd"
# Update password
$BIN_PATH/redis-cli $PASS_KEY config set $authcmd
repcmd=$(cat $CONF_FILE_PATH | grep ^replicaof)
echo "Preparing to switch to [slave], $repcmd"
# Execute switch to slave
$BIN_PATH/redis-cli $PASS_KEY $repcmd
sleep 1
status
else
echo "$APP_NAME - [PORT: $APP_PORT] cannot handle current mode [$mode]."
fi
}
# Display status
status(){
is_running
if [ $? -eq "0" ]; then
echo "$APP_NAME - [PORT: $APP_PORT] is running. pid=${pid}"
role
if [ $? -eq "0" ]; then
echo "Redis is master role"
elif [ $? -eq "1" ]; then
echo "Redis is slave role"
else
echo "Redis is unknown role"
fi
else
echo "$APP_NAME - [PORT: $APP_PORT] is not running."
fi
}
# Execute appropriate function based on input parameters
case "$1" in
"start")
start
;;
"stop")
stop
;;
"running")
is_running
;;
"status")
status
;;
"role")
role
;;
"restart")
restart
;;
"tomaster")
tomaster
;;
"toslave")
toslave
;;
"startmaster")
startmaster
;;
"startslave")
startslave
;;
"startauto")
startauto
;;
"slaveup")
slaveup
;;
"masterup")
masterup
;;
*)
usage
;;
esac
Make Script Executable
chmod +x redis.sh
Test Initial Setup
Restart Redis services on both servers:
./redis.sh restart
Switch Server B from slave to master:
./redis.sh tomaster
Switch Server A from master to slave:
./redis.sh toslave
Connect to both Redis instances:
On Server B, connect to local Redis and set variable 't' to 2:
set t 2
On Server A, connect to Server B's Redis and check variable 't', which should return 2:
get t
Automatic Master-Slave Failover
The following steps should be performed on both Server A and Server B:
Create Detection Script
Create a vip_keepalived.sh file:
vi /usr/local/redis/vip_keepalived.sh
Add the following content:
#!/bin/bash
BASE_PATH=/usr/local/redis
# Enable forwarding rules
iptables_up(){
while ([ `iptables -t nat -nL PREROUTING --line | grep 6379$ | grep 6378 | wc -l` -gt 1 ])
do
iptables -t nat -D PREROUTING -p tcp --dport 6378 -j REDIRECT --to-port 6379
done
while ([ `iptables -t nat -nL OUTPUT --line | grep 6379$ | grep 6378 | wc -l` -gt 1 ])
do
iptables -t nat -D OUTPUT -p tcp --dport 6378 -j REDIRECT --to-port 6379
done
if [ `iptables -t nat -nL PREROUTING --line | grep 6379$ | grep 6378 | wc -l` -eq "0" ]; then
iptables -t nat -A PREROUTING -p tcp --dport 6378 -j REDIRECT --to-port 6379
fi
if [ `iptables -t nat -nL OUTPUT --line | grep 6379$ | grep 6378 | wc -l` -eq "0" ]; then
iptables -t nat -A OUTPUT -p tcp --dport 6378 -j REDIRECT --to-port 6379
fi
}
# Remove forwarding rules
iptables_down(){
while([ `iptables -t nat -nL PREROUTING --line | grep 6379$ | grep 6378 | wc -l` -gt 0 ])
do
iptables -t nat -D PREROUTING -p tcp --dport 6378 -j REDIRECT --to-port 6379
done
while([ `iptables -t nat -nL OUTPUT --line | grep 6379$ | grep 6378 | wc -l` -gt 0 ])
do
iptables -t nat -D OUTPUT -p tcp --dport 6378 -j REDIRECT --to-port 6379
done
}
# Check if role needs to be switched
check(){
# Determine current role [master, slave]
# Returns 0 for master, 1 for slave, 2 for others, 9 if not running
$BASE_PATH/redis.sh role
redisRole=$?
echo "Current Redis role value: [$redisRole]"
if [ $redisRole -eq "0" ]; then
iptables_up
# Redis is running as master
$BASE_PATH/redis.sh masterup
if [ $? -eq "0" ]; then
$BASE_PATH/redis.sh toslave
iptables_down
fi
return 0
elif [ $redisRole -eq "1" ]; then
iptables_down
# Redis is running as slave
$BASE_PATH/redis.sh slaveup
if [ $? -eq "0" ]; then
return 1
else
# Check if the other node is not master before switching
$BASE_PATH/redis.sh masterup
if [ ! $? -eq "0" ]; then
# Slave connection to master is down, switch this node to master
$BASE_PATH/redis.sh tomaster
iptables_up
return 0
else
iptables_down
return 1
fi
fi
else
iptables_down
return $redisRole
fi
}
check
Make Script Executable
chmod +x /usr/local/redis/vip_keepalived.sh
Create Cron Job
echo "* * * * * /usr/local/redis/vip_keepalived.sh > /tmp/check_redis.log" | crontab
Setting Up Nginx for High Availability
Install Nginx
Reference previous installation guides, ensuring to include the --with-stream option:
# Install Nginx core modules
tar -zxvf nginx-1.22.1.tar.gz
cd nginx-1.22.1
# Add TCP proxy module
./configure --with-stream
make
make install
chmod a+rwx -R /usr/local/nginx/logs/
chmod a+rwx -R /usr/local/nginx/
/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx
# Set to start on boot
cp $PRGDIR/nginx /etc/init.d/
chmod +x /etc/init.d/nginx
chkconfig --add nginx
chkconfig nginx on
Configure Nginx for Redis High Availability
Add the following to the end of your nginx.conf file:
stream {
upstream redis_backend {
# Server A Redis mapping
server 192.168.88.102:6378;
# Server B Redis mapping
server 192.168.88.103:6378;
}
server {
# Proxy port 6380 - applications should connect to Redis via 127.0.0.1:6380
listen 6380;
proxy_pass redis_backend;
}
}
Restart Nginx to apply changes:
/usr/local/nginx/sbin/nginx -s quit
/usr/local/nginx/sbin/nginx
Summary of Redis High Availability Approaches
| Approach | Advantages | Disadvantages |
|---|---|---|
| Single Server | Simple deployment, only one server required | Service unavailable if the server fails |
| Sentinel Mode | Can continue service when less than half of nodes are unreachable | Requires at least 3 servers |
| Cluster Mode | Combines sentinel benefits with load balancing capabilities | Inherits sentinel limitations, lacks online scaling, more complex deployment |
| Mutual Master-Slave (Keepalived version) | Works with 2 servers, achieves mostly automated high availability | Time needed for failover (approximately 1.2 seconds). Requires additional VIP IP |
| Mutual Master-Slave (Method described in this article) | Works with 2 servers, achieves mostly automated high availability, no additional IP needed | Crontab minimum execution interval is 1 minute, so failover may take up to 1 minute. Service is bound to Nginx, so if Nginx fails, the service becomes unavailable |
The best approach depends on your specific requirements and constraints.