葡萄城技术团队头像
一次 502 故障复盘:多进程 SQLite 写锁如何击穿容错链封面图

一次 502 故障复盘:多进程 SQLite 写锁如何击穿容错链

第二次运行 SpreadJS 单文档 1000 人压测时,客户端出现 502。排查发现 4 个协同节点中有 3 个进程退出,而压测代理仍将新请求发往这些失效端口。

这套 demo 有两类存储:OT 存储负责协同文档与操作历史,应用业务库保存用户、文档附加信息和角色。本次故障的锁错误来自多进程共享的 SQLite 业务库——我们沿节点退出日志逐层排查,分别处理了写入冲突和错误传播两层问题。

从锁错误到进程退出

节点日志包含以下信息,下面摘录了错误文本,省略调用栈:

SQLITE_BUSY: database is locked
UnhandledPromiseRejection: ... internal error: insert doc error
Node.js v24.13.0

把数据库错误和进程退出记录对应起来后,故障路径是:多进程争用同一 SQLite 文件,等待写锁超时,业务写入返回错误;调用路径没有处理 Promise rejection,Node.js 进程退出;入口继续转发请求,客户端收到 502。

SQLite 的 WAL 模式可以改善读写并发,但不会让多个进程同时持有写锁。本次此前处理过启动时的 PRAGMA 竞争,运行期业务写入仍缺少完整的锁错误处理。

记录能够确认“第二次运行时暴露”,但没有足够的同条件日志说明为什么第一次没有触发,也没有给出三个节点依次退出的精确时间差。我们不据此判断是数据量增长、残留连接还是运行时长导致第二次更容易失败;要回答这一点,需要在固定负载和干净环境下重放。

在这里插入图片描述

图示:故障链与迁移方向。SQLite 是该 demo 的业务库,迁移 PostgreSQL 为本项目选择,非通用要求。

为什么入口没有摘除失效节点

本次故障使用的是压测目录中的最小 Node HTTP/WebSocket 代理,不是前面部署实验里的 nginx。

我们核对代理代码后看到,它使用静态 upstream 列表,分别轮询 HTTP 和 WebSocket 新连接。HTTP 上游连接失败时返回 502,WebSocket 上游失败时关闭连接;代码中没有周期健康探测,也没有在失败后将节点移出后续轮询。

因此,本次现象可以解释为代理继续选择失效端口,不应概括为“负载均衡器都会这样”。生产入口需要另外设计健康检查、失败摘除和重新加入。它们减少新请求进入失效节点,已有长连接仍需要客户端重连。

SQLite 写入路径具体改了什么

我们先保留 SQLite,补上有界重试和幂等,再将业务表迁移到 PostgreSQL。以下是核对当前源码得到的设置:

修改项本 demo 的处理
SQLite 锁等待busyTimeout 从 10 秒调至 30 秒
可重试错误仅 SQLITE_BUSY、SQLITE_LOCKED
重试次数初次执行失败后,最多再重试 25 次
应用层等待50 ms、100 ms、150 ms……按 50 ms 递增,公式上限 1500 ms
可重复写入角色写入使用 upsert;附加操作记录冲突时跳过重复项

对应的重试核心如下,已省略数据库封装:

for (let n = 0; ; n++) {
  try {
    return await db.run(sql, params);
  } catch (e) {
    if (!isBusyError(e) || n >= 25) throw e;
    await sleep(Math.min(50 * (n + 1), 1500));
  }
}

按这组参数,第 25 次等待为 1250 ms,尚未达到公式中的 1500 ms 上限。修复报告曾将其称为“指数退避”,但当前实现是线性递增,这里以源码为准。

30 秒是每次数据库调用内部的锁等待,不能理解为整个请求最多等待 30 秒。叠加重试后,总等待可能更长。我们将这些参数作为该 demo 的修复记录;实际业务还应设置请求总截止时间,并按幂等条件、并发量和用户可接受延迟重新设计。

哪些写入允许降级,哪些不能套用

当前中间件对两项 demo 附加写入捕获错误并记录 warn:操作附加记录 insertOpEx,以及最后修改信息 updateLastModifyInfo。失败后继续调用后续处理,使这类附加记录失败不再直接中断协同提交。

代价是附加操作记录可能不完整,最后修改人和时间也可能未更新。它们不能再作为完整审计依据。如果业务要求这些信息与文档提交原子一致,就不能照搬这种降级方式,需采用可靠记录与补偿机制。

回归代码还加入了进程级异常捕获后继续运行的保护。这是当时的实验措施。生产环境发生未处理异常后,进程状态可能不再可靠,我们更倾向于记录诊断、摘除实例,再由进程管理器重启,而非仅靠全局捕获长期维持运行。

两轮回归分别验证了什么

先修复 SQLite 写入,再把业务表迁到 PostgreSQL 的独立 app schema。两轮均为同机 1000 人、4 节点、每人每 30 秒编辑一次,逐步加入 30 秒,稳态 90 秒。

指标SQLite 修复后业务库迁移 PostgreSQL 后
实际加入用户10001000
编辑 ACK / 失败2998 / 03001 / 0
实际编辑速率33.3 op/s33.3 op/s
编辑 ACK p95345.9 ms419 ms
最终内容校验通过通过
4 个节点是否全程存活是是

ACK 是服务端的提交确认,p95 表示约 95% 的记录不超过该延迟。修复后的 SQLite 回归中,没有新增 SQLITE_BUSY、未处理 rejection 或全局异常保护触发记录。迁移后的回归也保持节点存活、内容校验通过。

这两轮说明在所测窗口内没有再出现该故障,不能替代长期稳定性验证。它们与最终分机有线性能测试也不是同一条件,不应用来宣称迁移 PostgreSQL 后延迟必然提高或降低。

这次排查涉及业务库、调用路径和代理三层问题。其他采用多进程共享本地数据库的项目,建议依次检查写锁处理、失败后的错误传播,以及节点退出后的入口摘除机制。换数据库能改变并发写入方式,但重试边界、写入幂等性和健康检查仍需单独设计。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/powertoolsteam/article/details/170614517

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--