一台 4 核 8G 已经跑了 6 个站点,我是怎么把第 7 个塞进去的

接到迁移任务的时候,目标服务器是 4 核 8G、200G 磁盘,上面装了 1Panel 面板。

听起来是一台很普通的机器。真正让它难办的,是后半句。

这台机器上已经压着别人的线上流量。

当时在面板里数出来的站点数,比文档里写的多得多。是 6 个,不是 1 个。一个接口站,两个 PC 站,两个移动端站,一个品牌站。

Web 层是一个 OpenResty 容器,用 host 网络模式直接占着 80 和 443。数据层是一个 MySQL 容器。应用层是一个 PHP-FPM 容器。

现在要在同一台机器上再叠 5 个容器。PostgreSQL、Redis、Go 后端、Python 管理后台,还有一个 H5 静态站。

这时候最常听到的一句话是「4 核 8G,应该够吧」。

这三个字在这件事上是废话。真正的风险在另外一个问题上。

内存不够的时候,被关闭的进程有可能不是你建立的进程

Linux 在系统内存耗尽时,内核会启动 OOM Killer,全称 Out-Of-Memory Killer。它的做法是挑一个进程杀掉,回收内存。它的挑选逻辑不是谁占得多杀谁这么简单,而是按一套打分规则来选目标,也就是 oom_score。打分时会考虑进程的运行时间、优先级,以及它占用内存的那个重要性。

这意味着,你的新服务把内存吃爆,被杀掉的完全可能是别人跑了半年的数据库。

在这台机器上,这个后果是具体的。如果被杀的是别人的 MySQL 容器,6 个站点同时不可用,里面还含着别人的生产合同项目。如果被杀的是我自己的某个容器,我的服务挂了,别人无感。

所以这次部署的第一原则,是把故障的影响面锁死在我自己的容器里。

阶段 1:把应该够换成 12 个数

我给这次部署设了一道放行门槛。数据不齐,后面的阶段一律不开工,整段命令可以一次粘进面板终端。

bash 复制代码
echo "================ 部署前实测 $(date '+%F %T') ================"

echo ""; echo "---- ① 系统与规格 ----"
uname -srm
echo "CPU 核数: $(nproc)"
free -h | head -2

echo ""; echo "---- ② 磁盘 ----"
df -h /

echo ""; echo "---- ③ 全部容器(含已停止)----"
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"

echo ""; echo "---- ④ 容器实时内存 / CPU ----"
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}"

echo ""; echo "---- ⑤ 宿主机内存 TOP10 ----"
ps aux --sort=-%mem | head -11 | awk 'NR==1{print "USER MEM% CPU% COMMAND"} NR>1{printf "%-10s %5s %5s %s\n",$1,$4,$3,$11}'

echo ""; echo "---- ⑥ 关键端口占用 ----"
ss -ltnp 2>/dev/null | grep -E ':80 |:443 |:18080|:18081|:18090|:18091|:15432|:16379|:5432 |:6379 ' || echo "(关键端口全部空闲)"

echo ""; echo "---- ⑦ 面板现有站点目录 ----"
ls -la /opt/1panel/www/sites/ 2>/dev/null

echo ""; echo "---- ⑧ Docker 网络 / 磁盘占用 ----"
docker network ls
docker system df

echo ""; echo "---- ⑨ Docker 镜像加速配置 ----"
cat /etc/docker/daemon.json 2>/dev/null || echo "(无 daemon.json)"

echo ""; echo "---- ⑩ 出网能力 ----"
printf "goproxy.cn   : "; curl -sI --max-time 8 https://goproxy.cn 2>/dev/null | head -1 || echo "不通"
printf "pypi 清华源  : "; curl -sI --max-time 8 https://pypi.tuna.tsinghua.edu.cn/simple/ 2>/dev/null | head -1 || echo "不通"
printf "github.com   : "; curl -sI --max-time 8 https://github.com 2>/dev/null | head -1 || echo "不通"
printf "docker hub   : "; curl -sI --max-time 8 https://registry-1.docker.io/v2/ 2>/dev/null | head -1 || echo "不通"

echo ""; echo "---- ⑪ Swap ----"
swapon --show 2>/dev/null || echo "(无 swap)"

echo ""; echo "---- ⑫ 既有业务健康(确认未受影响)----"
printf "既有站点本地回显: "; curl -sI --max-time 8 -H 'Host: <别人的接口域名>' http://127.0.0.1/ 2>/dev/null | head -1 || echo "取不到"

那 12 项里,最要紧的是第 12 条。它在开工前先给别人的业务拍一张体检照,后面每一个阶段结束都重跑一次做对照。

顺带记一个免 DNS 验证的小技巧。curl -H 'Host: 域名' http://127.0.0.1/ 打到本机,并伪造一个 Host 头。这等价于域名已经解析到本机。DNS 还没切换的时候,就能把整条路验完。

实测回来,我的预估被打脸了

数据回来的那一刻,最意外的是这一组。

arduino 复制代码
总内存      7.5Gi
used        1.5Gi
buff/cache  5.9Gi
available   6.0Gi
Swap        1.9G

可用内存 6.0Gi,远超我设的 3.5G 放行线。

再看现网三个容器真实吃了多少。MySQL 容器 635.8 MiB。PHP-FPM 容器 42.44 MiB。面板那个 OpenResty 容器 35.03 MiB。三个加起来大约 713 MiB,只占 8G 的 9%。

我以为 MySQL 会吃 1.5 到 2G,实测只有 636M。

这说明我原来的内存划法过于保守了。

在同机跑着 6 个别人站点的机器上,保守是优点,不是缺点。

预估错在保守方向,代价只是浪费一点资源。预估错在激进方向,代价是别人的生产事故。这两种错误的代价完全不对等。

划线,给每个容器钉一个上限

实测之后做的第一件事,是补一个原本缺失的东西,内存上限。

这是我在这次部署前发现的第二个隐患。项目自带的 docker-compose.yml 没有给任何服务设内存限制。在一台专属机器上这或许无所谓,但在同机共存的环境里,这是一颗定时炸弹。

补法是在每个 service 下加 deploy.resources.limits.memory。

app-postgres 给 768M。依据是 shared_buffers=256MB 加连接开销,富余留足。app-redis 给 192M,纯缓存用途,maxmemory 另行设 128MB。app-gin 给 384M,Go 运行时加连接池,实际常驻 50 到 80M。app-admin 给 512M。它跑的是 gunicorn -w 4 -k gevent,4 个 worker 各自一份堆。app-h5 给 64M,纯静态 nginx。合计上限大约 1.9G,实际占用通常在 0.6 到 1.0G 之间。

配套给 PostgreSQL 加启动参数。

yaml 复制代码
    command:
      - postgres
      - -c
      - shared_buffers=256MB
      - -c
      - max_connections=100
      - -c
      - effective_cache_size=512MB

这个补丁的意义,远大于它省下的那点内存。它把 OOM 的影响面从整机压缩成这套服务自己。极端情况下被杀的,只会是这 5 个容器里的某一个。

顺带一个意外收获。把端口改成只绑 127.0.0.1,也就是写成 "127.0.0.1:18080:8080"。这样公网天然不可达,比绑 0.0.0.0 再靠防火墙收口要彻底。防火墙是第二道保险,不是唯一一道。

改完用一条命令验证是否真的生效。不启动,只解析。

bash 复制代码
docker compose -f docker-compose.server.yml config
# 期望看到:memory: 402653184   ← 384M 的字节数

期望看到 memory: 402653184,那是 384M 的字节数。

总内存要求接近 8G,实测 7.5Gi。可用内存要求大于等于 3.5G,实测 6.0Gi。5 个关键端口要求全部空闲,实测均无监听。可达 Docker 镜像源这一项,已经配了内网加速。可达 github.com 这一项当时是空响应,这一格先算未定。根分区可用要求大于等于 50G,实测 177G。既有业务基线要求 200 或 301,实测 301。现网容器清单要求已记录,实测 3 个。

相关推荐
对象存储与RustFS2 小时前
JuiceFS + 对象存储:把 S3 变成 POSIX 文件系统实测
后端·rust·开源
她的男孩2 小时前
企业接口照样拦得住:独立 Flyway、@RequiresFeature 与离线许可证
java·spring boot·后端
十年Java程序媛2 小时前
Lambda 与函数式接口|别只会复制 ()->{},底层规则和坑一次性讲清
java·spring boot·后端
yunwei372 小时前
eBPF 入门开发实践教程一:Hello World,基本框架和开发流程
linux·后端·性能优化
全栈Agent 小李2 小时前
【无标题】
前端·后端·agent·ai编程·全栈·cursor·mcp
高频因子挖掘机2 小时前
历史 K 线突然少一天?用交易日历、停牌信息和数据校验逐步排查
后端·github·api
行者全栈架构师3 小时前
从 55% 到 6%:一个快餐营养规划器的算法迭代实录
后端·算法·架构
余槐i3 小时前
无慢查询 RT 却飙升:cProfile 定位 FastAPI 事件循环阻塞
后端·python·性能优化·fastapi·asyncio
AINative软件工程3 小时前
LLM 应用的 Adaptive Batching 工程实践:动态合批把吞吐提升 3 倍,但延迟的坑你踩过吗
后端·llm·ai编程