fix(runtime): strip inherited jemalloc background thread config for subprocesses

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

View File

@@ -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()

View File

@@ -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 分配器中已释放但尚未归还的空闲页交还操作系统。

View File

@@ -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