接到迁移任务的时候,目标服务器是 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 个。