一、背景与迁移场景

我们公司有一套Confluence用了很多年,一直装在本地一台老旧的服务器上。单机运行平时没什么大问题,但一到下午全员用的时候,页面加载就很慢,偶尔还卡死。领导说要升级成Data Center集群,把工作拆给好几台服务器,这样挂一台也不会全体罢工。听起来很美好,但真正的麻烦在迁移那天才开始。

1.1 我的迁移路线

我选择的方式是:把原有服务器上的Confluence完全停掉,然后打包整个安装目录和data目录,复制到新的集群环境里。中间也会用官方提供的迁移工具,但事实上手工步骤更多。大体流程是:

  • 先停掉旧服务器的Confluence服务;
  • 备份数据库;
  • 备份本地数据目录(也就是home目录),尤其是附件;
  • 在集群节点上恢复;
  • 再启动集群。

当时我对这个流程胸有成竹。因为做过很多次小迁移,无非是复制粘贴。可这次还没等到启动成功,迁移就中途报错了。

1.2 失败那一刻的报错

迁移到达约三分之二时,新集群的一个节点上突然跳出一大段异常。我看了一眼,主要说了事务日志对不上,还有一个附件块和预期的不一致。具体内容我就不原样贴出来了,关键是系统提示:

  • 数据库事务日志的LSN位置和备份文件记录的位置不一样;
  • home目录里的附件元数据与数据库文件中记录的存储路径不匹配;
  • 启动过程中有会话回滚,导致附件索引重建失败。

看到这里,我整个人都懵了。因为迁移前我明明做过完整备份,怎么还会发生这种问题呢?后来才明白,问题不发生在备份阶段,而是发生在迁移过程中,中途失败导致了“半迁移”状态,让事务日志和附件存储之间出现了裂缝。

二、事务日志和附件存储到底是个啥

要讲清楚这次事故,得先让没接触过的朋友理解两个基础概念。

2.1 事务日志

Confluence的数据库里,每一笔修改(新建页面、编辑评论、改动权限)都会先写进数据库的事务日志,然后再落盘到实际的数据表。事务日志的作用就是保证万一系统突然断电或者崩溃,数据不会丢,也能恢复到正常状态。

在集群环境里,事务日志更关键。因为多个节点同时读写,如果日志顺序对不上,就会像两个人各挤一次门,谁都进不去。迁移时如果日志和表数据不在同一个时间点上,数据库就认为数据不一致,不允许启动。

2.2 附件存储

Confluence的附件,比如上传的图片、文档,不是放在数据库里的,而是以文件形式存在home目录下的attachment文件夹内。数据库只保存一份元数据,记录文件名、路径、上传时间等。这样设计的好处是文件读写快,但坏处是:如果附件文件和数据库记录对不上,用户看到的页面显示小图标,点开就是404。

这次迁移失败,正是踩中了这两点的雷:事务日志没有干净地截断,附件文件的校验值又和数据库不一致。

三、校验全记录:一边查一边崩溃

我发现问题后,没有立刻去重启服务,而是先做了一轮完整的一致性校验。这一步非常必要,因为如果不知道哪里坏了,修复就会靠猜,靠猜就会越修越乱。

3.1 先确认事务日志状态

我用的数据库是PostgreSQL,Confluence对它支持很好。我通过一条SQL查询,看一下当前数据库里的检查点等信息。但我的示例都统一用bash来展示操作,所以你看到的是一个bash脚本,里面调用psql命令。

#!/bin/bash
# 技术栈:bash(数据库操作通过psql命令)
# 目的:查看当前PostgreSQL事务日志和恢复状态

PGHOST="192.168.1.10"          # 数据库服务器地址
PGUSER="confluence"            # 数据库账号
PGDB="confluencedb"            # 数据库名称

# -X 是禁止读取psql配置文件,保证输出干净
# -A 是无对齐模式,方便后续脚本解析
# -t 是只返回查询结果,不显示表头和额外信息
psql -h "$PGHOST" -U "$PGUSER" -d "$PGDB" -X -A -t <<'SQL'
SELECT current_setting('wal_level');
SELECT pg_current_wal_lsn() AS current_lsn;
SELECT pg_last_committed_xact();
SQL

上面这段脚本能帮我拿到数据库的wal级别、当前日志序列号LSN。如果LSN和备份时记录的不一样,说明日志已经写到了后面,不能再拿旧的备份文件做无缝恢复。

3.2 检查附件存储的完整性

然后我检查附件目录。我的home目录里存放着所有附件的文件。为了发现不一致,我用find命令把所有文件名和大小列出来,再和数据库里的附件元数据表比对。

#!/bin/bash
# 技术栈:bash
# 目的:生成附件文件的校验清单,方便和数据库元数据比对

ATTACH_DIR="/var/opt/confluence/data/attachment"  # 附件实际存放目录
OUTPUT_FILE="/tmp/attachment_manifest.txt"        # 存放清单的文件路径

# 如果清单文件存在,就删掉重新生成,避免旧数据干扰判断
if [ -f "$OUTPUT_FILE" ]; then
  rm -f "$OUTPUT_FILE"
fi

# 计算每个文件的SHA256哈希值,并记录相对路径
# 输出格式:哈希值  相对路径
find "$ATTACH_DIR" -type f -exec sha256sum {} \; | sed "s|$ATTACH_DIR/||" > "$OUTPUT_FILE"

# 统计一共生成了多少条附件记录,方便和数据库数量比较
echo "附件目录中的文件总数:"
wc -l < "$OUTPUT_FILE"

# 再把数据库里的附件数量查出来
psql -h 192.168.1.10 -U confluence -d confluencedb -X -A -t -c \
"SELECT COUNT(*) FROM attachments;" 

执行之后,我得到了两个数量。第一个是文件系统里的附件数量,第二个是数据库里的附件元数据数量。两个数量对不上,差了十几条。那一刻我就知道,这不是偶发,而是迁移时确实丢失了一些文件。

3.3 找出具体不一致的文件

光看总数不够,我还要知道具体是哪几个文件出了问题。最好的方法是对数据库里的路径和文件系统里的文件做差集。

#!/bin/bash
# 技术栈:bash
# 目的:用 comm 命令找出数据库里有记录但文件系统缺少的文件

# 第一步:从数据库导出所有附件路径,放到临时文件里
# 注意confluence数据库的表里路径字段一般是title和path组合,我简化成filepath
psql -h 192.168.1.10 -U confluence -d confluencedb -X -A -t -c \
"SELECT filepath FROM attachments;" | sort > /tmp/db_paths.txt

# 第二步:从上面生成的清单里,只提取路径列,并且排序
cut -d' ' -f2- /tmp/attachment_manifest.txt | sort > /tmp/fs_paths.txt

# 第三步:对比两个文件列表
# 输出在数据库中存在、但文件系统里没有的记录
comm -23 /tmp/db_paths.txt /tmp/fs_paths.txt

运行完这串命令,屏幕上列出了十几个附件路径。这正是数据库里认为存在、但磁盘上已经不见了的文件。问题找到了。

四、修复一致性:让日志和附件重新对齐

知道了问题所在,接下来的事情就是让事务日志和附件存储回到同一个时间点。

4.1 回滚到干净的备份

既然迁移失败已经把状态搞混了,最好的解决办法不是硬着头皮往前,而是回到迁移开始前那个干净的备份点。我重新恢复了一整套备份:数据库先恢复到备份时的时间点,然后把home目录里的附件也整个恢复回去。

这里有个注意点:数据库和附件必须来自同一个备份时刻。如果你数据库用昨天的备份,附件用前天的,那一样对不上。所以我是用同一个备份脚本同时导出的。

4.2 用rsync增量补齐缺失文件

恢复完成之后,我再运行一次rsync,让新集群的附件目录和本地源目录完全一致。rsync的好处是可以断点续传,而且只传变化的文件,特别适合大附件场景。

#!/bin/bash
# 技术栈:bash
# 目的:用rsync把本地附件目录完整同步到集群节点

rsync -avz --delete \
  --chmod=u=rwX,g=rwX,o=rX \
  -e "ssh -p 22" \
  /var/opt/confluence/data/attachment/ \
  confluence@172.20.10.5:/data/confluence/home/attachment/

# 参数说明:
# -a 归档模式,保留权限、软链接、时间属性
# -v 显示详细进度,方便我这种人观察
# -z 传输时压缩,节省带宽
# --delete 删除目标目录里源端没有的文件,保证两边一模一样

这里一定要加--delete,否则目标目录里残留的脏文件还会继续影响新集群。跑完rsync,我再做一次附件清单比对,这时候数量终于一致了。

4.3 校验事务日志的连续性

附件修好只是第一半,事务日志也得检查。我在恢复数据库时,特意使用了带时间点恢复的方式,把日志停在了迁移开始前的那个审阅点。具体做法是在恢复完成后执行这条命令:

#!/bin/bash
# 技术栈:bash
# 目的:确认数据库已恢复到一致状态,并查看当前时间点

psql -h 192.168.1.10 -U confluence -d confluencedb -X -A -t <<'SQL'
SELECT pg_is_in_recovery();
SELECT pg_last_wal_replay_lsn();
SELECT pg_last_xact_replay_timestamp();
SQL

只要pg_is_in_recovery一直显示f,就代表数据库不是处于恢复中,而是正常运行状态。此时的LSN就是一致的锚点。在这个之后再启动Confluence集群,就不会再出现事务日志错乱的报错。

五、这件事让我学到的几个道理

5.1 迁移前一定要做“干跑”演练

你以为做了备份就行了,其实不是。迁移到集群和普通备份恢复有一个很大的不同:集群会把数据分成多个节点管理,你必须在正式操作前,在开发环境完整走一遍同样的步骤。我曾经觉得演练浪费时间,但这次如果不是在测试环境先跑过一遍,我可能连怎么排查都不会。

5.2 一致性校验不应该等到失败后才做

把一致性校验放在迁移过程中、每一步之后,比最后再校验要安全得多。比如你复制完数据库,可以先查一下LSN和表条数;复制完附件,马上比对清单。一顿饭的工夫而已,却能在问题萌芽时就发现。

5.3 事务日志和附件存储是两套并行系统

很多新人会把Confluence数据理解成一个整体的文件夹,错了。数据库里的内容,和home目录下的附件文件,必须时刻保持映射。任何一个文件丢失或路径变化,都会导致页面出现裂开。迁移时一定要分别确保两边一致,而且从同一个备份源出来。

5.4 这个方案的特点

这个迁移方案的优势是透明、可控,出了问题能根据日志和哈希值逐一排查。缺点也很明显:一旦中途失败,恢复过程很麻烦,而且需要你手动处理一致性,对初学者不友好。如果有条件,可以用官方推荐的迁移工具进行在线迁移,但底层原理也是一样的,还是要理解事务日志和附件存储的关系。

六、文章总结

这次Confluence迁移从本地服务器到Data Center集群,中途失败得让我印象深刻。我通过对事务日志的状态查询和附件存储的哈希比对,找出了两边不一致的根源,然后通过恢复同一时间点的数据库备份和附件数据,再用rsync修正文件库,最终让集群顺利启动。整个过程看起来简单,其实每一步都需要细心。

如果你也正在做类似的迁移,我希望这篇记录能帮你少走弯路。记住最重要的三件事:备份要完整、校验要提前、日志和附件要同步。数据迁移不只是把文件复制过去,更是让逻辑和物理都在正确的位置上。只要做到这几个字,再大的集群也不是问题。