以下是分步骤的完整指令指南:
第一步:在生产服务器(PROD)执行导出
在生产服务器的宿主机上运行。我们直接通过 Docker 导出并压缩,不产生巨大的临时 .sql 文件。
bash
# 1. 导出并压缩(推荐加上 --single-transaction 保证不锁表)
docker exec prod-mysql-container /usr/bin/mysqldump -u root -p'prod_password' \
--single-transaction --routines --triggers prod_db_name | gzip > /tmp/prod_data.sql.gz
# 2. (可选) 检查文件大小
ls -lh /tmp/prod_data.sql.gz
第二步:将数据传送到预发布服务器(STAGING)
在生产服务器上使用 scp 将压缩包推送到预发布服务器。
bash
# 将压缩包传给预发布服务器的 /tmp 目录
scp /tmp/prod_data.sql.gz root@staging_server_ip:/tmp/
第三步:在预发布服务器(STAGING)执行导入
登录预发布服务器,将数据导入到一个临时库。
bash
# 1. 创建临时数据库
docker exec staging-mysql-container mysql -u root -p'stag_password' \
-e "DROP DATABASE IF EXISTS staging_db_temp; CREATE DATABASE staging_db_temp;"
# 2. 解压并导入(使用 zcat 直接流式导入,不占双倍磁盘)
zcat /tmp/prod_data.sql.gz | docker exec -i staging-mysql-container mysql -u root -p'stag_password' staging_db_temp
第四步:执行库/表切换(实现毫秒级停机)
因为 MySQL 没有 RENAME DATABASE 命令,最稳妥的方法是使用 RENAME TABLE 批量切换。
1. 自动切换 SQL 脚本
为了避免手动输入每一个表名,我们在预发布宿主机执行这一段命令,它会自动生成迁移 SQL:
bash
#!/bin/bash
# --- 配置变量 ---
CONTAINER_NAME="462ae8bc5476"
STAG_PWD="xxxxxxx"
TARGET_DB="ai_pblxxxxxxxx"
TEMP_DB="staging_db_temp"
OLD_DB="ai_pblxxxxxx_old"
echo "🚀 开始数据库切换流程..."
# 1. 自动处理备份库:如果存在则删除,然后重新创建
# 这样可以确保 OLD_DB 是空的,不会与本次移入的表产生冲突
echo "清理并初始化备份库 $OLD_DB..."
docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -e "DROP DATABASE IF EXISTS $OLD_DB; CREATE DATABASE $OLD_DB;"
# 2. 获取临时库中所有的表名
echo "正在获取表列表..."
TABLES=$(docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -sN -e "SELECT table_name FROM information_schema.tables WHERE table_schema = '$TEMP_DB';")
# 检查是否获取到了表,防止空库操作
if [ -z "$TABLES" ]; then
echo "❌ 错误:在 $TEMP_DB 中未发现任何表,请检查导入是否成功。"
exit 1
fi
# 3. 循环切换每一个表
echo "正在执行原子切换..."
for TABLE in $TABLES; do
echo "正在处理表: $TABLE"
# 步骤A: 尝试把正式库的旧表移到 OLD 库
# 如果正式库原本没有这个表(比如新加的),2>/dev/null 会忽略报错
docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -e "RENAME TABLE $TARGET_DB.$TABLE TO $OLD_DB.$TABLE;" 2>/dev/null
# 步骤B: 把临时库的新表移到正式库
docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -e "RENAME TABLE $TEMP_DB.$TABLE TO $TARGET_DB.$TABLE;"
done
echo "---"
echo "✅ 切换完成!"
echo "当前在线库: $TARGET_DB"
echo "本次同步的旧数据已备份至: $OLD_DB (原 $OLD_DB 已被覆盖)"
第五步:清理与验证
- 验证:登录预发布环境检查数据是否已更新。
- 清理临时库:
bash
# 删除临时空库
docker exec staging-mysql-container mysql -u root -p"$STAG_PWD" -e "DROP DATABASE staging_db_temp;"
# 删除宿主机压缩包
rm /tmp/prod_data.sql.gz
- 旧数据保留 :建议保留
staging_db_old24 小时。如果发现生产同步过来的数据有问题,可以按同样方法快速切回去。
总结:手动流程 vs Python 脚本
| 特性 | 手动 Shell 流程 | Python 中心化脚本 |
|---|---|---|
| 操作复杂度 | 需在两台机器间来回登录切换 | 只要在一台机器运行即可 |
| 网络利用 | 需先产生临时文件再 scp | 可以直接通过 SSH 隧道流式传输(更快) |
| 错误处理 | 需要人肉盯着每一行输出 | 自动捕获异常并发送飞书通知 |
| 安全性 | 密码容易残留在 history 中 |
密码保存在配置文件中 |
建议 :如果你是偶尔操作一次 ,用上面的 Shell 命令最直接;如果你每周/每天都要同步一次 ,
建议用 Python 脚本,并配合飞书通知。
实现下面是具体的脚本:
python
import subprocess
import datetime
import requests
import json
# --- 1. 配置信息 (现在两台机器都作为远程目标) ---
FEISHU_WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/你的-TOKEN"
PROD_SERVER = {
"host": "1.2.3.4", # 生产 IP
"user": "root", # SSH 用户
"container": "prod-mysql",
"db_name": "prod_db",
"password": "prod_password"
}
STAG_SERVER = {
"host": "5.6.7.8", # 预发布 IP
"user": "root", # SSH 用户
"container": "staging-mysql",
"db_name": "staging_db",
"temp_db": "staging_db_temp",
"old_db": "staging_db_old",
"password": "stag_password"
}
# --- 2. 辅助函数 ---
def send_feishu(status, message):
"""发送飞书通知"""
color = "green" if status == "success" else "red"
title = "🟢 跨服同步成功" if status == "success" else "🔴 跨服同步失败"
payload = {
"msg_type": "interactive",
"card": {
"header": {"title": {"tag": "plain_text", "content": title}, "template": color},
"elements": [{"tag": "div", "text": {"tag": "lark_md",
"content": f"**时间**: {datetime.datetime.now()}\n**详情**: {message}"}}]
}
}
requests.post(FEISHU_WEBHOOK_URL, json=payload)
def run_remote(server, cmd, input_data=None):
"""在指定的远程服务器上执行命令"""
ssh_cmd = f"ssh -o ConnectTimeout=10 {server['user']}@{server['host']} \"{cmd}\""
result = subprocess.run(ssh_cmd, shell=True, capture_output=True, text=True, input=input_data)
if result.returncode != 0:
raise Exception(f"服务器 {server['host']} 执行失败: {result.stderr.strip()}")
return result.stdout.strip()
# --- 3. 主逻辑 ---
def main():
start_time = datetime.datetime.now()
try:
print(f"🔗 正在建立 {PROD_SERVER['host']} -> {STAG_SERVER['host']} 的同步隧道...")
# 1. 在预发布远程创建临时库
run_remote(STAG_SERVER,
f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e 'DROP DATABASE IF EXISTS {STAG_SERVER['temp_db']}; CREATE DATABASE {STAG_SERVER['temp_db']};'")
# 2. 核心:流式同步 (跨越两个 SSH 连接)
# 逻辑:从 A 导出 -> 管道 -> 在 B 执行导入
sync_pipeline = (
f"ssh {PROD_SERVER['user']}@{PROD_SERVER['host']} "
f"\"docker exec {PROD_SERVER['container']} mysqldump -u root -p'{PROD_SERVER['password']}' --single-transaction --routines --triggers {PROD_SERVER['db_name']} | gzip -c\" "
f"| ssh {STAG_SERVER['user']}@{STAG_SERVER['host']} "
f"\"zcat | docker exec -i {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' {STAG_SERVER['temp_db']}\""
)
print("🚀 数据传输中 (这可能需要几分钟,请稍候)...")
subprocess.run(sync_pipeline, shell=True, check=True)
# 3. 远程执行原子切换逻辑
print("🔄 正在远程执行原子切换...")
# 获取表名 (在预发布服务器执行)
fetch_tables_cmd = f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -sN -e \\\"SELECT table_name FROM information_schema.tables WHERE table_schema = '{STAG_SERVER['temp_db']}';\\\""
tables = run_remote(STAG_SERVER, fetch_tables_cmd).split('\n')
# 准备旧备份库
run_remote(STAG_SERVER,
f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e 'DROP DATABASE IF EXISTS {STAG_SERVER['old_db']}; CREATE DATABASE {STAG_SERVER['old_db']};'")
# 批量 Rename
for t in tables:
t = t.strip()
if not t: continue
rename_sql = f"RENAME TABLE {STAG_SERVER['db_name']}.{t} TO {STAG_SERVER['old_db']}.{t}, {STAG_SERVER['temp_db']}.{t} TO {STAG_SERVER['db_name']}.{t};"
try:
run_remote(STAG_SERVER,
f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e \"{rename_sql}\"")
except:
# 兼容正式库不存在该表的情况
run_remote(STAG_SERVER,
f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e \"RENAME TABLE {STAG_SERVER['temp_db']}.{t} TO {STAG_SERVER['db_name']}.{t};\"")
duration = datetime.datetime.now() - start_time
send_feishu("success",
f"✅ 同步成功!\n- 源: {PROD_SERVER['host']}\n- 目标: {STAG_SERVER['host']}\n- 耗时: {duration.seconds}秒")
print("✨ 任务完成!")
except Exception as e:
print(f"❌ 任务失败: {e}")
send_feishu("error", f"❌ 同步失败!\n错误详情: {str(e)}")
if __name__ == "__main__":
main()