Linux入门教程:使用 HAProxy + Keepalived 构建基于 Docker 的高可用负载均衡服务(二),适合具有 Docke


本文主要介绍把 HAProxy + Keepalived 服务组合形成单独的镜像,并能够自定义配置项。适合具有 Docker 和 Bash 相关基础的开发、运维等同学。

使用 Docker 组合构建 haproxy-keepalived 服务

上图为我们要构建的 haproxy-keepalived 服务的工作原理图,原理表述如下:

START:

根据 Bash 脚本和 Docker 容器的环境变量生成 HAProxy 和 Keepalived 的配置文件 在容器内部启动 HAProxy 和 Keepalived 服务

STOP:

ENTRYPOINT 使用 trap 接收到传递来的 SIGTERM 信号 使用 kill -ITEM $pid 的方式,结束掉 HAProxy & Keepalived 两个服务。

下边会按照上述步骤介绍如何工作。

生成 HAProxy & Keepalived 配置文件

haproxy_cfg_init.sh
#!/bin/bash

HAPROXY_DIR="/usr/local/etc/haproxy"
TMP_HAPROXY="/tmp/haproxy"

TMP_FILE="${TMP_HAPROXY}/tmp.cfg"
TMP_TEMPLATE="${TMP_HAPROXY}/template.cfg"

TEMPLATE_FILE="/haproxy/template.cfg"
HAPROXY_CFG="${HAPROXY_DIR}/haproxy.cfg"

# mkdir to prevent not exists
mkdir -p ${HAPROXY_DIR}
mkdir -p ${TMP_HAPROXY}
rm -rf ${HAPROXY_DIR}/haproxy.cfg

# delete old haproxy.cfg and touch tmp.cfg
rm -rf ${HAPROXY_CFG}
rm -rf ${TMP_FILE}
touch ${TMP_FILE}

# copy template.cfg to tmp dir
cp ${TEMPLATE_FILE} ${TMP_TEMPLATE}

COUNTER=1
while [ ${COUNTER} -gt 0 ]
do
    haproxy_item=$(printenv haproxy_item${COUNTER})

    if [ -z "${haproxy_item}" ]
    then
        break;
    fi

    # echo haproxy_item to /tmp/haproxy/tmp.cfg
    echo "\${haproxy_item${COUNTER}}" >> ${TMP_FILE}

    let COUNTER+=1
done
echo "Success generate tmp.cfg!"

sed -i "/#CUSTOM/r ${TMP_FILE}" ${TMP_TEMPLATE}
echo "Success generate template.cfg"

touch ${HAPROXY_CFG}
envsubst < ${TMP_TEMPLATE} > ${HAPROXY_CFG}

echo "Success generate haproxy.cfg, file content is below:"
echo "----------------haproxy.cfg------------------"
cat ${HAPROXY_CFG}
echo "------------------ End ----------------------"

容器在启动的时候,传入 haproxy_item_${CURSOR} 环境变量,上述脚本根据传入的环境变量与 template.cfg 文件结合,生成最终的 /usr/local/etc/haproxy/haproxy.cfg 配置文件。

大致相似的原理,根据 init_keepalived_conf.sh 脚本,与传入的环境变量结合,使用环境变量替换 keepalived_template.conf 文件中的环境变量,来生成 最终的 /etc/keepalived/keepalived.conf 配置文件。

#!/bin/bash
set -e

TEMPLATE="/keepalived/keepalived_template.conf"
TMP_TEMPLATE="/tmp/keepalived/tmp_keepalived_template.conf"
KEEPALIVED_FILE="/etc/keepalived/keepalived.conf"

# mkdir to prevent not exist
mkdir -p /tmp/keepalived/
mkdir -p /etc/keepalived/

# copy template to /tmp/keepalived/tmp_keepalived_template.conf
cp ${TEMPLATE} ${TMP_TEMPLATE}

envsubst < ${TMP_TEMPLATE} > ${KEEPALIVED_FILE}
echo "Success generate ${KEEPALIVED_FILE}"
echo "----------------------keepalived.conf----------------------"
cat ${KEEPALIVED_FILE}
echo "--------------------------- End ---------------------------"

容器内部启动 HAProxy & Keepalived 服务

查看 Dockerfile 文件,这个 haproxy-keepalived 镜像是基于 haproxy:1.7.9 基础镜像来做的修改。为了符合一个容器启动 HAProxy & Keepalived 两个服务的预期,我们增加了一个 docker-entrypoint-override.sh 的脚本来作为 haproxy-keepalived 镜像的 ENTRYPOINT。

在 docker-entrypoint-override.sh 中,我们分别用如下两条命令启动来启动两个服务:

HAProxy: exec /docker-entrypoint.sh “$@” &

直接后台运行 haproxy:1.7.9 官方镜像的原 docker-entrypoint.sh 来启动 haproxy

Keepalived: exec /start_keepalived.sh “$@” &

后台运行 start_keepalived.sh 脚本来启动 Keepalived 服务

接下来会稍微详细说一下 start_keepalived.sh 脚本:

#!/bin/bash

LANUCH_SCRIPT="keepalived --dont-fork --dump-conf --log-console --log-detail --log-facility 7 --vrrp -f /etc/keepalived/keepalived.conf"

eval $LANUCH_SCRIPT
while true; do
  k_pid=$(pidof keepalived)
  if [ -n "$k_id" ]; then
    break;
  fi

  kill -TERM $(cat /var/run/vrrp.pid)
  kill -TERM $(cat /var/run/keepalived.pid)
  echo "ERROR: Keepalived start failed, attempting restart ."
  eval $LANUCH_SCRIPT
done

echo "Keepalived started successfuly!"

在上述脚本中,我们在运行 LANUCH_SCRIPT 后,又做了一个循环判断。原因是在我们使用这个自定义的 haproxy-keepalived 服务的时候,在重启容器后 Keepalived 并不能正常启动,会有个报错:

Keepalived: Daemon is already running

但实际上,这个 Daemon 我们已经给它 kill 掉了。我们这里再执行一次判断,如果 $k_id 值不为空,则 Keepalived 启动成功;如果 $k_id 值为空,我们会再 kill 一次 Keepalived 的进程,然后重启 Keepalived。

容器停止,Graceful Shutdown

在容器停止的时候,需要做 Graceful Shutdown 的操作,在这里的目标就是:容器关闭前干掉 HAProxy & Keepalived 的进程。

仍然回到 docker-entrypoint-override.sh 脚本,在脚本中我们定义了接收 SIGITERM 信号的处理事件:

trap "stop; exit 0;" SIGTERM SIGINT

# handler for SIGINT & SIGITEM
stop() {
  echo "SIGTERM caught, terminating <haproxy & keepalived> process..."

  # terminate haproxy
  h_pid=$(pidof haproxy)

  kill -TERM $h_pid > /dev/null 2>&1
  echo "HAProxy had terminated."

  # terminate keepalived
  kill -TERM $(cat /var/run/vrrp.pid)
  kill -TERM $(cat /var/run/keepalived.pid)
  echo "Keepalived had terminated."

  echo "haproxy-keepalived service instance is successfuly terminated!"
}

当容器被 stop 或者 kill 的时候,会触发这个 handler 事件。我们在事件中对两个服务做了下简单的处理。

如何使用

首先,准备两台 Linux 虚拟机,假定 IP 分别为:192.168.0.41、192.168.0.42,并找到一个 VIP,假定为 192.168.0.40。

在 192.168.0.41 机器上新建 /data/haproxy-keepalived/docker-compose.yml 文件:

root@haproxy-master:/data/haproxy-keepalived# cat docker-compose.yml
version: '3'
services:

  haproxy-keepalived:
    image: "pelin/haproxy-keepalived"
    privileged: true
    network_mode: host
    restart: always
    environment:
      KEEPALIVED_STATE: "MASTER"
      KEEPALIVED_INTERFACE: "ens18"
      KEEPALIVED_PRIORITY: "105"
      KEEPALIVED_V_ROUTER_ID: "40"
      KEEPALIVED_VIP: "192.168.0.40"
      haproxy_item1: |-
        listen app-1
            bind *:4000
            mode http
            maxconn 300
            balance roundrobin
            server server1 192.168.0.21:4001 maxconn 300 check
            server server2 192.168.0.22:4002 maxconn 300 check
         listen app-2
            bind *:5000
            mode http
            maxconn 300
            balance roundrobin
            server server1 192.168.0.21:5001 maxconn 300 check
            server server2 192.168.0.22:5002 maxconn 300 check

注意上述配置:
KEEPALIVED_STATE             # Keepalived 节点初始状态
KEEPALIVED_INTERFACE         # Keepalived 绑定的网卡
KEEPALIVED_PRIORITY          # 权重
KEEPALIVED_V_ROUTER_ID       # router_id
KEEPALIVED_VIP               # 虚拟 IP

其中,KEEPALIVED_INTERFACE 需要根据实际情况而定。比如笔者用的 Ubuntu 16.04 的主网卡是 ens18,Ubuntu 14.04 的网卡是 eth0。

然后使用如下命令开启服务:

root@haproxy-keepalived:/data/haproxy-keepalived# docker-compose up

在启动后,访问浏览器:192.168.0.41:1080/stats 即可看到有界面出现

同理,在 42 机器上做上述操作,其中 KEEPALIVED_STATE 需要修改为 BACKUP

如何扩展

目前实现的功能:

根据环境变量动态调整 HAProxy & Keepalived 配置 容器 Graceful Shutdown 容器能够正常 Restart,Crashed 后能正常自动重启

TODO:

直接 Bind 配置文件进容器

如果需要自定义一个 haproxy-keepalived 镜像的话,可以参考上边的思路来做。当然,如果场景可以使用 Bind 的方式,那自然是更简单了。

如果可以直接 Bind 配置文件进容器,上述很多脚本都可以省略掉。但是,我们更寄期望于在容器编排工具中,使用环境变量的方式来初始化配置文件。为什么有这样的考虑?

当有多台机器的时候,我们需要一台一台去修改 Bind 的配置文件。虽然可以使用 Ansible 之类的自动化机制来做,但是在这种场景下把配置和服务分离并不太合适; 对以后做服务的自动伸缩机制友好。服务在伸缩之后,通过服务发现我们需要更新我们的 LB 的配置。如何更新?自然是通过我们的中心容器编排工具来自动更新。去触发脚本更新与我们的期望不符。

LICENSE

Source code: https://coding.net/u/Pelin_li/p/haproxy-keepalived/git

Based on MIT LICENSE: https://coding.net/u/Pelin_li/p/haproxy-keepalived/git/blob/master/LICENSE

相关内容