优先级: 中,无数据丢失,但会静默地从错误的账户发送面向用户的私信(PM),且在启动后更改的任何站点设置在后台作业内部均可能错误。
平台: 自托管,标准 discourse_docker 双容器配置(data + web_only)。核心版本 2026.7.0-latest(30d8364f0ab)。非浏览器特定问题 —— 属于服务器端问题。
描述
实际结果: 在 Sidekiq 守护进程重新派生(例如在 Demon::Sidekiq.rss_memory_check 中因 RSS 内存重启后),新的 Sidekiq 进程会从 unicorn 主进程的启动时快照 中读取站点设置,而非从数据库中读取。自主进程启动以来更改的任何设置,在后台作业内部均会静默出错,直到满足以下任一条件:(a) 在该 Sidekiq 进程存活期间再次更改该设置,或 (b) 重启容器。
我站点的可见症状:自动化系统私信(post_hidden、flags_agreed_and_post_deleted、自动解除暂停)是从 site_contact_username 的 旧 值发送的。数据库中已保存了修正后的值五天,且工作人员操作日志显示该时间段内无写入操作。与此同时,通过 Web 请求发送的相同类型的消息使用了正确的用户,因为 Web 工作进程已处理了 MessageBus 刷新,而 Sidekiq 未处理。
预期结果: 新派生的守护进程应读取当前的站点设置。无论 Sidekiq 自主进程启动以来重启了多少次,Sidekiq 中的 SiteSetting 值都应与数据库保持一致。
复现步骤
- 启动自托管实例。记录当前的
site_contact_username(称为userA)。 - 在管理后台 → 设置中,将
site_contact_username更改为userB。所有运行中的进程均正确获取此值(MessageBus/site_settings→SiteSetting.refresh!)。 - 终止容器内的 Sidekiq 进程,以便 unicorn 主进程重新派生它(
kill <sidekiq_pid>;或者只需等待Demon::Sidekiq.rss_memory_check在其 RSS 超过 1000 MB 阈值时重启它 —— 在繁忙的站点上,这会自动发生)。 - 触发任何通过
Jobs::SendSystemMessage投递的系统消息 —— 例如,让帖子因社区举报而被隐藏,这会从Post#hide!中排队:send_system_message。 - 打开生成的私信。
观察结果: 私信由 userA 撰写 —— 即 主进程 启动时当前的值,而非 userB。
预期结果: 由 userB 撰写。
分析
Discourse.after_fork 调用 SiteSetting.after_fork(lib/site_setting_extension.rb:736):
def after_fork
@process_id = nil
ensure_listen_for_changes
end
它从未调用 refresh!,因此子进程通过写时复制继承父进程的 current 设置哈希,并在其整个生命周期内保持该哈希,除非有 后续 更改被广播。
ensure_listen_for_changes(lib/site_setting_extension.rb:713)在子进程中实际上也是一个空操作,因为 @subscribed 被继承为 true:
def ensure_listen_for_changes
return if @listen_for_changes == false
unless @subscribed
MessageBus.subscribe(SITE_SETTINGS_CHANNEL) { |message| ... }
@subscribed = true
end
end
它之所以能正常工作,仅仅是因为 MessageBus.after_fork 先运行(lib/discourse.rb:1064)并针对继承的回调注册表恢复了总线线程。
守护进程是从主进程进行的普通 fork(lib/demon/base.rb:181),且 Sidekiq 在生产环境中常规地重新派生:当 RSS 超过 DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000 时,Demon::Sidekiq.rss_memory_check 会重启它。每次派生都会复活一个两周前曾为真的值。
这是一个长期存在的问题,而非回归错误:自 8fc2549(2014 年)以来,after_fork 一直具有此形态,且在当前的 main 分支上未变。
为何容易被忽视: 在首次启动时,主进程和 Sidekiq 共享相同的(正确的)快照,且在 Sidekiq 存活期间,它确实会接收更改广播。只有在主进程启动 之后 更改的设置,且在 之后 派生的 Sidekiq 进程中,才会出现偏差 —— 即它会静默开始出错,且不会在任何地方产生错误。
对联系人用户之外的影响
site_contact_username 只是可见的情况,因为它将用户名标记在用户阅读的私信上。相同的陈旧性适用于作业内部咨询的任何站点设置 —— 速率限制、电子邮件/通知设置、功能开关、插件设置 —— 且无错误,无日志行。操作员会合理地得出结论“设置无法保存”,并去寻找覆盖数据库的其他原因。
建议的修复方案
在 SiteSetting.after_fork 中,丢弃继承的状态并重新读取:
def after_fork
@process_id = nil
@subscribed = false
ensure_listen_for_changes
refresh!
end
@subscribed = false 也使子进程自己的订阅变得明确,而不是依赖 MessageBus.after_fork 来恢复继承的那个。
操作员的变通方法
./launcher restart web_only(完全重启容器,以便主进程在启动时重新读取设置)。在管理后台重新保存设置仅修复 当前存活 的 Sidekiq 进程 —— 下一次重新派生会静默地将其恢复原状。