源鲸笔记

← 返回

2 核 1.7G 跑四个容器:内存约束下的部署取舍

2026-11-10 · 部署

手上这台云主机是很久以前买的入门规格:2 核、1.7G 内存、40G 磁盘。跑了快二十天没重启,最近又往上加了一个服务,正好把内存账重新算了一遍。

结论先写在前面:能跑什么,取决于你愿意放弃什么。这台机器上能同时跑聊天服务、机器人、反向代理和推送服务,不是因为优化做得好,而是因为该砍的都砍了。

四个容器的内存账

先看现状。宿主机 1735MB 物理内存,四个容器的上限是这样切的:

容器mem_limit作用
机器人1024m主业务,最吃内存
聊天服务256mWebSocket + 消息存储
反向代理96m统一入口、自动证书
推送服务64m自建 ntfy

加起来 1440m,物理内存 1735m,看起来还剩 295m。但实际 free -m 显示可用只有 585m——因为内核、docker 守护进程、日志缓冲这些本身要占两三百兆。

Mem:  1735 total, 1150 used, 91 free, 676 buff/cache, 585 available
Swap: 4095 total,  532 used, 3563 free

负载 0.38,说明 CPU 一点都不紧张。瓶颈从头到尾都是内存。

mem_limit 是保命,不是限速

很多人把 mem_limit 理解成"这个容器最多用这么多",其实更准确的说法是"超过这么多,内核会先杀你"。

不设上限的话,任何一个容器内存泄漏都会把宿主机拖进 OOM,内核挑一个分值最高的进程杀掉——通常是占内存最多的那个,也就是你的主业务。设了上限之后,被杀的是越界的那一个,其他服务不受影响。

所以 mem_limit 的价值不在"省",而在"划定牺牲顺序"。给最不重要的服务设最紧的限额,出问题时先死它。

swap 用了一半,是个警告

4G swap 用了 532m。这个数字本身不危险,但含义要看清:有一部分常驻内存已经被换出到磁盘上了。被换出去的东西一旦要访问,就得从磁盘读回来,延迟从纳秒级变成毫秒级。

对聊天服务这种要维持长连接的东西,这通常表现为"偶尔卡一下"。所以 vm.swappiness 值得调低,让内核尽量别把热数据换出去,宁可多杀几个冷进程。

三条硬取舍

在这台机器上,我给自己定了三条规矩,都是"不做什么":

第三条大概是最划算的。一个每十分钟跑一次的生成脚本,用 cron 触发时内存占用峰值可能 80m、持续两秒;做成常驻服务就是 80m 一直挂着,一天下来多占 6900 秒的峰值内存。

几个实际踩到的坑

镜像体积比运行时更值得关注

新加的那个推送服务,镜像压缩后 36.8MB、解开 124MB,运行时实际占用二十几兆。选型时我特意看了这一点——同样功能的东西,如果是个 Electron 或者带完整 JVM 的,光基础镜像就能吃掉这块板子。

重建容器会短暂中断

改了挂载或者网络配置之后,docker compose up -d 会重建容器,期间服务是不可用的。这台机器上大概两三秒,用户感知不到。但如果你有长连接客户端,那一下会断线重连。

我的做法是:改配置之前先确认"这两秒中断能不能接受",不能接受就挑凌晨做。

日志会把磁盘吃光

docker logs 默认没有大小限制。一个高频输出的服务跑一周就能写出几个 G。要么在 compose 里配 logging.options.max-size,要么定期清理。我一开始没管,磁盘从 20% 涨到 33% 才发现。

反直觉的一点

折腾了这么久,最大的感受是:小内存机器上,架构选择比代码优化重要得多。

你花一天时间把一个服务的内存从 200m 优化到 150m,收益是 50m;但只要你换掉一个"必须有常驻进程"的技术选型,收益直接是 200m 起步。前者是手艺,后者是判断。

1.7G 这个数字,逼着你每次都先问一句:这东西非跑不可吗?能换成静态的吗?能跑完就退出吗?问完这三句,大部分方案自己就消失了。