fix(docker): enable jemalloc background thread only in the main process

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
InfinityPacer
2026-09-28 18:02:10 +08:00
committed by InfinityPacer
parent 3b2cd28983
commit 31ca107e41
7 changed files with 71 additions and 9 deletions

View File

@@ -204,6 +204,9 @@ def run_application() -> None:
)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# 只在主进程开启分配器后台回收,浏览器等子进程不继承
from app.runtime.gc import enable_allocator_background_thread
enable_allocator_background_thread()
start_tray()
run_api_server()

View File

@@ -89,6 +89,9 @@ def get_memory_usage() -> float:
_JEMALLOC_ARENAS_ALL_PURGE = b"arena.4096.purge"
# 把调用线程的线程缓存(tcache)交还所属 arena
_JEMALLOC_THREAD_TCACHE_FLUSH = b"thread.tcache.flush"
# 后台回收线程开关与数量上限,均可在运行时写入
_JEMALLOC_BACKGROUND_THREAD = b"background_thread"
_JEMALLOC_MAX_BACKGROUND_THREADS = b"max_background_threads"
@functools.cache
@@ -134,6 +137,32 @@ def flush_thread_allocator_cache() -> bool:
return mallctl(_JEMALLOC_THREAD_TCACHE_FLUSH, None, None, None, 0) == 0
def enable_allocator_background_thread(max_threads: int = 1) -> bool:
"""
在当前进程内开启 jemalloc 后台回收线程,按衰减时间把空闲脏页归还系统。
后台线程不能写进 ``MALLOC_CONF``:该环境变量与 ``LD_PRELOAD`` 一起被所有子进程继承,
Chromium 的 GPU 进程在后台线程开启时无法启动,浏览器仿真会整体 abort。运行时开启只
作用于主进程。用户在 ``MALLOC_CONF`` 中显式配置过后台线程时尊重其选择,不再改写。
Args:
max_threads: 后台线程上限
Returns:
是否实际开启
"""
if "background_thread" in os.environ.get("MALLOC_CONF", ""):
return False
mallctl = _jemalloc_mallctl()
if mallctl is None:
return False
limit = ctypes.c_size_t(max_threads)
if mallctl(_JEMALLOC_MAX_BACKGROUND_THREADS, None, None, ctypes.byref(limit), ctypes.sizeof(limit)) != 0:
return False
enabled = ctypes.c_bool(True)
return mallctl(_JEMALLOC_BACKGROUND_THREAD, None, None, ctypes.byref(enabled), ctypes.sizeof(enabled)) == 0
def release_allocator_memory() -> Optional[str]:
"""
把 C 分配器中已释放但尚未归还的空闲页交还操作系统。

View File

@@ -285,11 +285,12 @@ RUN mkdir -p \
# final 阶段: 安装运行时依赖和配置最终镜像
FROM prepare_package AS final
# jemalloc 默认按 CPU 数×4 建 arena 且没有后台回收线程,空闲 arena 的脏页会长期计入 RSS。
# 后台线程按 5 秒衰减归还脏页(muzzy 直接归还,避免 MADV_FREE 页继续计入 RSS);
# arena 限制为 8 个,带 GIL 的 Python 大块分配竞争很小。用户可在 compose 中覆盖 MALLOC_CONF。
# jemalloc 默认按 CPU 数×4 建 arena,空闲 arena 的脏页会长期计入 RSS。脏页按 5 秒衰减归还
# (muzzy 直接归还,避免 MADV_FREE 页继续计入 RSS);arena 限制为 8 个,带 GIL 的 Python 大块分配竞争很小。
# 后台回收线程由主进程启动时自行开启,不能写在这里:子进程继承 MALLOC_CONF 后 Chromium 无法启动。
# 用户可在 compose 中覆盖 MALLOC_CONF。
ENV LD_PRELOAD="/usr/local/lib/libjemalloc.so" \
MALLOC_CONF="background_thread:true,max_background_threads:1,narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0" \
MALLOC_CONF="narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0" \
MOVIEPILOT_DOCKER_SUPERVISOR="true"
# 引入支持 amr 编码的静态 ffmpeg

View File

@@ -761,7 +761,7 @@ flowchart LR
| 指标 | 当前值 |
|---|---:|
| Python 模块 | 1051 |
| 内部导入边 | 8,920 |
| 内部导入边 | 8,921 |
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
| Direct egress | 53(债务已清零,53 条精确 containment) |
@@ -771,7 +771,7 @@ flowchart LR
| Model/Oper 自动事务与自建 Session | 0 |
| 组合根外 `SystemConfigOper()` | 0 |
整理失败反馈由 `app.application.transfer.feedback` 集中投影;已整理下载的站点字幕复用 `app.chain.transfer` 自动整理队列;网络连通性检测按 `app.application.nettest` 归档,并覆盖 HTTP 与 WebSocket 探测。未启用智能助手的消息音乐交互由 `app.chain.music_interaction` 包中的解析展示与订阅协调模块处理;宿主依赖边现为 8,920,音乐交互包未新增 SCC。Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。当前 `app.startup.lifecycle` 为 572、`app.factory` 为 584、`app.main` 为 586。性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。
整理失败反馈由 `app.application.transfer.feedback` 集中投影;已整理下载的站点字幕复用 `app.chain.transfer` 自动整理队列;网络连通性检测按 `app.application.nettest` 归档,并覆盖 HTTP 与 WebSocket 探测。未启用智能助手的消息音乐交互由 `app.chain.music_interaction` 包中的解析展示与订阅协调模块处理;宿主依赖边现为 8,921,音乐交互包未新增 SCC。Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。当前 `app.startup.lifecycle` 为 572、`app.factory` 为 584、`app.main` 为 586。性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。
架构专项验证分为两个 CI 投影:`Check event semantic policy` 先运行依赖、Adapter、出口和 Event
语义门禁,`Check host architecture snapshot` 再执行快照测试及一次

View File

@@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
| 指标 | 当前值 | 解释 |
|---|---:|---|
| 宿主 Python 模块 / 内部依赖边 | 1051 / 8,920 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档 |
| 宿主 Python 模块 / 内部依赖边 | 1051 / 8,921 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档 |
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |

View File

@@ -1097,8 +1097,8 @@
"runtime_only": true
}
},
"edge_count": 8920,
"edge_sha256": "68bc7f63a754251cc91fcc5ed5c8652e2492f60faa805f9533eedbccc8517bdd",
"edge_count": 8921,
"edge_sha256": "e34590c0ec863317ddeec72b19cd935a13b48e51bd56b31f5a3aae93f26a4953",
"edges": [
"app -> app.foundation",
"app -> app.foundation.environment",
@@ -6578,6 +6578,7 @@
"app.main -> app.adapters.system.stdio",
"app.main -> app.factory",
"app.main -> app.runtime",
"app.main -> app.runtime.gc",
"app.main -> app.runtime.settings",
"app.main -> app.runtime.stop",
"app.main -> app.runtime.topology",

View File

@@ -147,3 +147,31 @@ def test_thread_cache_flush_is_a_no_op_without_jemalloc(linux, monkeypatch):
assert runtime_gc.flush_thread_allocator_cache() is False
assert trim.calls == []
def test_background_thread_enabled_at_runtime(linux, monkeypatch):
"""后台回收线程在主进程内开启,先设上限再打开开关。"""
monkeypatch.delenv("MALLOC_CONF", raising=False)
mallctl = _FakeSymbol()
_use_lib(monkeypatch, _FakeLib(mallctl=mallctl))
assert runtime_gc.enable_allocator_background_thread() is True
assert [call[0] for call in mallctl.calls] == [b"max_background_threads", b"background_thread"]
assert all(call[1] is None and call[2] is None for call in mallctl.calls)
def test_background_thread_respects_explicit_malloc_conf(linux, monkeypatch):
"""用户在 MALLOC_CONF 里显式配置后台线程时不再改写。"""
monkeypatch.setenv("MALLOC_CONF", "background_thread:false")
mallctl = _FakeSymbol()
_use_lib(monkeypatch, _FakeLib(mallctl=mallctl))
assert runtime_gc.enable_allocator_background_thread() is False
assert mallctl.calls == []
def test_background_thread_is_a_no_op_without_jemalloc(linux, monkeypatch):
monkeypatch.delenv("MALLOC_CONF", raising=False)
_use_lib(monkeypatch, _FakeLib(malloc_trim=_FakeSymbol()))
assert runtime_gc.enable_allocator_background_thread() is False