做在线代码执行、算法OJ、低代码平台的脚本引擎,或者给大模型接一个“代码工具”,都会撞到同一个需求:让一份完全不可信的代码在服务器上安全地跑完,再把输出拿回来。Docker、代码沙箱、容器池,这三个词几乎是我这两年绕不开的主线。最近我把整套方案在生产环境重新过了一遍,从镜像设计到资源限制,从容器池调度到逃逸防护,踩了不少坑,也把很多“文档里不会写”的经验沉淀下来了。这篇不聊Docker入门,就把代码沙箱该用什么配置、容器池怎么设计与调度、安全加固要做到什么程度,以及常见的现场问题一次性写完。适合的人群很明确:准备搭在线评测系统、编程实训平台、远程执行API,或者想给自动化工具加一个安全代码执行后端的同学。
1. 沙箱方案选型与容器池的整体定位
1.1 为什么生产环境最终选了Docker
先把选型逻辑交代清楚。让不可信代码在服务器上跑,可选的路子无非几类:进程级沙箱、容器、虚拟机。
进程级沙箱(比如只靠 seccomp、ptrace 或者一些用户态拦截库)性能确实好,但隔离维度太弱。恶意代码不需要拿到root,只要利用一个内核漏洞或者可写内存区域,就能看到同一台宿主机上其它进程的数据,再往后就是横向移动。这类方案适合做“限制普通业务代码”,不适合做“对抗恶意代码”。
虚拟机最安全,内核隔离、设备隔离都很彻底,但资源开销和启动速度都扛不住在线执行场景。一个VM从启动到能跑代码,几分钟就过去了,而用户提交的代码通常希望在两三秒内返回结果,这个矛盾几乎无解。
Docker容器处在两者中间。它通过 Linux 命名空间隔离进程、文件系统、网络等视图,通过 cgroup 做资源配额,再通过 Capabilities、Seccomp 收窄权限,能做到“看起来像一台独立机器,但共用宿主机内核”。代价是共享内核带来额外的攻击面,所以容器只能当作沙箱的其中一环,后面必须有完整的安全加固。但要让代码执行服务真正常态化运行,容器几乎是性能和安全性之间最合理的折中方案。
1.2 容器池解决的是什么问题
如果只是单次执行,一条 docker run 就搞定了,但放到高并发的在线执行场景,每次现创建容器会在性能和稳定性上双重失控。
一个未预热的新容器,从拉镜像(如果本地没有)到创建可写层,再到启动 init 进程,最后再 exec 用户的代码,实测冷启动路径普遍在 300ms 到 800ms 以上。这中间还不包括网络初始化、DNS 配置、卷挂载等环节。在用户请求密集时,冷启动会直接拉高接口 P99,还会让宿主机 CPU 和 IO 出现毛刺。
容器池的思路很直白:提前把容器创建好、启动好,放进一个空闲队列,来任务时直接拿走用,用完归还。和数据库连接池、HTTP 连接池一个逻辑。但容器池比连接池复杂的地方在于,容器的“状态会变脏”,跑过一次不可信代码之后,这个容器是否还能复用,需要策略判断。
池化的价值从实测数据上看非常明显:预创建的容器执行一次任务,核心耗时只有 docker exec 的时间,通常低于 20ms,相比冷启动有几十倍的差距。这就是容器池在整套架构里不可替代的原因。
1.3 沙箱和池子要分两层看
做这一套系统,最怕把“沙箱”和“池子”混成一个问题。其实它们是两件事。
沙箱解决的是“代码能不能跑得安全”,关注点在于隔离、权限、资源上限、系统调用策略。容器池解决的是“任务能不能跑得快、跑得稳”,关注点在于预创建、调度、复用、回收。
设计时最好把这两层在代码里也拆开。池子只负责容器生命周期管理,不关心容器内部执行什么;执行引擎只负责塞代码、收输出,不关心容器是哪里来的。这样后续更换运行时、升级镜像、增加多语言池子,都不会牵一发动全身。
我见过不少失败的改造,都是把容器池做成一个大而全的类,既要管理容器,又要解析代码输出,还要处理超时重试,最后任何一个小改动都要动核心逻辑。保持单向依赖,池子在上,执行在下,会省掉很多维护成本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 镜像与资源约束的关键细节
2.1 镜像到底该怎么做
代码沙箱的镜像看起来简单,实际坑不少。核心原则是:镜像只装运行代码所必需的东西,能少装一样就少装一样。这不仅是为了体积,更是为了减少攻击面。
基础镜像我优先推荐 Debian slim 系列,比如 debian:stable-slim。虽然 Alpine 体积更小,但 Alpine 的 musl libc 和主流发行版的 glibc 行为有差异,很多编译型语言和二进制包在 Alpine 上会出现莫名其妙的问题,排查成本很高。如果业务语言比较纯粹,比如只跑 Python 和 Node,Alpine 可以考虑;一旦涉及编译、原生依赖,老老实实用 slim 更稳妥。
镜像内部最好做多阶段构建。编译型语言(Go、Rust、C/C++)在同一镜像里既放编译器又放运行库,会让体积翻好几倍。正确做法是编译阶段用完整工具链镜像,运行阶段只拷贝编译产物和动态库。
如果是解释型语言,也要控制安装范围。以 Python 为例,pip install 不要一把梭装完所有依赖,应该先列清单,只装业务需要的库,并且要固定版本。我建议每个语言池子单独一个镜像,而不是做一个“全家桶镜像”。全家桶镜像虽然管理简单,但镜像体积动辄几个G,拉取慢、构建慢,容器启动时的 overlay 层也明显更重。
镜像构建完成后,还有一步很多人会漏掉:把镜像内的 /tmp、/home 等目录清理干净,并且不要默认创建什么启动脚本之外的可写目录。镜像越“干净”,容器被污染后回收重建的成本就越低。
2.2 资源限制参数不是随便填的
Docker 的资源限制参数文档都有,但实际该给多少,需要按业务场景慢慢压测。
CPU 限制我用的是 NanoCpus,它直接表示可用的 CPU 纳秒数。一个容器限制 1 核就是 1000000000。如果宿主机的任务主要是短小代码,可以给 0.5 核,但要注意有些语言运行时初始化(比如 JVM)需要瞬时的多核能力,限制太死会导致启动变慢。更细的做法是配合 CpuQuota 和 CpuPeriod 做微调,但生产环境用 NanoCpus 已经够直观。
内存限制上,Memory 和 MemorySwap 要成对设置。常见误解是只设置 Memory,然后以为容器只能用到这么多内存。实际上如果不限制 MemorySwap,容器在宿主机压力大的时候可能使用 swap,延迟飙升且很难排查。我的做法是把 MemorySwap 设置成和 Memory 一样,等于彻底禁用容器内部的 swap。另外,如果给容器挂了 tmpfs,tmpfs 的大小要从内存里预留出来,计算并发容量时要算进去。
PidsLimit 是我强烈建议设置的参数,它直接限制了容器内能创建的进程数,能有效挡掉 fork 炸弹。128 个进程对于绝大多数测试代码都够用,但 Java 或者 Node 这类多线程程序需要给更多余量,可以按语言分开设。
Ulimits 也有用。fsize 限制单文件最大大小,可以防代码向磁盘写超大文件;nofile 限制打开文件数,防文件描述符耗尽。
下面是我在某个生产环境使用的核心配置模板,可以直接作为参考:
bash复制docker create \
--name sandbox-python \
--cpus 1 \
--memory 512m \
--memory-swap 512m \
--pids-limit 128 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=128m \
--cap-drop ALL \
--security-opt no-new-privileges \
--network none \
--ulimit fsize=1048576 \
--ulimit nofile=1024 \
python-sandbox:latest
2.3 并发容量怎么算
并发容量不是拍脑袋定的,核心依据是宿主机的总资源和单个容器资源上限。
假设宿主机是 8 核 32G,单容器限 1 核、512M 内存,纯粹按资源线性算可以跑 8 个 CPU 密集任务、64 个内存级任务,但实际不能这么乐观。原因有几个:宿主机本身要留资源给 Docker 守护进程、监控、日志采集;tmpfs 会占用实际内存;容器调度和执行过程中会有瞬时资源峰值;多个容器同时编译时 IO 开销也不小。
我一般按宿主机总资源的 70% 到 80% 来计算安全并发数。8 核 32G 的机器,限制单容器 1 核、512M,并发上限控制在 6 到 8 个比较稳。预留 20% 以上给系统余量,能有效避免偶发任务导致宿主机 CPU 打满。
池子的容量策略也跟着这个数字走。空闲容器数不要超过最大并发的三分之一,比如最大并发 8,空闲保持 2 到 3 个就够了。空闲太多,内存白白占用;空闲太少,突发流量时容易冷启动,把延迟拉高。
3. 执行链路与容器池的调度实现
3.1 容器生命周期与状态机
容器池里的容器不该只有“在用”和“空闲”两个状态,至少要有下面几个:
- CREATED:镜像创建完毕,尚未启动
- READY:已经启动并完成预加载,等待任务
- BUSY:正在执行任务
- RECYCLING:执行完且需要回收重建
- DEAD:异常退出,等待清理
管理进程把容器从 CREATED 推送到 READY 的过程要提前完成。镜像更新或者容器回收后,后台线程立即补一个新容器进入空闲队列,而不是等到请求来了才去创建。这个“预创建”的思路是容器池性能优势的关键。
容器创建和容器启动要分开做。很多 SDK 封装了 run 方法,看起来一步到位,但 run = create + start,中间完全可以拆开。批量预热时,先连续 create 一批容器,再逐个 start,可以节省阻塞等待的时间。
3.2 容器池的实现骨架
我给一个比较精简的 Node.js + dockerode 实现骨架,核心突出 acquire、release、recycle 三个操作。
javascript复制const Docker = require('dockerode');
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
class SandboxPool {
constructor({ image, minIdle = 2, maxSize = 8, limits = {} }) {
this.image = image;
this.minIdle = minIdle;
this.maxSize = maxSize;
this.limits = limits;
this.idle = new Set();
this.busy = new Set();
this.recyclable = new Set();
}
async createContainer() {
const container = await docker.createContainer({
Image: this.image,
Cmd: ['/bin/sleep', 'infinity'],
User: '1000:1000',
WorkingDir: '/tmp',
Env: ['HOME=/tmp'],
HostConfig: {
NanoCpus: (this.limits.cpu || 1) * 1e9,
Memory: this.limits.memory || 512 * 1024 * 1024,
MemorySwap: this.limits.memory || 512 * 1024 * 1024,
PidsLimit: this.limits.pids || 128,
ReadonlyRootfs: true,
Tmpfs: { '/tmp': 'rw,noexec,nosuid,size=128m' },
CapDrop: ['ALL'],
SecurityOpt: ['no-new-privileges'],
NetworkMode: 'none',
Ulimits: [{ Name: 'fsize', Soft: 1048576, Hard: 1048576 }]
}
});
await container.start();
return container;
}
async acquire() {
if (this.idle.size === 0) {
if (this.busy.size + this.idle.size >= this.maxSize) {
throw new Error('pool exhausted');
}
await this.createContainer();
}
const container = this.idle.values().next().value;
this.idle.delete(container);
this.busy.add(container);
return container;
}
release(container, dirty = false) {
this.busy.delete(container);
if (dirty) {
this.recyclable.add(container);
} else {
this.idle.add(container);
}
}
async recycleLoop() {
for (const container of this.recyclable) {
try {
await container.remove({ force: true });
await this.createContainer();
} catch (err) {
console.error('recycle failed', err);
}
this.recyclable.delete(container);
}
if (this.idle.size < this.minIdle) {
const need = this.minIdle - this.idle.size;
for (let i = 0; i < need; i++) {
await this.createContainer();
}
}
}
}
关键点有几个。Cmd 设置为 sleep infinity,让容器以长驻进程方式存活,而不是执行完就退出。User 必须指定非 root 用户,配合镜像里创建好的用户。ReadonlyRootfs 把根文件系统变成只读,所有需要写的路径都必须显式挂 tmpfs。
池子的 release 方法接收一个 dirty 标记。任务执行过程中只要出现了异常情况,比如超时、输出超标、疑似资源异常,执行引擎都要把 dirty 置为 true,这个容器直接进回收队列,不再复用。
3.3 执行任务与超时控制
容器池里的任务执行,最终都落在 docker exec 上。exec 和 run 不同,它不创建新容器,而是在一个正在运行的容器里拉起新进程,因此非常快。
以 Python 为例,执行引擎伪代码是这样:
javascript复制async function executeInContainer(container, code, timeoutMs) {
const exec = await container.exec({
Cmd: ['sh', '-c', `timeout ${Math.floor(timeoutMs / 1000)}s python3 -c ${JSON.stringify(code)}`],
AttachStdout: true,
AttachStderr: true
});
const stream = await exec.start({ hijack: true });
let output = '';
let timer = setTimeout(async () => {
await container.stop({ t: 1 });
throw new Error('execution timeout');
}, timeoutMs);
stream.on('data', (chunk) => {
output += chunk.toString();
});
await new Promise((resolve) => stream.on('end', resolve));
clearTimeout(timer);
return output;
}
超时控制必须做双重保险。容器内部用 timeout 命令限制单条命令的执行时长,这是第一层;外层 exec 启动后挂一个定时器,超时后直接停掉容器,这是第二层。靠容器内部 timeout 有时不够,因为代码可能创建了不受 timeout 管制的子进程;靠外层定时器也要小心,因为如果容器内部自己不退出,停容器又可能需要时间。
docker stop 默认会等待 10 秒才发 SIGKILL,这个等待在沙箱场景里太长了。建议显式传 t: 1,意思是 1 秒后强制杀掉。如果发现容器进程树复杂,可以再调低。
执行完的容器是否复用,需要判断。我的经验是:只要代码正常退出、没超时、输出长度在限制内,容器可以复用。但复用次数要有上限,比如同一个容器最多跑 20 次任务,到次数后强制回收重建。这样可以规避长时间复用带来的环境污染和性能劣化。
3.4 对外API的设计建议
对外接口不需要设计得太复杂。一个典型的同步执行接口就够:
code复制POST /v1/exec
body: { "lang": "python", "code": "print(1)", "timeout": 5000 }
resp: { "stdout": "1\n", "stderr": "", "exitCode": 0, "duration": 120 }
同步接口的好处是调用方逻辑简单,拿到的结果天然包含完整的执行上下文。但同步接口对执行时长要有个上限,一般超过 30 秒的任务应该走异步通道,否则 HTTP 连接和进程占用时间都会成为瓶颈。
异步通道就是提交后返回 taskId,执行引擎完成任务后通过轮询或者回调通知结果。考虑到容器池本身是单机的,异步模式的实现成本不高,但收益在长任务场景下非常明显。
输出容量也要限。除了容器里的 fsize 限制,API 层最好再设一个输出字节数上限,比如 1MB,一旦超过就截断并停止任务,防止恶意代码无限输出打爆内存和网络带宽。
4. 安全加固:容器不是保险箱
4.1 先认清容器的边界
很多人以为容器里跑代码就是安全的,这误解很危险。容器和宿主机共享一个内核,恶意代码即使出不了容器,也可能通过内核漏洞、资源耗尽等手段影响宿主机上的其它进程,更不用说如果运维配置失误,把宿主机目录或者 Docker socket 挂进去了,那就等于直接拿到宿主机的钥匙。
所以安全加固不能只依赖容器本身,必须主动把权限、系统调用、网络通路全部收窄。下面这几项是我在代码沙箱场景里认为必须做到的。
4.2 必须收敛的六项配置
- 非 root 用户运行。镜像里创建专用用户,通过
USER指令切过去,运行时再显式加--user,双重保证。容器内是 root 意味着一旦有漏洞,攻击者直接拿到容器内最高权限,很多提权路径会顺畅很多。 - 丢弃所有 Linux Capabilities。
--cap-drop ALL,一个都不留。Capabilities 是容器内进程能做特权操作的基础,全部丢了之后,mount、ptrace、setuid 等操作全部被禁止。这是防止容器逃逸最有效的一道闸门。 - 禁止获取新的权限。
--security-opt no-new-privileges确保容器内进程即使有 setuid 文件也无法切换到更高权限。这个参数和cap-drop ALL是黄金搭档。 - 根文件系统只读。
--read-only让容器根目录无法写入,再单独挂 tmpfs 到需要写的路径。恶意代码在只读根上写不了东西,能有效遏制落盘型攻击。 - 网络完全隔离。
--network none最安全,容器彻底没有网络。如果业务确实需要访问外网,不要直接开桥接网络,而是通过宿主机上的代理进程转发,并且做白名单和流量审计。 - 保持 seccomp 默认配置。不要设置
seccomp=unconfined。Docker 默认的 seccomp 配置已经禁掉了大量危险系统调用,对一些业务场景可能偏严格,但这是额外的保险。
4.3 纵深防御与运行时选型
上面这些参数做完,容器基本具备了基础的防逃逸能力,但距离“对抗有预谋的攻击”还差一段距离。原因在于,共享内核这个根本问题没有解决,再多的系统调用拦截也只是概率性防御。
如果目标环境的安全等级很高,建议把运行时切到用户态内核方案,比如 gVisor 的 runsc。runsc 拦截了几乎所有的系统调用,在自己的用户态内核里模拟执行,容器内进程看到的内核和宿主机内核是完全隔离的。代价是性能有明显下降,文件 IO、网络、多线程密集任务损耗会比较明显,但对于短小的代码评测场景,这个代价通常可以接受。
更彻底的方案是 Kata Containers 这类轻量虚拟机运行时,每个容器实际是一个微型 VM,内核隔离做到位,但内存开销和启动耗时又上来了。能否接受,取决于业务对延迟的敏感度。
我的建议是:先用 Docker 默认 runc 配齐上面六项加固,跑一段时间观察实际被攻击面;如果分析和执行场景经常处理高对抗性代码,再逐步把高风险池切到 runsc。让所有容器都在大批量任务上跑高安全运行时,资源浪费会很严重。
4.4 绝对不要做的几件事
有些配置在普通业务容器里没大问题,但在代码沙箱里就是灾难,写了基本等于裸奔。
第一,不要挂载宿主机的 Docker socket。很多自动化工具想通过“容器里的 Docker”调度更多容器,结果一条卷挂载就把宿主机的 Docker 控制权交给了容器内进程,从容器内可以直接操作宿主机所有容器,逃逸就是这么发生的。
第二,不要加 --privileged。这个参数等于放开了所有 Capabilities,喝多了比如:容器直接挂载宿主磁盘、加载内核模块、修改宿主机网络配置,全都能干。
第三,不要加 --pid=host、--network=host、--ipc=host 这类共享命名空间的参数。它们的本意是提升性能,但在不可信代码场景里,等于主动把宿主机进程视图和通信通道裸露给恶意代码。
第四,不要容忍容器内以 root 运行。代码里出现 sudo 或者 chown 操作成功的日志,要第一时间定位并补齐用户配置。
5. 常见问题实录与排查手册
5.1 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 任务经常超时,容器停不掉 | 容器内进程树过于复杂,stop 等待时间过长 | 设置 docker stop -t 1,或直接用 docker rm -f |
| 容器池空闲但新任务仍然很慢 | 空闲容器被系统淘汰,池状态和实际情况不一致 | 增加池状态同步,使用 docker inspect 校验容器真实状态 |
| 执行几小时后容器明显变慢 | 容器内残留进程或临时文件过多 | 设置复用上限,定期回收重建 |
| 输出异常大,接口内存暴涨 | 输出流没有做大小限制 | 应用层字节计数截断,并设置 ulimit fsize |
| 合法代码报权限错误 | cap-drop ALL 和 seccomp 限制导致部分系统调用不可用 |
确认是哪些 syscall 被拦,按需放行或换运行时 |
| 容器内没有网络,业务代码报域名解析失败 | network none 模式下无 DNS |
通过代理层转发请求,不要在容器里直接配 DNS |
| 镜像更新后容器池不停创建旧版本容器 | 没有清理旧容器,也没有设置 image digest 校验 | 更新镜像时强制重建整个池 |
| 偶发容器 exec 会话挂起 | Docker API 流式连接异常,或客户端崩溃 | 增加 exec 会话的心跳与超时,超时后 kill exec 进程 |
5.2 容器池状态漂移
容器池最容易踩的坑是“池状态和真实容器状态不一致”。比如某个容器因为宿主机重启、OOM 被杀、手动误删而消失了,但池子里的 Set 还保留着它,acquire 时会把一个不存在的容器发出去,任务直接报错。
解决思路是每次 acquire 之前不直接信任内存状态,而是对一个容器执行轻量检查,比如 container.inspect() 看 State.Running 是否为 true。成本不高,但能避免大量诡异故障。更稳妥的做法是定期用后台线程去同步整个池的容器列表,把不存在或者异常退出的容器从内存数据结构中摘除,并补充新容器。
宿主机重启是这种问题的最大来源。生产环境里我会在宿主机的启动脚本里加一段逻辑,启动后先调用接口让容器池服务清空所有现有状态,再按配置重新预热。这个细节不处理,每次宿主机重启,池子就会带病运行很久。
5.3 超时后容器收不干净
在线执行场景里,超时比正常返回更常见,也是最容易造成资源泄漏的点。代码卡在死循环、等待网络、占用 CPU 不释放,这些都是家常便饭。
很多团队处理超时的方式是直接杀掉 exec 进程,但 docker exec 的进程被 kill 后,容器内部由该进程创建的子进程可能还活着。比如一段 Python 代码用 multiprocessing 起了子进程,外部杀掉主进程,子进程继续运行,容器看起来不忙,但 CPU 和内存并不释放。
我的处理策略很简单直接:任务超时后,这个容器直接进回收队列,不再尝试保留。docker stop 设置短超时,如果 stop 都停不下来,直接 docker rm -f。虽然这样会损失容器复用的效率,但能保证池子长时间稳定运行。回收速度可以用快照容器、镜像本身很轻、从创建到启动不到半秒这些优势来抵消。
5.4 磁盘和日志堆积
代码沙箱跑得越久,磁盘问题越突出。来源主要有几个:容器可写层残留、镜像越来越多、Docker 日志文件无限增长。
容器本身如果用了 --read-only 和 tmpfs,可写层基本不会膨胀,但保不齐某些代码通过很多奇技淫巧往可写目录塞文件,或者容器被强制杀掉后日志没有及时清理。我会给 Docker daemon 配置日志轮转,只保留单个文件 2MB、最多 3 个备份,就能避免日志撑爆磁盘。
镜像堆积是另一个问题。每次更新镜像后,旧镜像如果没清理,叠加层会占掉大量磁盘空间。我的做法是每次镜像更新后执行一次精简动作:docker image prune -f,并配合 CI 流程在构建完成后主动清理异体镜像。生产服务器上磁盘告警是一件非常干扰注意力的事情,最好在流程层面就杜绝。
5.5 seccomp 挡掉合法代码
Docker 默认 seccomp 配置在用 runc 运行时,基本不会误伤常规代码,但有两种情况需要排查。
一种是代码里使用了一些高级系统调用,比如某些语言运行时调用了受管控的 clone3、io_uring 相关调用,在内核版本和 Docker 版本组合较旧的时候,可能被默认策略拦截。解决办法是升级内核和 Docker,优先采用支持较新 syscall 语义的运行时。
另一种是业务代码本身做了很底层的事,比如一些小众语言需要通过特定 syscall 实现并发。这时不要图省事直接 unconfined,应该逐个确认哪些 syscall 需要放行,写一个自定义 seccomp profile,只放行确实必要的调用。这个工作量不大,但安全收益明显高于一刀切关闭管控。
5.6 从单机到多机的扩展思路
容器池做到一定程度,单机资源就会有天花板,这时要开始想多机扩展。
扩展不是简单地把容器池服务部署到多台机器,而是要解决任务路由和数据一致性的问题。一个朴素的方案是在容器池服务前面加一层调度器,根据每台机器当前的空闲容器数、任务排队长度来做加权路由。这个调度器不需要多复杂,用 Redis 维护一个简单的容量表就够了。
镜像同步也是多机扩展的麻烦事。每台宿主机都要有相同的镜像,最好搭建内部镜像仓库,让各宿主机从内网拉取,避免公网拉镜像的不可控延迟。任务执行的前置条件必须是“镜像已存在本地”,否则就认为该机器暂时不可用,这样调度器就不会把任务发给没有镜像的机器。
我个人建议先别急着上 Kubernetes。代码沙箱场景用 K8s 的成本主要体现在集群运维和网络模型复杂度上,如果单机 8 核 32G 或者 16 核 64G 能覆盖业务初期的并发量,先把单机池子做稳,把调度、回收、监控都跑顺,再谈多机扩展,会少踩很多弯路上的坑。
写在最后
这套方案从最早“能用”到后来“稳定”,我最大的体会是:Docker 命令本身不是难点,难点在于对“不可信代码”始终保持敬畏。每增加一个便利功能,都要先想它会不会成为逃逸的入口。容器池真正的成熟标志不是空闲容器数量稳定,而是“什么时候应该放弃复用这个容器”的判断逻辑变得可靠。我的默认原则是:任何异常,容器直接作废;任何怀疑,宁可多回收一个,也不多留一个。稳定运行最重要的不是把性能压到极限,而是保证每一个从池子里取出的容器都是干净的。希望这些配置和踩坑记录能帮你少走一些弯路,也欢迎在实际落地中按自己的场景继续打磨这套结构。
