OPS / PRODUCTION HARDENING · INCIDENT · CAPACITY

运维与底座

本页是 reits-knowledge 431 / 432 / 433 号文档的同步副本,不是独立结论。所有数字都来自 2026-07-30 在生产机 EC2 54.206.197.2 上的实测与主动演练。事实源以 reits-knowledge 为准;本页与之不一致时,以 reits-knowledge 为准。

PROVENANCE · 本页为同步副本

事实源为 reits-knowledge 431 / 432 / 433 号文档

门户仓不保存原始结论,只保存 content/knowledge/ 下的逐字节副本与一份 INDEX.json;副本由 npm run knowledge:sync 生成。两道门禁分别守两件事:npm run knowledge:verify(内部为 --check --require-source)比对镜像与源仓的 sha256,源仓缺失即硬失败,它需要一份并排检出的 reits-knowledge,因此在开 PR 前于本地运行; npm run knowledge:selfcheck 比对镜像与 INDEX.json 自身(每份副本的 sha256 / 字节数 / 行数、有无孤儿文件、白名单是否与脚本一致),不需要源仓,因此 .gitea/workflows/ci.yml 里跑的是这一道。CI 的 job 只检出本仓,跨仓比对在那里无法成立,所以本页不声称 CI 做了跨仓比对。

SYNCED AT
2026-08-03 08:53 UTC
UPSTREAM
reits-knowledge/docs/ecosystem
MIRRORED / WHITELISTED
21 / 21

ERRATA · 2026-07-31 文档审计

勘误:11 条旧结论已被推翻或状态已变

源文档在 2026-07-31 各自加了顶部勘误章,做法是不删原文、另起一章逐条裁决。本页照办:本节逐条写「原结论 → 裁决 → 依据」,下文正文保留 2026-07-30 的原始快照并在被推翻处挂标记,而不是静默改掉数字。凡本节与本页其余部分冲突,以本节为准。

已被推翻433 E-1 / E-5433 号

原结论「零个 upstream 块、零 keepalive」是本轮实测发现的最重大缺陷,也是流量上来时第一个会炸的地方(临时端口与 TIME_WAIT 耗尽)。

2026-07-31 实测依据缺陷已修复。sudo nginx -T | grep -cE '^\s*upstream ' → 12;sudo nginx -T | grep -cE '^\s+keepalive [0-9]+;' → 12(12 个 upstream 各一条 keepalive 32)。修复落在 /etc/nginx/conf.d/00-upstreams.conf,该文件首行自述 added 2026-07-30 (nginx layer, batch 2),即在 433 取数之后的同日批次。proxy_http_version 1.1 由 24 处升至 26 处。新的最近瓶颈改为 im-core-postgres-1 容器 memory.max=268435456(256MiB)而 shared_buffers=128MB 占掉一半,max_connections=100 是假保护。

已被推翻433 E-2433 号

原结论「零个 conf 使用 map $http_upgrade 标准写法」,据此判定边缘未就绪。

2026-07-31 实测依据实为 1 个,在 /etc/nginx/nginx.conf 第 39–42 行(default upgrade; "" "";),$connection_upgrade 全配置树 29 处引用。同批次的 434 号 §2.1 在同一天就已把它记录在案,两份同日文档自相矛盾;原因是 433 只检索了 conf.d/。取数口径已写死:凡「nginx 有没有 X」必须用 nginx -T 展开全部 include 取数。

已被推翻431 E-1431 号

原结论compose 里 memswap_limit == mem_limit 即代表该容器禁用 swap,且 docker inspect 可作为取证依据。

2026-07-31 实测依据两条取证准则同时被证伪。14/14 运行中容器 docker inspect -f '{{.HostConfig.MemorySwap}}' 全部等于 Memory(都声明禁 swap),而内核 /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.swap.max 只有 3 个是 0(im-core-110 / im-worker / im-ws-gateway),8 个是 max(完全不限 swap:postgres、redis、info-portal-web、nodebb、grafana、nodebb-mongo、nodebb-redis、gitea),3 个是有限非零值(alloy 512MiB、loki 768MiB、docker-socket-proxy 64MiB)。11/14 不一致,因此「读 inspect 等价于读内核」也不成立。alloy 的真实天花板是 512MiB RSS + 512MiB swap = 1GiB。

状态已过期431 E-4431 号

原结论告警体系为零:Grafana + Loki + Alloy 在跑,但零条告警规则。

2026-07-31 实测依据实现已落地,闭环判据未全部满足,因此记「部分闭环」而非「已完成」。Grafana 已 provision 12 条规则(/etc/grafana/provisioning/alerting/rules-reits-rc5.yaml);reits-alert-watch.timer(2 分钟)与 reits-alert-probe.timer(5 分钟)均 active;nginx / postgresql / mariadb 的 OnFailure 均已指向 reits-alert-notify@*。未满足:其中 1 条 systemd-unit-failed 规则被该文件首行注释自述为已证伪的死规则、gen_rules.py 与手工修补不同步、站外探活装在一台会休眠的本地工作站上、且「人为制造故障能收到通知」无留证。

状态已过期431 E-2431 号

原结论「18 个容器逐个内存封顶」仍是待办,容器无内存上限。

2026-07-31 实测依据逐容器封顶已落地:14/14 运行中容器均有非零 HostConfig.Memory。但该项自己写死的完成态是「上限之和 + 两个数据库的 MemoryLow + build.slice 硬上限不超过物理内存」:14 个上限之和 7680MiB + postgres MemoryLow 2048MiB + mariadb MemoryMin 256MiB + build.slice MemoryMax 1500MiB = 11484MiB,而物理内存 7783MiB,超配约 47%。总量约束未满足,不得记为已完成。

状态已过期431 E-3 / 433 E-4431 号

原结论环境基线为 17 个 docker 容器;MinIO 是事故当日的幸存者之一。

2026-07-31 实测依据docker ps -q | wc -l → 14。minio 与 nats 两个容器均已不存在,凡以它们存在为前提的记载只在事故当日成立。生效 nginx conf 数同时由 16 升至 18(ls /etc/nginx/conf.d/*.conf | wc -l)。

已被推翻431 E-6 / 433 E-6431 号

原结论消息总线是 NATS;MinIO 是切片 origin;把 noop publisher 换成 NATS 发布是路线图条目。

2026-07-31 实测依据im-core 仓 go.mod 无任何 NATS 依赖;internal/config/config.go:32-33 只有 RealtimeBusLocal = "local" 与 RealtimeBusRedis = "redis",:953-955 其余取值一律 unsupported REALTIME_BUS;internal/config/config_test.go:452 把 REALTIME_BUS=nats 列为必须被拒绝的用例。outbox publisher 合法取值只有 disabled / noop / webhook / webhook-push(config.go:19-22),不存在 NATS publisher。

判据不完整431 E-7431 号

原结论可靠性层已补齐:这台机器已从「被杀就死、无人知晓」变成会自愈、会告警。

2026-07-31 实测依据原文未错,但只覆盖宿主进程存活,不覆盖容器内数据库的启动期正确性。docker logs im-core-postgres-1 | grep -c 'deadlock detected' → 5,全部发生在 2026-07-30(11:18:01 pid21 / 11:18:02 pid20 / 11:19:18 pid20 / 11:19:18 pid21 / 11:19:18.735 pid23),三个进程互等,两次带 at character 2210;而 show log_lock_waits 当前为 off,这类等锁事件默认不留痕。N=3 单机就足以触发,不需要多实例。归属 435 号。

状态已过期433 E-3433 号

原结论proxy_read_timeout 分布 = 120s×13 / 30s×4 / 300s×3 / 1h×1,共 21 处。

2026-07-31 实测依据按值计数实测为 120s×12 / 30s×4 / 300s×3 / 3600s×2,共 21 处。「四档并存、无统一口径」这一判读仍然成立,具体分布已变。

判据不完整433 E-7433 号

原结论WebSocket 就绪度判定为「后端就绪、边缘未就绪」。

2026-07-31 实测依据只审了边缘。服务端侧复核:im-core 全仓 WS 层 Ping / SetReadDeadline / ReadDeadline 零命中,internal/httpapi/realtime_handlers.go:63 是空 websocket.AcceptOptions{},心跳纯被动。nginx /websock/ 的 proxy_read_timeout 3600s 与服务端零心跳叠加,半开连接最长挂 1 小时。这是服务端设计缺口,不是边缘配置债。归属 435 号。

新发现433 E-8433 号

原结论(433 未覆盖)新品牌域的边缘可观测性。

2026-07-31 实测依据map $host $reits_edge_service_name 零条 probatlas.com 表项,而 log_format reits_edge_json 用 $reits_edge_service_name 作 service_name 字段。实测已有 12 个 probatlas.com 别名可服务,但其访问日志 service_name 全部落到 default "nginx-edge"。后果:按 service_name 分服务的 Loki 查询与 Grafana 告警对新品牌域全部静默失效。

经复核仍然站得住的原结论

逐条列出,避免把「有勘误」读成「全文作废」。

  • 431 §1 的 AL2023 / systemd 252 / cgroup v2 + PSI / 4 核 7.8G 环境基线。
  • 431 §2.1–2.5 五项 P0 整改本身及其演练验收证据(kill -9 自愈、OOM 档位无 -1000、build.slice 节流、listen_addresses='*')。
  • 431 TODO-05 / TODO-06:备份与 EBS 快照仍为零;TODO-09:月度重启演练仍未建立。
  • 433 的并发利用率极低、容量瓶颈不存在这一判定方向。
  • 433 的 worker_connections 1024 与进程 fd limit 65535 失配、tcp_max_syn_backlog 512 与 somaxconn 4096 失配。
  • 433 的端到端延迟以跨太平洋 RTT 与 TLS 握手为主而非后端处理。
  • 433 的公开首页 private, no-store 与静态资源 immutable 方向倒置。
SOURCE OF TRUTH · reits-knowledge434 号《nginx CVE-2026-42533 暴露面分析与配置红线》

reits-knowledge/docs/ecosystem/434-nginx-cve-2026-42533-exposure-analysis-2026-07-30.md

sha256 dbba8830f22f5a1e8eda7f7deceffca2… · 170 行 · 同步时间 2026-08-03 08:53 UTC

  • 434 号:nginx CVE-2026-42533 暴露面判定与 R-1 配置红线(2026-07-31 复核通过,无需勘误)
SOURCE OF TRUTH · reits-knowledge435 号《多实例底座落地计划(im-core Multi-Instance Foundation Landing Plan)》

reits-knowledge/docs/ecosystem/435-im-core-multi-instance-foundation-landing-plan-2026-07-31.md

sha256 373ad8aefbb557abb56206b06e83a267… · 1093 行 · 同步时间 2026-08-03 08:53 UTC

  • 435 号:多实例底座落地计划,承接 431 E-7 的 postgres 启动期 DDL 死锁与 433 E-7 的 WS 零心跳

A · OPS HARDENING LEDGER · 431

运维加固台账:5 层已完成,4 层仍是零

逐层状态。「已完成」一律附演练验收证据;拿不出演练证据的只进待办,不进已完成。

已完成进程存活自愈

nginx 被杀后能自己起来,有 kill -9 真杀演练证据

已完成OOM 选择偏好

nginx / postgres / mariadb 三个单元全部脱离 OOMScoreAdjust=0 裸奔态

已完成构建隔离

slice 内申请 3G 被节流在 1046MB、oom_kill=0

已完成重启竞态根因

postgres 只绑 127.0.0.1 导致 gitea 连不上库的根因已消除,不再依赖过渡性等待脚本

已完成残留清理与配置目录瘦身

机器人集群残留移除、nginx 配置 342 → 16,全部内容已打包备份

待办告警体系

Grafana + Loki + Alloy 在跑,但零条告警规则

待办备份策略与 EBS 快照

6 类数据存储无统一备份口径,无 DLM 快照策略

待办容器逐个内存封顶(18 个)

构建已进 slice,容器仍无内存上限

进行中nginx 配置进 git + 漂移检测

在另一刀 knife/ops-config-converge 中

一句话结论:这台机器已经从「被杀就死、无人知晓」变成「被杀会自己起来、构建再也杀不死它」,但它仍然是一台「出事没人知道、坏了没备份可回」的机器。存活性(P0)已经补齐并有演练证据;可观测性与可恢复性(告警、备份、快照)仍然是零。

实例
EC2 54.206.197.2私网 172.31.19.46 · ap-southeast-2
操作系统
Amazon Linux 2023init 为 systemd 252
cgroup
cgroup v2 + PSI 可用决定可用 MemoryHigh 软节流而非只有 MemoryMax 硬杀
CPU / 内存
4 核 / 7.8Gswap 2G
磁盘
256G · 已用 67%无自动快照
工作负载
17 容器 + 5 业务单元另有宿主 PostgreSQL 与宿主 MariaDB
nginx
1.28.1(发行版包安装)第三方模块需换包才能加进来
两个基线事实决定了本轮整改的技术选型——它们不是背景,而是选型依据。
实测事实它决定了什么
cgroup v2 + PSI 均可用可以用 MemoryHigh 软节流而不是只有 MemoryMax 硬杀,这是构建隔离能做到「节流 102736 次、oom_kill=0」的前提。若只有 cgroup v1,构建超限只能被硬杀。
systemd 版本是 252RestartSteps / RestartMaxDelaySec 要 systemd 254+ 才存在,252 上写了会被解析为未知键。因此 nginx 采用固定间隔 RestartSec=5 并用 StartLimitBurst 兜住无限重启。

A · P0 REMEDIATIONS WITH DRILL EVIDENCE

5 项 P0 整改,逐项含演练验收证据

全部为 2026-07-30 在生产机上直接执行的实测与主动演练,含 kill -9 真杀进程、cgroup 内真申请 3G 内存。不是纸面推演,也不是 --dry-run。

nginx 自愈 + OOM 保护

431 · 2.1已完成 2026-07-30

问题:nginx 单元原为 Restart=no。被内核 OOM killer 杀掉后永久不自愈,13 个域名全部不可达(432 号 RC-3)。

/etc/systemd/system/nginx.service.d/10-reliability.conf
Restart
always原值 no 是本次中断从「死一个进程」放大成「死 10 分钟」的直接原因
RestartSec
5固定退避;252 上没有指数退避参数
OOMScoreAdjust
-900让内核优先选别人;不用 -1000
OOMPolicy
continue单个 worker 被 OOM 杀掉时主进程继续
MemoryAccounting
yes否则 MemoryMin / MemoryLow 不生效
MemoryMin / MemoryLow
64M / 256M硬保护线 / 软保护线

判定 主进程被 kill -9 后 8 秒内自动重启,站点恢复 200。自愈成立。

宿主 PostgreSQL OOM 策略修正

431 · 2.2已完成 2026-07-30

问题:OOMScoreAdjust 原为 -1000,语义是完全豁免。当内存耗尽而所有大户都被豁免时,内核找不到可杀目标,故障形态从「杀掉一个进程」退化为「整机 hang」。

/etc/systemd/system/postgresql.service.d/20-oom-reliability.conf
OOMScoreAdjust
-1000 → -900高度保护但不豁免,保留内核的可杀目标
PG_OOM_ADJUST_FILE
/proc/self/oom_score_adjpostmaster 保持受保护
PG_OOM_ADJUST_VALUE
0fork 出的 backend 复位为可被杀;官方文档指出不设此项 is unwise
Restart / RestartSec
on-failure / 10数据库正常停止不应被拉起
StartLimitBurst
3避免数据损坏场景下无限循环拉起
MemoryMin / MemoryLow
768M / 2G硬保护线 / 软保护线

判定 -900(保护但可杀)优于 -1000(完全豁免)。

MariaDB OOM 保护

431 · 2.3已完成 2026-07-30

问题:OOMScoreAdjust 为 0(裸奔),而同机 dockerd 官方单元自带 -500 → 内核在内存竞争时结构性地优先杀 nginx 和 mariadb(432 号 RC-4)。

/etc/systemd/system/mariadb.service.d/10-oom.conf
OOMScoreAdjust
-800
Restart / RestartSec
on-failure / 10
StartLimitBurst
3
MemoryMin
256M

判定 档位原则:恢复代价越高、影响面越广,越靠负。

构建隔离(build.slice + safe-build)

431 · 2.4已完成 2026-07-30

问题:事故的触发动作是在生产机上直接运行前端构建(npm ci 509 个包 + vinext build),未设任何资源上限,内存被吃爆(432 号 Trigger)。

/etc/systemd/system/build.slice + /usr/local/bin/safe-build
MemoryHigh
1G软上限:超过后持续回收并节流,进程变慢但不死
MemoryMax
1500M硬上限:真突破则 cgroup 内 OOM,杀构建而非杀 nginx
MemorySwapMax
512M限制构建把 2G swap 全部吃掉
CPUWeight / IOWeight
20 / 20默认 100,构建让路给在线服务
NODE_OPTIONS
--max-old-space-size=1024第二道防线:Node 自己的 GC 在 1G 前就激进回收

判定 事故触发路径已被封死:同样的构建动作现在只会把自己拖慢,杀不到 nginx。

重启竞态根因修复(postgres listen_addresses)

431 · 2.5已完成 2026-07-30

问题:listen_addresses 为 'localhost,172.17.0.1'。开机时序实测 postgres 11:19:06 启动、docker 11:19:23 启动,postgres 启动时 docker0 网卡尚不存在;PostgreSQL 只要能绑上一个地址就照常启动,故只绑了 127.0.0.1,且该参数只能在启动时设定 → 永不自愈,容器内 gitea 每次重启都要人工救。

postgresql.conf listen_addresses + pg_hba.conf
listen_addresses
'localhost,172.17.0.1' → '*'通配地址不依赖任何具体网卡存在,竞态从根上消除
pg_hba 纵深防御
追加 0.0.0.0/0 与 ::/0 的 reject既有规则限定回环与 172.17.0.0/16 走 scram-sha-256
过渡方案
已移除等待 docker0 的 systemd drop-in根因已修,不再需要

判定 listen_addresses 是「绑定哪些本机地址」,不是「允许谁连」。用 '*' 换掉枚举网卡地址,消灭的是「网卡出现晚于数据库」这一整类竞态。

EXECUTION DISCIPLINE · 写死,不可协商

生产机上任何构建、npm ci、镜像构建、数据回填等重内存批处理任务,必须经 safe-build 或显式 --slice=build.slice 执行。

直接裸跑构建等同于重放本次事故。

顺带完成的清理。删前核查一并记录,否则无法证明没有删掉源码。
项结果核查与备份
机器人集群 universe 残留移除 3 个 systemd 单元 + 95M 目录删前核查:21 个未提交文件全部为运行时产物(pid / heartbeat / lock / .bak)与一份进度文档,无任何源码改动
nginx 配置目录瘦身342 → 16 个纯生效 .conf清除 326 个手改遗留的 .bak 类文件;nginx -t 通过。真实价值不是省磁盘,是消除「哪一份是生效的」这一歧义
备份cleanup-backup-20260730T113014Z3 个 tar.gz,位于 /home/ec2-user/

A · OPEN ITEMS

待办清单:9 项待办 + 1 项进行中

下列每一项都还没做。它们不是「已规划所以等于安全」——存活性已补齐,可观测性与可恢复性仍为零,本节就是那个零的明细。

TODO-01
P0
告警规则

现状:Grafana + Loki + Alloy 三件套都在跑,但零条告警规则

缺口:本次全站中断 10 分钟,发现方式是人工 curl。观测栈部署完毕却一条规则都没配,等于装了摄像头没接监视器

TODO-02
P0
OnFailure= 通知

现状:全部 systemd 单元均未配置

缺口:nginx 现在会自愈了,但自愈事件本身无人知晓。若进入反复重启,除了翻 NRestarts 没有任何主动信号

TODO-03
P0
站外黑盒探活

现状:无

缺口:所有观测手段都在这台机器上。机器整体不可达时,观测栈跟着一起不可达——这正是最需要告警的场景

TODO-04
P0
黄金信号告警规则(延迟 / 流量 / 错误 / 饱和度)

现状:无

缺口:已有 PSI 可直接做饱和度信号,433 号已给出延迟基线;数据齐了,规则是空的

TODO-05
P1
备份策略(6 类数据存储)

现状:无统一备份口径

缺口:宿主 PostgreSQL、宿主 MariaDB 与容器侧存储共 6 类,周期 / 保留期 / 恢复演练全部未定义。本次「无数据损坏」属于运气

TODO-06
P1
EBS 快照 DLM 策略

现状:无

缺口:256G 盘已用 67%,无自动快照。实例级故障目前无任何恢复点

TODO-07
P1
swap 扩容

现状:当前 2G

缺口:7.8G 内存 + 2G swap 对 17 容器 + 2 个数据库偏紧。构建已被 slice 限住,容器侧仍无上限

TODO-08
P1
18 个容器逐个内存封顶

现状:容器无内存上限

缺口:构建路径已封死,但任何一个容器内存泄漏仍可复现同一场景。受害者会换成别的容器,且档位保护不等于容量保证

TODO-09
P2
月度重启演练

现状:无

缺口:已修好一个已知竞态,但没有任何机制能发现下一个同类竞态。演练必须真重启,不是 systemctl restart 单个服务

WIP-01
WIP
nginx 配置进 git + 漂移检测

现状:在另一刀 knife/ops-config-converge

缺口:前置条件(配置目录 342 → 16)已由本刀完成

SOURCE OF TRUTH · reits-knowledge431 号《生产运维加固台账(Production Ops Hardening Ledger)》

reits-knowledge/docs/ecosystem/431-production-ops-hardening-ledger-2026-07-30.md

sha256 c92fcf64439a839c91235e82d2fba7c9… · 519 行 · 同步时间 2026-08-03 08:53 UTC

  • 第 0 章:九层状态总结论
  • 第 1 章:环境基线(AL2023 / systemd 252 / cgroup v2 + PSI)
  • 第 2 章:5 项 P0 整改与演练验收证据
  • 第 3 章:清理与备份
  • 第 4 章:TODO-01…09 与 WIP-01

B · BLAMELESS POSTMORTEM · 432

INC-2026-0730-01:全站中断约 10 分钟

Google SRE blameless postmortem。不点名个人、不追究「谁按了回车」,只追究「为什么系统允许这个动作造成全站中断」。单一负责人:生产基础设施值班(Production Infrastructure On-call)。

SEVERITY / STATUS
SEV-1 · 已关闭P0 整改全部落地并完成演练验收;P1 / P2 Action Items 转入 431 号第 4 章跟踪
DURATION
约 10 分钟从 nginx 被终止到 nginx 恢复服务
DETECTION
人工 curl无告警规则、无站外探活、无 OnFailure= 通知——没有任何自动信号
DATA LOSS
无旧域 13 个 + 新域全部不可达;gitea 在恢复过程中一度 502
TRIGGER · 触发,不是根因

在生产机上直接运行前端构建(npm ci 509 个包 + vinext build),未设任何资源上限

Trigger 不是根因。一台承载 13 个域名的生产机,不应该存在「一条普通运维命令就能让全站中断 10 分钟」的路径。

RC-1…RC-5 是本次中断的根因;RC-6 是恢复过程中发现的独立潜伏缺陷,与本次中断无因果关系但严重性不低于它。
ID根因类型为什么它是根因闭环
RC-1构建无隔离缺失的约束生产机上不存在任何资源隔离机制。因此任何重内存批处理任务——构建、数据回填、镜像构建——都能耗尽整机内存。触发动作恰好是构建,换成任何一种都会得到同样结果部分闭环
RC-2OOM killer 选中 nginx错误的默认档位内核按 oom_score 选目标,nginx 的 OOMScoreAdjust=0 让它成为合法候选。在内核视角下,没有任何信息表明 nginx 比构建进程更重要已闭环
RC-3nginx 单元为 Restart=no,被杀后永久不自愈缺失的自愈这是把「毫秒级进程终止」放大成「10 分钟全站中断」的决定性一环。若配置为 Restart=always,同一次 OOM kill 的对外影响会是数秒级抖动已闭环
RC-4OOM 保护档位在同机内结构性不均衡结构性缺陷dockerd 官方单元自带 -500,nginx 与 mariadb 为 0。内存竞争的受害者被系统性地固定为最关键的边缘入口与数据面。RC-2 说「nginx 可以被杀」,RC-4 说「nginx 会被优先杀」已闭环
RC-5无任何告警规则缺失的可观测性观测栈已部署但零条规则,且无 OnFailure= 通知、无站外探活。「全站已挂」没有任何自动传播路径。这一条决定了中断时长的下界不是由技术恢复速度决定,而是由「有没有人恰好在看」决定未闭环
RC-6宿主 PostgreSQL 只绑定 127.0.0.1,容器内 gitea 永远连不上数据库恢复过程中发现的独立潜伏缺陷(非本次中断的原因)开机时序竞态:postgres 早于 docker 启动,docker0 网卡尚不存在,绑定失败只发 warning;listen_addresses 不可 reload → 该状态永不自愈,每次服务器重启都埋着一次「全生态代码托管不可用」部分闭环
Trigger 裸跑构建内存耗尽(7.8G)RC-1 构建无隔离
内存耗尽内核 OOM killer 选中 nginxRC-2 + RC-4 档位不均衡
nginx 被终止13 个域名全部不可达RC-3 Restart=no,永久不自愈
全站不可达中断拉长到约 10 分钟RC-5 零告警,靠人工 curl 才发现

根因链的形状说明整改优先级:链条的形状说明了整改的优先级:RC-3 的修复收益最高(一行 Restart=always 就能把 10 分钟压到数秒),RC-1 的修复最彻底(封死整个触发类别),RC-5 未修则任何未来事故的时长仍不可控。

B · TIMELINE

时间线:11:19:01 开机,11:22–11:24 恢复

时刻全部来自生产机实测(单元启动时间、进程 PID、恢复动作时刻),不是事后回忆。灰底行为窗口内事件,无独立时刻证据。

生产机开机

系统启动时间

宿主 PostgreSQL 启动。docker 尚未启动,docker0 网卡不存在 → 172.17.0.1 绑定失败仅发 warning,postgres 照常 active,只绑 127.0.0.1(RC-6 成立的瞬间)

单元启动时间

dockerd 启动,docker0 网卡出现。比 postgres 晚 17 秒——而 postgres 的绑定决策已在 11:19:06 冻结且不可 reload 更改

单元启动时间

在生产机上直接运行前端构建(npm ci 509 个包 + vinext build),未设资源上限 → 内存逐步耗尽(Trigger)

事故动作

内核 OOM killer 触发,选中 nginx(OOMScoreAdjust=0,而 dockerd 为 -500)→ nginx 进程被终止

RC-2 / RC-4

nginx 单元 Restart=no → 不自愈。旧域 13 个 + 新域全部不可达。无告警触发

RC-3 / RC-5

通过人工 curl 发现全站不可达(唯一的发现渠道)

RC-5

人工恢复窗口:重启 nginx 恢复站点;处置 gitea → PostgreSQL 连接问题(gitea 一度 502),并由此定位到 RC-6

恢复动作时刻

P0 整改逐项落地并完成演练验收(见本页第一块)

431 号第 2 章

17 秒:时间线里最值得注意的一点:11:19:06 与 11:19:23 之间的 17 秒决定了 gitea 在这台机器上每次重启后能否连上数据库。这 17 秒不是异常,是这台机器的正常开机时序——缺陷在于配置假设了 docker0 已存在。

WHAT WENT WELL · 5

做对了什么

  • 恢复动作聚焦且未造成二次伤害:17 个容器与两个数据库未被牵连,最终零数据损坏。
  • 恢复过程被当成了排查过程。gitea 的 502 没有被当作噪音,而是被追到了 RC-6。这次事故最大的产出不是修好了 nginx,是找到了 RC-6。
  • 整改在同一天完成,且每一项都做了主动演练验收,不是「改完看着像对了」。
  • 整改深入到了「档位设计」层面:识别出「过度保护也是缺陷」比「加保护」难,也更有价值。
  • RC-6 的修复选择了根因级方案而非绕过,并移除了那个过渡 drop-in。
WHAT WENT WRONG · 7

做错了什么

  • 生产机允许无隔离地运行重内存批处理任务。
  • 关键边缘入口没有配置自愈。修复成本是一个 drop-in 文件,收益是把中断时长压缩两个数量级。
  • OOM 保护档位从未被统一审视过——它不是配置错误,而是没有人把它当作一个需要整体设计的对象。
  • 观测栈部署完成但告警规则为零。这比「没有观测栈」更危险,因为它会造成「我们有监控」的错觉。这一项至今仍未完成。
  • 发现故障的唯一手段是人工。
  • 隐式的开机时序依赖没有任何验证机制。除 RC-6 之外是否还有同类竞态,目前无法回答。
  • nginx 配置目录长期处于无法辨识生效版本的状态(342 个文件 / 326 个 .bak)。
WHERE WE GOT LUCKY · 5

哪里运气好

  • 事故发生在白天,且当时有人在。若发生在凌晨,中断会持续到有人醒来。这一条至今仍是运气,因为告警仍未落地。
  • 内核 OOM killer 没有选中 gitea、mongo、MinIO。若选中数据面,零数据损坏这个结果就不成立了。
  • 没有发生数据损坏。这一条尤其侥幸,因为备份策略至今为零——若真损坏,没有任何恢复点可用。
  • RC-6 被暴露在一次有人值守的事故中。若它在一次例行重启后独自出现,最可能的结局是「人工救一下就好了」,从而永远不被追到根因。
  • 构建耗尽内存的时点,17 个容器中没有任何一个恰好处于内存高峰。容器至今仍无内存上限。

B · ACTION ITEMS

Action Items:5 项已完成,8 项待办,1 项进行中

每条含单一 owner 与可度量完成态。「已完成」的完成态一律是实测值,不是「已配置」。未完成即未完成,不在此处粉饰。

Action Items 与其对应根因、可度量完成态、以及在 431 号台账中的锚点。
ID动作对应根因 / 教训可度量完成态状态锚点
AI-01nginx 单元加 Restart=always / RestartSec=5 / OOMScoreAdjust=-900 / OOMPolicy=continue / MemoryAccounting=yes / MemoryMin=64M / MemoryLow=256MRC-3、RC-2实测:kill -9 后 8 秒内重启(7603 → 57189),NRestarts=1,站点 200已完成431 · 2.1
AI-02构建隔离:build.slice(MemoryHigh=1G / MemoryMax=1500M / MemorySwapMax=512M / CPUWeight=20 / IOWeight=20)与包装器 safe-buildRC-1实测:memory.events 显示 high 102736、oom_kill 0,占用压在约 1046MB,17 容器正常,6 个受测域名全部 200已完成431 · 2.4
AI-03统一同机 OOM 保护档位:postgres -1000 → -900 并加 PG_OOM_ADJUST_*;mariadb 0 → -800;两者均加 Restart=on-failure / RestartSec=10 / StartLimitBurst=3 与 MemoryMinRC-4同机档位满足 nginx=-900、postgres=-900、mariadb=-800、dockerd=-500,且无任何单元为 -1000已完成431 · 2.2 / 2.3
AI-04RC-6 根因修复:listen_addresses 改为 '*';pg_hba.conf 追加 reject 兜底;移除等待 docker0 的过渡 drop-inRC-6三层同时成立:5432 公网不可达、pg_hba_file_rules 错误 0 条、gitea 容器内 172.17.0.1:5432 open已完成431 · 2.5
AI-05清理故障处置阻力:nginx 配置目录 342 → 16 个纯生效 .conf;移除机器人集群残留三单元与 95M 目录「做错了什么」第 7 条nginx -t 通过,配置目录内无 .bak 类文件;清理内容已打包备份(3 个 tar.gz)已完成431 · 3.1 / 3.2
AI-06配置告警规则(黄金信号:延迟 / 流量 / 错误 / 饱和度)RC-5Grafana 中告警规则数 > 0 且至少覆盖四个黄金信号各一条;人为制造一次故障能收到通知待办431 · TODO-01 / TODO-04
AI-07为关键 systemd 单元配置 OnFailure= 通知RC-5、「运气好」第 1 条nginx / postgresql / mariadb / 5 个业务单元均有 OnFailure=;故意失败能收到通知待办431 · TODO-02
AI-08建立站外黑盒探活(不在本机)RC-5、「运气好」第 1 条探活点部署在本机之外;整机断电场景下仍能告警。本条是 AI-06 无法替代的待办431 · TODO-03
AI-09定义 6 类数据存储的备份策略(周期 / 保留期 / 恢复演练)「运气好」第 3 条6 类存储各有成文口径,且每类至少完成一次恢复演练并留存证据。仅有备份、未验证恢复的不计完成待办431 · TODO-05
AI-10配置 EBS 快照 DLM 策略「运气好」第 3 条DLM 策略生效,能查到至少一个自动生成的快照,且 RPO 成文待办431 · TODO-06
AI-1118 个容器逐个内存封顶RC-1(残余面)、「运气好」第 5 条18/18 容器均有内存上限;上限之和 + 两个数据库的 MemoryLow + build.slice 硬上限不超过物理内存待办431 · TODO-08
AI-12swap 扩容RC-1(缓冲面)swap 容量与 AI-11 的封顶方案配套成文并生效待办431 · TODO-07
AI-13建立月度重启演练(真重启整机)RC-6、「做错了什么」第 6 条每月一次真重启,重启后全部服务无需人工介入即恢复;每次留存证据。这是唯一能发现「下一个 RC-6」的机制待办431 · TODO-09
AI-14nginx 配置进 git + 漂移检测「做错了什么」第 7 条配置在 git 中,且漂移检测能发现机上手改。前置条件(342 → 16)已由 AI-05 完成进行中431 · WIP-01
根因覆盖检查。RC-5 明确标为未闭环——这是本次事故留下的最大风险。
根因已被覆盖的动作是否已闭环说明
RC-1 构建无隔离AI-02 已完成;AI-11 / AI-12 待办部分闭环触发路径已封死,容器侧同类风险仍在
RC-2 OOM 选中 nginxAI-01 已完成已闭环—
RC-3 nginx 不自愈AI-01 已完成已闭环—
RC-4 档位不均衡AI-03 已完成已闭环—
RC-5 零告警AI-06 / AI-07 / AI-08 全部待办未闭环未闭环。这是本次事故留下的最大风险:下一次事故的时长仍然由「有没有人恰好在看」决定
RC-6 postgres 绑定竞态AI-04 已完成;AI-13 待办部分闭环本实例已修,同类竞态无发现机制
SOURCE OF TRUTH · reits-knowledge432 号《2026-07-30 全站中断事故复盘(Blameless Postmortem)》

reits-knowledge/docs/ecosystem/432-full-site-outage-postmortem-2026-07-30.md

sha256 3e4626dbdccc85b1e6e980eaab3cfa7c… · 233 行 · 同步时间 2026-08-03 08:53 UTC

  • Metadata / Problem Summary / Background / Impact
  • Root Causes 与 Trigger(RC-1…RC-6 与根因链)
  • Timeline(实测时刻)
  • Lessons Learned(做对 / 做错 / 运气好)
  • Action Items(AI-01…AI-14 与根因覆盖检查)

C · CAPACITY BASELINE · 433

底座容量基线:资源富余,问题全在配置形态上

本节是基线快照,不是恒定事实;引用时必须连同测量日期 2026-07-30 一起引用。并发与配置类为机上实测,延迟类为外部跨太平洋客户端实测(RTT 110–160ms(外部跨太平洋客户端,复测必须记录发起位置))。

请求路径与瓶颈层:客户端 → TLS → nginx → 上游 → 后端一次请求穿过五层。跨太平洋 RTT 110 到 160 毫秒是固有底噪;TLS 握手净耗约 0.76 秒,约等于 5 个 RTT;nginx 边缘的连接上限是 4096,实测 established 33、约 2.8 QPS,利用率约 0.8%,容量富余;反代到上游一层在 2026-07-30 取数时是最重的瓶颈, 零个 upstream 块、零 keepalive,却有 24 处 proxy_http_version 1.1,因此每个请求都与后端新建 TCP 连接——该缺陷已在同日晚些时候修复, 2026-07-31 复测为 12 个 upstream 各带一条 keepalive 32,因此这一层的「最重瓶颈」判定已被 433 号勘误 E-1 推翻;后端处理本身只占总时延的 10% 到 14%。下方色带按 www.reits.tech 实测总时长 3.324 秒等比拆分为 TCP、TLS、服务端首字节与传输四段。REQUEST PATH · 2026-07-30 实测(外部跨太平洋客户端)客户端跨太平洋RTT 110–160ms固有底噪TLS 握手connect 0.128sappconnect 0.889s净耗 0.76s ≈ 5 RTTnginx 边缘上限 4 × 1024 = 4096established 33 · 2.8 QPS利用率 0.8% · 富余反代到上游07-30: upstream 0 · keepalive 007-31: upstream 12 × keepalive 32每请求新建 TCP · 已由 433 E-1 推翻后端应用developer 0.19sbbs 0.14s仅占总时延 10–14%LATENCY COMPOSITION · www.reits.tech 总 3.324s(DNS 0.003s 在此刻度下不可见)TCP 0.128sTLS 0.761s服务端首字节 1.039s传输 1.396s判读:约 86%–90% 的用户感知时延发生在网络与握手层,不在应用层。首页 private, no-store 使服务端首字节这一段每次访问都要重付。
有余量,当前不是瓶颈固有成本或口径不一致随流量线性放大的缺陷客户端侧固有底噪
有余量并发余量

极大富余。配置上限 4096 连接,实测 established 33 个、约 2.8 QPS → 利用率约 0.8%

缺陷真正的瓶颈

不是容量,是连接复用:零个 upstream 块、零 keepalive,却有 24 处 proxy_http_version 1.1 → 每个反代请求都与后端新建 TCP 连接

需关注延迟构成

首字节 1.9s 的主要成分是跨太平洋 RTT + TLS 握手,不是后端处理(developer 后端仅 0.19s、bbs 仅 0.14s)

缺陷缓存

静态资源正确(immutable 一年),公开资讯首页完全不可缓存(private, no-store)——方向恰好反了

需关注WebSocket

网关容器已在跑,但 nginx 侧零个 conf 使用 map $http_upgrade 标准写法,仅 3 个 conf 硬编码 Upgrade 头

需关注并发天花板

worker_connections 1024 是人为设置的低天花板(进程实际 fd limit 为 65535,worker_rlimit_nofile 未声明)

一句话结论:这台机器的资源是富余的,问题全在配置形态上。0.8% 的并发利用率意味着容量扩容在当前阶段是伪需求;真正会在流量上来时先炸的是「零 keepalive 导致的临时端口与 TIME_WAIT 消耗」和「公开首页不可缓存导致的后端放大」。

C · CONCURRENCY & CONNECTION REUSE

nginx 利用率 0.8%,但每个反代请求都在新建 TCP 连接

这两件事同时为真,所以「扩容」与「修复」不是同一件事:前者在当前基线下是伪需求,后者是流量增长时第一个会炸的地方。

并发配置与实测负载。每一项都写明取数命令,目的是让日后复测可以逐项对齐。
项实测值测量方法说明
worker_processesauto(→ 4)nginx -T与 CPU 核数一致,正确
worker_connections1024nginx -T人为设置的低天花板
理论总连接上限40964 × 1024反代场景每请求占 2 个连接,实际可服务并发约为该值一半
worker_rlimit_nofile未声明nginx -T | grep系统层已给到 65535,配置侧没用
nginx 进程 fd limit65535/proc/<pid>/limits1024 不是被系统逼出来的值
established 连接数33ss -tan state established | wc -l—
请求速率约 2.8 QPS访问日志行数 ÷ 采样秒数—
load average约 1.0/proc/loadavg4 核机器,1.0 ≈ 25% 单核饱和度,仍有 3 倍余量
并发利用率约 0.8%33 ÷ 4096当前阶段的容量瓶颈不存在
DEBT-01 · 本轮实测发现的最重大缺陷

upstream 块 0 个 · keepalive 指令 0 条 · proxy_http_version 1.1 声明 24 处

proxy_http_version 1.1 的唯一实际收益就是能复用上游连接,而复用必须由 upstream 块中的 keepalive N 开启。当前是「声明了协议版本却没有开启它带来的唯一好处」。

每一个经过反代的请求都要与后端新建一次 TCP 连接,随后进入 TIME_WAIT。在 2.8 QPS 下没有任何可观察症状(这正是它长期未被发现的原因),但它随流量线性放大——临时端口约 2.8 万、TIME_WAIT 默认驻留 60 秒,粗算的端口耗尽点在数百 QPS 量级。

已被推翻433 E-1 / E-5该缺陷已在 2026-07-30 的 nginx layer batch 2 中修复。2026-07-31 实测 sudo nginx -T 得 12 个 upstream,各带一条 keepalive 32,proxy_http_version 1.1 升至 26 处。上方是 2026-07-30 取数当时的快照,「最重大缺陷」与「流量上来时第一个会炸的地方」两项判定均已不再成立。

C · WEBSOCKET READINESS

WebSocket:后端就绪、边缘未就绪(0 个标准写法 / 3 个硬编码)

硬编码写法对该 location 下的所有请求都声明 upgrade 语义,包括普通 HTTP 请求。标准写法让 Connection 头随请求是否携带 Upgrade 而变化。当前形态下任何新增 WS 端点都需要复制一份硬编码块——这是会随端点数量线性增长的配置债。

GATEWAY
已在运行im-core-im-ws-gateway-1 已在运行,监听 127.0.0.1:18081
map $http_upgrade
0 个 conf取数只扫了 conf.d/,该口径已被 433 E-2 判为有缺陷
硬编码 Upgrade 头
3 个 confim-core-api.conf · log-reits-tech.conf · reits-bbs.conf
proxy_read_timeout
120s×13 · 30s×4 · 300s×3 · 1h×1对长连接而言 30s 与 120s 都不足以维持空闲 WS 连接。21 处配置分成四档且无统一口径,意味着同一个 WS 客户端连到不同子域会有不同的断连行为。
内核网络参数。唯一与 nginx 配置明显失配的是 tcp_max_syn_backlog=512 对 somaxconn=4096。
参数实测值判读
net.core.somaxconn4096与 nginx 总连接上限同量级,当前不构成瓶颈
net.ipv4.tcp_max_syn_backlog512明显小于 somaxconn。突发建连场景下先于 somaxconn 触顶
net.ipv4.ip_local_port_range32768-60999约 2.8 万端口,这是「零 keepalive」的耗尽面
net.ipv4.tcp_tw_reuse2仅对回环之外启用 TIME_WAIT 复用,缓解但不消除端口消耗
net.core.netdev_max_backlog1000网卡收包队列,当前 2.8 QPS 下无压力

C · LATENCY & CACHE

延迟分解:86%–90% 的感知时延在网络与握手层

外部跨太平洋客户端 curl -w 五段耗时。RTT 110–160ms(外部跨太平洋客户端,复测必须记录发起位置),RTT 是所有数字的固有底噪,必须与数字一同记录。

端到端延迟分解。order 的 time_connect 0.497s 约为 RTT 量级 4 倍,是唯一需要单独排查的延迟异常。
站点DNS连接(TCP)TLS首字节总后端处理
www.reits.tech0.003s0.128s0.889s1.928s3.324s—
developer(门户)————1.401s0.19s
bbs————1.347s0.14s
order—0.497s——1.545s—
判读 1

DNS 不是问题:0.003s。

判读 2

TLS 是最大的单项成本:连接 0.128s → TLS 完成 0.889s,握手净耗约 0.76s,约等于 5 个 RTT。ssl_session_cache 对复用连接的客户端生效,对首次访问无效。

判读 3

后端处理不是瓶颈:developer 总 1.401s 而后端仅 0.19s;bbs 总 1.347s 而后端仅 0.14s。约 86%–90% 的用户感知时延发生在网络与握手层,任何以「优化后端代码」为手段的提速收益上限只有 10%–14%。

判读 4

order 的 TCP 连接 0.497s 是异常项,约为 RTT 量级的 4 倍,指向连接建立路径上的额外开销,是唯一需要单独排查的延迟异常。

判读 5

www.reits.tech 首字节 1.928s 中 TLS 完成于 0.889s,故服务端首字节生成 + 传输约 1.04s;结合首页 no-store,这 1.04s 是每一次访问都要重新付出的。

缓存策略实测。静态资源正确,公开首页反而不可缓存——方向恰好反了。首页体积:gzip 73958 B(约 72 KB) / 原始 377224 B(约 368 KB)(压缩比 约 5.1 : 1)。
站点 / 资源实测响应头判读
www.reits.tech / www.probatlas.com 首页private, no-cache, no-store, max-age=0, must-revalidate公开资讯首页被声明为完全不可缓存。private + no-store 同时出现,等于禁止 CDN、禁止浏览器、禁止中间缓存
bbs无 cache-control 头交给下游启发式判断,行为不确定
developer(本站)无 cache-control 头同上,属策略缺失而非策略错误
静态资源public, max-age=31536000, immutable正确。一年 + immutable,配合内容哈希文件名是标准做法
HTTP/2
已开启13 处
gzip
已开启6 处
ssl_session_cache
已开启10 处
sendfile
已开启—
tcp_nopush
已开启—
brotli
未开启0(需换包或动态模块)
open_file_cache
未开启0
proxy_cache
未开启0

判读写死:缓存策略的方向恰好反了。静态资源(可缓存性最强)配置正确;而公开资讯首页(最该被 CDN 与浏览器缓存)被打上 private, no-store。结合服务端首字节约 1.04s,这意味着每一次首页访问都完整重跑一遍服务端渲染并重传 72 KB,且 CDN 无法分担任何一部分。这是当前形态下投入产出比最高的可优化项。

C · KNOWN DEBT

已知技术债 9 项

每项含实测证据、影响与处置口径。第 9 章另有 14 项复测清单,逐项列出命令与本次基线值,下次容量评估可直接照此执行。

DEBT-01…DEBT-09。DEBT-04 明确判定为不属于本机 nginx 范围,000 是预期结果而非故障。
ID技术债实测证据影响处置口径优先级
DEBT-01零 upstream / 零 keepalive,却有 24 处 proxy_http_version 1.1433 第 2 章每个反代请求新建 TCP 连接,消耗临时端口(约 2.8 万)与 TIME_WAIT 槽位;随流量线性放大引入 upstream 块 + keepalive N最高
DEBT-02公开资讯首页 private, no-store 完全不可缓存433 第 6.2 章每次访问重跑服务端渲染 + 重传 72 KB,CDN 无法分担;同时放大 DEBT-01首页应改为可公开缓存高
DEBT-03未声明 default_servernginx -T 无 default_server未匹配 Host 的请求落到首个 80 监听块(developer),行为是隐式的显式声明一个 default_server(返回明确的 404/444)中
DEBT-04media 子域本机无 server 块media.reits.tech 与 media.probatlas.com 均返回 000(TLS 握手阶段即失败)媒体资产走 CloudFront + us-east-1 ACM,不在本机 nginx 迁移范围明确判定为不属于本机 nginx 范围,000 是预期结果范围外
DEBT-05worker_connections 1024 是人为低天花板worker_rlimit_nofile 未声明,进程 fd limit 实为 65535当前利用率 0.8%,不构成瓶颈;但抬高时必须与 worker_rlimit_nofile、tcp_max_syn_backlog 一起调低优先(无实际压力),调整时按联动口径执行低
DEBT-06tcp_max_syn_backlog=512 与 somaxconn=4096 失配433 第 4 章突发建连场景下半连接队列先于 somaxconn 触顶与 DEBT-05 同批处理低
DEBT-07零个 conf 使用 map $http_upgrade;proxy_read_timeout 四档并存(120s×13 / 30s×4 / 300s×3 / 1h×1)433 第 3 章新增 WS 端点需复制硬编码块;同一 WS 客户端连不同子域断连行为不一致统一为 map $http_upgrade 标准写法,并统一 WS location 的 proxy_read_timeout 口径中
DEBT-08bbs 与 developer 无 cache-control 头433 第 6.2 章下游启发式缓存,行为不确定显式声明,与 DEBT-02 同批中
DEBT-09brotli / open_file_cache / proxy_cache 三项均未开启433 第 6.1 章压缩率与静态文件元数据缓存均有余量未用;proxy_cache 缺失使 DEBT-02 无法在边缘层兜底brotli 需换包或动态模块,成本高于另两项,评估时分开计价中
HARD CONSTRAINT · 长期规划必须写进去

reits.tech 必须永久续费

gitea 与全生态 Go module path 锚定该域。域名过期等同于全生态依赖解析失效。它不是「旧域、迟早可以放弃」,而是物理锚点。

11 张 certbot lineage 覆盖 12 个新域名,统一到期 2026-10-28,验证方式已由 manual 改为 webroot(可自动续期)

SOURCE OF TRUTH · reits-knowledge433 号《底座容量基线与瓶颈实测(Platform Capacity Baseline & Bottleneck Measurements)》

reits-knowledge/docs/ecosystem/433-platform-capacity-baseline-2026-07-30.md

sha256 625692aee446b18e695f79fd2b040c19… · 285 行 · 同步时间 2026-08-03 08:53 UTC

  • 第 1–2 章:并发配置、利用率与连接复用缺陷
  • 第 3–4 章:WebSocket 就绪度与内核网络参数
  • 第 5–6 章:端到端延迟分解与缓存压缩策略
  • 第 7 章:域名与证书现状(含 gitea 永久锚定)
  • 第 8–9 章:DEBT-01…09 与 14 项复测清单