家庭数据中心系列 数据变更感知到自动导出:构建 WordPress 双活同步自动化运维的最后一跳(已废弃)

1 前言

我之前的博客架构是家庭数据中心(主节点+热备节点) + 腾讯云(容灾节点),属于比较典型的”单节点读写”方案。由于日常只有主节点负责处理数据库的读写请求,所以数据库之间并不需要实时同步:每当我新增文章、修改内容,或者批准、回复评论,导致 WordPress 的数据库发生变化时,如果刚好有心情,我会手动将主节点的 MariaDB 中的 wordpress 库导出为 wordpress.sql 文件,并放进 Syncthing 的同步目录里。接下来,Syncthing 会将这个文件同步到热备节点和容灾节点的指定目录,这2个节点上的 inotify 脚本监测到目录发生变化后,自动触发数据库导入脚本,将 wordpress.sql 文件导入对应的 MariaDB 中,从而完成主节点和其他节点之前的数据同步。

不过现在,博客架构升级成了家庭数据中心”主写副读” + Racknerd 芝加哥节点”主读”的双活架构,平时正常的访问流量会命中APO缓存中的内容,而需要回源的访问请求会通过 Cloudflare Tunnel的单个 tunnel 中多个 connector 回源到不同节点。由于 Cloudflare Tunnel 的多 connector 负载均衡策略相当”随心所欲且任性”,虽然理论上,在多 connector 场景下,回源请求会优先命中离用户最近的芝加哥节点,但实际使用中,依然无法保证所有请求都乖乖听话,尤其当主从节点之间的数据库尚未同步完成时,就可能出现评论丢失、文章回滚,甚至页面展示内容不一致等问题。

更关键的是,过去靠我手动导出 wordpress.sql 并触发同步的方式,操作虽然简单,但确实没太大技术含量,不仅机械、无趣,还很容易忘,很没有”逼格”,感觉不太符合我的身份~~。因此,在新架构下,为了尽可能保证家庭数据中心中的”主写副读”节点变更数据后,能在短时间内同步到芝加哥”主读节点”,避免回源流量命中芝加哥”主读”节点时出现内容差异,我不得不重新思考数据同步机制,希望实现一整套更智能、更自动的数据库变更感知与自动化同步流程——这也是这周我没写双活架构而写这篇文章的原因,因为这是升级成wordpress双活架构之后,要实现日常自动化运维这个目的而必须解决的关键问题。

2 如何判断 “主写”节点WordPress 是否发生内容更新

2.1 wordpress数据库变更感知

上一节内容中我提到了”更自动的数据库变更感知”,要实现这一功能,就涉及到如何选择一种最合适的数据库内容变更检测机制。一般来说,我们可以考虑的方案主要有几种:

  • 使用 binlog(Binary Log)机制:这是最直接、最精确的方式。MySQL/MariaDB 的 binlog 会记录所有对数据库的写操作(INSERT、UPDATE、DELETE),非常适合用于主从复制、增量同步或数据恢复。但这需要在数据库层启用 binlog、配置文件支持、且要解析日志内容或引入中间件系统,部署和维护成本较高。
  • 使用触发器(Trigger)机制:可以为数据库中的关键表设置 AFTER INSERT/UPDATE/DELETE 触发器,将变更记录写入一个独立表或发送到消息队列。这种方式更灵活,但修改了原有数据库结构,也容易引入额外负担和副作用。
  • 使用文件系统层级的变更检测(如 inotify):理论上可以监测数据库文件是否有读写行为变化,但实际中这在 Linux 上才能实现,且 MariaDB 的文件变更频率非常高(不等于内容发生实质改变),在 macOS 等平台也不可用,不适合做精准检测(我的wordpress主节点是跑在macos上)。
  • 周期性全量比较(如 checksum 或 diff):暴力又简单,但效率低下,且无法做到实时响应变更。不过,回到 WordPress 这个具体场景,我们并不需要那么”全量”地感知所有数据库变更。绝大多数重要的数据更新,都集中在几个核心表中:
  • wp_posts:文章、页面、附件都在这张表里。字段 post_modified_gmt 表示最后修改时间,post_date_gmt 表示创建时间;
  • wp_comments:所有评论数据都在这里,关键字段是 comment_date_gmt;

因此,我们可以通过以下两个 SQL 语句,快速获取 WordPress 内容是否在最近发生过变化:

SELECT MAX(GREATEST(post_modified_gmt, post_date_gmt)) FROM wp_posts;
SELECT MAX(comment_date_gmt) FROM wp_comments;

如果某次查询的最大时间戳晚于上一次记录,就说明 WordPress 站点发生了实际更新(无论是新增评论、编辑文章、发布页面等),这时就可以触发数据库的导出和同步流程。

这种方式不需要改动数据库结构、不依赖 binlog、不引入中间件,部署极其轻量,非常适合做为自动同步系统的”变更感知信号”。

2.2 创建定时查询数据库变更的脚本

2.2.1 源码部署的mariadb

2.2.1.1 前置工作:在工作目录下添加”.my.cnf”文件(可选)

步骤总结:

  1. 打开终端,进入当前用户的主目录:
cd ~
  1. 创建 .my.cnf 文件(推荐用编辑器,比如 nano 或 vim):
vim .my.cnf
  1. 写入以下内容并保存(根据你的实际数据库用户名和密码填写):
[client]
user=root
password=你的数据库密码
  1. 设置权限(防止其他用户读取):
chmod 600 .my.cnf
  1. 测试是否生效:
mysql -e "SHOW DATABASES;"

说明

  • 只要你用这个用户执行 mysql 或 mysqldump 命令,都会自动读取 ~/.my.cnf 中 [client] 段的信息。
  • 如果你有多个用户登录系统,每个用户都可以有自己的 ~/.my.cnf 文件,互不干扰。
  • 不建议将 .my.cnf 放在系统级目录(如 /etc/my.cnf)里暴露密码,放在用户主目录下更安全可控。
  • 这一节内容不是必须的,只不过如果不想在检测脚本中直接使用”mysql -uxxx -pxxx”的方式来显示指定用户名和密码的话可以如此操作,毕竟显示指定的方式不太安全,当然,不介意的朋友可以跳过这节内容。

2.2.1.2 创建查询脚本

  1. 进入当前用户的主目录,新建一个脚本目录并进入:
cd ~
mkdir -p script
cd script
  1. 创建database-query.sh(推荐用编辑器,比如 nano 或 vim):
vim database-query.sh
  1. 写入以下内容并保存:
#!/bin/bash

# 设置路径
STATE_FILE="/usr/local/var/wp_sync_state"
DUMP_DIR="/usr/local/var/wp_dumps"
DB_NAME="wordpress"
DATE_NOW=(date +%Y%m%d%H%M%S)

# 运行 SQL 查询获取最新修改时间
LATEST_POST_TIME=(mysql -N -e "SELECT MAX(GREATEST(post_modified_gmt, post_date_gmt)) FROM wp_posts;" DB_NAME)
LATEST_COMMENT_TIME=(mysql -N -e "SELECT MAX(comment_date_gmt) FROM wp_comments;" DB_NAME)

CURRENT_STATE="{LATEST_POST_TIME}|{LATEST_COMMENT_TIME}"

# 如果 state 文件不存在,初始化并退出
if [ ! -f "STATE_FILE" ]; then
  echo "CURRENT_STATE">"STATE_FILE"
  echo "初次运行,记录当前状态:CURRENT_STATE"
  exit 0
fi

# 读取上一次状态
PREV_STATE=(cat "STATE_FILE")

# 比较当前状态与上一次
if [ "CURRENT_STATE" != "PREV_STATE" ]; then
  echo "数据库发生变更,触发导出和同步..."

  # 更新状态文件
  echo "CURRENT_STATE" > "STATE_FILE"

  # 导出数据库(注意替换为你的实际路径)
  DUMP_FILE="DUMP_DIR/wp_dump_DATE_NOW.sql"
  mysqldumpDB_NAME > "DUMP_FILE"

  # 同步动作(你可以替换成 rsync/scp 等)
  # scp "DUMP_FILE" user@chicago:/data/wp_sync/

else
  echo "无变化,无需同步。"
fi

注:脚本最后关联了发现数据库变化之后执行的同步wordpress.sql文件的方式,这里只是一个示范,具体选择什么方式根据大家自己的环境而定,比如选择scp、rsync或者干脆通过syncthing的自动同步来实现。如果要通过syncthing自动同步,需要在导出数据库时替换为syncthing同步的文件目录。

  1. 给脚本设置执行权限:
chmod +x database-query.sh

另,也提供一个不使用.my.cnf来存放账号密码,直接使用-u -p参数的脚本:

#!/bin/bash

# 设置路径
STATE_FILE="/usr/local/var/wp_sync_state"
DUMP_DIR="/usr/local/var/wp_dumps"
DB_NAME="wordpress"
DATE_NOW=(date +%Y%m%d%H%M%S)

# 运行 SQL 查询获取最新修改时间
LATEST_POST_TIME=(mysql -uroot -pyourpassword -N -e "SELECT MAX(GREATEST(post_modified_gmt, post_date_gmt)) FROM wp_posts;" DB_NAME)
LATEST_COMMENT_TIME=(mysql -uroot -pyourpassword -N -e "SELECT MAX(comment_date_gmt) FROM wp_comments;" DB_NAME)

CURRENT_STATE="{LATEST_POST_TIME}|{LATEST_COMMENT_TIME}"

# 如果 state 文件不存在,初始化并退出
if [ ! -f "STATE_FILE" ]; then
  echo "CURRENT_STATE">"STATE_FILE"
  echo "初次运行,记录当前状态:CURRENT_STATE"
  exit 0
fi

# 读取上一次状态
PREV_STATE=(cat "STATE_FILE")

# 比较当前状态与上一次
if [ "CURRENT_STATE" != "PREV_STATE" ]; then
  echo "数据库发生变更,触发导出和同步..."

  # 更新状态文件
  echo "CURRENT_STATE" > "STATE_FILE"

  # 导出数据库(注意替换为你的实际路径)
  DUMP_FILE="DUMP_DIR/wp_dump_DATE_NOW.sql"
  mysqldumpDB_NAME > "DUMP_FILE"

  # 同步动作(你可以替换成 rsync/scp 等)
  # scp "DUMP_FILE" user@chicago:/data/wp_sync/

else
  echo "无变化,无需同步。"
fi

注1:这种方式的最大问题是在另一个终端中执行ps aux | grep mysql命令时,就可能直接看到:

mysql -uroot -ppassword -e ...

在多人共享环境、CI/CD 构建中都存在潜在的密码泄露风险,如果只是自己个人使用环境倒不是很所谓。

注2:主写节点成功导出wordpress库的sql文件之后,其实还可以通过bark给手机发送系统级通知:

curl -s "https://bark.example.com/your_token/wordpress通知/wordpress数据库导入成功" > /dev/null

当然,导入wordpress库的sql文件的节点,也可以以相同的方式通过bark给手机发送通知。

2.2.2 docker部署的mariadb

2.2.2.1 前置工作:在宿主机创建”.my.cnf”文件并挂载到mariadb容器中

一、创建 “.my.cnf”文件(宿主机)

在宿主机某个目录下(比如mariadb的docker目录)创建:

vim /docker/mariadb/.my.cnf

内容如下(以 root 用户为例):

[client]
user=root
password=你的数据库密码

注意:

  • user= 要和你连接 MariaDB 时用的用户一致(一般是 root)。
  • password= 请填入实际密码,不要加引号。
  • 这个文件权限建议设为 600(只有你能读):
chmod 600 /docker/mariadb/.my.cnf

二、挂载”.my.cnf”文件到容器

使用docker run格式命令的”-v”参数将宿主机的”.my.cnf”挂载到容器内部:

-v /docker/mariadb/.my.cnf:/root/.my.cnf:ro

说明:”-v /docker/mariadb/.my.cnf:/root/.my.cnf:ro” 表示把你宿主机的配置文件挂载到容器内 root 用户的家目录中,其中的”:ro” 表示只读挂载,更安全。

如果是docker-compose方式部署,可以用以下方式挂载:

    volumes:
      - /docker/mariadb/.my.cnf:/root/.my.cnf:ro

2.2.2.2 创建查询脚本

这个查询脚本是在宿主机上运行的,创建步骤和之前类似,我就不重复了,最后脚本内容如下:

#!/bin/bash
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

# Docker 容器名(MariaDB 容器)
CONTAINER=mariadb

# 数据库名
DB=wordpress

# MySQL 用户和密码(建议设置低权限备份用户)
USER=root
PASS='p@ssw0rd'

# 输出 SQL 的路径(Syncthing 同步目录)
OUTPUT="/mnt/sync/wordpress/db/wordpress.sql"

# 临时文件防止未完成写入就同步
TMP_OUTPUT="{OUTPUT}.tmp"

# 状态文件路径
STATE_FILE=/tmp/wp-db-last-update.txt

# 获取 WordPress 数据库的最新修改时间(文章 + 评论)
LATEST_TIME=(docker exec CONTAINER \
  mysql -N -uUSER -pPASS -e "
    SELECT UNIX_TIMESTAMP(
      GREATEST(
        (SELECT MAX(GREATEST(post_modified_gmt, post_date_gmt)) FROMDB.wp_posts),
        (SELECT MAX(comment_date_gmt) FROM DB.wp_comments)
      )
    );
  ")

# 如果状态文件不存在,初始化
if [ ! -f "STATE_FILE" ]; then
  echo "LATEST_TIME">"STATE_FILE"
  echo "[INIT] 状态文件初始化完成"
  exit 0
fi

# 读取上次记录时间
LAST_TIME=(cat "STATE_FILE")

# 对比判断
if [ "LATEST_TIME" -gt "LAST_TIME" ]; then
  echo "[+] 检测到数据库变更,开始导出"

  # 导出 SQL 到临时文件
  docker exec CONTAINER \
    mysqldump -uUSER -pPASS --single-transaction --max_allowed_packet=64MDB \
    > "TMP_OUTPUT"

  # 检查导出成功才覆盖
  if [? -eq 0 ]; then
    mv "TMP_OUTPUT" "OUTPUT"
    echo "[✓] 导出成功,已移动到 OUTPUT"
    echo "LATEST_TIME" > "STATE_FILE"
  else
    echo "[✗] 导出失败,保留上次状态"
    rm -f "TMP_OUTPUT"
  fi

else
  echo "[=] 没有数据库变更,跳过导出"
fi

2.3 定时运行数据库查询脚本

这个不同系统的实现方式就不一样了,对于linux系统或者macos系统,可以使用crontab来实现:

crontab -e

添加如下内容(每 5 分钟检测一次):

*/5 * * * * /path/to/database_query.sh >> /tmp/wp-detect.log 2>&1

注意事项

  • 脚本中默认容器名是 mariadb,你可以用 docker ps 查看实际容器名。
  • 如果容器是通过 docker-compose 启动的,容器名可能是 yourproject_mariadb_1,需要改脚本里的 CONTAINER=。
  • 如果你在 docker run 时指定了 –volume,也可以把 STATE_FILE 放到宿主机持久化目录中。

2.4 用服务的方式运行数据库变更监测脚本(可选)

2.4.1 “cron”方式的缺点

一般来说,如果只是在本地环境,想非常轻量地运行探测脚本,其实使用 cron 就足够了。它简单、稳定,只需要设定一个定时任务,就可以每隔几分钟执行一次数据库变更检测逻辑,非常适合无人值守的常规需求。

但是对我而言,情况要复杂一些。我并不是希望这个探测脚本全年无休地运行,因为每周我都会在周一进行 WordPress 新文章的发布,并伴随着一系列内容整理工作,比如调整文章格式、更新站点地图、临时开启 TranslatePress 插件的自动翻译功能来翻译新增的文章内容,然后在翻译完成后再关闭插件的自动翻译功能(为了省钱~)。

这些操作在一两个小时内集中完成,而这个时间段内数据库内容其实是频繁变动的。这时候如果探测脚本还在运行,就会频繁检测到数据库”变化”,触发不必要的同步流程,甚至影响文章尚未定稿的内容。

因此,我的理想使用方式是:每周一发布文章前,先停掉数据库变更探测脚本,待发布流程和所有必要操作完成后再手动恢复运行。这个控制流程在 cron 的机制下显得非常繁琐——我总不可能每周一都临时修改 crontab 配置、注释掉任务,再在发布后重新改回来吧?

这种场景下,将探测脚本包装成一个系统服务(如 Linux下的systemd service 或 macOS 下的 launchd plist 项)就更合适了。这样我可以像控制普通服务一样,使用 systemctl stop detector 或 launchctl unload 暂停任务,在需要的时候再一条命令恢复运行。既可控,又省心。

2.4.2 Linux下的systemd service

还是以之前的脚本”database_query.sh”为例,如果其路径是”/root/script/database-query.sh”

新建一个service文件:

vim /etc/systemd/system/database-query.service

粘贴并保存如下内容:

[Unit]
Description=WordPress Change Detection Service

[Service]
ExecStart=/root/script/database-query.sh
Restart=always

[Install]
WantedBy=multi-user.target

然后运行如下命令:

systemctl daemon-reexec
systemctl daemon-reload
systemctl enable database-query.service
systemctl start database-query.service

需要暂时停止探测的时候,只需要运行如下命令即可:

systemctl stop database-query.service

2.4.3 macos下的launchd 管理守护进程

macOS 没有 systemctl,但有launchd。你可以创建一个 plist 文件,例如:

  1. 保存脚本到 /usr/local/bin/database-query.sh并赋可执行权限:
chmod +x /usr/local/bin/database-query.sh
  1. 创建 LaunchAgent 文件(比如~/Library/LaunchAgents/com.local.databasequery.plist):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.local.databasequery</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/database-query.sh</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>StartInterval</key>
  <integer>300</integer>
  <key>StandardOutPath</key>
  <string>~/databasequery.log</string>
  <key>StandardErrorPath</key>
  <string>~/databasequery.err</string>
</dict>
</plist>
  1. 加载启动:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.local.databasequery.plist
launchctl list | grep com.local.databasequery
  1. 停止/卸载:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.local.databasequery.plist
  1. 检查日志输出:
tail -f ~/databasequery.log
tail -f ~/databasequery.err

相比 crontab,这么做的优势:

特性 crontab Systemd / launchd
精确控制启停 无法手动中断一个 job 可以随时启/停服务
日志输出更稳定 需手动 redirect 支持日志文件配置
状态跟踪 没有 systemctl status/launchctl list
自动重启 不支持 崩溃后自动重启

3 总结

这部分内容,算是我在 WordPress 双活节点架构下,实现自动化运维的最后一块拼图。严格来说,即便不做这套数据库变更感知机制,每次有更新后手动导出 wordpress.sql 文件并放入 Syncthing 同步目录,一样可以完成数据同步。毕竟,文章的发布、评论的审核这些操作,本就发生在我明确”有空也有心情”的时候,一切都在掌控之中。

但问题也正是出在这种”掌控感”——它并不可靠。忘记导出一次、同步延迟几分钟,在之前”单点读写”结构下都无所谓,可在”主写副读 + 异地主读”的双活架构下(且回源还不可控),这样的懈怠可能就会造成内容差异,甚至引发数据冲突。而作为系统架构的设计者,我不允许自己的懒惰成为稳定性的变量

于是,秉承着「可以不用,但必须得有」的工程哲学,这个小小的自动化模块,还是被我加进来了。它不会每天都发挥作用,但一旦需要,它就在那儿,悄无声息地把本该手动的事自动化完成。

或许,除了我,没有人会关心这套机制是否存在、是否精准;但作为这套系统的唯一用户,也是唯一维护者,我清楚,这不仅仅是为了”跑得更快”,更是为了构建一种自洽的秩序感:系统会在我忘记的时候记得,在我不在的时候运转。

最终,写这一套机制,不只是为了多一项功能,而是为了让我能少一点担心,多一点”它一定没问题”的笃定

注:当 WordPress 收到新的评论时,无论评论是否已被审核,都会立刻写入数据库的 wp_comments 表中,状态由 comment_approved 字段标识:’0′ 表示未批准,’1′ 表示已批准,’spam’ 表示垃圾评论,’trash’ 表示已删除。因此,即便评论处于”待审核”状态,依然会引发一次数据库变更。进一步地,当我手动审核并批准该评论时,comment_approved 字段会更新为 ‘1’,这同样属于一次数据库写操作。也就是说,一条评论从提交到最终通过审核,在我现在的架构下至少会触发两次数据库同步事件。

在当前”变更驱动同步”的机制下,这种行为虽会带来额外同步操作,但对内容一致性反而是一种保障。

分享这篇文章
博客内容均系原创,转载请注明出处!更多博客文章,可以移步至网站地图了解。博客的RSS地址为:https://blog.tangwudi.com/feed,欢迎订阅;如有需要,可以加入Telegram群一起讨论问题。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
       
zh_CN