1 博客双活架构第三次优化的起因
前几天,我准备对博客双活架构中的 Mac mini 节点做一次很常规的维护操作:升级一下 Portainer 的版本。结果,一个看起来并不起眼的小问题,最终却把整个节点的适用性彻底暴露了出来——在 Mac mini 上执行:
docker pull portainer/portainer-ce:latest
却意外报错:
error getting credentials - err: exit status 1, out: keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again
乍一看像是 Portainer 或 Docker 的问题,但稍微一查就会发现,这件事其实跟 Portainer 本身几乎没关系。
真正的根源在于:macOS 下 Docker 默认使用 Keychain(钥匙串) 存储登录凭据,而在 非交互式会话(例如 SSH、后台任务、开机后无人登录的状态)中,Keychain 是无法弹出 GUI 进行解锁的。这在 Mac mini 这种长期无人值守的环境 里尤其常见。
一句话概括原因就是:Docker 想从 Keychain 里拿凭据,但当前 session 不允许任何用户交互,于是直接失败。
对应的解决方法其实也不复杂,只要关闭 Docker 对 Keychain 的凭据依赖即可。具体做法是编辑:
~/.docker/config.json
删除其中的:
"credsStore": "desktop"
或:
"credsStore": "osxkeychain"
操作完成后,再次执行 docker pull,镜像拉取果然恢复正常。
但真正的问题,并不在这里。因为紧接着我发现:Docker Desktop 中原本运行的多个容器开始出现异常,服务无法正常启动,即使重启 Mac mini 也无法恢复。折腾了一段时间后,我最终只能选择 重新安装 Docker Desktop,并重新部署所有关键容器。
好在这个过程并不算耗时,二十分钟左右就全部恢复完成了。但也正是这次经历,让我第一次非常清晰地意识到一件事:Docker Desktop + Mac mini,这个组合,本身就不适合作为博客双活架构中的“节点”。
原因并不复杂,也不是这一次操作失误导致的,而是结构性的:
- 双活架构中的节点,本质上应该是「类服务器」
- 需要 无人值守
- 重启后要能 确定性恢复运行状态
- 不能依赖桌面 UI
- 更不能依赖 Keychain 这类强绑定用户会话的机制
而 Docker Desktop + macOS 的现实情况恰恰相反:
- 强依赖 GUI
- 强依赖用户登录态
- 内部状态复杂,不具备可预测性
- 一旦环境出问题,恢复路径高度不确定
这并不是 Docker Desktop 做得不好,而是它的定位本来就不是“服务器级运行环境”。
这次 Portainer 升级引发的问题,只是一个触发点。真正让我下定决心的,是它再次验证了一个结论:Mac mini 这个节点,已经不适合继续承担博客双活架构中的运行职责。
基于这个判断,我开始了博客双活节点的第三次优化。
2 第三次优化的核心调整
这次第三次优化,从硬件和基础设施的角度来看,确实算不上一次“大动干戈”:没有新增云服务商,也没有引入新的中间件组件,整体资源规模并未发生明显变化。但在架构层面,变化其实非常明确——双活体系中各个节点的地位和职责,被重新定义了。
一个最典型的变化,体现在 intermini 这个节点身上——在之前的文章中,intermini 虽然已经出现过,但更多承担的是辅助性角色,比如作为 Simply Static 的导出节点存在,并未真正进入博客双活架构的核心视野。macmini 在导出并同步数据库文件时,也只是顺带通过 syncthing 将数据同步到 intermini,它更像是一个“工具节点”,而不是服务节点。
而在这一次调整中,intermini 被正式纳入对外服务体系,从一个可有可无的配角,转变为与芝加哥 VPS 对等的只读节点之一,真正参与到博客内容的对外发布和双活冗余中。
在之前的双活架构中,macmini 既承担写操作,又对外提供读服务。这个设计本身没有问题,但随着使用时间的增加,其短板逐渐显现。macmini 本质上是一台桌面设备,依赖 Docker Desktop、用户登录态和系统组件。一旦遇到系统升级、桌面服务异常或无人值守重启,整体行为就可能变得不可预测。这对于需要长期稳定运行的博客系统来说,是一个潜在隐患。
因此,这次优化的核心思路非常直接:让 macmini 退居“控制位”,不再直接承担服务功能。
在新的结构中,macmini 上的 WordPress 实例被明确定位为只写节点,只负责主动写操作,如发布文章、修改页面或调整站点配置。所有“对外展示内容”的职责,则交由intermini和芝加哥VPS节点来完成。也就是说,所有改变系统状态的操作依旧集中在 macmini,而对外访问完全由读节点处理:

至于把内容从写节点传递到2个只读节点,我仍旧采用的之前那种简单但可靠的方法:在完成修改后,将 WordPress 数据库导出为完整的 wordpress.sql 文件,并通过 rsync 同步到 intermini 和芝加哥 VPS 两个节点。这种方式虽不如实时数据库复制“高大上”,但状态清晰、行为可控,排查和恢复成本低,非常适合小规模个人博客。
intermini 和芝加哥节点都是debian12的系统,Docker 容器不依赖桌面 UI,也不依赖用户会话。这两个节点被定位为完全对等的只读双活节点,负责对外提供博客访问服务。任何节点出现异常,流量可以自然切换到另一个节点,无需人为干预。
在2个只读节点上,我部署了相同的监控脚本,持续监听相同路径的目录/docker/wordpress/db/。一旦检测到新的 wordpress.sql 文件同步到位,就会自动触发数据库重建和导入流程,并把导入结果通过 Bark 发送通知到我的 iPhone 和 Mac。整个过程不需要节点之间直接通信,只要文件同步成功并通过 MD5 校验,内容就会自动更新。
这种设计的一个关键好处是:节点之间几乎不存在强耦合。macmini 不需要感知读节点是否在线,读节点之间也无需了解彼此状态,每个节点只对“文件是否出现”作出反应,使整体复杂度保持在可控范围。
当然,博客中仍有被动写操作,例如用户评论。为了避免单点故障,我对 Cloudflare Worker 的评论处理逻辑进行了调整,使评论能同时写入 macmini、intermini 和芝加哥 VPS 节点。这里的多点写入并非追求严格强一致性,而是保证评论数据在节点切换时不会丢失。在个人博客场景下,这种权衡是可接受的。
整体来看,第三次优化并没有增加系统复杂度,反而通过职责重新划分,让每类节点只做最擅长、最稳定的事情:macmini 负责写入和控制,intermini 与芝加哥节点负责读取和对外服务,而数据流动方式尽量直观、易于验证。
附加收益是,读节点以后可以自然横向扩展——只要新的节点能够接收并导入 wordpress.sql,即可加入对外服务,同时系统依然保留了一个清晰、可控的“中心”,不会因为扩展而失去秩序。
在实际使用体验上,这次调整也有明显变化:写作过程和对外展示被彻底解耦。 以前 macmini 同时承担写和读职责时,后台修改文章或调整结构的中间状态,可能被访客实时看到。现在 macmini 不对外提供读服务,我可以在本地反复修改、推翻和重来,而不影响当前发布内容。只有确认修改完成并导出数据库同步到读节点后,博客才会一次性切换到新内容。从使用感受上,这更像一次明确可控的发布动作,而不是边改边生效,让写作和调整更从容。
关于双活架构的详细设计和节点职责划分,请参考我之前的文章:博客架构的第二次重构:VPS搬家引发的服务迁移与双活容灾实践 和 WordPress多活架构(简版)在个人博客中的落地方案。
3 一次意外暴露出来的mariadb数据库版本细节问题
在这次第三次优化的整体过程中,大部分步骤都推进得比较顺利,真正让我卡住的,其实只有一个地方:从 macmini 使用 rsync 将 wordpress.sql 同步到 intermini 之后,导入到数据库失败,结果直接导致 intermini 上的 wordpress 库被清空了。
刚遇到这个问题时,我其实是有点懵的。因为无论是目录监听、自动触发,还是数据库导入脚本,intermini 上用的都是和芝加哥节点完全一致的一套逻辑,而芝加哥节点一直运行得非常稳定,从未出现过类似问题。
这意味着,问题大概率不在脚本逻辑本身,而是在某个“更底层、但又不显眼”的地方。
为了快速定位问题,我直接绕开监控脚本,在 intermini 上手动执行类似以下数据库导入命令进行排查:
docker exec -i mariadb mysql -uroot -ppassword wordpress < /docker/wordpress/db/wordpress.sql
结果立刻报错:
ERROR at line 1: Unknown command '\-'.
这个错误提示本身并不算友好,但它至少明确了一点:问题出在 SQL 文件的第一行。 于是我用 head 命令查看了 wordpress.sql 的开头内容:
head -n 20 /docker/wordpress/db/wordpress.sql
结果如下:

可以看到,文件的第一行是一条特殊格式的注释:
/*M!999999\- enable the sandbox mode */
也就是说,intermini 上的 MariaDB 在解析这条注释时,直接把它当成了非法命令,而不是正常跳过的注释,从而导致整个导入流程在第一行就中断了。
但这里又出现了一个非常关键的问题:同样的 wordpress.sql 文件,为什么在芝加哥节点上导入是完全正常的?
继续顺着这个线索往下查,我对比了三个节点上的 MariaDB 版本,使用命令类似如下:
docker exec -it mariadb mysql --version #这里的mariadb是容器名称
- macmini:

版本是10.11.15。 - 芝加哥 VPS:

版本也是10.11.15。 - intermini:

版本是10.11.6。
到这里,问题基本就清晰了——虽然 10.11.15 和 10.11.6 同属于 10.11 这个大版本,但 MariaDB 在某些次版本之间,对特殊注释(尤其是 /*M! ... */ 这种 MariaDB 特有扩展注释)的解析行为,确实存在差异。
更具体地说,就是 intermini 上较低版本的 MariaDB(10.11.6)并不能正确识别这类注释格式,于是在导入 SQL 时直接报错;而芝加哥节点使用的 10.11.15 则没有这个问题。
为了验证这个判断,我没有去改 SQL 文件本身,而是选择了最直接的方式:将 intermini 上的 MariaDB 版本升级到 10.11.15。升级完成后,用完全相同的 wordpress.sql 再次执行导入命令,结果一次成功,没有再出现任何异常。
至此,这个问题的根因也就彻底确认了。
现在回头来看,其实这是一个非常典型、但又很容易被忽略的坑点:在创建 Docker 容器时,我使用的是 mariadb:10.11 这样的镜像标签,并没有显式锁定小版本号。 而在多节点架构中,只要有一个节点的次版本略有不同,就可能在极端情况下触发这种“不明显但致命”的兼容性问题。
好在这次踩坑的代价并不算大,但它也再次提醒我:在多节点、可复制的架构里,版本一致性本身就是系统稳定性的一部分,而不是一个可有可无的细节。
如果不想升级intermini上的mariadb版本,其实也可以解决这个导入问题,只需要在导入SQL文件之前对wordpress.sql文件进行预处理——删掉第一行的注释即可,可以使用以下命令实现:
sed -i '/^\/\*M!/d' /docker/wordpress/db/wordpress.sql
不过,建议还是直接把所有节点的mariadb版本进行统一,避免遇到类似这次的莫名其妙的问题。
4 首页加固:从结构稳定到访问稳定
在完成第三次优化之后,博客在结构层面已经变得非常清晰:写入集中、数据分发明确、读节点职责单一。从系统内部来看,它已经具备了一个多活架构应有的稳定形态。
但在实际运行中,我还是额外做了一件并不“核心”、却非常实用的事情——对首页进行了加固处理。
之所以单独把首页拎出来,是因为首页在整个博客访问链路中有一个很特殊的位置:它几乎承载了所有访客的第一跳,同时又是最容易受到结构调整影响的页面。哪怕后端已经非常稳定,只要首页存在不确定性,整体体验就会被明显放大。
在这次优化中,我对首页采取了比普通页面更保守的策略:通过静态化和强制缓存,让首页在绝大多数时间里,直接由边缘节点返回结果,而不是回源到任何一个 WordPress 实例。这样一来,首页的可用性被从“依赖后端状态”,提升到了“依赖边缘缓存是否存在”。
这一点在使用 Cloudflare APO 的场景下,尤其值得单独说明。很多人会下意识地认为,启用了 APO 之后,首页就一定是稳定、可缓存的。但在实际行为上,APO 的首页缓存状态,仍然与 WordPress 的工作状态存在关联。一旦回源请求过程中触发了异常——比如数据库连接错误、PHP 致命错误,甚至某些插件级别的异常响应——Cloudflare 在这一轮请求中就可能无法获取到一个“可缓存的有效响应”,从而导致首页缓存失效。
换句话说,APO 并不是一个完全脱离源站健康状态的“黑盒缓存”——当 WordPress 处于异常或不稳定状态时,首页往往是最先暴露问题的页面。
正因如此,在这次第三次优化中,我选择将首页的稳定性目标进一步前移:即便后端某个节点短时间内出现异常,首页依然由已经存在的静态结果直接响应,而不是被迫回源验证当前系统状态。
需要说明的是,这一步并不属于多活架构的“必要组成部分”,它也不参与节点间的数据流转逻辑。它更像是一层额外的缓冲垫:当内部结构发生调整、节点切换,甚至短暂失稳时,首页依然可以保持一个相对稳定、可预期的对外表现。
关于首页静态化和缓存策略的具体实现方式,需要在自己使用的缓存供应商进行相关规则设置,以cloudflare为例,直接使用一条简单的缓存规则即可:

关于cloudflare缓存规则的设置细节,我在之前的文章中(参见:家庭数据中心系列 cloudflare教程(六) CF Cache Rules(缓存规则)功能介绍及详细配置教程)已经详细介绍过,这里不再多说。这一章更想强调的,是它在整体结构中的位置——它不改变系统内部的职责划分,却有效降低了系统边界层的不确定性。
从结果来看,这种“结构先稳定、边界再加固”的顺序是值得的。内部可以继续演进,而对外呈现给访问者的体验,却被牢牢固定在一个更安全的区间内。
5 后话:什么才算是多活架构的完整形态
回过头来看这一次的第三次优化,它并不是在节点数量上做文章,也不是追求“同时在线、同时写入”的表面热闹,而是在控制权与数据流向上做了一次明确切割。
如果只停留在“多个 WordPress 节点同时提供服务”,那充其量只是多副本;如果写入、数据库、控制逻辑仍然纠缠在同一节点上,那也很难称得上真正的多活。
这次结构调整的关键变化在于:写入被集中控制,数据被主动分离,而对外服务则保持冗余。 macmini 不再承担日常读服务,但它依然是事实上的控制中枢——写入发生在那里,数据库导出和分发也在那里完成,结构性操作也从那里发起;而其他节点只做一件事:稳定、可预期地对外提供内容访问能力。
从这个角度看,多活架构的“完整形态”并不是每个节点都能做所有事,而是:控制面是清晰的;数据流向是单向且可控的;服务节点可以随时增减,而不影响整体一致性。
这种结构并不追求理论上的最强一致,也不刻意展示复杂度,但它在工程上是稳的。也正因为如此,第三次优化并没有引入太多“新技术”,更多是对已有结构的一次角色重新分配。哪些能力值得集中,哪些能力必须分散,这些判断本身,可能比具体实现方式更重要。
至于这套结构是不是“终点”,答案显然不是。它只是当前约束条件下,一个更偏向稳定性、可维护性和可控性的阶段性选择。等到下一次再动它,大概率也不会是因为“还能怎么炫”,而是因为某个新的真实问题,逼着结构继续向前走一步。
而那时候,再记录一次就好。
我也是因为你说了PostgreSQL的问题,而我又在mariaDB里遇到没精确指定小版本的坑,所以觉得应该记录一下,以后就学乖了。
Docker里跑数据库还是有些蛋疼的,就像咱们月初时的讨论,我是被PostgreSQL坑了,同仓库同版本号的镜像居然底层操作系统不一样,Docker里跑数据库需要指定精确到最小版本和系统版本的镜像