Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ ENV PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
TZ=Asia/Shanghai

# Install tzdata and set timezone
# Install tzdata, jemalloc and set timezone
# jemalloc 替代 glibc malloc,大幅减少内存碎片化
RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata && \
apt-get install -y --no-install-recommends tzdata libjemalloc2 && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# 使用 jemalloc 替代 glibc malloc
# jemalloc 会主动将空闲内存归还给操作系统,避免 RSS 持续增长
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# jemalloc 配置:启用后台线程回收、脏页衰减时间 5 秒(默认 10 秒)
ENV MALLOC_CONF="background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000"

WORKDIR /app

# Copy only requirements first for better caching
Expand All @@ -31,4 +38,4 @@ COPY . .
EXPOSE 7861

# Default command
CMD ["python", "web.py"]
CMD ["python", "web.py"]
93 changes: 86 additions & 7 deletions web.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""

import asyncio
import ctypes
import gc
import os
from contextlib import asynccontextmanager

Expand Down Expand Up @@ -36,6 +38,66 @@
# 全局凭证管理器
global_credential_manager = None

# ==================== 内存管理 ====================

# 尝试获取 libc 的 malloc_trim 函数
_libc = None
_malloc_trim = None
try:
_libc = ctypes.CDLL("libc.so.6")
_malloc_trim = _libc.malloc_trim
_malloc_trim.argtypes = [ctypes.c_size_t]
_malloc_trim.restype = ctypes.c_int
except (OSError, AttributeError):
pass # 非 Linux 或不支持 malloc_trim


def _get_rss_mb() -> float:
"""获取当前进程的 RSS(MB),仅 Linux 可用"""
try:
with open("/proc/self/status") as f:
for line in f:
if line.startswith("VmRSS:"):
return int(line.split()[1]) / 1024
except Exception:
pass
return 0.0


async def _memory_trim_loop():
"""
定期执行 GC + malloc_trim,将 glibc 堆中的空闲页归还给操作系统。
glibc 的 malloc 默认不会主动归还内存,导致 Python 进程的 RSS 持续增长。
每 60 秒执行一次,每次开销极低(< 1ms)。
"""
while True:
try:
await asyncio.sleep(60)

rss_before = _get_rss_mb()

# 强制 GC 回收循环引用
gc.collect()

# 调用 malloc_trim 归还空闲内存给 OS
if _malloc_trim is not None:
_malloc_trim(0)

rss_after = _get_rss_mb()
freed = rss_before - rss_after

if freed > 10: # 只在释放了 >10MB 时记录
log.info(f"[MEM] malloc_trim: {rss_before:.0f}MB → {rss_after:.0f}MB (释放 {freed:.0f}MB)")
elif rss_after > 500: # RSS > 500MB 时始终记录
log.debug(f"[MEM] malloc_trim: {rss_before:.0f}MB → {rss_after:.0f}MB")

except asyncio.CancelledError:
raise
except Exception as e:
log.warning(f"[MEM] malloc_trim 异常: {e}")

_memory_trim_task = None


@asynccontextmanager
async def lifespan(app: FastAPI):
Expand Down Expand Up @@ -70,6 +132,16 @@ async def lifespan(app: FastAPI):
except Exception as e:
log.error(f"保活服务启动失败: {e}")

# 启动内存回收任务(定期 GC + malloc_trim)
global _memory_trim_task
_memory_trim_task = asyncio.create_task(
_memory_trim_loop(), name="memory_trim"
)
if _malloc_trim is not None:
log.info("[MEM] 内存回收任务已启动(每60秒执行 gc.collect + malloc_trim)")
else:
log.info("[MEM] 内存回收任务已启动(仅 gc.collect,malloc_trim 不可用)")

yield

# 清理资源
Expand All @@ -81,20 +153,27 @@ async def lifespan(app: FastAPI):
except Exception as e:
log.error(f"关闭保活服务时出错: {e}")

# 停止内存回收任务
if _memory_trim_task and not _memory_trim_task.done():
_memory_trim_task.cancel()
try:
await _memory_trim_task
except asyncio.CancelledError:
pass

# 首先关闭所有异步任务
try:
await shutdown_all_tasks(timeout=10.0)
log.info("所有异步任务已关闭")
except Exception as e:
log.error(f"关闭异步任务时出错: {e}")

# 然后关闭凭证管理器
if global_credential_manager:
try:
await global_credential_manager.close()
log.info("凭证管理器已关闭")
except Exception as e:
log.error(f"关闭凭证管理器时出错: {e}")
# 然后关闭凭证管理器(使用单例实例而非未赋值的全局变量)
try:
await credential_manager.close()
log.info("凭证管理器已关闭")
except Exception as e:
log.error(f"关闭凭证管理器时出错: {e}")

log.info("GCLI2API 主服务已停止")

Expand Down
Loading