
Python后端部署排障15容器退出137就一定是内存不够吗应用突然退出终端显示137。有人立即把内存限制扩大一倍重新启动后服务似乎恢复了。真正的问题可能是人工执行了强制终止也可能是平台在宽限期结束后发送SIGKILL这些场景同样可以留下137。资源故障最危险的地方是现象容易记住原因却需要额外证据。这篇不靠耗尽宿主机制造“真实感”。我们把写满实验放进仅8MiB的tmpfs把进程、内存和CPU也限制在专属小容器里。发生ENOSPC后脚本只删除自己创建的文件再验证新写入成功。你观察的是明确的失败边界与恢复条件实验结束后同一台主机上的其他业务仍应保持原状。E14完整答案同一请求的两个日志落点执行python3 lessons/15/answer_previous.py。脚本使用status返回的容器ID不假定容器名称也不去读取其他项目日志。下面是完整答案E14: one safe request id crosses edge/app, without payload or credentials.importjsonfrompathlibimportPathimportsubprocessimportsysimporttimefromurllib.requestimportRequest,urlopenfromuuidimportuuid4 ROOTPath(__file__).resolve().parents[2]defmain():resultsubprocess.run([sys.executable,str(ROOT/ops/labctl.py),status],cwdROOT,checkTrue,textTrue,capture_outputTrue)statejson.loads(result.stdout)ride14_uuid4().hexquery_secretquery_uuid4().hexbody_secretbody_uuid4().hextoken_secrettoken_uuid4().hexpayload{sku:E14,supplier:body_secret[:32],quantity:1,unit_cents:701,shipping_cents:9}requestRequest(state[http_url].rstrip(/)/quotes?tokenquery_secret,datajson.dumps(payload).encode(),headers{Content-Type:application/json,X-Request-ID:rid,Authorization:Bearer token_secret})withurlopen(request,timeout8)asresponse:quotejson.load(response)assertresponse.status201assertresponse.headers[X-Request-ID]ridassertquote[total_cents]710activeapp_state[active]matched{}for_inrange(30):forservicein(edge,active):logsubprocess.run([docker,logs,--since,2m,state[containers][service]],checkTrue,textTrue,capture_outputTrue)combinedlog.stdoutlog.stderrforsecretin(query_secret,payload[supplier],token_secret):assertsecretnotincombined,Sensitive marker reached logs: service rows[]forlineincombined.splitlines():try:recordjson.loads(line)exceptjson.JSONDecodeError:continueifrecord.get(request_id)rid:rows.append(record)ifrows:assertany(int(row[status])201forrowinrows)matched[service]rowsiflen(matched)2:breaktime.sleep(0.1)assertlen(matched)2,Need both edge and active app evidenceprint(json.dumps({status:PASS,exercise:E14,request_id:rid,quote_id:quote[id],log_evidence:matched,query_body_token_absent:True}))if__name____main__:main()等待循环最多三秒用来容纳日志写入的轻微延迟。它不是失败后无限重试更不会重发创建请求POST只发一次随后只读日志。每一轮都扫描敏感标记并且只把匹配request_id的JSON记录留下。若没有两个落点脚本明确失败不用“可能是日志延迟”把证据缺口悄悄跳过。先列出预算才谈“资源不够”一个请求会消耗CPU时间、内存、文件描述符、连接、数据库锁和等待时间。Docker内存限制只约束其中一项。SQLite请求执行得慢可能是写锁争用连接迟迟不结束可能是上游等待文件打开失败可能是进程的FD上限。把这些都归结成“机器配置低”通常只会暂时推迟下一次故障。课程应用使用非root用户、单个Uvicorn进程和受限容器。这里的单进程是可理解的实验边界不是通用吞吐建议。SQLite写入串行增加应用进程或蓝绿同时提供大量写流量不会把单文件数据库变成高写入集群。规划资源时应先明确并发请求上限、数据库等待预算和拒绝策略再考虑扩容。CPU限制影响可用计算时间。CPU饱和时进程可能仍存活健康请求也可能间歇成功但排队延迟逐渐增长。内存限制影响进程可持有的工作集与缓存内存不足时可能分配失败也可能被内核终止。pids限制约束进程和线程的数量防止失控派生它不是HTTP并发数。文件描述符限制约束打开文件与socket连接数达到上限时常见的是创建新连接失败而不是整个进程立即退出。读取限制时既看配置又看运行事实。Compose文件说明期望值docker inspect说明容器创建时实际得到的值运行时统计说明当前用量。看到配置写了限制不等于现有容器已经重建应用该配置。相反看到瞬时用量低也不能证明没有短时峰值应把采样时间与退出时间对齐。容器限制与宿主余量也不是同一个数字。某个容器被限制在较小范围内即使宿主还有很多空闲内存它仍可能碰到自己的边界反过来多容器限制之和大于宿主可用资源也可能产生整机压力。课程给每个实验小而明确的上限同时限制只创建本次几个实例不把限额理解成允许无限启动容器的理由。观察CPU时应结合延迟和队列长度理解。一个高CPU的计算任务可能仍在按预期完成而一个CPU很低的服务也可能阻塞在数据库锁上。低用量不等于健康高用量不等于故障。实际排查要问请求是否超过目标耗时、失败是否增加、受限资源是否触顶、阻塞点与触发时间是否一致。只有把资源曲线与业务症状连起来调整额度才有依据。137是一条线索不是一份OOM结论类Unix环境常用128加信号编号表示进程因信号退出137对应SIGKILL的常见编码。SIGKILL可以来自多种来源人工强杀、停止宽限期耗尽、容器运行时动作以及内存限制触发的OOM终止。它不让Python执行finally因此仅靠应用日志缺少收尾记录也分不清来源。下面是可以保留的Docker状态检查方式。容器ID从本次状态获取避免把其他容器的退出原因当作本次故障importjsonimportsubprocessdefinspect_exit(container_id:str)-dict:resultsubprocess.run([docker,inspect,container_id],checkTrue,textTrue,capture_outputTrue,)containerjson.loads(result.stdout)[0]statecontainer[State]return{ExitCode:state[ExitCode],OOMKilled:state[OOMKilled],Error:state[Error],StartedAt:state[StartedAt],FinishedAt:state[FinishedAt],MemoryLimit:container[HostConfig][Memory],PidsLimit:container[HostConfig][PidsLimit],}如果ExitCode是137而OOMKilled为false就不能把报告写成“已确认OOM”。应结合操作时间线、停止命令、平台事件和相关内核证据继续判断。OOMKilled为true则提供了容器运行时识别到OOM的直接线索还需确认是哪一次运行、是否已经重启、限制值与峰值是什么。不要用上一次退出的状态解释当前这次请求。课程的drill limits收集受控容器限制与退出证据。强制终止演示可以用来说明137与OOMKilled的区别但不因此宣称进行了真实OOM压测。本篇选择ENOSPC作为可稳定、可局部恢复的资源耗尽实验我们没有必要为了课程把宿主机推到不稳定状态。8MiB的磁盘为什么能写满又为什么不会填宿主盘tmpfs是由内存支撑的文件系统容量限制与宿主普通磁盘目录不同。E15创建一个容量8MiB的独立挂载点并把实验脚本只读挂入容器。容器没有网络、根文件系统只读、运行用户为10001唯一用于写入的实验区域是这个小挂载。脚本还读取/proc/self/mountinfo和statvfs确认目标确实为tmpfs且容量在限定范围内普通目录会被拒绝。写入以一MiB块进行尝试次数有上限因此不存在无限写盘循环。空间不足时捕获具体errno.ENOSPC而不是把所有OSError都视为目标故障权限不足、只读文件系统或路径不存在应当暴露出来。finally删除本实验创建的fill.bin再写入recovered并读回。只有错误出现且恢复成功两段证据都成立练习才通过。# 下一篇提供完整调用答案实验脚本本身也随项目交付。python3 lessons/16/answer_previous.pyENOSPC也可能来自inode耗尽不能总按字节容量处理。日常排障中df -h查看块容量df -i查看inodedu统计可见目录内容。若删除大文件后空间没有释放应检查是否仍有进程持有已删除文件的FD名字消失与存储释放不是同一件事。第4篇的有限FD实验与本篇互相呼应恢复的动作取决于实际资源不是统一“重启一下”。tmpfs并不是免费的磁盘。它占用受管理的内存资源在不同宿主配置下还涉及交换行为因此本实验同时给容器内存上限填充范围也远小于该上限。我们要观察的是文件系统容量不足而不是竞争性地触发内存终止。如果把tmpfs设得接近甚至超过容器可用内存进程可能先被终止得到的就不是原先设计的ENOSPC实验。另一个细节是写入成功不等于所有持久化保证都成立。普通文件写操作可能先进入缓存数据库还涉及事务提交与同步策略。本题只证明一个受限tmpfs中的分配失败和释放恢复并不宣称证明了断电持久性。换成真实数据库故障演练时应围绕提交结果、日志恢复和业务读回单独设计不能把这里的一次write返回值拿去替代数据库耐久性证据。超时是资源预算的一部分Nginx返回504说明它等待上游时超出了相关预算并不代表上游代码已经停止。Python还可能继续计算、等待锁或提交事务。如果客户端立刻重试一个没有幂等保护的创建请求两个请求就可能分别成功形成重复报价。代理无法仅凭超时判断业务结果客户端也不能把“没收到响应”解释成“服务器什么都没做”。设计超时需要顺着调用链检查客户端总预算、代理连接与读取预算、应用内部外部调用预算、数据库忙等待预算。内层预算通常应给外层留下返回可解释错误的时间但具体值要按业务耗时和错误处理设计不能只把所有数字设成相同值。连接超时与读取超时描述的阶段不同盲目同时扩大只会延长资源被占用的时间。背压的目标是让系统在超出能力时有界地拒绝或排队。无限队列会把短时峰值变成长时延迟再变成内存问题。重试若没有次数、退避和整体期限限制会在系统最繁忙时增加更多负载多个客户端同步重试还会形成周期性尖峰。读取请求也需要重试预算写入请求则还要处理幂等性与结果查询。本课程保留报价version冲突保护但没有为创建接口宣称完整幂等键协议。本课练习E15失败和释放后的成功都要出现运行一个本次专属容器把/pit/tmpfs设置为8MiB证明写入得到errno28删除自己的填充文件后能够重新写入并读回结束时容器不存在。记录容量、错误编号、恢复布尔值与清理布尔值。禁止把宿主目录当作填充目标也不要为了复现OOM扩大实验范围。下一篇给出E15完整答案并把Linux、网络、代理和业务检查组织成一次有时间线的故障演练。你将故意停掉当前上游先保留502和容器状态再恢复并读回同一张报价让排障过程也有可验证的终点。课程目录《Linux、网络与后端部署排障从“接口能跑”走到“服务能维护”》 · 上一篇第14篇 · 下一篇第16篇