Skip to content

飞书网关子进程改由预加载过的 forkserver 派生,内存按机器人数大幅下降 - #119

Merged
wxkingstar merged 1 commit into
mainfrom
claude/feishu-gateway-forkserver
Oct 9, 2026
Merged

wxkingstar merged 1 commit into
mainfrom
claude/feishu-gateway-forkserver

Conversation

@wxkingstar

Copy link
Copy Markdown
Owner

问题

飞书网关每个机器人一个子进程。飞书 SDK 的包入口会 from .api import *,导入任何子模块都会加载全部开放平台接口(约 1 万个模块),每个子进程单独导入后 PSS 约 176MiB(Linux runtime 镜像实测)。7 个机器人就把 1.5GiB 的容器上限吃满;重启时所有子进程同时导入,CPU 打满、内存冲顶,网关会陷入 OOMKilled 循环,每次重启所有飞书机器人断线。

改动

  • forkserver 预加载:新增 gateway_feishu/preload.py,forkserver 先导入子进程用到的模块,再 gc.freeze(),子进程 fork 后共享这些页。不冻结 GC 时,回收会写脏共享页,几乎省不下来。
  • SDK 模块级事件循环:SDK 在 lark_oapi.ws.client 导入时建了一个循环,长连接跑在上面;fork 后各子进程会共用同一个 epoll。preload 里先关掉,child.run_process 里每个子进程另建一个。
  • supervisor 不加载 SDK:子进程入口 run_child_process 延迟导入 child,supervisor 自身仍约 76MB。
  • 父进程存活检测:forkserver 被子进程持有的存活管道吊着,supervisor 死后子进程的父进程号不变;改看 multiprocessing.parent_process().is_alive()。
  • 启动取消安全:首次启动要等预加载,process.start 在线程里跑;停机取消调度时进程仍会起来,spawn 现在等启动做完并挂到 child.process,排空时照常先停进程再释放租约。
  • 退避重启、停机分批排空、租约逻辑不变;ForkedProcess 把 multiprocessing 进程包成原来的 asyncio 子进程接口。

实测(runtime 镜像,Linux)

方式 7 个子进程合计 PSS
原来:各自导入 1252 MiB
预加载后 fork 1020 MiB
预加载 + gc.freeze() 后 fork(本 PR) 284 MiB + forkserver 49 MiB

另在容器里验证:子进程用不可达数据库启动时照常记 feishu_child_failed 并以 1 退出;supervisor 被 SIGKILL 后 parent_process().is_alive() 立刻变 False(父进程号不变)。

测试

  • 新增 tests/unit/test_feishu_forkserver.py:退出码回报、terminate、子进程确实来自预加载的 forkserver、每个子进程的 SDK 循环独立、启动中被取消时进程挂到 child 上(撤掉修复时该用例失败)。
  • 飞书 / 网关相关 1042 个测试通过;ruff、mypy 通过。
  • 未验证:真实飞书长连接长时间运行后的内存曲线(引用计数仍会逐步写脏部分共享页),上线后观察。

无数据库迁移,无配置变更。

🤖 Generated with Claude Code

- 飞书 SDK 一导入就加载全部开放平台接口(约 1 万个模块),每个机器人子进程单独导入
  要 170MB 以上;机器人一多,容器内存贴着上限,启动时同时导入还会冲顶 OOM。
- 新增 preload:forkserver 先导入 child/service 与 asyncpg 方言,关掉 SDK 模块级
  事件循环(fork 后会共用 epoll),再 gc.freeze();子进程 run_process 里另建循环。
  runtime 镜像实测 7 个机器人:约 330MiB(原约 1250MiB)。
- supervisor 不再导入 SDK:子进程入口 run_child_process 延迟导入 child。
- 子进程改看 multiprocessing 的 supervisor 哨兵判断父进程存活(forkserver 被子进程
  持有的管道吊着,父进程号不再变化)。
- spawn 被取消时等线程里的启动做完,把进程挂到 child 上,排空时先停进程再释放租约。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wxkingstar
wxkingstar merged commit 9280f29 into main Oct 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant