目录
[设置主机名与 hosts(两台均执行)](#设置主机名与 hosts(两台均执行))
[安装 GitLab 和相关依赖(两台都装)](#安装 GitLab 和相关依赖(两台都装))
[备节点禁止 GitLab 自动启动](#备节点禁止 GitLab 自动启动)
[配置 SSH 免密登录](#配置 SSH 免密登录)
[PostgreSQL 流复制](#PostgreSQL 流复制)
[修改主节点 gitlab.rb 配置](#修改主节点 gitlab.rb 配置)
[备节点 GitLab 配置(设置为流复制从库)](#备节点 GitLab 配置(设置为流复制从库))
[修改备节点 gitlab.rb 配置](#修改备节点 gitlab.rb 配置)
[停止备节点 GitLab 服务](#停止备节点 GitLab 服务)
[在备节点启动 PostgreSQL 为从库](#在备节点启动 PostgreSQL 为从库)
[方式2:配置 lsyncd(与 rsync 二选一即可)](#方式2:配置 lsyncd(与 rsync 二选一即可))
[启动 lsyncd](#启动 lsyncd)
[验证 lsyncd 运行状态](#验证 lsyncd 运行状态)
[配置 Keepalived](#配置 Keepalived)
[启动 Keepalived(两台)](#启动 Keepalived(两台))
[允许 VRRP 协议入站](#允许 VRRP 协议入站)
[验证 VIP(主节点应有)](#验证 VIP(主节点应有))
[GitLab 服务控制](#GitLab 服务控制)
[Keepalived 控制](#Keepalived 控制)
[验证 VIP 是否生效](#验证 VIP 是否生效)
[GitLab 高可用(HA)运维手册](#GitLab 高可用(HA)运维手册)
[手动恢复从机身份(降级为 standby)](#手动恢复从机身份(降级为 standby))
本方案通过在两台物理服务器部署 GitLab,结合 Rsyncd 实现实时文件同步、Keepalived 实现虚拟 IP(VIP)漂移和主备自动切换,搭建高可用(HA)GitLab 服务架构。
- 主节点(gitlab-primary):正常运行 GitLab 服务,对外提供访问。
- 备节点(gitlab-standby):实时同步主节点配置与数据,待命状态;主节点故障时,自动接管 VIP 并启动 GitLab 服务。
- 共享 VIP:192.168.252.140/24,客户端始终通过该地址访问 GitLab。
注意:本方案适用于 GitLab Omnibus 安装方式(官方包安装),不适用于源码或 Docker 部署。
环境准备
主机信息
| 角色 | 主机名 | IP 地址 | 操作系统 |
|---|---|---|---|
| 主节点 | gitlab-primary | 192.168.252.102 | Ubuntu 24+ |
| 备节点 | gitlab-standby | 192.168.252.103 | 同主节点 |
文档中部分命令示例使用 192.168.252.150 作为主节点 IP,需根据实际环境替换为 192.168.252.102。
设置主机名与 hosts(两台均执行)
主节点(192.168.252.102)
sudo hostnamectl set-hostname gitlab-primary
echo "192.168.252.102 gitlab-primary" | sudo tee -a /etc/hosts
echo "192.168.252.103 gitlab-standby" | sudo tee -a /etc/hosts
备节点(192.168.252.103)
sudo hostnamectl set-hostname gitlab-standby
echo "192.168.252.102 gitlab-primary" | sudo tee -a /etc/hosts
echo "192.168.252.103 gitlab-standby" | sudo tee -a /etc/hosts
使用 ip a 确认网卡名称(如 ens33、eth0、enp0s3 等),后续 Keepalived 配置需用到。
安装 GitLab 和相关依赖(两台都装)
# 安装依赖
sudo apt update && sudo apt install -y lsyncd keepalived rsync
# 添加官方源(清华镜像)
rm -rf /usr/share/keyrings/gitlab_gitlab-ce-archive-keyring.gpg
curl -fsSL https://packages.gitlab.com/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/gitlab_gitlab-ce-archive-keyring.gpg
rm -rf /etc/apt/sources.list.d/gitlab_gitlab-ce.list
echo "deb [signed-by=/usr/share/keyrings/gitlab_gitlab-ce-archive-keyring.gpg] https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/ubuntu/noble noble main" | sudo tee /etc/apt/sources.list.d/gitlab-ce.list
# 安装 GitLab CE
sudo apt update && sudo apt install -y gitlab-ce
仅在主节点初始化(192.168.252.102 上运行):
gitlab-ctl reconfigure不要在 192.168.252.103 上运行 reconfigure,先保持未初始化状态。
备节点禁止 GitLab 自动启动
gitlab-ctl stop
systemctl disable gitlab-runsvdir
配置 SSH 免密登录
主节点配置免密登录备节点
ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
ssh-copy-id gitlab-standby
验证命令:
ssh 'gitlab-standby'

备节点配置免密登录主节点
ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
ssh-copy-id gitlab-primary
PostgreSQL 流复制
修改主节点 gitlab.rb 配置
编辑 /etc/gitlab/gitlab.rb,添加以下配置:
# 基础配置
external_url 'http://192.168.252.140'
# 允许数据库监听物理网卡 IP 以供备机连接
postgresql['listen_address'] = '127.0.0.1,192.168.252.150'
# 配置流复制专用虚拟用户及密码
postgresql['sql_replication_user'] = "gitlab_replica"
postgresql['sql_replication_password'] = "SecureReplicaPass123!"
# 设置认证策略:仅允许备机 IP 通过 md5 方式进行 replication 连接
postgresql['pg_hba_replication'] = [
"host replication gitlab_replica 192.168.252.103/32 md5"
]
# 允许内网网段访问
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32', '192.168.252.0/24']
# 关键同步参数:设置日志级别为 replica 并预留发送槽位
postgresql['wal_level'] = "replica"
postgresql['max_wal_senders'] = 10
postgresql['max_replication_slots'] = 10

应用配置并重启(主节点)
sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart postgresql
备节点 GitLab 配置(设置为流复制从库)
修改备节点 gitlab.rb 配置
编辑 /etc/gitlab/gitlab.rb:
# 基础配置(与主节点保持一致)
external_url 'http://192.168.252.140'
停止备节点 GitLab 服务
sudo gitlab-ctl stop
在备节点启动 PostgreSQL 为从库
/opt/gitlab-ha/demote_to_standby.sh
# 验证复制状态
sudo gitlab-psql -c "SELECT * FROM pg_stat_replication;"

成功标志:disabled,表示当前为只读从库。
文件同步
方式1:rsync(推荐)
创建同步脚本
touch /opt/gitlab-ha/sync-gitlab.sh;chmod +x /opt/gitlab-ha/sync-gitlab.sh
脚本内容:
#!/bin/bash
# 关键部分注释:针对 GitLab 高性能热备数据同步脚本
# 行为:仅当 GitLab 存活时执行同步;否则跳过,不报错。
set -e
REMOTE_IP="192.168.252.103"
LOG_FILE="/var/log/gitlab_data_rsync.log"
log_info() {
local msg="$1"
printf "%s [INFO] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$msg" | tee -a "$LOG_FILE"
}
# 返回 0 表示 GitLab 存活,1 表示不存活
is_gitlab_alive() {
# 检查 gitlab-ctl status 是否所有关键服务都在运行
if ! output=$(gitlab-ctl status 2>&1); then
log_info "GitLab 状态检查命令失败,判定为不存活。"
return 1
fi
# 检查是否有任何服务处于 stopped 或 fail 状态
if echo "$output" | grep -qE "(down|fail|stop)"; then
log_info "检测到 GitLab 有服务未运行,判定为不存活。"
return 1
fi
# 进一步确认关键进程存在(如 puma、sidekiq)
if ! echo "$output" | grep -q "run: puma:" || ! echo "$output" | grep -q "run: sidekiq:"; then
log_info "关键服务(puma 或 sidekiq)未运行,判定为不存活。"
return 1
fi
return 0
}
# 加载钉钉发送函数
source /opt/gitlab-ha/send_dingtalk.sh
report_stats() {
local stats_output="$1"
local duration_sec="$2" # 接收耗时(秒)
local files_transferred=$(echo "$stats_output" | grep "Number of regular files transferred" | awk -F': ' '{print $2}' | xargs)
local total_size=$(echo "$stats_output" | grep "total size is" | awk '{print $4}')
local sent_size=$(echo "$stats_output" | grep -E "^sent " | awk '{print $2}')
# 格式化耗时为"X分Y秒"
local minutes=$((duration_sec / 60))
local seconds=$((duration_sec % 60))
local duration_str="${minutes}分${seconds}秒"
log_info "指标汇总 | 传输文件: ${files_transferred:-0} | 数据总量: ${total_size:-0} | 实际发送: ${sent_size:-0} | 耗时: ${duration_str}"
send_dingtalk_message "[$(date '+%Y-%m-%d %H:%M:%S')] 主机已完成Gitlab数据同步(不含postgresql数据库),指标汇总:
- 传输文件: ${files_transferred:-0}
- 数据总量: ${total_size:-0}
- 实际发送: ${sent_size:-0}
- 同步耗时: ${duration_str}"
}
sync_data_volumes() {
log_info "开始同步 GitLab 静态数据..."
local start_ts=$SECONDS
local tmp_output=$(mktemp)
rsync -aH -h --delete --numeric-ids --inplace --sparse --stats --outbuf=L \
--exclude='postgresql/' \
--exclude='redis/' \
--exclude='*.sock' \
--exclude='gitlab.rb' \
/var/opt/gitlab/ "${REMOTE_IP}:/var/opt/gitlab/" 2>&1 | tee "$tmp_output"
local result=$(cat "$tmp_output")
rm -f "$tmp_output"
local end_ts=$SECONDS
local duration=$((end_ts - start_ts))
report_stats "$result" "$duration"
log_info "耗时统计: $(( duration / 60 ))分$(( duration % 60 ))秒"
}
# ===== 主流程 =====
log_info "=========================================="
log_info "启动 GitLab 热备数据同步任务..."
if is_gitlab_alive; then
log_info "GitLab 存活,执行数据同步。"
sync_data_volumes
log_info "同步任务完成"
else
log_info "GitLab 未存活,跳过本次同步。"
fi
log_info "=========================================="
添加定时任务(主节点)
root@gitlab-primary:/opt/gitlab-ha# crontab -l
*/10 * * * * /bin/bash /opt/gitlab-ha/sync-gitlab.sh >> /var/log/gitlab_rsync.log 2>&1
方式2:配置 lsyncd(与 rsync 二选一即可)
创建配置文件
mkdir -p /etc/lsyncd
cat > /etc/lsyncd/lsyncd.conf.lua << 'EOF'
-- /etc/lsyncd/lsyncd.conf.lua
-- 针对 32核/64GB 配置的性能调优版
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
inotifyMode = "CloseWrite",
-- 充分利用 32 核 CPU,并发进程数上调
maxProcesses = 16,
-- 聚合 2000 个事件或等待 3 秒后触发同步,减少 SSD 小碎文件频繁 I/O
maxDelays = 2000,
delay = 3,
}
-- 同步配置目录
sync {
default.rsync,
source = "/etc/gitlab/",
target = "192.168.252.103:/etc/gitlab/",
rsync = {
archive = true,
compress = false -- SSD 环境下,局域网内可关闭压缩以节省 CPU
}
}
-- 同步数据目录
sync {
default.rsync,
source = "/var/opt/gitlab/",
target = "192.168.252.103:/var/opt/gitlab/",
rsync = {
archive = true,
compress = false,
-- 核心优化:排除 pgsql 物理目录以防数据库损坏
exclude = {
"postgresql/",
"redis/",
"**/*.sock",
"log/",
"tmp/",
"gitlab-rails/working/",
"nginx/proxy_cache/"
},
-- 针对 SSD 优化:跳过对未改变文件的校验,提升速度
_extra = { "--inplace", "--numeric-ids" }
}
}
EOF
启动 lsyncd
systemctl enable --now lsyncd
验证 lsyncd 运行状态
-
查看 lsyncd 运行状态
systemctl status lsyncd
-
监控实时同步日志
tail -f /var/log/lsyncd/lsyncd.log
-
查看同步统计概览
cat /var/log/lsyncd/lsyncd.status
文件一致性抽验(推荐)
-
比对配置文件(主备机分别执行)
md5sum /etc/gitlab/gitlab.rb
-
比对 Git 仓库大小(主备机分别执行)
du -sh /var/opt/gitlab/git-data/repositories
功能性验证(最可靠)
-
新建测试项目
-
在主节点(192.168.252.150)网页端创建新项目 sync-test
-
等待 3-5 秒(对应 lsyncd 配置的 delay = 3)
-
在备节点(192.168.252.103)查看项目是否存在:
ls -lh /var/opt/gitlab/git-data/repositories/@hashed/ | grep [项目哈希相关目录]
-
-
推送代码测试
- 向主节点推送较大 Commit
- 在备节点观察 /var/log/lsyncd/lsyncd.log,确认有 rsync 任务产生
关键排除项确认
检查备机数据库文件时间戳,确认 postgresql/ 目录未被同步:
# 在备机执行
ls -ld /var/opt/gitlab/postgresql/data
若文件修改时间停留在最后一次手动执行 force-full-sync.sh 的时间,说明 exclude 配置生效。
健康检查脚本
mkdir -p /opt/gitlab-ha
cat > /opt/gitlab-ha/check_gitlab_health.sh << 'EOF'
#!/bin/bash
# /opt/gitlab-ha/check_gitlab_health.sh
# 日志文件路径
LOG_FILE="/var/log/gitlab-health-check.log"
# 最大日志文件大小,单位:字节(此处为 5MB)
MAX_LOG_SIZE=5242880
# 用于 syslog 的标签
LOG_TAG="gitlab-health-check"
# 日志记录函数:同时输出到终端和日志文件,并自动限制日志大小
log() {
local msg="[ $(date '+%Y-%m-%d %H:%M:%S') ] $1"
echo "$msg" # 输出到标准输出(便于手动运行时查看)
# 检查日志文件是否存在且超过最大允许大小
if [[ -f "$LOG_FILE" ]] && [[ $(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0) -gt $MAX_LOG_SIZE ]]; then
# 清空日志文件,并记录轮转信息
echo "[ $(date '+%Y-%m-%d %H:%M:%S') ] Log file exceeded ${MAX_LOG_SIZE} bytes, truncating old log." > "$LOG_FILE"
fi
# 追加当前日志到文件
echo "$msg" >> "$LOG_FILE"
# 可选:同时写入系统日志(如 /var/log/syslog),失败则忽略
logger -t "$LOG_TAG" "$msg" 2>/dev/null || true
}
log "开始检查 GitLab 核心服务状态..."
# 检查 PostgreSQL 服务是否运行
if ! /usr/bin/gitlab-ctl status postgresql | grep -q "^run:"; then
log "检查失败:postgresql 未运行"
exit 1
else
log "检查通过:postgresql 正常运行"
fi
# 检查 Puma(GitLab Web 服务)是否运行
if ! /usr/bin/gitlab-ctl status puma | grep -q "^run:"; then
log "检查失败:puma 未运行"
exit 1
else
log "检查通过:puma 正常运行"
fi
# 检查 Nginx(前端反向代理)是否运行(可选但推荐)
if ! /usr/bin/gitlab-ctl status nginx | grep -q "^run:"; then
log "检查失败:nginx 未运行"
exit 1
else
log "检查通过:nginx 正常运行"
fi
log "所有核心组件均正常,GitLab 服务健康"
exit 0
EOF
chmod +x /opt/gitlab-ha/check_gitlab_health.sh
配置 Keepalived
主节点(192.168.252.102)
编辑 /etc/keepalived/keepalived.conf(将 ens5f3 替换为实际网卡名):
vrrp_script chk_gitlab {
script "/opt/gitlab-ha/check_gitlab_health.sh"
interval 10
weight -20
fall 2
rise 1
}
vrrp_instance VI_1 {
state MASTER
interface ens5f3
virtual_router_id 51
priority 100
advert_int 2
nopreempt # 禁止自动抢占
authentication {
auth_type PASS
auth_pass GitLabHA2026!
}
virtual_ipaddress {
192.168.252.140/24
}
track_script {
chk_gitlab
}
}
备节点(192.168.252.103)
编辑 /etc/keepalived/keepalived.conf(将 ens5f3 替换为实际网卡名):
vrrp_instance VI_1 {
state BACKUP
interface ens5f3
virtual_router_id 51
priority 90 # 确保 90 大于主机降级后的 80 (100-20)
advert_int 2
nopreempt # 建议开启非抢占模式,防止主机重启后立刻抢回
# 备机不要 track_script,否则优先级会一直处于 90-20=70 的状态
# track_script {
# chk_gitlab
# }
# 必须添加认证,否则无法接收主机的 VRRP 信号
authentication {
auth_type PASS
auth_pass GitLabHA2026!
}
virtual_ipaddress {
192.168.252.140/24
}
notify_master "/opt/gitlab-ha/takeover.sh"
notify_backup "/opt/gitlab-ha/demote_to_standby.sh"
}
启动 Keepalived(两台)
sudo systemctl enable --now keepalived
允许 VRRP 协议入站
sudo iptables -A INPUT -p 112 -j ACCEPT
# 保存规则(Ubuntu 24.04)
sudo apt install -y iptables-persistent
sudo netfilter-persistent save

验证 VIP(主节点应有)
ip addr show ens5f3 | grep 192.168.252.140
核心脚本
takeover.sh(备节点使用)
创建脚本文件
cd /opt/gitlab-ha
touch takeover.sh
chmod +x takeover.sh
脚本内容
#!/bin/bash
# 函数注释:当 Keepalived 切换至 MASTER 状态时,自动提升 PostgreSQL 并启动 GitLab 业务
# 修复说明:确保数据库在运行状态下接收 promote 信号,避免 PID 缺失错误
# 增强:增加 GitLab 服务健康检查,失败时自动重试 reconfigure + restart
LOG_FILE="/var/log/gitlab-ha.log"
DATA_DIR="/var/opt/gitlab/postgresql/data"
MAX_RETRIES=3
HEALTH_CHECK_URL="http://localhost/-/health"
# 加载钉钉发送函数
source /opt/gitlab-ha/send_dingtalk.sh
send_dingtalk_message "警告:从机正在自动提权为主机,请检查主机!!!"
echo "$(date): [MASTER] VIP detected, initiating promotion..." >> "$LOG_FILE"
# 1. 检查数据目录
if [ ! -d "$DATA_DIR" ]; then
echo "$(date): ERROR - PostgreSQL data directory not found!" >> "$LOG_FILE"
exit 1
fi
# 2. 确保数据库正在运行,以便接收提升信号
if ! sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_ctl -D "$DATA_DIR" status > /dev/null 2>&1; then
echo "$(date): Postgres is NOT running. Cleaning up and starting..." >> "$LOG_FILE"
rm -f "$DATA_DIR/postmaster.pid"
gitlab-ctl start postgresql
sleep 5 # 给予更充分的启动时间
fi
# 3. 执行提升操作
echo "$(date): Executing pg_ctl promote..." >> "$LOG_FILE"
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_ctl -D "$DATA_DIR" promote >> "$LOG_FILE" 2>&1
# 等待几秒让 promote 生效
sleep 5
# 4. 启动/刷新 GitLab 全量服务
echo "$(date): Reconfiguring and starting GitLab services..." >> "$LOG_FILE"
gitlab-ctl reconfigure >> "$LOG_FILE" 2>&1
gitlab-ctl restart >> "$LOG_FILE" 2>&1
# 5. 健康检查 + 自动重试
attempt=0
while [ $attempt -lt $MAX_RETRIES ]; do
echo "$(date): Performing health check (attempt $((attempt + 1))/$MAX_RETRIES)..." >> "$LOG_FILE"
# 使用 curl 检查健康端点(GitLab 14+ 支持 /-/health)
if curl -sf --max-time 30 "$HEALTH_CHECK_URL" > /dev/null 2>&1; then
echo "$(date): [SUCCESS] GitLab is now active and writable." >> "$LOG_FILE"
exit 0
else
echo "$(date): Health check FAILED. Retrying after reconfigure + restart..." >> "$LOG_FILE"
((attempt++))
if [ $attempt -lt $MAX_RETRIES ]; then
gitlab-ctl reconfigure >> "$LOG_FILE" 2>&1
gitlab-ctl restart >> "$LOG_FILE" 2>&1
sleep 30 # 等待服务稳定
fi
fi
done
# 如果所有重试都失败
echo "$(date): [CRITICAL] GitLab failed to become healthy after $MAX_RETRIES attempts." >> "$LOG_FILE"
exit 1
说明:虽然脚本名为 takeover.sh,但在当前配置中,主节点的 notify_master 会触发此脚本。实际上更常用于备机接管场景。若主节点重启后重新成为 MASTER,此脚本也会运行,但因服务已运行,影响有限。
demote_to_standby.sh(备节点使用)
#!/bin/bash
# /opt/gitlab-ha/demote_to_standby.sh
#
# 功能:将本机 GitLab PostgreSQL 降级为从库
# 默认模式:仅当本机持有 VIP (192.168.252.140) 时执行
# 强制模式:添加 --force 参数可跳过 VIP 检查(用于手动维护)
set -euo pipefail # 安全模式:出错即退出
VIP="192.168.252.140"
INTERFACE="ens5f3"
LOG_FILE="/var/log/gitlab/demote_to_standby.log"
# 加载钉钉通知函数
if [[ -f /opt/gitlab-ha/send_dingtalk.sh ]]; then
source /opt/gitlab-ha/send_dingtalk.sh
else
echo "警告: 未找到 send_dingtalk.sh,跳过钉钉通知" | tee -a "$LOG_FILE"
send_dingtalk_message() { :; } # 空函数占位
fi
# 日志函数
log() {
local msg="$1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $msg" | tee -a "$LOG_FILE"
}
# 检查是否持有 VIP
has_vip() {
ip addr show "$INTERFACE" 2>/dev/null | grep -q "$VIP"
}
# 解析参数
FORCE_MODE=false
if [[ "${1:-}" == "--force" ]]; then
FORCE_MODE=true
log "强制模式启用:跳过 VIP 检查"
fi
# -------------------------------
# 主逻辑
# -------------------------------
if ! $FORCE_MODE && ! has_vip; then
log "本机未持有 VIP ($VIP),且未启用强制模式 --force ,退出。"
exit 0
fi
# 发送告警(仅在非强制模式下视为异常)
if ! $FORCE_MODE; then
send_dingtalk_message "[$(date '+%Y-%m-%d %H:%M:%S')] 警告:检测到本机持有 VIP,正在自动降级为从库!请检查主节点状态。"
else
send_dingtalk_message "[$(date '+%Y-%m-%d %H:%M:%S')] 通知:管理员手动触发 PostgreSQL 从库重建(强制模式)。"
fi
log "开始执行 PostgreSQL 从库重建流程..."
# 停止 GitLab 全部服务
log "停止 GitLab 服务..."
sudo gitlab-ctl stop || log "警告:gitlab-ctl stop 非零退出,继续执行"
# 重点:只操作 PostgreSQL
PG_DATA_DIR="/var/opt/gitlab/postgresql/data"
log "停止 PostgreSQL 服务..."
sudo gitlab-ctl stop postgresql || {
log "警告:PostgreSQL 服务停止失败,尝试强制清理"
}
log "清理旧数据目录: $PG_DATA_DIR"
sudo rm -rf "$PG_DATA_DIR" || log "提示:数据目录可能不存在,忽略"
# 执行基础备份(从主库拉取)
MASTER_IP="192.168.252.150"
log "从主库 $MASTER_IP 同步全量数据..."
if ! sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_basebackup \
-h "$MASTER_IP" \
-U gitlab_replica \
-D "$PG_DATA_DIR" \
-P -v -R --wal-method=stream; then
log "错误:pg_basebackup 失败,退出"
exit 1
fi
# 修复权限
log "修复数据目录权限..."
sudo chown -R gitlab-psql:gitlab-psql "$PG_DATA_DIR"
sudo chmod 700 "$PG_DATA_DIR"
# 启动 PostgreSQL
log "启动 PostgreSQL 服务..."
sudo gitlab-ctl start postgresql || {
log "错误:PostgreSQL 启动失败"
exit 1
}
# 等待并验证流复制
log "等待 walreceiver 进程启动(最长 30 秒)..."
for i in {1..30}; do
if pgrep -f "walreceiver" > /dev/null; then
log "成功:walreceiver 已运行,流复制已建立"
break
fi
sleep 1
done
# 最终启动其他服务(可选)
log "启动 GitLab 其他服务(保持只读)..."
sudo gitlab-ctl start nginx puma sidekiq redis
log "PostgreSQL 从库重建完成"
# ⚠️ 注意:不要在这里 restart keepalived!
# Keepalived 应由自身状态管理,手动重启可能导致 VIP 抖动
强制全量同步脚本(首次启动后对齐数据)
#!/bin/bash
# ---------------------------------------------------------
# GitLab 镜像同步脚本:配置与数据全量同步(可选 PostgreSQL)
# 日志增强版:清晰标识是否同步 PostgreSQL
# ---------------------------------------------------------
SOURCE_DIR="/var/opt/gitlab/"
CONF_DIR="/etc/gitlab/"
TARGET_HOST="gitlab-standby"
LOG_FILE="/var/log/gitlab_rsync.log"
SYNC_POSTGRESQL=false
# 统一日志函数:带级别和时间戳
log() {
local level="$1"
local msg="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $msg" | tee -a "$LOG_FILE"
}
confirm_action() {
log "WARN" "此操作将使用本地数据覆盖 $TARGET_HOST 上的 GitLab 配置和数据。"
log "WARN" "备机上的现有数据可能丢失,请确保已做好预案。"
echo
echo "是否同步 PostgreSQL 数据目录?"
echo " [推荐选择 'N']:若备机使用流复制(如 pg_basebackup),请勿同步 PG 数据。"
echo " [仅选 'Y']:用于完全重建备机(如初次搭建或主库故障后恢复)。"
read -p "同步 PostgreSQL?(y/N): " -r SYNC_PG_INPUT
case "$SYNC_PG_INPUT" in
[yY]|[yY][eE][sS])
SYNC_POSTGRESQL=true
log "INFO" "用户选择:✅ 同步 PostgreSQL 数据目录"
;;
*)
SYNC_POSTGRESQL=false
log "INFO" "用户选择:❌ 不同步 PostgreSQL 数据目录(仅同步其他数据)"
;;
esac
echo
read -p "确认继续执行同步?(输入 'yes' 确认): " -r CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
log "INFO" "操作已取消。"
exit 0
fi
}
sync_gitlab_data() {
local target=$1
local rsync_opts=(
-avz
--delete
--exclude='log/'
--exclude='tmp/'
--exclude='**/*.sock'
--exclude='redis/redis.socket'
)
if [[ "$SYNC_POSTGRESQL" == false ]]; then
rsync_opts+=(
--exclude='postgresql/data/postmaster.pid'
--exclude='postgresql/data/postmaster.opts'
--exclude='postgresql/data/'
)
log "INFO" "rsync 排除规则:已排除 postgresql/data/ 目录"
else
rsync_opts+=(
--exclude='postgresql/data/postmaster.pid'
--exclude='postgresql/data/postmaster.opts'
)
log "INFO" "rsync 排除规则:保留 postgresql/data/(仅排除运行时文件)"
fi
log "INFO" "开始同步配置文件到 $target..."
if ! rsync -avz "$CONF_DIR" "$target:$CONF_DIR" >> "$LOG_FILE" 2>&1; then
log "ERROR" "配置文件同步失败"
return 1
fi
log "INFO" "开始同步数据目录到 $target..."
if ! rsync "${rsync_opts[@]}" "$SOURCE_DIR" "$target:$SOURCE_DIR" >> "$LOG_FILE" 2>&1; then
log "ERROR" "数据目录同步失败"
return 1
fi
}
main() {
if [[ $EUID -ne 0 ]]; then
echo "错误:必须以 root 权限运行此脚本。" >&2
exit 1
fi
# 初始化日志分隔
echo "==================================================" >> "$LOG_FILE"
log "INFO" "GitLab 镜像同步任务启动"
confirm_action
sync_gitlab_data "$TARGET_HOST"
local sync_status=$?
if [[ $sync_status -eq 0 ]]; then
log "SUCCESS" "同步任务成功完成"
log "INFO" "PostgreSQL 数据同步状态: $(if $SYNC_POSTGRESQL; then echo '已包含'; else echo '已跳过'; fi)"
else
log "ERROR" "同步任务失败,请检查日志:$LOG_FILE"
exit 1
fi
log "INFO" "GitLab 镜像同步任务结束"
echo "==================================================" >> "$LOG_FILE"
}
main
重要:首次部署必须在主节点执行此脚本,确保备机拥有完整初始数据。
调试与验证
GitLab 服务控制
sudo gitlab-ctl start # 启动所有 GitLab 服务
sudo gitlab-ctl stop # 停止所有 GitLab 服务
sudo gitlab-ctl status # 查看各子服务运行状态
备节点正常情况下应保持 stopped 状态,直到故障切换。
查看初始密码(主节点)
sudo cat /etc/gitlab/initial_root_password
访问 http://192.168.252.140/ 使用 root 用户登录(示例密码:kunW3M6wRwwPGSkks4ZIr2opBW8PW1/P71NlE7jPih8=)。
Keepalived 控制
sudo systemctl restart keepalived
sudo systemctl status keepalived
# 查看 Keepalived 日志
journalctl -u keepalived -f
验证 VIP 是否生效
在任意节点执行:
ip a

观察输出是否包含:
inet 192.168.252.140/24 scope global ...
存在则表示当前节点持有 VIP。
手动触发故障切换测试
-
主节点停止 GitLab 服务:
gitlab-ctl stop
-
观察主节点是否释放 VIP(ip a 不再显示 192.168.252.140)。
-
备节点检查是否获得 VIP 并启动 GitLab:
ip a
gitlab-ctl status
curl -I http://192.168.252.140
恢复主节点后,若启用 nopreempt,VIP 不会自动切回,需手动干预或重启备机 Keepalived。当前配置未启用 nopreempt,主节点恢复健康后会重新抢占 VIP。
注意事项与限制
- 数据库一致性风险:文件级同步无法保证 PostgreSQL 的 ACID 特性。主节点异常宕机可能导致备机数据损坏,建议配合定期备份(如 gitlab-backup create)使用。
- 首次部署必须全量同步:启动 Keepalived 前,务必在主节点执行 force-full-sync.sh,确保备机有完整初始数据。
- 版本一致性:主备节点 GitLab 版本(含补丁号)必须完全一致。
- 网络要求:主备间网络延迟 < 10ms,带宽充足,避免同步滞后。
- 监控建议:部署外部监控系统,检测 VIP 可达性、HTTP 响应、GitLab 服务状态等。
GitLab 高可用(HA)运维手册
日常检查
# 检查 Keepalived 状态
systemctl status keepalived
# 查看主机同步 GitLab 文件的日志
cat /var/log/gitlab_data_rsync.log
# 查看从机是否已提权为主机
cat /var/log/gitlab-ha.log
# 查看 GitLab 配置
cat /etc/gitlab/gitlab.rb
# 一般性修复(重新应用配置)
sudo gitlab-ctl reconfigure;sudo gitlab-ctl restart
# 官方健康检查
sudo gitlab-rake gitlab:check SANITIZE=true
# 修复可能的权限问题
sudo gitlab-rake gitlab:storage:check
# 查看主机是否持有vip
ip a | grep ens5f3
# 疑难杂症 重启解
reboot
测试主机宕机(模拟故障切换)
操作步骤(原主机执行)
# 停止 gitlab 模拟故障
sudo gitlab-ctl stop
验证(从机执行)
# 查看日志确认是否提权成功
tail -n 50 /var/log/gitlab-ha.log
# 检查 VIP(192.168.252.140)是否已漂移到本机 ens5f3 接口
ip a | grep ens5f3
成功标志:VIP 出现在从机上,且 gitlab-ha.log 有提权成功记录。

主机宕机恢复(从从机备份还原)
第一步:在从机(当前主)执行备份
# 验证 PostgreSQL 状态正常
gitlab-ctl status postgresql
# 验证数据库可读
sudo gitlab-psql -c "SELECT email, username FROM users ORDER BY created_at DESC LIMIT 5;"
# 执行备份脚本
cd /opt/gitlab-ha && ./gitlab-backup-pro.sh
# 将备份传输到原主机
scp -r /backup/gitlab root@192.168.252.150:/backup
第二步:在原主机(待恢复)准备还原
# 停止 Keepalived(防止 VIP 漂移)
systemctl stop keepalived
# 停止 GitLab 服务(确保无写入)
sudo gitlab-ctl stop
此时原主机处于"离线维护"状态。

第三步:在原主机执行数据还原(跳过配置文件)
# 列出备份文件(示例名:1770787575_2026_02_11_18.8.3_gitlab_backup.tar)
ls -l /backup/gitlab
# 执行还原(关键:--skip-config 不覆盖 /etc/gitlab/gitlab.rb)
cd /opt/gitlab-ha
./gitlab-restore-pro.sh --skip-config 1770803062_2026_02_11_18.8.3_gitlab_backup.tar
为什么加 --skip-config?避免覆盖原主机的 HA 配置(如角色、VIP 设置),防止主从角色混乱。
第四步:主机验证并恢复高可用
# 启动 Keepalived(恢复 HA 控制)
systemctl restart keepalived
# 检查 VIP 是否回到原主机(192.168.252.140 应出现在 ens5f3)
ip a | grep ens5f3
# 验证 PostgreSQL 主从复制状态(应有 0 行记录,因为从机还没回复到从模式)
sudo gitlab-psql -c "SELECT * FROM pg_stat_replication;"
成功标志:pg_stat_replication 返回一行,说明从机正在同步。
手动恢复从机身份(降级为 standby)
在当前从机(曾临时为主)执行:
# 执行降级脚本
/opt/gitlab-ha/demote_to_standby.sh
# 重启 Keepalived(重置监控状态 若主机健康则从机重启服务后放弃vip)
systemctl restart keepalived
# 可选主机强制同步gitlab数据到从机,按需选择,较为耗时
/opt/gitlab-ha/force-full-sync.sh
最终验证(原主机执行)
# 再次确认复制状态(应有 1 行)
sudo gitlab-psql -c "SELECT * FROM pg_stat_replication;"

此时系统恢复默认拓扑:
- 192.168.252.150:主(含 VIP 140)
- 192.168.252.140:从(standby)
关键注意事项
| 项目 | 说明 |
|---|---|
| --skip-config | 还原时必须使用,保留原主机 HA 配置 |
| Keepalived 控制 | 任何手动干预 GitLab 前,先停 Keepalived 防漂移 |
| PostgreSQL 角色 | 主库可写,从库只读;切换后需验证pg_is_in_recovery() |
| 日志位置 | /var/log/gitlab-ha.log是 HA 状态核心日志 |

核心脚本说明:
- check_gitlab_health.sh:检查 gitlab 运行状态,失败则触发高可用自动切换从机
- force-full-sync.sh:强制同步主机的 gitlab 数据和数据库到从机(高危)
- gitlab-backup-pro.sh:备份 gitlab 数据和数据库脚本
- gitlab-restore-pro.sh:还原 gitlab 数据和数据库脚本
- sync-gitlab.sh:增量同步主机 gitlab 数据和数据库到从机
- takeover.sh:从机提权为主机脚本
上述脚本请勿删除。