本文主要介绍把 HAProxy + Keepalived 服务组合形成单独的镜像,并能够自定义配置项。适合具有 Docker 和 Bash 相关基础的开发、运维等同学。
上图为我们要构建的 haproxy-keepalived 服务的工作原理图,原理表述如下:
START:
STOP:
下边会按照上述步骤介绍如何工作。
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
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 ---------------------------"
查看 Dockerfile 文件,这个 haproxy-keepalived 镜像是基于 haproxy:1.7.9 基础镜像来做的修改。为了符合一个容器启动 HAProxy & Keepalived 两个服务的预期,我们增加了一个 docker-entrypoint-override.sh 的脚本来作为 haproxy-keepalived 镜像的 ENTRYPOINT。
在 docker-entrypoint-override.sh 中,我们分别用如下两条命令启动来启动两个服务:
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 的操作,在这里的目标就是:容器关闭前干掉 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
目前实现的功能:
TODO:
如果需要自定义一个 haproxy-keepalived 镜像的话,可以参考上边的思路来做。当然,如果场景可以使用 Bind 的方式,那自然是更简单了。
如果可以直接 Bind 配置文件进容器,上述很多脚本都可以省略掉。但是,我们更寄期望于在容器编排工具中,使用环境变量的方式来初始化配置文件。为什么有这样的考虑?
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