2026-10-27 · 深度笔记

一台机器同时跑 API 和向量检索,内存怎么分、怎么定上限

在同一台云主机上跑推理 API 和向量库,内存是最容易打架的资源。这篇讲怎么按比例分配、cgroup 限额怎么设才不互相拖垮,以及 OOM 时该怀疑谁。

云主机运维内存向量检索

我有一台 16G 的机器,一开始跑得挺好:推理 API 占 9G,向量库占 4G,还剩 3G 富余。

后来我加了一个功能,会在检索时把 top-20 的文档全文拼进 prompt。这一下,向量库的内存占用涨到了 7G,API 涨到 12G,机器开始随机地丢请求。

最诡异的是日志:向量库偶尔报 Out of memory,但看系统内存还有 2G 空闲。

先搞清楚"内存不够"是哪种不够

Linux 的内存账本比想象中复杂,OOM 有三种完全不同的表现:

第一种,容器 cgroup 超限。 容器内的进程触碰了 memory.max,被内核杀掉,但宿主机上可能还有大量空闲内存。

# 看容器实际的 cgroup 限制(cgroup v2 路径)
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.events   # 关键:看 oom / oom_kill 计数

memory.events 里的 oom_kill 计数是最直接的证据。它非零,就说明发生过 cgroup 级别的 OOM。

第二种,宿主机物理内存耗尽。 这时候 free -m 里的 available 会很低。

free -m
# available 才是关键,不是 free

第三种,内存没耗尽但被回收了。 内核为了给新分配腾地方,把进程的页换出到 swap 或者直接丢弃。这时系统"available"看起来正常,但应用的延迟会突然飙高——一个平时 5ms 的查询变成 800ms。

第三种最阴险,因为它不报任何错误。

我那台机器当时就是混合情况:宿主机还有内存,但向量库的 cgroup 超限了;而 API 那个容器因为大量 mmap 读向量文件,页缓存被反复回收,延迟开始抖。

一台机器上两个服务的内存账

先算账再部署,别反过来。推理 API 的内存大致是:

模型权重(量化后) + KV cache(随上下文 × 并发增长) + 激活值峰值 + 框架开销

向量库的内存是:

向量数据(常驻) + 索引结构 + 写缓冲 + 查询时的临时结构

我按 16G 机器做过的分配:

项目分配依据
系统 + Docker1.5G实测,非估算
推理 API9G权重 4G + KV cache 3G + 余量 2G
向量库4G数据 2.5G + 索引 1G + 写缓冲 0.5G
预留1.5G给页缓存和突发

预留这一栏不能省。 我一开始留了 0.5G,后来发现页缓存完全不够用——向量库靠 mmap 读索引文件,没有页缓存的话每次查询都要走磁盘,延迟直接翻十倍。

cgroup 限额怎么设才不互相拖垮

这是关键。我的原则是:每个容器都必须设上限,不能让任何一个服务吃光全机。

services:
  api:
    image: my-api:latest
    restart: unless-stopped
    mem_limit: 9g
    memswap_limit: 9g        # 关键:和 mem_limit 相同 = 禁用 swap
    cpus: "4.0"
    environment:
      - TZ=Asia/Shanghai
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  vector:
    image: qdrant/qdrant:latest
    restart: unless-stopped
    mem_limit: 4g
    memswap_limit: 4g
    volumes:
      - vector-data:/qdrant/storage
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  vector-data:

memswap_limit 等于 mem_limit 这一行是重点。

默认情况下,Docker 的 mem_limit 只限制物理内存,容器还能用 swap。后果是:向量库触到限额时不会立刻被杀,而是进入换页状态,延迟从 5ms 飙到几百毫秒,而且表现为随机慢,不是失败。

把 memswap_limit 设成和 mem_limit 相同,等于告诉内核"这个容器不许用 swap"。超了就直接 OOM kill,症状明确,好排查。

这跟上一条那篇讲 OOM killer 会挑倒霉鬼是一个道理:快速失败比缓慢劣化好得多。

关于 swap 本身

如果机器上有 swap,我现在的态度是:留着,但别让服务用它。

理由是数据库和向量库对延迟敏感,换页对它们是灾难。系统层面的 swap 我会调到 1G 左右,专门用来接住那些"本来会 OOM 但只是一瞬间超了一点"的场景,让内核有时间收缩,而不是立刻杀掉。

# 查看 swap 使用情况
swapon --show
free -m

# 观察有没有在换页
vmstat 1
# si/so 列是 swap in/out,nonzero 说明正在换页

vmstat 的 si 和 so 是我判断"延迟抖动是不是换页造成的"的主要依据。只要它们持续非零,那就不该去查数据库参数,应该去查内存。

排查 OOM 该看什么

按这个顺序看,能省很多时间:

第一步,看 cgroup 事件。

# 系统范围的 OOM 记录
dmesg -T | grep -i "killed process" | tail -20

# 容器的
cat /sys/fs/cgroup/memory.events

dmesg 里那行 Killed process 12345 (python) total-vm:... 是黄金证据——它告诉你是哪个进程被杀的、当时占了多少。

第二步,看是不是被 cgroup 杀的。 如果 dmesg 里没有记录,但容器 memory.events 的 oom_kill 非零,那就是 cgroup 杀的,宿主机整体可能还很空闲。

第三步,看页缓存和 swap。

# 页缓存占用了多少
grep -E "^(Cached|Buffers|SReclaimable)" /proc/meminfo

# 谁在吃页缓存
vmtop - 2>/dev/null || pidstat -r 2 3

一个反直觉的做法:给向量库更多内存,而不是更少

我原本的直觉是"向量库很吃内存,给它少一点,剩下的给推理"。

实际上做反了。向量库的内存特征是大部分常驻且可预测——向量数据多少、索引多大,都是静态的。推理 API 的内存是突发型的——KV cache 随并发和上下文长度波动,激活值在请求高峰时冲高。

两者混在一台机器上,最危险的不是向量库吃多了,而是推理服务在高峰期把内存吃满,触发 OOM killer,然后内核按"坏分数"选倒霉鬼,杀掉的可能是向量库。

我遇到的就是这个:向量库被杀,API 反而活着,症状是"检索偶尔返回空结果"。

更糟的情况是内核不杀进程,而是回收页缓存——向量库靠 mmap 读索引,一被回收,每次查询都要走磁盘。表现是延迟毛刺,不是错误。

现在的做法是:

# 容器启动后确认限额生效
docker inspect -f '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' api
# 输出应该形如:9663676416 9663676416(两个值相等 = 无 swap)

什么时候该拆开

讲真话:上面这些技巧都是在"一台机器上塞两个服务"的前提下做优化。

如果你的场景符合下面任何一条,拆开比调优省事:

我的最后一台共用机器,是个小语料库 + 中等并发的内部工具,那种规模下共用确实划算。但只要过了这个规模,拆开带来的稳定性收益很快就超过省下的那点机器钱。

收尾

内存这件事的教训:OOM 不是"内存不够",是"内存分配的权利没界定清楚"。

我在那台机器上花的时间,八成是在调三个参数:mem_limit、memswap_limit、以及预留比例。剩下的才是真正的容量规划。

具体的数字(几个 G、留多少余量)跟你的模型大小、语料规模强相关,得按实测算。结构性的做法是通用的:先算账、再设限、硬禁 swap、留页缓存空间,落到具体机器上以实际规格为准。

接着读