Docker 镜像自动同步系统 – 轻量级集群镜像分发方案
项目背景
在多节点 Docker 集群环境中,镜像管理一直是个挑战。传统方式需要在每个节点手动拉取镜像,或搭建复杂的私有镜像仓库。本项目提供了一种轻量级的解决方案:自动检测本地镜像保存操作,并实时同步到集群其他节点。
后续考虑:可能会研究进一步提升分发效率,比如分块传输,tar压缩传输等。
核心特性
🚀 核心功能
自动监控与加载
基于 实时监控
inotifywait 目录自动加载新增的
/data/docker-tar 镜像文件到本地 Docker毫秒级时间戳记录,精确追踪每个操作
.tar
智能同步机制
基于 MD5 去重,避免重复同步同一镜像自动识别本地文件与远程接收文件,只转发本地创建的文件防止镜像在集群内循环传播(A->B->C->A 的死循环)
高可靠性设计
3 次自动重试机制(docker load 和 rsync 同步分别重试)后台重试队列,每 10-30 秒检查并立即重试失败任务详细的同步状态记录(每个节点的成功/失败状态)文件锁机制,避免并发重复处理
完善的日志管理
每天自动轮转日志,保留 14 天MD5 记录文件每周轮转并自动去重定期清理临时文件和旧日志每周深度清理超过 7 天的 tar 文件
丰富的运维工具
: 全面的健康检查(6 项检查)
check-docker-sync: 手动同步指定镜像文件
sync-tar: 查看文件同步状态
check-sync-status: 手动重试失败的同步任务
retry-sync: 手动清理旧文件
cleanup-docker-sync
🔧 技术亮点
1. 远程文件识别机制(防止循环转发)
系统通过多重判断识别文件来源:
方法 1: 检查文件的 和
ctime 差异方法 2: 解析 rsync 日志中的
mtime 记录方法 3: 检查发送标记文件(
recv 标记)
.sending
# 核心逻辑
if is_remote_file "$FILE"; then
# 远程接收的文件,只加载不转发
log_msg "识别为远程接收文件: $basename_file (将只加载,不转发)"
else
# 本地文件,加载后自动同步到其他节点
sync_to_nodes "$FILE" "$MD5"
fi
2. 双重重试机制
Docker Load 重试: 每 10 秒检查,最多 3 次重试同步重试: 每 30 秒检查,最多 3 次重试智能跳过已成功的节点,只重试失败的节点
3. 并行同步 + 状态记录
# 并行同步到所有节点
for node in "${NODES[@]}"; do
{
sync_to_node "$file" "$md5" "$node"
} &
done
wait
# 状态记录格式: 节点IP:状态:时间戳
# 192.168.1.100:success:1699876543
# 192.168.1.101:failed:1699876544
4. 文件锁避免重复处理
# 使用 MD5 作为锁,避免同一文件被多次处理
PROCESS_LOCK="$PROCESSING_DIR/${MD5}.lock"
if ! mkdir "$PROCESS_LOCK" 2>/dev/null; then
log_msg "文件正在处理中,跳过"
continue
fi
架构设计
系统组件
┌─────────────────────────────────────────────────────────────────┐
│ Docker 镜像同步系统 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ inotifywait │───▶│ loadtar.sh │───▶│ docker load │ │
│ │ 监控服务 │ │ 主控脚本 │ │ 镜像加载 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 同步判断逻辑 │ │
│ │ is_remote? │ │
│ └──────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ 本地文件 远程文件 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ rsync 同步到 │ │ 仅加载 │ │
│ │ 其他节点 │ │ 不转发 │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 重试队列 │ │
│ │ 后台处理 │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
文件组织
/opt/docker-sync/
└── loadtar.sh # 主控脚本
/data/docker-tar/ # 镜像存放目录
├── my-image-v1.tar
└── my-image-v2.tar
/etc/
├── docker-nodes.txt # 节点配置文件
└── rsyncd.conf # rsync 配置
/var/log/
├── docker-load.log # 主日志
├── docker-loaded.log # 已加载的 MD5 记录
├── docker-synced.log # 已同步的 MD5 记录
├── docker-sync-status/ # 同步状态目录
│ └── <md5>.status # 每个文件的同步状态
├── docker-sync-retry.queue # 同步重试队列
├── docker-load-retry.queue # 加载重试队列
└── rsyncd.log # rsync 日志
/usr/local/bin/
├── sync-tar # 手动同步工具
├── check-docker-sync # 健康检查工具
├── check-sync-status # 状态查看工具
├── retry-sync # 手动重试工具
└── cleanup-docker-sync # 清理工具
安装部署
系统要求
操作系统: Ubuntu 18.04+, Debian 10+, CentOS 7+, RHEL 7+必要软件:
Dockerrsyncinotify-toolsiproute2 / iproute
依赖安装
# Ubuntu/Debian
sudo apt update
sudo apt install -y docker.io inotify-tools rsync iproute2
# CentOS/RHEL
sudo yum install -y docker rsync inotify-tools iproute
sudo systemctl start docker
sudo systemctl enable docker
快速部署
下载安装脚本
# 假设脚本已保存为 install-docker-sync.sh
chmod +x install-docker-sync.sh
运行安装脚本
sudo ./install-docker-sync.sh
按提示操作
================================================
Docker镜像同步系统 配置脚本 v2.1
================================================
请选择操作:
1. 安装镜像同步系统
2. 卸载镜像同步系统
请选择 [1/2]: 1
[步骤 1] 检查必要命令
[成功] 依赖检查通过
[步骤 2] 配置同步节点
请配置需要同步的节点IP地址
本脚本将自动检测当前机器IP并跳过
[信息] 检测到当前机器IP地址:
- 192.168.1.10
请添加需要同步的节点IP地址:
1. 输入IP地址 (例如: 192.168.1.100)
2. 输入多个IP地址,用空格分隔
3. 输入 'done' 完成配置
请输入节点IP地址 (或 'done'): 192.168.1.20 192.168.1.30
[成功] 添加节点: 192.168.1.20
[成功] 添加节点: 192.168.1.30
请输入节点IP地址 (或 'done'): done
[信息] 配置的节点列表:
- 192.168.1.20
- 192.168.1.30
[成功] 节点配置完成
验证安装
# 检查服务状态
systemctl status docker-load
systemctl status rsyncd
# 运行健康检查
check-docker-sync
节点配置文件
安装完成后,节点 IP 保存在 :
/etc/docker-nodes.txt
192.168.1.20
192.168.1.30
192.168.1.40
使用方法
基本使用流程
1. 保存镜像为 tar 文件
# 方式 1: 直接保存到监控目录
docker save nginx:latest -o /data/docker-tar/nginx-latest.tar
# 方式 2: 先保存到其他位置,再移动
docker save redis:7 -o /tmp/redis.tar
mv /tmp/redis.tar /data/docker-tar/
2. 自动处理过程
系统会自动执行以下操作:
检测文件: inotifywait 监控到新文件稳定性检查: 等待 3 秒确认文件写入完成计算 MD5: 防止重复处理判断来源: 识别本地文件 vs 远程文件加载镜像: 加载到本地同步到集群: 如果是本地文件,并行同步到所有节点重试失败任务: 后台自动重试失败的加载和同步
docker load -i
3. 查看实时日志
# 查看 systemd 日志(推荐)
journalctl -u docker-load -f
# 查看文件日志
tail -f /var/log/docker-load.log
运维工具使用
1. 健康检查
check-docker-sync [test_node]
# 示例输出
========================================
Docker镜像同步系统健康检查
检查时间: 2024-11-07 10:30:00
========================================
【1/6】检查目录结构...
✔ /data/docker-tar 目录存在且可写
权限: drwxr-xr-x 2 root root 4096 Nov 7 10:00 /data/docker-tar
【2/6】检查服务状态...
✔ docker-load 服务运行中
✔ rsyncd 服务运行中
✔ docker 服务运行中
【3/6】检查端口监听...
✔ rsync 端口 873 正在监听
【4/6】测试文件监控...
✔ inotify 文件监控正常
【5/6】测试rsync连接...
✔ rsync 连接到 127.0.0.1 正常
【6/6】检查磁盘空间...
✔ 磁盘空间充足: 50000MB 可用
========================================
检查完成: 6/6 项通过
🎉 系统状态正常!
2. 手动同步镜像
# 手动同步指定文件到所有节点
sync-tar /data/docker-tar/my-image.tar
# 限制带宽同步(单位: KB/s)
sync-tar /data/docker-tar/large-image.tar 10000 # 限制 10MB/s
3. 查看同步状态
# 查看所有同步状态概览
check-sync-status
# 查看特定文件的同步状态
check-sync-status /data/docker-tar/nginx-latest.tar
# 示例输出
========================================
文件: nginx-latest.tar
MD5: a1b2c3d4e5f6g7h8i9j0
========================================
同步状态:
✔ 192.168.1.20 - 成功 (2024-11-07 10:25:30)
✔ 192.168.1.30 - 成功 (2024-11-07 10:25:32)
✗ 192.168.1.40 - 失败 (2024-11-07 10:25:35)
4. 手动重试失败任务
# 重试所有失败的节点
retry-sync /data/docker-tar/nginx-latest.tar
# 重试指定节点
retry-sync /data/docker-tar/nginx-latest.tar 192.168.1.40
5. 清理旧文件
# 清理 7 天前的日志
cleanup-docker-sync 7
# 清理 30 天前的日志
cleanup-docker-sync 30
日志管理
日志文件说明
| 日志文件 | 作用 | 轮转策略 | 保留时间 |
|---|---|---|---|
|
主日志(加载、同步操作) | 每天或超过 100MB | 14 天 |
|
rsync 传输日志 | 每天或超过 100MB | 14 天 |
|
已加载镜像的 MD5 记录 | 每周或超过 50MB | 8 周 |
|
已同步镜像的 MD5 记录 | 每周或超过 50MB | 8 周 |
|
每个文件的同步状态 | 超过 14 天自动清理 | 14 天 |
|
同步重试队列 | 实时更新 | – |
|
加载重试队列 | 实时更新 | – |
日志查看命令
# 查看主日志
tail -f /var/log/docker-load.log
# 查看 rsync 日志
tail -f /var/log/rsyncd.log
# 查看 systemd 日志
journalctl -u docker-load -f
journalctl -u rsyncd -f
# 查看最近 100 行日志
journalctl -u docker-load -n 100
# 查看特定时间段日志
journalctl -u docker-load --since "2024-11-07 10:00:00" --until "2024-11-07 11:00:00"
自动清理策略
系统配置了三层自动清理:
Logrotate(每天):
轮转日志文件压缩旧日志删除超过保留期的日志
每日清理任务():
/etc/cron.daily/docker-sync-cleanup
清理临时文件(,
.tmp,
.part)清理超过 30 天的压缩日志截断超大的 MD5 记录文件
.rsync-tmp*
每周深度清理():
/etc/cron.weekly/docker-sync-deep-cleanup
清理超过 7 天的 tar 文件去重优化 MD5 记录文件清理超过 60 天的压缩日志
故障排查
常见问题
1. 镜像加载失败
现象: 日志中出现
✗ 加载失败
排查步骤:
# 1. 检查 tar 文件是否完整
tar -tf /data/docker-tar/your-image.tar
# 2. 手动尝试加载
docker load -i /data/docker-tar/your-image.tar
# 3. 查看加载重试队列
cat /var/log/docker-load-retry.queue
# 4. 检查磁盘空间
df -h /var/lib/docker
解决方案:
重新保存镜像文件清理 Docker 磁盘空间: 等待自动重试(最多 3 次)
docker system prune -a
2. 同步失败
现象: 日志中出现
✗ 同步失败
排查步骤:
# 1. 检查网络连通性
ping 192.168.1.20
# 2. 测试 rsync 连接
rsync -avz --timeout=10 /tmp/test 192.168.1.20::tarrecv/
# 3. 检查远程节点 rsync 服务状态
ssh 192.168.1.20 "systemctl status rsyncd"
# 4. 查看同步重试队列
cat /var/log/docker-sync-retry.queue
# 5. 查看 rsync 日志
tail -f /var/log/rsyncd.log
解决方案:
确保远程节点的 rsync 服务运行正常检查防火墙规则(开放 873 端口)手动重试:
retry-sync /data/docker-tar/your-image.tar 192.168.1.20
3. 文件监控未触发
现象: 保存 tar 文件后没有任何日志输出
排查步骤:
# 1. 检查 docker-load 服务状态
systemctl status docker-load
# 2. 查看服务日志
journalctl -u docker-load -n 50
# 3. 测试文件监控
touch /data/docker-tar/test.tar
journalctl -u docker-load --since "10 seconds ago" | grep test.tar
# 4. 检查 inotify 限制
cat /proc/sys/fs/inotify/max_user_watches
解决方案:
重启服务: 增加 inotify 限制:
systemctl restart docker-load
echo 524288 > /proc/sys/fs/inotify/max_user_watches
4. 循环同步问题
现象: 同一个镜像在集群内反复传播
排查步骤:
# 1. 检查日志中的文件来源判断
grep "识别为" /var/log/docker-load.log
# 2. 查看 rsync 日志
tail -100 /var/log/rsyncd.log
# 3. 检查是否有 sending 标记残留
ls -la /var/log/docker-processing/
解决方案:
系统已自动防止循环传播(通过 判断)清理标记文件:
is_remote_file更新到最新版本脚本
rm -f /var/log/docker-processing/*.sending
防火墙配置
如果使用防火墙,需要开放 rsync 端口:
# iptables
iptables -A INPUT -p tcp --dport 873 -j ACCEPT
# firewalld
firewall-cmd --permanent --add-port=873/tcp
firewall-cmd --reload
# ufw
ufw allow 873/tcp
性能优化
1. 网络优化
# 调整 rsync 带宽限制
sync-tar /data/docker-tar/large-image.tar 50000 # 50MB/s
# 编辑 loadtar.sh 调整默认超时时间
# 默认: --timeout=300
2. 并发优化
系统已默认开启并行同步,无需额外配置。如需调整并发数,可修改 中的同步逻辑。
loadtar.sh
3. 磁盘空间管理
# 定期清理旧镜像文件
cleanup-docker-sync 3 # 清理 3 天前的日志
# 清理 Docker 未使用的镜像
docker system prune -a
# 调整每周深度清理保留时间
# 编辑 /etc/cron.weekly/docker-sync-deep-cleanup
# 修改: find "$DATA_DIR" -name "*.tar" -mtime +7 -delete
4. 日志优化
如果日志增长过快:
# 调整 logrotate 配置
sudo vim /etc/logrotate.d/docker-sync
# 修改轮转策略
daily → hourly # 每小时轮转
rotate 14 → rotate 7 # 减少保留天数
size 100M → size 50M # 降低轮转阈值
安全建议
1. 网络隔离
建议在内网环境使用使用 VPN 或专用网络连接节点配置 的
/etc/rsyncd.conf 限制访问来源
hosts allow
2. 权限控制
# 确保目录权限正确
chmod 755 /data/docker-tar
chown root:root /data/docker-tar
# 限制日志文件权限
chmod 644 /var/log/docker-*.log
3. 定期审计
# 查看同步历史
check-sync-status
# 审计日志
grep "同步成功|同步失败" /var/log/docker-load.log
# 检查异常 IP
grep -oP 'd+.d+.d+.d+' /var/log/rsyncd.log | sort | uniq -c
卸载
选择性卸载
运行卸载脚本会提供两个选项:
sudo ./install-docker-sync.sh
# 选择 2. 卸载镜像同步系统
# 选项 1: 仅停止服务并删除脚本配置 (保留数据和日志)
# 选项 2: 完全清理 (删除所有 tar 文件和日志文件)
手动卸载
如果无法运行脚本,可手动卸载:
# 停止服务
systemctl stop docker-load rsyncd
systemctl disable docker-load rsyncd
# 删除服务文件
rm -f /etc/systemd/system/docker-load.service
rm -f /etc/systemd/system/rsyncd.service
systemctl daemon-reload
# 删除脚本和配置
rm -rf /opt/docker-sync
rm -f /usr/local/bin/{sync-tar,check-docker-sync,cleanup-docker-sync,check-sync-status,retry-sync}
rm -f /etc/rsyncd.conf
rm -f /etc/docker-nodes.txt
rm -f /etc/logrotate.d/docker-sync
rm -f /etc/cron.daily/docker-sync-cleanup
rm -f /etc/cron.weekly/docker-sync-deep-cleanup
# 清理数据(可选)
rm -rf /data/docker-tar
rm -f /var/log/docker-*.log*
rm -f /var/log/rsyncd.log*
rm -rf /var/log/docker-sync-status
使用场景
场景 1: Kubernetes 多节点镜像预热
在 Kubernetes 集群中,提前将镜像同步到所有节点,避免首次 Pod 调度时拉取镜像的延迟。
# 在主节点保存镜像
docker pull myapp:v1.2.3
docker save myapp:v1.2.3 -o /data/docker-tar/myapp-v1.2.3.tar
# 自动同步到所有工作节点
# 所有节点的 Docker 都会自动加载该镜像
场景 2: 离线环境镜像分发
在无法访问互联网的环境中,将镜像批量分发到集群节点。
# 1. 在有网环境下载镜像
docker pull nginx:latest redis:7 mysql:8.0
# 2. 保存镜像
for image in nginx:latest redis:7 mysql:8.0; do
docker save "$image" -o "/data/docker-tar/${image//:/,}.tar"
done
# 3. 自动同步到所有离线节点
场景 3: CI/CD 流水线镜像分发
在 CI/CD 流程中,构建完成的镜像自动分发到所有测试/生产节点。
# GitLab CI / Jenkins Pipeline
stages:
- build
- distribute
build:
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker save myapp:$CI_COMMIT_SHA -o /data/docker-tar/myapp-$CI_COMMIT_SHA.tar
# 自动触发同步到所有节点
场景 4: 多数据中心镜像同步
通过配置跨数据中心的节点列表,实现镜像的跨区域同步。
# /etc/docker-nodes.txt
# 数据中心 A
10.1.1.10
10.1.1.11
# 数据中心 B
10.2.1.10
10.2.1.11
与其他方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本方案 | 轻量级、零依赖、自动化、高可靠 | 需要每个节点安装 | 中小型集群、离线环境 |
| Harbor | 功能强大、企业级、支持镜像扫描 | 复杂、需要数据库、资源占用高 | 大型企业、需要权限管理 |
| Docker Registry | 官方支持、简单 | 功能少、无 Web UI、无 HA | 小型开发环境 |
| 手动分发 | 无需安装 | 效率低、易出错、无自动化 | 临时需求 |
总结
Docker 镜像自动同步系统是一个轻量级、高可靠、易维护的镜像分发方案,特别适合以下场景:
中小型 Docker 集群(5-50 节点)离线或内网环境需要快速镜像预热的场景不想维护复杂镜像仓库的团队
通过智能的文件识别、可靠的重试机制、完善的日志管理,确保镜像能够快速、准确地分发到集群每个节点。
完整安装脚本
以下是完整的 安装脚本源码:
install-docker-sync.sh
#!/bin/bash
# Docker镜像自动同步系统 - 轻量级安装脚本
# 版本: 2.1
# 用途: 仅配置IP和部署脚本,不安装系统包
#!/bin/bash
# Docker镜像自动同步系统 - 轻量级安装脚本
# 版本: 2.1
# 用途: 仅配置IP和部署脚本,不安装系统包
set -e
# 颜色定义
RED='33[0;31m'
GREEN='33[0;32m'
YELLOW='33[1;33m'
BLUE='33[0;34m'
CYAN='33[0;36m'
NC='33[0m'
# 配置变量
INSTALL_DIR="/opt/docker-sync"
DATA_DIR="/data/docker-tar"
NODES_FILE="/etc/docker-nodes.txt"
SCRIPT_VERSION="2.1"
# 工具函数
print_banner() {
echo -e "${CYAN}================================================${NC}"
echo -e "${CYAN} Docker镜像同步系统 配置脚本 v${SCRIPT_VERSION}${NC}"
echo -e "${CYAN}================================================${NC}"
echo ""
}
print_step() {
echo -e "${GREEN}[步骤 $1]${NC} $2"
}
print_info() {
echo -e "${BLUE}[信息]${NC} $1"
}
print_success() {
echo -e "${GREEN}[成功]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[警告]${NC} $1"
}
print_error() {
echo -e "${RED}[错误]${NC} $1"
}
# 检查是否为root用户
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "此脚本需要root权限运行"
echo "请使用: sudo $0"
exit 1
fi
}
# 检查必要命令
check_dependencies() {
print_step 1 "检查必要命令"
local missing_deps=()
for cmd in docker rsync ip inotifywait systemctl; do
if ! command -v "$cmd" &> /dev/null; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
print_error "缺少必要命令: ${missing_deps[*]}"
echo ""
print_info "请先手动安装依赖:"
echo " Ubuntu/Debian: apt install docker.io inotify-tools rsync iproute2"
echo " CentOS/RHEL: yum install docker rsync inotify-tools iproute"
echo ""
exit 1
fi
print_success "依赖检查通过"
echo ""
}
# 配置节点信息
configure_nodes() {
print_step 2 "配置同步节点"
echo -e "${CYAN}请配置需要同步的节点IP地址${NC}"
echo "本脚本将自动检测当前机器IP并跳过"
echo ""
# 获取当前机器IP
CURRENT_IPS=$(ip addr show | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | cut -d'/' -f1)
if [[ -n "$CURRENT_IPS" ]]; then
print_info "检测到当前机器IP地址:"
echo "$CURRENT_IPS" | while read ip; do
echo " - $ip"
done
echo ""
fi
NODES=()
# 预设一些常见IP段供选择
echo -e "${CYAN}请添加需要同步的节点IP地址:${NC}"
echo "1. 输入IP地址 (例如: 192.168.1.100)"
echo "2. 输入多个IP地址,用空格分隔"
echo "3. 输入 'done' 完成配置"
echo ""
while true; do
read -p "请输入节点IP地址 (或 'done'): " input
if [[ "$input" == "done" ]]; then
break
fi
if [[ -z "$input" ]]; then
continue
fi
# 处理多个IP地址
for ip in $input; do
if [[ "$ip" =~ ^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$ ]]; then
# 检查是否为本机IP
if echo "$CURRENT_IPS" | grep -q "^$ip$"; then
print_warning "IP $ip 是本机地址,已自动跳过"
else
NODES+=("$ip")
print_success "添加节点: $ip"
fi
else
print_error "无效的IP地址: $ip"
fi
done
done
if [[ ${#NODES[@]} -eq 0 ]]; then
print_warning "未配置任何远程节点"
read -p "是否继续安装单机模式? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
echo ""
print_info "配置的节点列表:"
for node in "${NODES[@]}"; do
echo " - $node"
done
echo ""
# 保存节点配置
if [[ ${#NODES[@]} -gt 0 ]]; then
printf '%s
' "${NODES[@]}" > "$NODES_FILE"
else
touch "$NODES_FILE"
fi
print_success "节点配置完成"
echo ""
}
# 创建目录结构
create_directories() {
print_step 3 "创建目录结构"
mkdir -p "$INSTALL_DIR" "$DATA_DIR" "/var/log"
chown root:root "$DATA_DIR"
chmod 755 "$DATA_DIR"
print_success "目录结构创建完成"
echo ""
}
# 安装加载脚本
install_load_script() {
print_step 4 "安装自动加载脚本"
cat > "$INSTALL_DIR/loadtar.sh" <<'EOF'
#!/bin/bash
WATCH_DIR="/data/docker-tar"
LOCK="/run/docker-load.lock"
SYNC_LOCK="/run/docker-sync.lock"
LOG="/var/log/docker-load.log"
LOADED_LOG="/var/log/docker-loaded.log"
SYNCED_LOG="/var/log/docker-synced.log"
SYNC_STATUS_DIR="/var/log/docker-sync-status"
RETRY_QUEUE="/var/log/docker-sync-retry.queue"
NODES_FILE="/etc/docker-nodes.txt"
CURRENT_HOSTNAME=$(hostname)
MAX_RETRY=3
# 确保日志文件和目录存在
touch "$LOG" "$LOADED_LOG" "$SYNCED_LOG" "$RETRY_QUEUE"
mkdir -p "$SYNC_STATUS_DIR"
PROCESSING_DIR="/var/log/docker-processing"
mkdir -p "$PROCESSING_DIR"
LOAD_RETRY_QUEUE="/var/log/docker-load-retry.queue"
touch "$LOAD_RETRY_QUEUE"
MAX_LOAD_RETRY=3
# 日志函数 - 带毫秒级时间戳
log_msg() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local milliseconds=$(date '+%N' | cut -c1-3)
echo "[$timestamp.$milliseconds] $1" >> "$LOG"
}
# 获取本机IP地址
get_local_ips() {
ip addr show | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | cut -d'/' -f1
}
# 获取文件的MD5
get_file_md5() {
local file="$1"
md5sum "$file" 2>/dev/null | awk '{print $1}'
}
# 添加到 docker load 重试队列
add_to_load_retry_queue() {
local file="$1"
local md5="$2"
local retry_count="${3:-1}"
local is_remote="${4:-false}"
# 格式: file_path|md5|retry_count|timestamp|is_remote
echo "${file}|${md5}|${retry_count}|$(date +%s)|${is_remote}" >> "$LOAD_RETRY_QUEUE"
log_msg "添加到加载重试队列: $(basename "$file") (第${retry_count}次重试)"
}
# 处理 docker load 重试队列
process_load_retry_queue() {
local temp_queue="/tmp/load-retry-queue-$$"
local retry_lock="/run/docker-load-retry.lock"
# 加锁避免并发处理
flock -n "$retry_lock" bash -c "
if [[ ! -s '$LOAD_RETRY_QUEUE' ]]; then
exit 0
fi
# 读取队列
cp '$LOAD_RETRY_QUEUE' '$temp_queue'
> '$LOAD_RETRY_QUEUE' # 清空队列
while IFS='|' read -r file md5 retry_count timestamp is_remote; do
# 检查文件是否仍然存在
if [[ ! -f "$file" ]]; then
log_msg "加载重试失败: 文件不存在 $file"
continue
fi
# 检查是否已经加载过
if grep -q "^$md5$" '$LOADED_LOG' 2>/dev/null; then
log_msg "文件已加载,跳过重试: $(basename "$file")"
continue
fi
# 立即执行重试
local basename_file=$(basename "$file")
log_msg "处理加载重试任务: $basename_file (第${retry_count}次尝试)"
if flock -n '$LOCK' docker load -i "$file" >> '$LOG' 2>&1; then
echo "$md5" >> '$LOADED_LOG'
log_msg "✔ 重试加载成功: $basename_file (MD5: $md5)"
# 只有本地文件才同步到其他节点
if [[ "$is_remote" == "false" ]]; then
log_msg "本地文件,将同步到其他节点: $basename_file"
{
sync_to_nodes "$file" "$md5"
} &
fi
else
log_msg "✗ 重试加载失败: $basename_file (第${retry_count}次)"
# 如果未达到最大重试次数,立即重新加入队列
if [[ $retry_count -lt $MAX_LOAD_RETRY ]]; then
local next_retry=$((retry_count + 1))
echo "$file|$md5|$next_retry|$(date +%s)|$is_remote" >> '$LOAD_RETRY_QUEUE'
log_msg "立即重新尝试: $basename_file (第${next_retry}次)"
else
log_msg "⚠ 达到最大重试次数($MAX_LOAD_RETRY次),放弃加载: $basename_file"
fi
fi
done < '$temp_queue'
rm -f '$temp_queue'
" 2>>"$LOG" || true
}
# 检查文件是否是通过rsync接收的(改进版:多重判断)
is_remote_file() {
local file="$1"
local basename_file=$(basename "$file")
# 方法1: 检查文件的inode变化时间(ctime)和修改时间(mtime)
# rsync接收的文件,通常mtime会被保留,但ctime是新的
local ctime=$(stat -c %Z "$file" 2>/dev/null || echo 0)
local mtime=$(stat -c %Y "$file" 2>/dev/null || echo 0)
local now=$(date +%s)
local ctime_age=$((now - ctime))
# 方法2: 检查rsync日志(扩大查找范围)
local recent_log=$(tail -100 /var/log/rsyncd.log 2>/dev/null | grep "$basename_file")
# 判断逻辑:
# 1. 如果文件很新(ctime < 15秒)
# 2. 且rsync日志中有recv记录
# 3. 则认为是远程文件
if [[ $ctime_age -lt 15 ]]; then
if [[ -n "$recent_log" ]] && echo "$recent_log" | grep -q "recv"; then
log_msg "通过rsync日志确认为远程文件: $basename_file (ctime age: ${ctime_age}s)"
return 0 # 是远程文件
fi
fi
# 方法3: 检查是否存在同步标记文件
local sync_marker="$PROCESSING_DIR/${basename_file}.sending"
if [[ -f "$sync_marker" ]]; then
log_msg "检测到发送标记,认为是本地正在发送的文件: $basename_file"
return 1 # 是本地文件(正在发送中)
fi
# 默认:文件较旧或没有rsync日志,认为是本地文件
if [[ $ctime_age -lt 15 ]]; then
log_msg "未找到rsync接收日志,认为是本地文件: $basename_file (ctime age: ${ctime_age}s)"
fi
return 1 # 是本地文件
}
# 检查文件是否已同步到指定节点
is_synced_to_node() {
local md5="$1"
local node="$2"
local status_file="$SYNC_STATUS_DIR/${md5}.status"
if [[ -f "$status_file" ]]; then
grep -q "^${node}:success$" "$status_file" 2>/dev/null
return $?
fi
return 1
}
# 记录同步状态
record_sync_status() {
local md5="$1"
local node="$2"
local status="$3" # success 或 failed
local status_file="$SYNC_STATUS_DIR/${md5}.status"
# 先删除该节点的旧记录
if [[ -f "$status_file" ]]; then
sed -i "/^${node}:/d" "$status_file"
fi
# 添加新记录
echo "${node}:${status}:$(date +%s)" >> "$status_file"
}
# 添加到重试队列
add_to_retry_queue() {
local file="$1"
local md5="$2"
local node="$3"
local retry_count="${4:-1}"
# 格式: file_path|md5|node|retry_count|timestamp
echo "${file}|${md5}|${node}|${retry_count}|$(date +%s)" >> "$RETRY_QUEUE"
log_msg "添加到重试队列: $(basename "$file") -> $node (第${retry_count}次重试)"
}
# 同步到指定节点(带重试)
sync_to_node() {
local file="$1"
local md5="$2"
local node="$3"
local retry_count="${4:-0}"
local basename_file=$(basename "$file")
# 检查是否已成功同步
if is_synced_to_node "$md5" "$node"; then
log_msg "跳过已同步: $basename_file -> $node"
return 0
fi
log_msg ">>> 开始同步到节点 $node: $basename_file (尝试 $((retry_count + 1))/$MAX_RETRY)"
if rsync -avz --timeout=300 "$file" "$node::tarrecv/" >> "$LOG" 2>&1; then
record_sync_status "$md5" "$node" "success"
log_msg "✔ 同步成功: $basename_file -> $node"
return 0
else
record_sync_status "$md5" "$node" "failed"
log_msg "✗ 同步失败: $basename_file -> $node"
# 如果未达到最大重试次数,加入重试队列
if [[ $retry_count -lt $((MAX_RETRY - 1)) ]]; then
add_to_retry_queue "$file" "$md5" "$node" $((retry_count + 1))
else
log_msg "⚠ 达到最大重试次数,放弃同步: $basename_file -> $node"
fi
return 1
fi
}
# 同步到所有节点
sync_to_nodes() {
local file="$1"
local md5="$2"
local basename_file=$(basename "$file")
local success_count=0
local total_nodes=0
# 创建发送标记文件,防止inotify重复触发
local sync_marker="$PROCESSING_DIR/${basename_file}.sending"
touch "$sync_marker"
log_msg "=========================================="
log_msg "开始同步到所有节点: $basename_file (MD5: $md5)"
# 获取本机IP列表
local local_ips=$(get_local_ips)
# 临时文件记录结果
local temp_result="/tmp/sync-result-$$"
rm -f "$temp_result"
while read -r node; do
# 跳过空行和注释
[[ -z "$node" || "$node" =~ ^[[:space:]]*# ]] && continue
# 跳过本机IP地址
if echo "$local_ips" | grep -q "^$node$"; then
log_msg "跳过本机节点: $node"
continue
fi
((total_nodes++))
# 并行同步
{
if sync_to_node "$file" "$md5" "$node" 0; then
echo "success" >> "$temp_result"
fi
} &
done < "$NODES_FILE"
# 等待所有同步任务完成
wait
# 统计成功数量
if [[ -f "$temp_result" ]]; then
success_count=$(wc -l < "$temp_result")
rm -f "$temp_result"
fi
if [[ $success_count -gt 0 ]]; then
log_msg "节点同步完成: $basename_file (成功: ${success_count}/${total_nodes})"
# 记录到已同步日志
echo "$md5" >> "$SYNCED_LOG"
else
log_msg "所有节点同步失败: $basename_file (0/${total_nodes})"
fi
log_msg "=========================================="
# 删除发送标记
rm -f "$sync_marker"
}
# 处理重试队列
process_retry_queue() {
local temp_queue="/tmp/retry-queue-$$"
local retry_lock="/run/docker-retry.lock"
# 加锁避免并发处理
flock -n "$retry_lock" bash -c "
if [[ ! -s '$RETRY_QUEUE' ]]; then
exit 0
fi
# 读取队列
cp '$RETRY_QUEUE' '$temp_queue'
> '$RETRY_QUEUE' # 清空队列
while IFS='|' read -r file md5 node retry_count timestamp; do
# 检查文件是否仍然存在
if [[ ! -f "$file" ]]; then
log_msg "重试失败: 文件不存在 $file"
continue
fi
# 立即执行重试
log_msg "处理重试任务: $(basename "$file") -> $node (第${retry_count}次)"
sync_to_node "$file" "$md5" "$node" "$retry_count"
done < '$temp_queue'
rm -f '$temp_queue'
" 2>>"$LOG" || true
}
log_msg "Docker自动加载服务启动 (主机: $CURRENT_HOSTNAME)"
# 启动同步重试处理后台任务 (每30秒检查一次,立即重试)
{
while true; do
sleep 30
process_retry_queue
done
} &
# 启动加载重试处理后台任务 (每10秒检查一次,立即重试)
{
while true; do
sleep 10
process_load_retry_queue
done
} &
# 监控文件事件(移除create,避免rsync写入时多次触发)
inotifywait -m -e close_write,moved_to --format '%w%f' "$WATCH_DIR"
--exclude '.(part|tmp|rsync-tmp)' 2>>"$LOG" | while read FILE; do
[[ $FILE != *.tar ]] && continue
log_msg "检测到文件: $(basename "$FILE")"
sleep 3 && [[ -f $FILE ]] || continue # 文件稳定检查
log_msg "文件稳定性检查通过: $(basename "$FILE")"
MD5=$(get_file_md5 "$FILE")
basename_file=$(basename "$FILE")
log_msg "计算MD5完成: $basename_file (MD5: $MD5)"
# 使用MD5作为锁文件,避免重复处理同一个文件
PROCESS_LOCK="$PROCESSING_DIR/${MD5}.lock"
# 尝试获取锁,如果已经在处理中则跳过
if ! mkdir "$PROCESS_LOCK" 2>/dev/null; then
log_msg "文件正在处理中,跳过: $basename_file (MD5: $MD5)"
continue
fi
# 确保退出时删除锁
trap "rm -rf '$PROCESS_LOCK'" EXIT
# 检查是否已加载过(基于MD5)
if grep -q "^$MD5$" "$LOADED_LOG" 2>/dev/null; then
# 文件已加载,检查是否需要补发
if [[ ! $(is_remote_file "$FILE") ]]; then
# 本地文件且已加载,检查是否已完成同步
if grep -q "^$MD5$" "$SYNCED_LOG" 2>/dev/null; then
log_msg "文件已加载且已同步: $basename_file (MD5: $MD5)"
else
log_msg "文件已加载但未同步,重新发起同步: $basename_file (MD5: $MD5)"
{
sync_to_nodes "$FILE" "$MD5"
} &
fi
else
log_msg "跳过重复文件: $basename_file (MD5: $MD5)"
fi
rm -rf "$PROCESS_LOCK"
continue
fi
# 检查是否为远程接收的文件
IS_REMOTE=false
if is_remote_file "$FILE"; then
IS_REMOTE=true
log_msg "识别为远程接收文件: $basename_file (将只加载,不转发)"
else
log_msg "识别为本地文件: $basename_file"
fi
log_msg "开始加载镜像: $basename_file (MD5: $MD5)"
if flock -n "$LOCK" docker load -i "$FILE" >>"$LOG" 2>&1; then
echo "$MD5" >> "$LOADED_LOG"
log_msg "✔ 加载成功: $basename_file (MD5: $MD5)"
# 只有本地创建的文件才自动同步到其他节点
if [[ "$IS_REMOTE" == false ]]; then
log_msg "本地文件,将同步到其他节点: $basename_file"
{
sync_to_nodes "$FILE" "$MD5"
} &
else
log_msg "远程文件,跳过转发到其他节点: $basename_file"
fi
else
local exit_code=$?
log_msg "✗ 加载失败: $basename_file (MD5: $MD5, 退出码: $exit_code)"
# 加入重试队列
add_to_load_retry_queue "$FILE" "$MD5" 1 "$IS_REMOTE"
fi
# 处理完成,删除锁
rm -rf "$PROCESS_LOCK"
done
EOF
chmod +x "$INSTALL_DIR/loadtar.sh"
print_success "自动加载脚本安装完成"
echo ""
}
# 配置systemd服务
configure_systemd() {
print_step 5 "配置系统服务"
# Docker加载服务
cat > /etc/systemd/system/docker-load.service <<EOF
[Unit]
Description=Docker Auto Load Service
Description=自动加载Docker镜像tar文件
After=docker.service network-online.target
Wants=docker.service
[Service]
Type=simple
ExecStart=$INSTALL_DIR/loadtar.sh
Restart=always
RestartSec=10s
User=root
StandardOutput=journal
StandardError=journal
TimeoutStopSec=300
# 安全设置
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
# rsync服务配置
cat > /etc/rsyncd.conf <<EOF
# rsync daemon configuration
uid = root
gid = root
use chroot = no
max connections = 50
timeout = 300
log file = /var/log/rsyncd.log
transfer logging = yes
log format = %t %a %m %f %b
# Docker镜像同步模块
[tarrecv]
path = $DATA_DIR
read only = no
write only = no
list = yes
hosts allow = 127.0.0.1
EOF
# 收集所有需要允许的网段
NETWORKS=""
# 1. 添加本机所有网段
print_info "收集本机网段..."
LOCAL_IPS=$(ip addr show | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | cut -d'/' -f1)
while read -r local_ip; do
[[ -z "$local_ip" ]] && continue
local_network=$(echo "$local_ip" | cut -d'.' -f1-3).0/24
if [[ -z "$NETWORKS" ]]; then
NETWORKS="$local_network"
elif ! echo "$NETWORKS" | grep -q "$local_network"; then
NETWORKS="$NETWORKS,$local_network"
fi
print_info " 添加本机网段: $local_network"
done <<< "$LOCAL_IPS"
# 2. 添加远程节点网段
if [[ -f "$NODES_FILE" && -s "$NODES_FILE" ]]; then
print_info "收集远程节点网段..."
while read -r node; do
[[ -z "$node" || "$node" =~ ^[[:space:]]*# ]] && continue
# 提取网段
network=$(echo "$node" | cut -d'.' -f1-3).0/24
# 去重
if ! echo "$NETWORKS" | grep -q "$network"; then
NETWORKS="$NETWORKS,$network"
print_info " 添加远程网段: $network"
fi
done < "$NODES_FILE"
fi
# 3. 一次性添加所有网段到配置文件
if [[ -n "$NETWORKS" ]]; then
sed -i "s#hosts allow = 127.0.0.1#hosts allow = 127.0.0.1,$NETWORKS#" /etc/rsyncd.conf
print_success "已配置允许的网段: 127.0.0.1,$NETWORKS"
fi
# rsync systemd服务
cat > /etc/systemd/system/rsyncd.service <<EOF
[Unit]
Description=Fast Remote File Copy Program Daemon
ConditionPathExists=/etc/rsyncd.conf
[Service]
ExecStart=/usr/bin/rsync --daemon --no-detach
Restart=on-failure
RestartSec=5s
PIDFile=/var/run/rsyncd.pid
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now docker-load
systemctl enable --now rsyncd
print_success "系统服务配置完成"
echo ""
}
# 安装辅助脚本
install_utility_scripts() {
print_step 6 "安装辅助工具"
# 同步脚本
cat > /usr/local/bin/sync-tar <<'EOF'
#!/bin/bash
# Docker镜像tar文件同步脚本
# 用法: sync-tar <tar_file> [带宽限制KB/s]
TAR_FILE="$1"
BANDWIDTH_LIMIT="${2:-50000}" # 默认50MB/s
# 参数检查
if [[ -z "$TAR_FILE" ]]; then
echo "用法: $0 <tar_file> [bandwidth_limit_kb/s]"
echo "例: $0 my-image.tar 100000"
exit 1
fi
if [[ ! -f "$TAR_FILE" ]]; then
echo "错误: 文件不存在: $TAR_FILE"
exit 1
fi
if [[ "$TAR_FILE" != *.tar ]]; then
echo "错误: 必须是.tar文件"
exit 1
fi
NODES_FILE="/etc/docker-nodes.txt"
if [[ ! -f "$NODES_FILE" ]]; then
echo "错误: 节点配置文件不存在: $NODES_FILE"
exit 1
fi
# 统计变量
TOTAL_NODES=0
SUCCESS_COUNT=0
FAIL_COUNT=0
FAILED_NODES=()
echo "开始同步镜像文件: $TAR_FILE"
echo "带宽限制: ${BANDWIDTH_LIMIT}KB/s"
echo "目标节点:"
cat "$NODES_FILE"
echo "----------------------------------------"
# 并行同步到所有节点
while IFS= read -r node; do
[[ -z "$node" || "$node" =~ ^[[:space:]]*# ]] && continue
((TOTAL_NODES++))
{
echo "[$(date '+%H:%M:%S')] 开始同步到 $node"
if rsync -avz --progress --timeout=600 --bwlimit="$BANDWIDTH_LIMIT"
"$TAR_FILE" "$node::tarrecv/" 2>&1; then
echo "[$(date '+%H:%M:%S')] ✔ 同步完成: $node"
else
echo "[$(date '+%H:%M:%S')] ✗ 同步失败: $node"
echo "$node" >> "/tmp/sync-failed-$$"
fi
} &
done < "$NODES_FILE"
wait
if [[ -f "/tmp/sync-failed-$$" ]]; then
FAIL_COUNT=$(wc -l < "/tmp/sync-failed-$$")
SUCCESS_COUNT=$((TOTAL_NODES - FAIL_COUNT))
mapfile -t FAILED_NODES < "/tmp/sync-failed-$$"
rm -f "/tmp/sync-failed-$$"
else
SUCCESS_COUNT=$TOTAL_NODES
FAIL_COUNT=0
fi
echo "----------------------------------------"
echo "同步完成!"
echo "总节点数: $TOTAL_NODES"
echo "成功: $SUCCESS_COUNT"
echo "失败: $FAIL_COUNT"
if [[ $FAIL_COUNT -gt 0 ]]; then
echo "失败节点:"
printf ' %s
' "${FAILED_NODES[@]}"
exit 1
else
echo "所有节点同步成功!"
exit 0
fi
EOF
# 健康检查脚本
cat > /usr/local/bin/check-docker-sync <<'EOF'
#!/bin/bash
# Docker镜像同步系统健康检查
# 用法: check-docker-sync [test_node]
set -e
TEST_NODE="${1:-127.0.0.1}"
PASS_COUNT=0
TOTAL_CHECKS=6
echo "========================================"
echo "Docker镜像同步系统健康检查"
echo "检查时间: $(date)"
echo "========================================"
# 1. 目录检查
echo "【1/6】检查目录结构..."
if [[ -d /data/docker-tar && -w /data/docker-tar ]]; then
echo " ✔ /data/docker-tar 目录存在且可写"
echo " 权限: $(ls -ld /data/docker-tar)"
((PASS_COUNT++))
else
echo " ✗ /data/docker-tar 目录不存在或不可写"
fi
# 2. 服务状态检查
echo "【2/6】检查服务状态..."
services=("docker-load" "rsyncd" "docker")
all_running=true
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
echo " ✔ $service 服务运行中"
else
echo " ✗ $service 服务未运行"
all_running=false
fi
done
if $all_running; then
((PASS_COUNT++))
fi
# 3. 端口监听检查
echo "【3/6】检查端口监听..."
if ss -ltnp | grep -q ':873.*rsync'; then
echo " ✔ rsync 端口 873 正在监听"
echo " $(ss -ltnp | grep ':873')"
((PASS_COUNT++))
else
echo " ✗ rsync 端口 873 未监听"
fi
# 4. inotify触发测试
echo "【4/6】测试文件监控..."
TEST_FILE="/data/docker-tar/inotify-test-$(date +%s).tar"
touch "$TEST_FILE"
sleep 3
if journalctl -u docker-load --since "10 seconds ago" | grep -q "$TEST_FILE"; then
echo " ✔ inotify 文件监控正常"
((PASS_COUNT++))
else
echo " ✗ inotify 文件监控未触发"
fi
rm -f "$TEST_FILE"
# 5. rsync连接测试
echo "【5/6】测试rsync连接..."
if echo "test" | rsync -avz --timeout=10 - "$TEST_NODE::tarrecv/test" 2>/dev/null; then
echo " ✔ rsync 连接到 $TEST_NODE 正常"
rsync -avz --timeout=10 --delete "$TEST_NODE::tarrecv/test" /dev/null 2>/dev/null || true
((PASS_COUNT++))
else
echo " ✗ rsync 连接到 $TEST_NODE 失败"
fi
# 6. 磁盘空间检查
echo "【6/6】检查磁盘空间..."
AVAILABLE_SPACE=$(df /data | awk 'NR==2 {print $4}')
if [[ $AVAILABLE_SPACE -gt 1048576 ]]; then # 1GB
echo " ✔ 磁盘空间充足: $((AVAILABLE_SPACE/1024))MB 可用"
((PASS_COUNT++))
else
echo " ⚠ 磁盘空间不足: $((AVAILABLE_SPACE/1024))MB 可用 (建议 > 1GB)"
fi
# 总结
echo "========================================"
echo "检查完成: $PASS_COUNT/$TOTAL_CHECKS 项通过"
if [[ $PASS_COUNT -eq $TOTAL_CHECKS ]]; then
echo "🎉 系统状态正常!"
exit 0
else
echo "⚠ 发现问题,请检查上述失败项"
exit 1
fi
EOF
# 清理脚本
cat > /usr/local/bin/cleanup-docker-sync <<'EOF'
#!/bin/bash
# Docker同步系统清理脚本
# 用法: cleanup-docker-sync [days]
KEEP_DAYS="${1:-7}"
echo "开始清理 $KEEP_DAYS 天前的日志文件..."
find /var/log -name "docker-*.log.*" -mtime +$KEEP_DAYS -delete
find /var/log -name "rsyncd.log.*" -mtime +$KEEP_DAYS -delete
find /data/docker-tar -name "*.tmp" -mtime +1 -delete
find /data/docker-tar -name "*.part" -mtime +1 -delete
echo "清理完成"
EOF
# 同步状态查看脚本
cat > /usr/local/bin/check-sync-status <<'EOF'
#!/bin/bash
# 查看文件同步状态
# 用法: check-sync-status [tar_file]
SYNC_STATUS_DIR="/var/log/docker-sync-status"
RETRY_QUEUE="/var/log/docker-sync-retry.queue"
if [[ -n "$1" ]]; then
# 查看特定文件的同步状态
TAR_FILE="$1"
if [[ ! -f "$TAR_FILE" ]]; then
echo "错误: 文件不存在: $TAR_FILE"
exit 1
fi
MD5=$(md5sum "$TAR_FILE" | awk '{print $1}')
STATUS_FILE="$SYNC_STATUS_DIR/${MD5}.status"
echo "========================================"
echo "文件: $(basename "$TAR_FILE")"
echo "MD5: $MD5"
echo "========================================"
if [[ -f "$STATUS_FILE" ]]; then
echo "同步状态:"
while IFS=':' read -r node status timestamp; do
local time_str=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
if [[ "$status" == "success" ]]; then
echo " ✔ $node - 成功 ($time_str)"
else
echo " ✗ $node - 失败 ($time_str)"
fi
done < "$STATUS_FILE"
else
echo "未找到同步记录"
fi
else
# 显示所有同步状态概览
echo "========================================"
echo "Docker镜像同步状态概览"
echo "========================================"
if [[ -d "$SYNC_STATUS_DIR" && -n "$(ls -A $SYNC_STATUS_DIR 2>/dev/null)" ]]; then
echo ""
echo "已同步的镜像:"
for status_file in "$SYNC_STATUS_DIR"/*.status; do
[[ ! -f "$status_file" ]] && continue
local md5=$(basename "$status_file" .status)
local success_count=$(grep -c ":success:" "$status_file" 2>/dev/null || echo 0)
local failed_count=$(grep -c ":failed:" "$status_file" 2>/dev/null || echo 0)
local total=$((success_count + failed_count))
echo " MD5: $md5"
echo " 成功: $success_count/$total 节点"
if [[ $failed_count -gt 0 ]]; then
echo " 失败节点:"
grep ":failed:" "$status_file" | cut -d':' -f1 | sed 's/^/ - /'
fi
done
else
echo "暂无同步记录"
fi
echo ""
echo "========================================"
echo "重试队列:"
echo "========================================"
if [[ -s "$RETRY_QUEUE" ]]; then
local retry_count=$(wc -l < "$RETRY_QUEUE")
echo "等待重试的任务: $retry_count 个"
echo ""
while IFS='|' read -r file md5 node retry_count timestamp; do
echo " 文件: $(basename "$file")"
echo " 节点: $node"
echo " 重试次数: $retry_count"
local time_str=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
echo " 加入队列时间: $time_str"
echo ""
done < "$RETRY_QUEUE"
else
echo "重试队列为空"
fi
fi
EOF
# 手动重试脚本
cat > /usr/local/bin/retry-sync <<'EOF'
#!/bin/bash
# 手动重试失败的同步任务
# 用法: retry-sync <tar_file> [node_ip]
TAR_FILE="$1"
TARGET_NODE="$2"
SYNC_STATUS_DIR="/var/log/docker-sync-status"
NODES_FILE="/etc/docker-nodes.txt"
if [[ -z "$TAR_FILE" ]]; then
echo "用法: $0 <tar_file> [node_ip]"
echo " tar_file: 要重新同步的tar文件"
echo " node_ip: 可选,指定节点IP。不指定则重试所有失败节点"
exit 1
fi
if [[ ! -f "$TAR_FILE" ]]; then
echo "错误: 文件不存在: $TAR_FILE"
exit 1
fi
MD5=$(md5sum "$TAR_FILE" | awk '{print $1}')
STATUS_FILE="$SYNC_STATUS_DIR/${MD5}.status"
echo "开始手动重试同步: $(basename "$TAR_FILE")"
echo "MD5: $MD5"
echo "========================================"
if [[ -n "$TARGET_NODE" ]]; then
# 重试指定节点
echo "重试节点: $TARGET_NODE"
if rsync -avz --progress "$TAR_FILE" "$TARGET_NODE::tarrecv/"; then
# 更新状态
sed -i "/^${TARGET_NODE}:/d" "$STATUS_FILE" 2>/dev/null
echo "${TARGET_NODE}:success:$(date +%s)" >> "$STATUS_FILE"
echo "✔ 同步成功"
else
echo "✗ 同步失败"
exit 1
fi
else
# 重试所有失败的节点
if [[ ! -f "$STATUS_FILE" ]]; then
echo "未找到同步记录,将同步到所有节点"
TARGET_NODES=$(cat "$NODES_FILE" | grep -v '^#' | grep -v '^s*$')
else
# 获取失败的节点
TARGET_NODES=$(grep ":failed:" "$STATUS_FILE" | cut -d':' -f1)
fi
if [[ -z "$TARGET_NODES" ]]; then
echo "没有需要重试的节点"
exit 0
fi
SUCCESS=0
FAILED=0
for node in $TARGET_NODES; do
echo "重试节点: $node"
if rsync -avz "$TAR_FILE" "$node::tarrecv/" 2>&1; then
sed -i "/^${node}:/d" "$STATUS_FILE" 2>/dev/null
echo "${node}:success:$(date +%s)" >> "$STATUS_FILE"
echo " ✔ 成功"
((SUCCESS++))
else
echo " ✗ 失败"
((FAILED++))
fi
done
echo "========================================"
echo "重试完成: 成功 $SUCCESS, 失败 $FAILED"
fi
EOF
chmod +x /usr/local/bin/sync-tar
chmod +x /usr/local/bin/check-docker-sync
chmod +x /usr/local/bin/cleanup-docker-sync
chmod +x /usr/local/bin/check-sync-status
chmod +x /usr/local/bin/retry-sync
print_success "辅助工具安装完成"
echo ""
}
# 配置日志轮转
configure_logrotate() {
print_step 7 "配置日志轮转和定期清理"
# 1. 配置 logrotate
cat > /etc/logrotate.d/docker-sync <<EOF
# Docker同步系统日志轮转配置
/var/log/docker-load.log /var/log/rsyncd.log {
daily # 每天轮转
rotate 14 # 保留14天
size 100M # 超过100M立即轮转
compress # 压缩旧日志
delaycompress # 延迟一天压缩(方便查看昨天日志)
missingok # 文件不存在不报错
notifempty # 空文件不轮转
create 0644 root root # 创建新日志文件权限
sharedscripts # 只执行一次脚本
postrotate
# 重新加载rsync服务以释放旧日志文件句柄
systemctl reload rsyncd >/dev/null 2>&1 || true
# 向docker-load服务发送USR1信号(如果支持的话)
pkill -USR1 -f 'loadtar.sh' 2>/dev/null || true
endscript
}
# MD5记录文件单独配置(这些文件会持续增长)
/var/log/docker-loaded.log /var/log/docker-synced.log {
weekly # 每周轮转
rotate 8 # 保留8周(约2个月)
size 50M # 超过50M立即轮转
compress
delaycompress
missingok
notifempty
create 0644 root root
# 清理重复的MD5记录,只保留最近的
prerotate
if [ -f /var/log/docker-loaded.log ]; then
# 保留最近10000条记录
tail -10000 /var/log/docker-loaded.log > /var/log/docker-loaded.log.tmp
mv /var/log/docker-loaded.log.tmp /var/log/docker-loaded.log
fi
if [ -f /var/log/docker-synced.log ]; then
tail -10000 /var/log/docker-synced.log > /var/log/docker-synced.log.tmp
mv /var/log/docker-synced.log.tmp /var/log/docker-synced.log
fi
endscript
}
EOF
# 2. 配置定期清理的cron任务
cat > /etc/cron.daily/docker-sync-cleanup <<'EOF'
#!/bin/bash
# Docker同步系统每日自动清理任务
LOG_DIR="/var/log"
DATA_DIR="/data/docker-tar"
# 清理压缩后的旧日志(超过30天)
find "$LOG_DIR" -name "docker-*.log.*.gz" -mtime +30 -delete 2>/dev/null
find "$LOG_DIR" -name "rsyncd.log.*.gz" -mtime +30 -delete 2>/dev/null
# 清理临时文件和残留文件
find "$DATA_DIR" -name "*.tmp" -mtime +1 -delete 2>/dev/null
find "$DATA_DIR" -name "*.part" -mtime +1 -delete 2>/dev/null
find "$DATA_DIR" -name ".rsync-tmp*" -mtime +1 -delete 2>/dev/null
find /tmp -name "sync-success-*" -mtime +1 -delete 2>/dev/null
find /tmp -name "sync-failed-*" -mtime +1 -delete 2>/dev/null
find /tmp -name "sync-result-*" -mtime +1 -delete 2>/dev/null
find /tmp -name "retry-queue-*" -mtime +1 -delete 2>/dev/null
# 清理超过14天的同步状态文件
find /var/log/docker-sync-status -name "*.status" -mtime +14 -delete 2>/dev/null
# 清理超大的MD5记录文件(超过5MB则截断到最近1万条)
for file in "$LOG_DIR/docker-loaded.log" "$LOG_DIR/docker-synced.log"; do
if [ -f "$file" ]; then
size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
if [ "$size" -gt 5242880 ]; then # 5MB
tail -10000 "$file" > "$file.tmp" && mv "$file.tmp" "$file"
fi
fi
done
# 记录清理日志
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 完成定期清理" >> "$LOG_DIR/docker-cleanup.log"
EOF
chmod +x /etc/cron.daily/docker-sync-cleanup
# 3. 添加每周深度清理(可选)
cat > /etc/cron.weekly/docker-sync-deep-cleanup <<'EOF'
#!/bin/bash
# Docker同步系统每周深度清理
DATA_DIR="/data/docker-tar"
LOG_DIR="/var/log"
# 清理超过7天的tar文件
find "$DATA_DIR" -name "*.tar" -mtime +7 -delete 2>/dev/null
# 清理所有旧的压缩日志
find "$LOG_DIR" -name "*.gz" -mtime +60 -delete 2>/dev/null
# 优化MD5记录文件,去重
if [ -f "$LOG_DIR/docker-loaded.log" ]; then
sort -u "$LOG_DIR/docker-loaded.log" > "$LOG_DIR/docker-loaded.log.tmp"
mv "$LOG_DIR/docker-loaded.log.tmp" "$LOG_DIR/docker-loaded.log"
fi
if [ -f "$LOG_DIR/docker-synced.log" ]; then
sort -u "$LOG_DIR/docker-synced.log" > "$LOG_DIR/docker-synced.log.tmp"
mv "$LOG_DIR/docker-synced.log.tmp" "$LOG_DIR/docker-synced.log"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 完成每周深度清理" >> "$LOG_DIR/docker-cleanup.log"
EOF
chmod +x /etc/cron.weekly/docker-sync-deep-cleanup
print_success "日志轮转和自动清理配置完成"
print_info " - 日志文件: 每天轮转,保留14天,超过100M立即轮转"
print_info " - MD5记录: 每周轮转,保留8周,自动截断到1万条"
print_info " - 定期清理: 每天清理临时文件,每周深度清理"
echo ""
}
# 运行系统测试
run_system_test() {
print_step 8 "运行系统测试"
print_info "执行健康检查..."
if /usr/local/bin/check-docker-sync; then
print_success "健康检查通过"
else
print_warning "健康检查发现问题,请查看上述输出"
fi
# 测试创建tar文件
print_info "测试自动加载功能..."
TEST_IMAGE="redis:7"
if docker pull "$TEST_IMAGE" >/dev/null 2>&1; then
TEST_FILE="$DATA_DIR/test-install-$(date +%s).tar"
if docker save "$TEST_IMAGE" -o "$TEST_FILE"; then
print_info "创建测试镜像: $TEST_FILE"
sleep 5
# 检查是否被处理
if journalctl -u docker-load --since "10 seconds ago" | grep -q "$TEST_FILE"; then
print_success "自动加载测试通过"
else
print_warning "自动加载测试超时"
fi
# 清理测试文件
rm -f "$TEST_FILE"
else
print_warning "无法创建测试镜像文件"
fi
else
print_warning "无法下载测试镜像"
fi
echo ""
}
# 显示安装完成信息
show_completion_info() {
print_step 9 "配置完成"
echo -e "${GREEN}🎉 Docker镜像自动同步系统安装完成!${NC}"
echo ""
echo -e "${CYAN}系统信息:${NC}"
echo " 安装目录: $INSTALL_DIR"
echo " 数据目录: $DATA_DIR"
echo " 配置文件: $NODES_FILE"
echo ""
echo -e "${CYAN}命令工具:${NC}"
echo " 健康检查: ${YELLOW}check-docker-sync${NC}"
echo " 手动同步: ${YELLOW}sync-tar <image.tar>${NC}"
echo " 查看状态: ${YELLOW}check-sync-status [tar_file]${NC}"
echo " 手动重试: ${YELLOW}retry-sync <tar_file> [node_ip]${NC}"
echo " 手动清理: ${YELLOW}cleanup-docker-sync [days]${NC}"
echo ""
echo -e "${CYAN}日志管理:${NC}"
echo " 日志目录: /var/log/"
echo " 主日志: docker-load.log (每天轮转,保留14天)"
echo " rsync日志: rsyncd.log (每天轮转,保留14天)"
echo " MD5记录: docker-{loaded,synced}.log (每周轮转,自动去重)"
echo " 同步状态: /var/log/docker-sync-status/*.status"
echo " 重试队列: /var/log/docker-sync-retry.queue"
echo " 自动清理: 每天清理临时文件,每周深度清理"
echo " 清理记录: ${YELLOW}cat /var/log/docker-cleanup.log${NC}"
echo ""
echo -e "${CYAN}重试机制:${NC}"
echo " 最大重试次数: 3次"
echo " 重试间隔: 指数退避 (1分钟, 2分钟, 3分钟)"
echo " 自动重试: 每5分钟检查一次重试队列"
echo " 智能去重: 已成功同步的节点不会重复发送"
echo ""
echo -e "${CYAN}监控命令:${NC}"
echo " 查看服务状态: ${YELLOW}systemctl status docker-load rsyncd${NC}"
echo " 查看实时日志: ${YELLOW}journalctl -u docker-load -f${NC}"
echo " 查看历史日志: ${YELLOW}tail -f /var/log/docker-load.log${NC}"
echo ""
echo -e "${CYAN}使用方法:${NC}"
echo " 1. 保存Docker镜像: ${YELLOW}docker save image:tag -o /data/docker-tar/image.tar${NC}"
echo " 2. 系统会自动加载到本地Docker"
echo " 3. 系统会自动同步到其他节点"
echo " 4. 其他节点收到后会自动加载"
echo ""
if [[ -f "$NODES_FILE" && -s "$NODES_FILE" ]]; then
echo -e "${CYAN}已配置的同步节点:${NC}"
cat "$NODES_FILE" | while read node; do
[[ -n "$node" ]] && echo " - $node"
done
echo ""
fi
echo -e "${CYAN}提示: 如果需要在其他节点上安装,请运行相同的安装脚本${NC}"
echo ""
}
# 卸载函数
uninstall() {
print_banner
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${RED}开始卸载镜像同步系统${NC}"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# 询问是否清理全部数据
echo -e "${CYAN}选择卸载方式:${NC}"
echo "1. 仅停止服务并删除脚本配置 (保留数据和日志)"
echo "2. 完全清理 (删除所有 tar 文件和日志文件)"
echo ""
read -p "请选择 [1/2]: " -n 1 -r uninstall_type
echo ""
echo ""
if [[ ! "$uninstall_type" =~ ^[12]$ ]]; then
print_error "无效的选择"
exit 1
fi
print_step 1 "停止并禁用服务"
systemctl stop docker-load 2>/dev/null || print_warning "docker-load 服务未运行"
systemctl stop rsyncd 2>/dev/null || print_warning "rsyncd 服务未运行"
systemctl disable docker-load 2>/dev/null || true
systemctl disable rsyncd 2>/dev/null || true
print_success "服务已停止"
echo ""
print_step 2 "删除脚本和配置文件"
rm -rf /opt/docker-sync
rm -f /usr/local/bin/sync-tar
rm -f /usr/local/bin/check-docker-sync
rm -f /usr/local/bin/cleanup-docker-sync
rm -f /usr/local/bin/check-sync-status
rm -f /usr/local/bin/retry-sync
rm -f /etc/systemd/system/docker-load.service
rm -f /etc/systemd/system/rsyncd.service
rm -f /etc/rsyncd.conf
rm -f /etc/docker-nodes.txt
rm -f /etc/logrotate.d/docker-sync
rm -f /etc/cron.daily/docker-sync-cleanup
rm -f /etc/cron.weekly/docker-sync-deep-cleanup
systemctl daemon-reload
print_success "脚本和配置文件已删除"
echo ""
if [[ "$uninstall_type" == "2" ]]; then
print_step 3 "清理所有数据和日志文件"
echo -e "${RED}警告: 即将删除以下内容:${NC}"
echo " - /data/docker-tar 目录下所有 tar 文件"
echo " - /var/log 目录下所有 docker-* 和 rsyncd.log 日志文件"
echo ""
read -p "确认要清理全部数据吗? (yes/N): " -r
echo ""
if [[ "$REPLY" == "yes" ]]; then
# 清理 tar 文件
find /data/docker-tar -name "*.tar" -delete 2>/dev/null || true
print_info "已删除所有 tar 文件"
# 清理日志文件
rm -f /var/log/docker-*.log* 2>/dev/null || true
rm -f /var/log/rsyncd.log* 2>/dev/null || true
rm -rf /var/log/docker-sync-status 2>/dev/null || true
print_info "已删除所有日志文件"
# 可选: 删除整个数据目录
read -p "是否删除整个数据目录 /data/docker-tar? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf /data/docker-tar
print_info "已删除数据目录 /data/docker-tar"
fi
print_success "数据和日志已全部清理"
else
print_warning "已取消数据清理,数据和日志已保留"
fi
echo ""
else
print_info "数据和日志已保留:"
echo " - 数据目录: /data/docker-tar"
echo " - 日志目录: /var/log/docker-*.log*"
echo " 如需手动清理,请执行:"
echo " rm -rf /data/docker-tar"
echo " rm -f /var/log/docker-*.log*"
echo ""
fi
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}卸载完成!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
}
# 安装函数
install() {
print_banner
echo -e "${GREEN}开始安装镜像同步系统${NC}"
echo ""
read -p "是否继续安装? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "安装已取消"
exit 0
fi
echo ""
check_root
check_dependencies
configure_nodes
create_directories
install_load_script
configure_systemd
install_utility_scripts
configure_logrotate
run_system_test
show_completion_info
print_success "配置脚本执行完成!"
}
# 主函数
main() {
print_banner
# 显示主菜单
echo -e "${CYAN}请选择操作:${NC}"
echo "1. 安装镜像同步系统"
echo "2. 卸载镜像同步系统"
echo ""
read -p "请选择 [1/2]: " -n 1 -r choice
echo ""
echo ""
case "$choice" in
1)
install
;;
2)
uninstall
;;
*)
print_error "无效的选择"
exit 1
;;
esac
}
# 错误处理
trap 'print_error "配置过程中发生错误,请检查日志"; exit 1' ERR
# 执行主函数
main "$@"


