From 7191b2355552eecbdbc1aff730ca84def8022df8 Mon Sep 17 00:00:00 2001 From: InfinityPacer Date: Mon, 28 Sep 2026 18:08:53 +0800 Subject: [PATCH] fix(runtime): strip inherited jemalloc background thread config for subprocesses Co-Authored-By: Claude Opus 5.5 --- app/main.py | 4 +-- app/runtime/gc.py | 36 ++++++++++++++++++++----- tests/test_runtime_allocator_release.py | 30 +++++++++++++++------ 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/main.py b/app/main.py index 08d138fb9..4d01dd5b0 100644 --- a/app/main.py +++ b/app/main.py @@ -205,8 +205,8 @@ 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() + from app.runtime.gc import configure_allocator_background_thread + configure_allocator_background_thread() start_tray() run_api_server() diff --git a/app/runtime/gc.py b/app/runtime/gc.py index 2f4fb19a9..8d2c4b1d4 100644 --- a/app/runtime/gc.py +++ b/app/runtime/gc.py @@ -92,6 +92,8 @@ _JEMALLOC_THREAD_TCACHE_FLUSH = b"thread.tcache.flush" # 后台回收线程开关与数量上限,均可在运行时写入 _JEMALLOC_BACKGROUND_THREAD = b"background_thread" _JEMALLOC_MAX_BACKGROUND_THREADS = b"max_background_threads" +# MALLOC_CONF 中与后台线程相关、不能被子进程继承的配置项 +_BACKGROUND_THREAD_OPTIONS = frozenset({"background_thread", "max_background_threads"}) @functools.cache @@ -137,21 +139,25 @@ 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: +def configure_allocator_background_thread(max_threads: int = 1) -> bool: """ - 在当前进程内开启 jemalloc 后台回收线程,按衰减时间把空闲脏页归还系统。 + 让 jemalloc 后台回收线程只在主进程运行,并从子进程将继承的 ``MALLOC_CONF`` 中移除相关项。 - 后台线程不能写进 ``MALLOC_CONF``:该环境变量与 ``LD_PRELOAD`` 一起被所有子进程继承, - Chromium 的 GPU 进程在后台线程开启时无法启动,浏览器仿真会整体 abort。运行时开启只 - 作用于主进程。用户在 ``MALLOC_CONF`` 中显式配置过后台线程时尊重其选择,不再改写。 + 后台线程按衰减时间把空闲脏页归还系统,但 ``MALLOC_CONF`` 与 ``LD_PRELOAD`` 会被所有子进程 + 继承,Chromium 的 GPU 进程在后台线程开启时无法启动,浏览器仿真整体 abort。 + - ``MALLOC_CONF`` 已配置后台线程(v3.0.10 镜像默认值或用户显式配置)时,主进程启动时已按其生效,不再改写。 + - 未配置时通过 ``mallctl`` 在主进程内开启。 + - 两种情况下都从 ``os.environ`` 中去掉后台线程项。内建更新只替换程序代码、不改镜像环境变量, + 因此必须由代码清理,才能修复从 v3.0.10 镜像内建更新上来的实例。 Args: max_threads: 后台线程上限 Returns: - 是否实际开启 + 是否在运行时实际开启了后台线程 """ - if "background_thread" in os.environ.get("MALLOC_CONF", ""): + configured = _strip_background_thread_conf() + if configured: return False mallctl = _jemalloc_mallctl() if mallctl is None: @@ -163,6 +169,22 @@ def enable_allocator_background_thread(max_threads: int = 1) -> bool: return mallctl(_JEMALLOC_BACKGROUND_THREAD, None, None, ctypes.byref(enabled), ctypes.sizeof(enabled)) == 0 +def _strip_background_thread_conf() -> bool: + """从 ``os.environ["MALLOC_CONF"]`` 去掉后台线程项,返回原配置是否包含这些项。""" + conf = os.environ.get("MALLOC_CONF") + if not conf: + return False + options = [option for option in conf.split(",") if option] + kept = [option for option in options if option.split(":", 1)[0] not in _BACKGROUND_THREAD_OPTIONS] + if len(kept) == len(options): + return False + if kept: + os.environ["MALLOC_CONF"] = ",".join(kept) + else: + del os.environ["MALLOC_CONF"] + return True + + def release_allocator_memory() -> Optional[str]: """ 把 C 分配器中已释放但尚未归还的空闲页交还操作系统。 diff --git a/tests/test_runtime_allocator_release.py b/tests/test_runtime_allocator_release.py index d8cd728f4..5b90e57dd 100644 --- a/tests/test_runtime_allocator_release.py +++ b/tests/test_runtime_allocator_release.py @@ -149,29 +149,43 @@ def test_thread_cache_flush_is_a_no_op_without_jemalloc(linux, monkeypatch): assert trim.calls == [] + def test_background_thread_enabled_at_runtime(linux, monkeypatch): - """后台回收线程在主进程内开启,先设上限再打开开关。""" - monkeypatch.delenv("MALLOC_CONF", raising=False) + """未配置后台线程时在主进程内开启,先设上限再打开开关,子进程环境保持不变。""" + monkeypatch.setenv("MALLOC_CONF", "narenas:8,dirty_decay_ms:5000") mallctl = _FakeSymbol() _use_lib(monkeypatch, _FakeLib(mallctl=mallctl)) - assert runtime_gc.enable_allocator_background_thread() is True + assert runtime_gc.configure_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) + assert runtime_gc.os.environ["MALLOC_CONF"] == "narenas:8,dirty_decay_ms:5000" -def test_background_thread_respects_explicit_malloc_conf(linux, monkeypatch): - """用户在 MALLOC_CONF 里显式配置后台线程时不再改写。""" - monkeypatch.setenv("MALLOC_CONF", "background_thread:false") +def test_inherited_background_thread_conf_is_stripped(linux, monkeypatch): + """v3.0.10 镜像把后台线程写进了 MALLOC_CONF;主进程已按其生效,只需从子进程将继承的环境中去掉。""" + monkeypatch.setenv( + "MALLOC_CONF", + "background_thread:true,max_background_threads:1,narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0", + ) mallctl = _FakeSymbol() _use_lib(monkeypatch, _FakeLib(mallctl=mallctl)) - assert runtime_gc.enable_allocator_background_thread() is False + assert runtime_gc.configure_allocator_background_thread() is False assert mallctl.calls == [] + assert runtime_gc.os.environ["MALLOC_CONF"] == "narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0" + + +def test_conf_with_only_background_thread_is_removed(linux, monkeypatch): + monkeypatch.setenv("MALLOC_CONF", "background_thread:false") + _use_lib(monkeypatch, _FakeLib(mallctl=_FakeSymbol())) + + assert runtime_gc.configure_allocator_background_thread() is False + assert "MALLOC_CONF" not in runtime_gc.os.environ 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 + assert runtime_gc.configure_allocator_background_thread() is False