Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Automatic Master-Slave Failover for Redis High Availability on Two Servers

Tech May 12 2

Redis Deployment Options

When deploying Redis, several common approaches are available:

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

Software Requirements

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 Configuration 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 varible '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

The best approach depends on your specific requirements and constraints.

Tags: Redis

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.