nproc 报 72 核,cgroup 只给 8 核:容器真实限额与被限次数怎么读
📌 本文部分内容由 AI 辅助整理,已经人工核对。文中所有数字都来自我这台机器上真跑出来的输出,脚本零依赖、不需要 root,你可以在自己的容器里跑一遍——你跑出来的数会和我的完全不同,那才是重点。文末给了脚本全文。
nproc 报 72 核,cgroup 只给 8 核:容器真实限额与被限次数怎么读
前几天我在一个容器里跑东西,跑着跑着,echo alive 都开始返回退出码 1。
不是命令写错了。就是 echo,一个字都不带的 echo,失败了。
第一反应当然是查内存。free -g 显示还有 60 多 G 可用。查 OOM,dmesg 里干干净净。查磁盘,够用。日志全绿,服务看着都活着,就是任何新命令都起不来。
后来定位到根因,我第一次认真读了三个平时从没打开过的文件。这篇就是那次的记录。
一、第一层:nproc 和 free 读的是宿主机
在容器里跑服务的人几乎都干过同一件事:nproc 看一眼定 worker 数,free -g 看一眼定 batch 大小。
这两个命令读的是 /proc/cpuinfo 和 /proc/meminfo。而 /proc 这两项不被 cgroup 隔离——容器里看到的就是宿主机的。除非镜像里装了 lxcfs 之类的东西专门去伪装,否则你看到的是整台物理机。
限额不在那儿,在 cgroup 的文件里。
这是我这台机器的实际情况:
| 命令行告诉你 | cgroup 实际给的 | |
|---|---|---|
| CPU | nproc = 72 | cpu.max = 8.00 核 |
| 内存 | MemTotal = 125.16 GiB | memory.max = 16.00 GiB |
cpu.max 这个文件里躺着两个数:
800000 100000
意思是「每 100000 微秒的周期里,最多给你跑 800000 微秒的 CPU 时间」。800000 ÷ 100000 = 8,所以是 8 核。
如果你按 nproc 的 72 去开 worker、去设 OMP_NUM_THREADS,你开的是配额的九倍。这些线程不会报错,它们会互相抢那 8 核的时间片,然后你会得到一个「CPU 使用率不高但就是慢」的服务。
二、第二层:sched_getaffinity 也救不了你
网上关于这个问题的建议,最常见的一条是:别用 os.cpu_count(),用 sched_getaffinity,那个是容器感知的。
在我这台机器上,这条建议完全无效。看实测:
nproc / os.cpu_count() 72
sched_getaffinity 72
cpuset 允许的核 0-71
三个都是 72。cpuset.cpus.effective 是 0-71,也就是所有核都允许我用。
这不矛盾。CPU 的限制方式有两种:
- 绑核型(cpuset):只让你在 0-7 号核上跑。这种 affinity 能测出来。
- 配额型(
cpu.max/ v1 的cfs_quota_us):所有核都让你上,但每个周期你总共只能跑这么多微秒。affinity 完全测不出来。
绝大多数容器编排——Docker 的 --cpus、Kubernetes 的 limits.cpu——默认走的是配额型。所以那条流传很广的建议,在最常见的场景下恰好不管用。
顺带一提,Python 3.13 新加的 os.process_cpu_count() 也解决不了这个问题。官方文档对它的描述是:
Get the number of logical CPUs usable by the calling thread of the current process. Returns None if undetermined. It can be less than
cpu_count()depending on the CPU affinity.
关键在最后半句:它比 cpu_count() 少多少,取决于 CPU affinity。全文没提 cgroup 配额,因为它本来就不看那个。所以在配额型限制下它一样会返回 72。(我这台是 3.12,脚本里对这个函数做了存在性判断,有就读、没有就跳过。)
结论:想知道 CPU 配额,除了读 cpu.max 没有别的办法。
三、第三层:内核已经替你数好了,只是那几个文件没人看
前两层还只是「读数不准」。这一层是这篇文章我最想说的:
你根本不用猜「我是不是被限了」。内核为每一类限额都准备了触顶计数器,数就在文件里躺着。
三个文件,各管一类:
| 文件 | 关键字段 | 记的是什么 |
|---|---|---|
cpu.stat | nr_throttled / throttled_usec | 被 CPU 配额掐停过多少次、累计停了多久 |
memory.events | max / oom_kill | 内存触顶多少次、OOM 杀了几个进程 |
pids.events | max | fork 被拒了多少次 |
这是我那台机器的读数:
CPU 被掐停次数 915 占 740307 个调度周期的 0.124%
累计停了多久 21.4 分钟 纯等待,不是在算
内存触顶次数 0 memory.events: max
被 OOM 杀掉的进程 0 memory.events: oom_kill
fork 被拒次数 2143 pids.events: max
看清楚这三行的关系:
- 内存触顶 0 次,OOM 杀了 0 个。 而这恰恰是所有人第一时间会去查的那一项。查完发现是 0,就得出「不是内存问题」,然后卡住。
- CPU 被掐停 915 次,累计 21.4 分钟。 占全部调度周期的 0.124%,比例不高,但这 21 分钟里进程是可运行状态却被内核按住不给跑——它不是在算,是在等。表现出来就是零星的、复现不了的卡顿。
- fork 被拒 2143 次。 这个才是真凶。而
pids.events这个文件,我敢说大部分人从来没打开过。
一次故障里,最贵的不是修,是找。这三个计数器把「找」这一步从猜变成了读。
四、pids 这一项为什么最难认
因为它的报错形态完全不像「进程数超了」。
Linux 上线程也占一个 pid(内核里线程和进程都是 task),所以 pids.max 管的其实是「线程 + 进程」的总数。这就带来两个后果。
后果一:额度掉得比你想的快。
我这台机器:
进程/线程数上限 500
已用 465 (93.0%)
500 的额度,用掉 465。这里面绝大部分不是「我开了 465 个程序」,而是几个自带线程池的进程——一个 ffmpeg 起十几个编码线程,一个模型推理进程起几十个,几个一叠加就到顶了。
后果二:撞上限之后,报错五花八门。
内核在这里返回的错误码是 EAGAIN(11)。它的标准描述是:
Resource temporarily unavailable
「资源」是哪个资源,这句话一个字都没说。
这才是它难认的根本原因。同样一句 Resource temporarily unavailable,可能是非阻塞 socket 还没就绪,可能是文件锁没拿到,也可能是——你已经开不出新进程了。错误信息本身不区分。
传到不同的上层,它会长成不同的样子:
- Python 起子进程 →
BlockingIOError: [Errno 11] Resource temporarily unavailable - Python 起线程 →
RuntimeError: can't start new thread - shell 里 → 某个命令莫名其妙退出码 1
我那次就是最后一种。echo 失败,是因为 shell 要先 fork 一个子进程再去执行它,而 fork 不出来了。命令本身一点问题没有。
所以:当报错含糊、而 memory.events 里的 oom_kill 是 0 的时候,去看一眼 pids.events。
五、还有个坑:限额不一定设在你这一层
cgroup 是树状的。你的进程在某个节点上,但限额可能设在它的父节点、祖父节点。
最典型的是 Kubernetes:内存上限常常设在 pod 这一层,而你的容器那一层写着 max。
如果你只 cat /sys/fs/cgroup/memory.max 读到 max,就会得出「没有内存限制」的结论——错的。
正确做法是从 /proc/self/cgroup 里读出自己所在的路径,然后一路往上爬到根,逐层看,取最严的那个。脚本里的 climb() 和 effective() 干的就是这件事。
(在容器里,/proc/self/cgroup 通常被 cgroup namespace 改写成 0::/,也就是「我就是根」,这时候只有一层。但在裸机上跑 systemd 服务、或者有嵌套 cgroup 的场景,爬这一步是必需的。)
六、脚本
零依赖,纯标准库,不需要 root,cgroup v1 / v2 都认。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
cgroup_limits.py —— 读出容器的真实 CPU / 内存 / 进程数限额,
以及内核已经因为这些限额挡了你多少次。
读三组触顶计数器(这才是重点,它们记的是已经发生过的事):
· cpu.stat nr_throttled / throttled_usec 被 CPU 配额掐停多少次、累计多久
· memory.events max / oom_kill 内存触顶多少次、OOM 杀了几个
· pids.events max fork 被拒了多少次
限额可能设在父层(只读当前层会读到 max 而误判为不限),所以沿 cgroup
层级往上爬,取最严的那个。
零依赖,不需要 root,v1 / v2 都认。
python3 cgroup_limits.py # 完整报告
python3 cgroup_limits.py --json # 机器可读,接监控用
python3 cgroup_limits.py --advise 3.5 # 按「每 worker 约 3.5 GB」给出建议并发数
这些文件的语义以内核文档为准:
https://docs.kernel.org/admin-guide/cgroup-v2.html
https://docs.kernel.org/scheduler/sched-bwc.html
"""
import argparse
import json
import os
import sys
import unicodedata
CG_ROOT = "/sys/fs/cgroup"
# ---------------------------------------------------------------- 小工具
def dwidth(s):
"""按终端显示宽度算,中文占 2 列。不这么算表格必然错位。"""
return sum(2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in s)
def wpad(s, width):
return s + " " * max(0, width - dwidth(s))
def read(path):
"""读一个 cgroup 文件;不存在或没权限就返回 None,不抛。"""
try:
with open(path, "r") as f:
return f.read().strip()
except (IOError, OSError):
return None
def human_bytes(n):
if n is None:
return "—"
if n == "max":
return "不限"
step = 1024.0
for unit in ("B", "KiB", "MiB", "GiB", "TiB", "PiB"):
if abs(n) < step or unit == "PiB":
return "%.2f %s" % (n, unit) if unit != "B" else "%d B" % n
n /= step
def human_secs(us):
"""微秒 -> 人看得懂的时长。"""
if us is None:
return "—"
s = us / 1e6
if s < 60:
return "%.1f 秒" % s
if s < 3600:
return "%.1f 分钟" % (s / 60)
return "%.2f 小时" % (s / 3600)
def parse_kv(text):
"""把 'a 1\\nb 2' 这种 flat-keyed 文件解析成 dict。"""
out = {}
if not text:
return out
for line in text.splitlines():
parts = line.split()
if len(parts) == 2:
try:
out[parts[0]] = int(parts[1])
except ValueError:
out[parts[0]] = parts[1]
return out
# ---------------------------------------------------------------- 版本与路径
def detect_version():
"""v2 的标志是根目录下有 cgroup.controllers。"""
if os.path.exists(os.path.join(CG_ROOT, "cgroup.controllers")):
return 2
if os.path.isdir(os.path.join(CG_ROOT, "memory")):
return 1
return 0
def self_cgroup_path():
"""
/proc/self/cgroup 里 v2 的行长这样: 0::/some/path
在容器里通常被 cgroup namespace 改写成 0::/ ,也就是「我就是根」。
"""
txt = read("/proc/self/cgroup")
if not txt:
return "/"
for line in txt.splitlines():
f = line.split(":", 2)
if len(f) == 3 and f[0] == "0":
return f[2] or "/"
return "/"
def climb(rel, filename):
"""
从自己所在的 cgroup 一路往上走到根,逐层找 filename。
返回 [(层级路径, 该层的原始值), ...],从最近的一层开始。
为什么要爬:限额经常设在父层(比如 Kubernetes 的 pod 层设了内存上限,
容器层写着 max)。只读当前层会读到 "max",然后得出「没有限制」的错误结论。
"""
out = []
parts = [p for p in rel.strip("/").split("/") if p]
while True:
d = os.path.join(CG_ROOT, *parts) if parts else CG_ROOT
v = read(os.path.join(d, filename))
if v is not None:
out.append(("/" + "/".join(parts) if parts else "/", v))
if not parts:
break
parts.pop()
return out
def effective(rel, filename, parse):
"""
沿层级取「最严」的那个值。parse 把原始文本转成可比较的数(不限 -> None)。
返回 (数值, 生效层级, 该层原始文本)。
"""
best, best_at, best_raw = None, None, None
for path, raw in climb(rel, filename):
v = parse(raw)
if v is None:
continue
if best is None or v < best:
best, best_at, best_raw = v, path, raw
return best, best_at, best_raw
# ---------------------------------------------------------------- 采集
def p_max_int(raw):
"""'max' -> None(不限);其余转 int。"""
raw = raw.strip()
if raw == "max":
return None
try:
n = int(raw)
except ValueError:
return None
# v1 用一个巨大的数表示"不限",不是字符串 max
return None if n >= (1 << 62) else n
def p_cpu_max(raw):
"""v2 的 cpu.max 是 '<quota> <period>',返回等效核数。"""
f = raw.split()
if not f or f[0] == "max":
return None
try:
quota = int(f[0])
period = int(f[1]) if len(f) > 1 else 100000
except ValueError:
return None
return quota / float(period) if period else None
def collect():
ver = detect_version()
rel = self_cgroup_path()
d = {"cgroup_version": ver, "cgroup_path": rel}
if ver == 2:
cpu, cpu_at, cpu_raw = effective(rel, "cpu.max", p_cpu_max)
mem, mem_at, _ = effective(rel, "memory.max", p_max_int)
pid, pid_at, _ = effective(rel, "pids.max", p_max_int)
d["cpu_limit"] = cpu
d["cpu_limit_at"] = cpu_at
d["cpu_limit_raw"] = cpu_raw
d["mem_limit"] = mem
d["mem_limit_at"] = mem_at
d["pids_limit"] = pid
d["pids_limit_at"] = pid_at
d["mem_current"] = p_max_int(read(os.path.join(CG_ROOT, "memory.current")) or "max")
d["pids_current"] = p_max_int(read(os.path.join(CG_ROOT, "pids.current")) or "max")
d["cpu_stat"] = parse_kv(read(os.path.join(CG_ROOT, "cpu.stat")))
d["mem_events"] = parse_kv(read(os.path.join(CG_ROOT, "memory.events")))
d["pids_events"] = parse_kv(read(os.path.join(CG_ROOT, "pids.events")))
cs = read(os.path.join(CG_ROOT, "cpuset.cpus.effective"))
d["cpuset"] = cs or None
elif ver == 1:
q = read(CG_ROOT + "/cpu/cpu.cfs_quota_us")
p = read(CG_ROOT + "/cpu/cpu.cfs_period_us")
cpu = None
if q and p and int(q) > 0:
cpu = int(q) / float(int(p))
d["cpu_limit"] = cpu
d["cpu_limit_at"] = "(v1)"
d["cpu_limit_raw"] = "%s / %s" % (q, p)
d["mem_limit"] = p_max_int(read(CG_ROOT + "/memory/memory.limit_in_bytes") or "max")
d["mem_limit_at"] = "(v1)"
d["mem_current"] = p_max_int(read(CG_ROOT + "/memory/memory.usage_in_bytes") or "max")
d["pids_limit"] = p_max_int(read(CG_ROOT + "/pids/pids.max") or "max")
d["pids_limit_at"] = "(v1)"
d["pids_current"] = p_max_int(read(CG_ROOT + "/pids/pids.current") or "max")
st = parse_kv(read(CG_ROOT + "/cpu/cpu.stat"))
d["cpu_stat"] = {
"nr_throttled": st.get("nr_throttled"),
"nr_periods": st.get("nr_periods"),
"throttled_usec": (st.get("throttled_time") or 0) // 1000 or None,
}
d["mem_events"] = {}
d["pids_events"] = {}
d["cpuset"] = read(CG_ROOT + "/cpuset/cpuset.effective_cpus")
# 这三个是大多数人实际拿来做决策的数,也是错的来源
d["seen_cpu_count"] = os.cpu_count()
try:
d["seen_affinity"] = len(os.sched_getaffinity(0))
except (AttributeError, OSError):
d["seen_affinity"] = None
d["seen_process_cpu_count"] = (
os.process_cpu_count() if hasattr(os, "process_cpu_count") else None
)
mt = None
txt = read("/proc/meminfo")
if txt:
for line in txt.splitlines():
if line.startswith("MemTotal:"):
mt = int(line.split()[1]) * 1024
break
d["seen_memtotal"] = mt
return d
# ---------------------------------------------------------------- 报告
W = 26
def line(title):
print()
print(" ── %s " % title + "─" * max(0, 58 - dwidth(title)))
def row(k, v, note=""):
print(" %s%s%s" % (wpad(k, W), wpad(str(v), 20), note))
def report(d):
ver = d["cgroup_version"]
print()
print(" cgroup v%s 当前所在层级 %s" % (ver or "?", d.get("cgroup_path")))
if ver == 0:
print(" 没找到 cgroup 挂载点,这台机器可能不在容器里,或者挂载路径不标准。")
return
line("你看到的(读的是宿主机的 /proc,容器不隔离这些)")
row("nproc / os.cpu_count()", d["seen_cpu_count"], "个逻辑核")
row("sched_getaffinity", d["seen_affinity"], "个可用核")
if d["seen_process_cpu_count"] is not None:
row("os.process_cpu_count()", d["seen_process_cpu_count"], "个核 (Python 3.13+)")
row("free / MemTotal", human_bytes(d["seen_memtotal"]), "")
line("实际生效的(cgroup 说了算)")
cpu = d["cpu_limit"]
if cpu is None:
row("CPU 配额", "不限", "")
else:
row("CPU 配额", "%.2f 核" % cpu, "← %s [%s]" % (d.get("cpu_limit_raw"), d.get("cpu_limit_at")))
mem = d["mem_limit"]
row("内存上限", human_bytes(mem) if mem else "不限",
("← [%s]" % d.get("mem_limit_at")) if mem else "")
if d.get("mem_current"):
pctm = (100.0 * d["mem_current"] / mem) if mem else 0
row(" 已用", human_bytes(d["mem_current"]), ("(%.1f%%)" % pctm) if mem else "")
pid = d["pids_limit"]
row("进程/线程数上限", pid if pid else "不限",
("← [%s]" % d.get("pids_limit_at")) if pid else "")
if d.get("pids_current") is not None:
pctp = (100.0 * d["pids_current"] / pid) if pid else 0
row(" 已用", d["pids_current"], ("(%.1f%%)" % pctp) if pid else "")
if d.get("cpuset"):
row("cpuset 允许的核", d["cpuset"], "")
# 差距 —— 只陈述两个数,不做倍数渲染
if cpu and d["seen_cpu_count"]:
print()
print(" CPU:命令行告诉你 %d 个,配额只给 %.2f 个。" % (d["seen_cpu_count"], cpu))
if d.get("cpuset") and "-" in str(d["cpuset"]):
print(" 注意 cpuset 是全开的,所以 affinity 类的检测也看不出来这个限制,")
print(" 限制是配额型的(每个周期只准跑这么多微秒),不是绑核型的。")
if mem and d["seen_memtotal"]:
print(" 内存:free 告诉你 %s,上限只有 %s。"
% (human_bytes(d["seen_memtotal"]), human_bytes(mem)))
line("内核记的账(这几个计数器不用猜,是已经发生过的事)")
cs = d.get("cpu_stat") or {}
nt, npd = cs.get("nr_throttled"), cs.get("nr_periods")
if nt is None:
row("CPU 被掐停次数", "—", "读不到 cpu.stat")
else:
rate = ("%.3f%%" % (100.0 * nt / npd)) if npd else ""
row("CPU 被掐停次数", nt, "占 %s 个调度周期的 %s" % (npd, rate))
row(" 累计停了多久", human_secs(cs.get("throttled_usec")), "纯等待,不是在算")
if nt:
print(" ↑ 非零就说明配额真的不够用过。这段时间进程是可运行状态,")
print(" 但被内核按住不给跑,表现出来就是「莫名其妙的卡顿」。")
me = d.get("mem_events") or {}
if me:
row("内存触顶次数", me.get("max", 0), "memory.events: max")
row("被 OOM 杀掉的进程", me.get("oom_kill", 0), "memory.events: oom_kill")
pe = d.get("pids_events") or {}
if pe:
row("fork 被拒次数", pe.get("max", 0), "pids.events: max")
if pe.get("max"):
print(" ↑ 非零就说明真的开不出新进程了。这个错误最难认:")
print(" 它不报「进程数超了」,而是变成 OSError / BlockingIOError,")
print(" 甚至变成某个子命令「莫名其妙退出码 1」。")
print()
def advise(d, gb_per_worker):
line("并发数建议(按你给的每 worker %.2f GB 估)" % gb_per_worker)
cpu, mem = d["cpu_limit"], d["mem_limit"]
by_cpu = int(cpu) if cpu else None
by_mem = int(mem / (gb_per_worker * 1024 ** 3)) if mem else None
row("按 CPU 配额", by_cpu if by_cpu is not None else "不受限")
row("按内存上限", by_mem if by_mem is not None else "不受限")
cands = [x for x in (by_cpu, by_mem) if x is not None]
if cands:
n = max(1, min(cands))
print()
print(" 取小的那个:%d 个。" % n)
print(" 再留出主进程和 CPU 配额本身的余量,实际建议不超过 %d 个。" % max(1, n - 1))
pid = d["pids_limit"]
if pid:
free_pids = pid - (d.get("pids_current") or 0)
print(" 另外:进程数还剩 %d 个额度。每个 worker 如果自带线程池,"
% free_pids)
print(" 这个额度掉得比你想的快 —— 线程在 pids 里是算数的。")
print()
def main():
ap = argparse.ArgumentParser(
description="读出容器的真实 CPU / 内存 / 进程数限额,以及内核记录的触顶次数",
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--json", action="store_true", help="输出 JSON,接监控用")
ap.add_argument("--advise", type=float, metavar="GB",
help="按每个 worker 占用多少 GB,给出建议并发数")
a = ap.parse_args()
d = collect()
if a.json:
print(json.dumps(d, indent=2, ensure_ascii=False))
return 0
report(d)
if a.advise:
advise(d, a.advise)
return 0
if __name__ == "__main__":
sys.exit(main())
七、怎么用
第一步,在出问题的那个容器里直接跑。
python3 cgroup_limits.py
先看「实际生效的」那一段,跟你以为的对不对得上。再看「内核记的账」那一段——哪一项非零,哪一项就是你要查的方向。三项全零,说明瓶颈不在限额上,可以去查别的了,这也是有价值的信息。
第二步,定并发数的时候带上 --advise。
python3 cgroup_limits.py --advise 3.5
3.5 是你估的每个 worker 大概占多少 GB。它会按 CPU 配额和内存上限各算一遍,取小的那个:
按 CPU 配额 8
按内存上限 4
取小的那个:4 个。
再留出主进程和 CPU 配额本身的余量,实际建议不超过 3 个。
另外:进程数还剩 32 个额度。
第三步,接监控。
python3 cgroup_limits.py --json
三个触顶计数器都是单调递增的,天生适合做差分告警。比起对 CPU 使用率设阈值,nr_throttled 的增量是个精确得多的信号——使用率高不一定有问题,被掐停了就是真的不够用。
顺便说个刚发生的事。上面第三节那组读数是我开始写这篇文章时采的。写到这里我又采了一次:
| 写稿开始时 | 写完这一节时 | 增量 | |
|---|---|---|---|
pids.events 的 max | 2143 | 2179 | +36 |
cpu.stat 的 nr_throttled | 915 | 922 | +7 |
就在我敲字的这段时间里,这台机器又被拒绝了 36 次 fork。
这也解释了为什么单次读数意义有限:2143 这个累计值只告诉你「历史上撞过」,而 +36 才告诉你「现在正在撞」。 要做告警,一定是采两次做差。
八、这套方法不解决什么
说清楚边界,免得当银弹用:
- 它只回答「限额是多少、撞过几次」,不回答「该设多少」。 该给多少配额是业务问题,脚本不知道你的服务要干嘛。
nr_throttled非零不等于必须扩容。 短促的突发流量本来就会撞配额,看的是趋势和比例,不是有没有。我这台 0.124%,属于偶发。- 只覆盖 CPU / 内存 / pids 三类。 IO 限速(
io.max)、网络方向的限制不在里面——我这台io.max是空的,没设,也就没法真跑验证,所以我没写进去。没跑过的东西我不往脚本里塞。 - 计数器是容器生命周期内累计的。 重启就清零。判断「最近有没有撞」要靠两次采样做差,单次读数只能告诉你「历史上撞过」。
- cgroup v1 的字段名和 v2 不一样,脚本做了兼容,但 v1 那边我手上没有环境实测,只按内核文档写。如果你在 v1 上跑出了不对的结果,欢迎在评论里告诉我具体读数。
小结
- 容器里的
nproc和free读的是宿主机,可以差一个数量级。 sched_getaffinity只能测出绑核型限制。Docker--cpus和 K8slimits.cpu默认是配额型,测不出来。cpu.max里的两个数是「配额 / 周期」,相除就是等效核数。- Linux 上线程也占 pid,
pids.max管的是线程加进程的总数。 - fork 撞上限返回
EAGAIN,它的描述是「资源暂时不可用」,没说是哪个资源,所以极容易被引到别的方向去查。 - 三个触顶计数器:
cpu.stat的nr_throttled、memory.events的oom_kill、pids.events的max。 - 限额可能设在父层,读当前层会读到
max而误判为不限。 - 我这台机器的真实读数:命令行报 72 核、配额只有 8 核;
free报 125 GiB、上限只有 16 GiB;OOM 0 次,fork 被拒 2143 次。
最后留个问题:你在容器里跑服务的时候,是按 nproc 定的 worker 数,还是按编排文件里写的 limit 定的?
如果是前者,建议现在就跑一下脚本看看这两个数差多少。如果是后者——那你比我当时强,我当时是前者。
更多推荐


所有评论(0)