手上这台 2 核 4G 的轻量主机,一开始只跑一个 API 服务,后来陆续加了向量检索、对象存储同步、还有一个每十分钟跑一次的抓取任务。等到第五个服务的时候,我开始怀疑这台机器是不是太小了——后来发现问题不在机器,在管理方式。
端口:先画一张表,再动手
我最不愿意承认但确实踩过的事故:改了 nginx 的 upstream 端口,忘了同步改容器映射,docker compose up -d 之后 nginx 502,而我盯着应用日志看了半小时,因为应用日志一切正常——它监听的是旧端口,是我把 upstream 改成了新端口。
现在的做法是先写一张端口表,任何服务上线前先把三列填满:
| 服务 | 容器内端口 | 宿主机端口 | 对外暴露 |
|---|---|---|---|
| api | 8000 | 8000 | 否(走 nginx) |
| vector | 6333 | 6334 | 否 |
| sync | — | — | 否(定时任务) |
规律有三条:容器内端口随镜像定,别改;宿主机端口一律不跟容器内一样,留出调整余地;只有 nginx 对外暴露,其余服务连安全组都不开。
安全组这一层我一开始图省事开了个大的入站范围,后来扫描器一晚上就能扫出来。正确的做法是:安全组只放行 22、80、443,而且 22 的来源 IP 尽量收窄——如果你的云厂商支持安全组引用或组间互通规则,用它而不是把端口映射到宿主机的随机高位端口。端口不暴露在安全组层面,比在容器里做再复杂的认证都干净。
进程:别让它们"以为自己活着"
最难受的一类故障是服务假死:进程还在,端口还通,但请求全部超时。健康检查如果只检查"端口有没有人 listen",这种情况完全检测不出来。
我最后统一改成检查一个真实的轻量接口——比如 /healthz 内部做一次数据库 ping 并返回耗时:
@app.get("/healthz")
def healthz():
t0 = time.monotonic()
db.execute(text("SELECT 1"))
return {"ok": True, "ms": round((time.monotonic() - t0) * 1000, 1)}
注意这个接口必须真的依赖下游。如果它只返回 {"ok": true},那它检测不出数据库挂了,只是个摆设。
对 AI 服务还要多加一层:模型加载中、后端还在下载权重这两种状态要区分开。直接返回 503 会让负载均衡把它踢出集群,于是永远起不来——这种"因为不健康被反复重启"的循环,比直接崩溃难查得多。健康检查接口应该返回三种状态:健康、未就绪(503 但带 reason)、不健康(进程该退出)。
自动重启:重启策略不等于解决方案
Docker 的 restart policy 我一开始设成 always,以为万事大吉。结果某个服务因为代码 bug 陷入死循环崩溃,Docker 忠实地把它拉起来、再崩、再拉起来,日志刷屏,CPU 被吃满,另外几个服务跟着变慢。
现在的配置是区分对待:
- 无状态 API:
unless-stopped,崩了自动起 - 有状态服务(数据库、向量库):
unless-stopped+ 启动前检查数据目录锁 - 定时任务:不设自动重启。任务失败应该留在失败状态里等人看,而不是无限重试
第三点是我被坑最狠的。抓取任务因为上游改版连续失败,restart policy 让我在半夜收获了四千条一样的错误日志,还把上游接口打挂了——最后是我自己去关的。
资源限制:给每个服务一个"份子"
内存共享时最典型的故障是 OOM killer 挑倒霉鬼。内核不会杀内存占用最高的那个,它按"坏分数"选,可能是你以为最不该死的那个服务。症状是某个服务毫无征兆地消失,日志最后一行是某句无辜的输出。
现在每个服务都写死内存上限,超了就自己重启而不是拖别人下水:
services:
api:
image: my-api:latest
restart: unless-stopped
mem_limit: 1024m
cpus: "1.0"
environment:
- TZ=Asia/Shanghai
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
logging 那段也别跳过。默认的 json-file 日志不轮转,一个爱打日志的服务能把磁盘写满——我在另一台机器上见过日志文件比数据还大。
一个反直觉的结论
机器小的时候,减少服务数量比优化单个服务更有效。我那台 2 核 4G 最后稳定跑三个服务的做法是:把同步任务从常驻服务改成云函数/定时触发;把向量库换成单文件的嵌入式方案(数据量不大时,它比独立服务省得多);把日志全部输出到 stdout 交给日志采集。
机器没换,成本没涨,稳定性反而变好了。省下来的不是配置,是排障时间——而排障时间在项目里从来不算进成本账单,却是最贵的那部分。