快速搭建高可用的 HAProxy 集群

孙博 技术分享
haproxy keepalived 高可用 网关

春节前为大家推送了一篇安装 Alpine Linux 的小短文,得到了不少小伙伴的认可,在此感谢朋友们的支持。

我在文中有提到,我安装 Alpine 的目的是希望运行一个低开销、高性能的 haproxy + keepalived 三节点集群,而高可用恰巧也是很多服务所需要的,所以就有小伙伴私下问我具体的操作步骤。我回顾了一下安装与配置过程,其实并不复杂,那么正好趁放假有时间,我就整理整理给大家再分享一些我的经验吧。

在正式开始之前,我先简单介绍下 haproxykeepalived

HAProxy

作为高性能 HTTP 负载均衡器之一,对很多朋友来说,可能相比 haproxy 来说更熟悉的是 nginx,或者是基于 nginx 衍生的 openresty 等同类产品。而 nginx 不仅可以用来转发请求,还能直接托管静态文件,那为什么要选择 haproxy 呢?让我们来看看 haproxy 在官网是如何定义自己的:

haproxy is a free, very fast and reliable reverse-proxy offering high availability, load balancing, and proxying for TCP and HTTP-based applications. It is particularly suited for very high traffic web sites and powers a significant portion of the world's most visited ones. Over the years it has become the de-facto standard opensource load balancer, is now shipped with most mainstream Linux distributions, and is often deployed by default in cloud platforms. Since it does not advertise itself, we only know it's used when the admins report it :-)

https://www.haproxy.org/

可见 haproxy 除了可以代理 7 层的流量之外,对 4 层的协议也能做到较好的支持,这就意味着 mysql 也能使用 虚拟IP 进行访问,而且我也不需要在网关直接处理静态文件的能力,所以在这里我选择了 haproxy

Keepalived

keepalived 对很多研发小伙伴来说稍显陌生,因为除了有自己搭建高可用的场景之外,基本没有必要部署这么底层的软件,尤其是在 k8s 自带 ingress 的前提下,基本能够实现故障自动漂移等常见的问题。

Keepalived is a routing software written in C. The main goal of this project is to provide simple and robust facilities for loadbalancing and high-availability to Linux system and Linux based infrastructures. Loadbalancing framework relies on well-known and widely used Linux Virtual Server (IPVS) kernel module providing Layer4 loadbalancing. Keepalived implements a set of checkers to dynamically and adaptively maintain and manage loadbalanced server pool according their health. On the other hand high-availability is achieved by VRRP protocol. VRRP is a fundamental brick for router failover. In addition, Keepalived implements a set of hooks to the VRRP finite state machine providing low-level and high-speed protocol interactions. In order to offer fastest network failure detection, Keepalived implements BFD protocol. VRRP state transition can take into account BFD hint to drive fast state transition. Keepalived frameworks can be used independently or all together to provide resilient infrastructures.

https://keepalived.org/

keepalived 依靠 VRRP协议 实现了故障自动漂移的能力,当主节点发生故障时,它可以迅速自动将 虚拟 IP 按照优先级降序尝试漂移到备节点。这样的话,我们只要将域名绑定给虚拟 IP,就可以实现当主节点的负载均衡器无法访问时,由备节点迅速接替,从而防止服务发生不可用的故障。


关于本文的两大主角已经介绍完了,下面开始展示安装过程。

我的系统环境是 Alpine Linux 3.21.2 (Released Jan 08, 2025),系统安装过程如前文所述,系统安装完成后,没有额外安装其他软件。系统运行在由 PVE 虚拟出的三台 1C0.5G 的实例中,宿主机分别是 DELL R420 + DELL R630 + DELL R730 三台洋垃圾,可以说是丐中丐版了。三台主机名分别为 k3s-lb-01k3s-lb-02k3s-lb-03,由于 keepalived 需要再配置中显式指定优先级,所以我计划 01 部署在有万兆网卡的 730 上作为主、02 和 03 分别部署在 630 和 420 上依次作为 备 1、备 2。

系统环境介绍完,下面开始进入正题。

系统初始化

正如之前所述,刚装完的系统干干净净什么都没有,而我们一般为了方便,通常会希望让我们管理的机器可以以统一的方式进行登陆,而 ssh 证书方式登陆是相对安全且易用的一种。对于小规模、一般安全密级的集群来说 —— 就像我家的个人机柜中的几台服务器那样,我们只需要在堡垒机上生成一对公钥与私钥,将公钥配置给 Alpine 的一个账户(如:root),就可以在堡垒机上使用 ssh -i ~/.ssh/YOUR_PRIVATE_KEY root@172.18.xx.xxx 免密登陆了。

下面给出我所使用的脚本(init.sh)供大家参考,我的脚本主要是安装了 openssh,并配置 root 账户可以使用证书方式远程登陆、以及将登陆用的公钥配置给 root,同时确保 sshd 可以开机自启。

#!/bin/sh

# 安装 SSH
apk add --no-cache openssh

# 配置 SSH
sed -i "s/#PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config
sed -i "s/#Port.*/Port 22/g" /etc/ssh/sshd_config
# 推荐:禁用密码登录,只允许密钥登录
sed -i "s/#PasswordAuthentication.*/PasswordAuthentication no/g" /etc/ssh/sshd_config
sed -i "s/#PubkeyAuthentication.*/PubkeyAuthentication yes/g" /etc/ssh/sshd_config

# 生成主机密钥
ssh-keygen -A

# 配置密钥登录
echo 'Configure authorized_keys'
mkdir -p /root/.ssh/
touch /root/.ssh/authorized_keys
echo "CONTENT_OF_YOUR_PUBLIC_KEY" >> /root/.ssh/authorized_keys
chmod 700 /root/.ssh/
chmod 600 /root/.ssh/authorized_keys

# 添加到开机自启
rc-update add sshd default

# 启动 SSH 服务
rc-service sshd start

# 检查状态
rc-status sshd

# 重启
reboot

如果你希望直接使用上述脚本,只要替换公钥内容即可。

另外,本文是面向 Alpine 操作系统设计的,如果你仍然在使用 CentOS 的话,需要将 apk add 更换为 yum install,将 rc-updaterc-servicerc-statsus 命令更换为 systemctl 相关的指令。其他操作系统类似,请使用对应发行版常用的包管理工具命令替换。

脚本写完后,我们需要想办法让新装的这台机器可以直接执行。虽然使用 U盘 也是一种方案,但使用起来非常麻烦。就像尽管我是家庭机柜,但仍然需要跑到地下室去操作,就更别提在公司大规模配置时,IDC 里茫茫多的机器,光是找到就很困难的情况了。

如果你是和我一样,通过 PVE 做了虚拟化,但因此时的 Alpine 默认没有安装过 qemu-guest-agent,甚至都无法通过复制指令的方式来进行初始化就更麻烦了。因此,我们可以将脚本放在内网可达的路径上,先使用 apk add curl 的方式安装 curl,再通过 curl http(s)://xxxx.xx.xxx/init.sh | sh 的形式执行脚本做初始化,这样就可以再堡垒机上通过更高级的 Shell 工具(如 XShellWSL 等)连入了。

配置初始化

在运行 haproxykeepalived 之前,我们可以先把配置提前写好拷贝到机器中。

有关 haproxy 的配置我就不提太多了,既然小伙伴都想用了,那肯定是明白应该配置哪些内容。我的 haproxy.cfg 也很普通,唯一有些特殊的,就是在配置 stats 监控页面页面时,与很多网上的例子不同,我没有将明文的密码写到配置文件中,而是对齐进行了加密。

很多小伙伴在翻看网上的例子时,给出的大多是:stats auth admin:123456 这种形式的配置,而这种配置其实是很不安全的。很多时候我们为了方便,都是将配置统一保管在某个远程地址中,而如果一旦被人非法获取到,就能被人直接知道密码的内容,所以我们可以尝试使用加密的手段来进行配置。

还记得刚才在系统初始化时让你安装的 openssh 么,你只需要简单执行以下语句即可,而且多次、或者在多台负载分别执行 openssl passwd -1 YOUR_PASSWORD 生成的内容也会不一样,这是因为它每次会使用不同的 盐值 与你的密码合并生成一个 MD5 的结果。尽管 MD5 并不是一个特别可靠的加密手段,但总归还是比明文要安全多了。

k3s-lb-01:~/workspace# openssl passwd -1 123456
$1$yAC7DVoB$UNNZ5vehLU/mbLKBzVdzi1
k3s-lb-01:~/workspace# openssl passwd -1 123456
$1$PMa.Uoxa$QM8p/5sivux.N7Zv8iC4H1
k3s-lb-01:~/workspace# openssl passwd -1 123456
$1$HTymhWSG$8LJiZV0hZpo.JBYjcC1pz0
k3s-lb-01:~/workspace#

由于我的 haproxy.cfg 中有许多内网地址,我就不贴出来给大家看了,有需要参考可以随便在网上找些其他例子,也可以参考默认的配置,根据自己的需要做调整:

#---------------------------------------------------------------------
# Example configuration for a possible web application.  See the
# full configuration options online.
#
#   http://haproxy.1wt.eu/download/1.5/doc/configuration.txt
#
#---------------------------------------------------------------------

#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    # to have these messages end up in /var/log/haproxy.log you will
    # need to:
    #
    # 1) configure syslog to accept network log events.  This is done
    #    by adding the '-r' option to the SYSLOGD_OPTIONS in
    #    /etc/sysconfig/syslog
    #
    # 2) configure local2 events to go to the /var/log/haproxy.log
    #   file. A line like the following can be added to
    #   /etc/sysconfig/syslog
    #
    #    local2.*                       /var/log/haproxy.log
    #
    log         127.0.0.1 local2

    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

#---------------------------------------------------------------------
# main frontend which proxys to the backends
#---------------------------------------------------------------------
frontend main
    bind *:5000
    acl url_static       path_beg       -i /static /images /javascript /stylesheets
    acl url_static       path_end       -i .jpg .gif .png .css .js

    use_backend static          if url_static
    default_backend             app

#---------------------------------------------------------------------
# static backend for serving up images, stylesheets and such
#---------------------------------------------------------------------
backend static
    balance     roundrobin
    server      static 127.0.0.1:4331 check

#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
backend app
    balance     roundrobin
    server  app1 127.0.0.1:5001 check
    server  app2 127.0.0.1:5002 check
    server  app3 127.0.0.1:5003 check
    server  app4 127.0.0.1:5004 check

而对于 keepalived 配置就更加简单了。它可以让你通过自定义的脚本检查 haproxy 是不是正常运行,如果有问题的话,则会自动的将你指定的 虚拟IP 根据负载的优先级顺序降序切换到其他机器上,直到更高优先级的负载重新正常工作。

根据大多数一贯的做法,大家都是基于 killall -0 haproxy 的方式进行检测,虽然 killall 名字看起来吓人,但你使用 -0 的话并不会真的杀掉进程。假如进程仍然存活,则会返回状态码 0,而这通常意味着正常存活。

另外需要注意的是,整个 keepalived 集群中的所有负载,都必须有一个唯一不重复的 router_idpriority,下面我给大家看下我的配置,同时会做出一些解释。

! Configuration File for keepalived

global_defs {
   router_id k3s-lb-01
}

vrrp_script chk_haproxy {
    script 'killall -0 haproxy'
    interval 2
    weight -2
    fall 2       # 连续2次失败才认为服务下线
    rise 1       # 1次成功就认为服务上线
}

vrrp_instance haproxy-vip {
    interface eth0
    state MASTER
    priority 200

    virtual_router_id 51

    virtual_ipaddress {
        172.18.xx.yyy/24
    }

    track_script {
        chk_haproxy
    }

    notify_master "/etc/keepalived/notify_master.sh"
    notify_backup "/etc/keepalived/notify_backup.sh"
    notify_fault "/etc/keepalived/notify_fault.sh"

    authentication {
        auth_type PASS
        auth_pass YOUR_PASSWORD
    }
}

这个是我主节点的配置,它的路由 ID 与主机名一致,是 k3s-lb-01,默认 state 是 MASTER,优先级是 200,虚拟 IP 组的 ID 是 51,地址是 172.18.xx.yyy/24

那么对于其他备用节点来说,还有 k3s-lb-02k3s-lb-03 两个 BACKUP 的节点,优先级分别为 150100,而其他配置都是与主节点一致的。

至于 notify_master、notify_backup、notify_fault 的作用是在当节点状态发生切换时,可以帮你执行的脚本。你可以按自己需要配置,也可以不配置,直接忽略这几个项目即可。

软件初始化

配置文件都配好后,我们就可以安装软件了。

为了省事、以及所有机器的结果一致性,我会将需要安装的软件、或需要更改的配置,通过一段统一的脚本来进行,以下就是我所用的脚本内容(install.sh):

# 添加 edge 仓库
cat > /etc/apk/repositories << EOF
http://mirrors.aliyun.com/alpine/v3.21/main
http://mirrors.aliyun.com/alpine/v3.21/community
EOF

# 更新软件源
apk update

# 安装 bash
apk add --no-cache bash

# 安装 qemu-guest-agent
apk add --no-cache qemu-guest-agent

# 查找第一个可用的 vport 设备
VPORT_PATH=$(ls /dev/vport* 2>/dev/null | head -n 1)

if [ -n "$VPORT_PATH" ]; then
    echo "GA_PATH=\"$VPORT_PATH\"" > /etc/conf.d/qemu-guest-agent
    echo "Configured QEMU Guest Agent to use $VPORT_PATH"
else
    echo "Error: No vport device found"
    echo "Please check if virtio-serial device is properly configured in VM"
    exit 1
fi

# 安装最新版本的 HAProxy 和 Keepalived
apk add --no-cache haproxy keepalived

# 查看安装的版本
haproxy -v
keepalived -v

# 确保 keepalived 配置目录存在
mkdir -p /etc/keepalived

# 如果当前目录存在 keepalived.conf,则复制到配置目录
if [ -f "./keepalived.conf" ]; then
    cp "./keepalived.conf" /etc/keepalived/keepalived.conf
else
    echo "Error: keepalived.conf not found in current directory"
    echo "Please create keepalived configuration before starting the service"
    exit 1
fi

# 设置开机自启
rc-update add haproxy default
rc-update add keepalived default
rc-update add qemu-guest-agent default

# 启动服务
rc-service haproxy start
rc-service keepalived start
rc-service qemu-guest-agent start

# 检查状态
rc-status

# 验证服务是否正常启动
if ! rc-service keepalived status | grep -q "started"; then
    echo "Error: Keepalived failed to start"
    exit 1
fi

if ! rc-service haproxy status | grep -q "started"; then
    echo "Error: HAProxy failed to start"
    exit 1
fi

if ! rc-service qemu-guest-agent status | grep -q "started"; then
    echo "Error: QEMU Guest Agent failed to start"
    exit 1
fi

echo "Installation completed successfully"

其中需要解释以下的是 qemu-guest-agent,它可以用来在 PVE 与 虚拟机 之间建立通信,帮助 PVE 更好的管理、监控虚拟机。如果你并不是运行在 PVE 中的,你也可以将相关的代码全部移除。另外,与 CentOS 不同的是,Alpine 并不能直接使用 qemu-guest-agent 的默认配置,所以需要手动找到虚拟端口设备。如果你按照我之前给出的安装 Alpine 文章中所列出的步骤和版本安装系统的话,你很可能可以直接使用这段代码。

到这里为止,一些就都准备就绪了。


但是,如果我们的配置文件发生变更,需要更换配置时,需要怎么办?

其实一个标准的做法是,手动修改每台机器上的 haproxy.cfg,再依次到每台机器执行 rc-service haproxy restart

感觉好麻烦对不对?所以我再给大家提供一个相对简单的方式 —— 你只需要把 haproxy.cfg 事先写好,再想办法复制到你的 haproxy 负载上,利用一个脚本完成重启即可。而“复制”的方式有很多,最便捷的是通过 git,但这意味着你需要额外再进行安装及权限配置;还有一个办法就是我在用的,因为我的机器仅有 3 台,所以我就干脆直接依次登陆到这三台机器上,将 haproxy.cfgkeepalived.conf 保存在 ~/workspace 中,每当需要更新时变使用 vi 编辑 haproxy.cfg,使用 dG 清空内容,再按 i 进入编辑模式,利用 Shift + Insert 将我在其他地方编辑好的配置粘贴进来,完成之后按 ESC 输入 :wq 覆盖保存,接着运行提前保存好的 restart-haproxy.sh 脚本便可完成替换。

脚本内容如下:

#!/bin/bash

# 设置错误时退出
set -e

# 配置文件路径
HAPROXY_CFG="/etc/haproxy/haproxy.cfg"
BACKUP_CFG="/etc/haproxy/haproxy.cfg.backup"
NEW_CFG="$PWD/haproxy.cfg"
LOG_FILE="/var/log/haproxy-restart.log"

# 记录日志的函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 还原配置文件的函数
restore_config() {
    log "Restoring original configuration..."
    if [ -f "$BACKUP_CFG" ]; then
        cp "$BACKUP_CFG" "$HAPROXY_CFG"
        rc-service haproxy restart
        if rc-service haproxy status | grep -q "started"; then
            log "Successfully restored to previous configuration"
        else
            log "ERROR: Failed to restore HAProxy service!"
            exit 1
        fi
    else
        log "ERROR: Backup file not found!"
        exit 1
    fi
}

# 检查是否为 root 用户
if [ "$(id -u)" != "0" ]; then
    log "Error: This script must be run as root"
    exit 1
fi

# 检查新配置文件是否存在
if [ ! -f "$NEW_CFG" ]; then
    log "Error: New configuration file not found at $NEW_CFG"
    exit 1
fi

# 备份当前配置
log "Backing up current configuration..."
cp "$HAPROXY_CFG" "$BACKUP_CFG"

# 检查新配置文件语法
log "Checking new HAProxy configuration..."
if ! haproxy -c -f "$NEW_CFG"; then
    log "Error: New configuration test failed"
    exit 1
fi

# 替换配置文件
log "Applying new configuration..."
cp "$NEW_CFG" "$HAPROXY_CFG"

# 检查并安装通知脚本
install_notify_scripts() {
    local script_dir="/etc/keepalived"

    # 确保目录存在
    mkdir -p "$script_dir"

    # 创建通知脚本的函数
    create_notify_script() {
        local state=$1
        local state_upper=$(echo $state | tr '[:lower:]' '[:upper:]')
        local script_path="$script_dir/notify_${state}.sh"

        if [ ! -f "$script_path" ]; then
            log "Creating notify_${state}.sh"
            cat > "$script_path" << EOF
#!/bin/sh

# 记录日志的函数
log() {
    logger -t keepalived-notify "[${state_upper}] \$*"
    echo "\$(date '+%Y-%m-%d %H:%M:%S') [${state_upper}] \$*" >> /var/log/keepalived-state-change.log
}

# 记录状态变更
log "Becoming ${state_upper} state"
EOF
            chmod +x "$script_path"
        fi
    }

    # 创建各种状态的通知脚本
    create_notify_script "master"
    create_notify_script "backup"
    create_notify_script "fault"

    # 创建日志文件
    touch /var/log/keepalived-state-change.log
    chmod 644 /var/log/keepalived-state-change.log
}

# 检查并安装 keepalived 配置
install_keepalived_config() {
    local config_dir="/etc/keepalived"
    local config_file="$config_dir/keepalived.conf"
    local backup_file="$config_dir/keepalived.conf.backup.$(date +%Y%m%d_%H%M%S)"
    local hostname=$(hostname)

    # 确保目录存在
    mkdir -p "$config_dir"

    # 如果配置文件存在,先备份
    if [ -f "$config_file" ]; then
        log "Backing up existing keepalived configuration to $backup_file"
        cp "$config_file" "$backup_file"
    fi

    if [ -f "./keepalived.conf" ]; then
        log "Installing new keepalived configuration"

        if [ "$hostname" = "k3s-lb-01" ]; then
            # 主节点直接复制
            cp "./keepalived.conf" "$config_file"
            log "Installed keepalived.conf for master node $hostname"
        else
            # 备用节点需要修改配置
            local priority
            case "$hostname" in
                "k3s-lb-02") priority=150 ;;
                "k3s-lb-03") priority=100 ;;
                *)
                    log "Error: Unknown hostname $hostname"
                    return 1
                ;;
            esac

            # 修改配置文件
            sed "s/router_id k3s-lb-01/router_id $hostname/" "./keepalived.conf" | \
            sed "s/state MASTER/state BACKUP/" | \
            sed "s/priority 200/priority $priority/" > "$config_file"

            log "Modified keepalived.conf for backup node $hostname with priority $priority"
        fi
    else
        log "Error: keepalived.conf not found in current directory"
        return 1
    fi
}

# 在安装通知脚本之前安装配置文件
install_keepalived_config
install_notify_scripts

# 尝试重启 HAProxy
log "Attempting to restart HAProxy..."
if ! rc-service haproxy restart; then
    log "Error: HAProxy restart failed, rolling back..."
    restore_config
    exit 1
fi

# 等待服务启动
sleep 2

# 验证服务是否正常运行
if ! rc-service haproxy status | grep -q "started"; then
    log "Error: HAProxy failed to start, rolling back..."
    restore_config
    exit 1
fi

# 检查关键端口
PORTS_OK=true
for port in 8404 80; do
    if ! netstat -tlpn | grep -q ":$port"; then
        log "Warning: Port $port is not listening"
        PORTS_OK=false
    fi
done

if [ "$PORTS_OK" = false ]; then
    log "Error: Not all required ports are listening, rolling back..."
    restore_config
    exit 1
fi

log "HAProxy successfully restarted with new configuration"
log "Backup file stored at: $BACKUP_CFG"
exit 0

从这个脚本可以看出来,在重启 haproxy 前,会先备份之前的配置文件,如果运行一切正常,则继续,否则会自动进行回滚。同时,该脚本会试图根据不同的主机名来更新 keepalived.conf 的配置,非常适合集群规模不大的场景。

今天的字数很多,但大多是真正意义上可执行的脚本,希望对看到这里的你能有一些帮助。

在将 haproxy.cfgkeepalived.confinstall.shrestart-haproxy.sh 提前拷贝到 k3s-lb-01k3s-lb-02k3s-lb-03,并将 init.sh 提前部署在内网的情况下,全部完成到可用大致耗时 3 分钟左右,如果使用 vi 拷贝文件,额外需要大约 1 分钟,如有兴趣,建议一试。


全文完