Docker exec 与容器内常驻服务压测结论
结论:在容器里先放一个常驻小服务,让它和外部保持一条长连接,再由外部通过这条连接下命令,速度会明显高于每次都调用 docker exec。
本次压测里的“短任务”具体就是在容器里执行
true,它几乎什么都不做,只用来测“启动一次命令”本身要花多久。
如果每条命令仍然要在容器里新启动一次 true,大约能做到 900-1200 次/秒;如果只是让常驻服务收到消息后直接回包,可以到 1 万-2.5 万次/秒。
关键结果
16.7 次/秒
每次都走 docker exec 执行 true
909 次/秒
长连接,逐条执行 true
1,246 次/秒
长连接,连续下发 true
1 万-2.5 万次/秒
长连接,只发空请求并立刻回成功
四种方式说明
| 方式 | 怎么跑 | 这组数字说明什么 | 怎么看 |
|---|---|---|---|
| 1. 每次都用 docker exec | 外部每来一条命令,就让 Docker 进容器启动一次命令。本次命令是 true。 |
true 是 Linux 里的极轻量命令:直接成功退出,不打印内容。用它是为了尽量排除业务逻辑耗时,只看“启动命令”本身的成本。 |
写起来简单,但每条命令都要重新走一遍完整流程。这个环境里只有约 16 次/秒。 |
| 2. 长连接,只做空请求回包 | 容器里先启动一个常驻服务,它主动连到外部。外部发送的内容类似 {"kind":"noop"},意思是“不做事,只回包”。 |
服务收到后不执行 true,也不启动任何系统命令,只直接返回类似 {"rc":0} 的成功结果。这是在测“发消息、收消息”本身有多快。 |
如果以后很多操作可以直接在常驻服务里完成,不需要再启动新进程,这就是大致上限:约 1 万-2.5 万次/秒。 |
| 3. 长连接,逐条执行 true | 外部还是一条一条地下命令;本次发送的内容类似 {"kind":"shell","cmd":"true"}。 |
常驻服务收到后,在容器里执行一次 true,等它退出后再处理下一条。这相当于把 docker exec 换成长连接,但命令本身还是每次新启动。 |
这更接近“远程执行命令”的真实情况。这个环境里约 900 次/秒,比 docker exec 高很多。 |
| 4. 长连接,连续下发 true | 外部不等上一条 true 返回,就先连续发一批 {"kind":"shell","cmd":"true"};服务按编号返回结果。 |
这组数字说明:如果任务很多,而且彼此独立,连续下发能让整体处理量更高。 | 适合大量小任务。这个测试里同时最多压 64 条 true 时,整体约 1,246 次/秒,但单条命令平均等待会变长。 |
这四种不是四个互斥产品方案,而是把问题拆开看:
先看老办法执行 true 有多慢,再看长连接只发空请求有多快,再看“仍然每条命令都执行一次 true”时能提升多少,最后看连续下发很多个 true 对大批量任务有没有帮助。
过程细节
- 每次都用 docker exec:每条命令都要重新联系 Docker,再进入容器,再启动命令。本次启动的命令是
true,它只会立刻成功退出。 - 常驻服务建连:测试中只用一次
docker exec把常驻服务启动起来。之后常驻服务主动连到外部,后续命令都不再经过 Docker。 - 空请求回包:外部发
{"kind":"noop"},服务收到后什么命令都不执行,马上返回成功。这个数字不能代表真实命令速度,但能说明长连接本身足够快。 - 执行 true:外部发
{"kind":"shell","cmd":"true"},服务收到后在容器内启动true。这个数字更接近真实的“远程下命令”。 - 连续下发:外部不等上一条
true结束就继续发下一条。这样整体处理量更高,但单条命令可能要排队,所以等待时间会变长。
普通 Bridge 网络:box-bh
| 方式 | 次数 | 按单条延迟折算 | 按真实总耗时折算 | 中位等待 | 95% 等待 | 99% 等待 |
|---|---|---|---|---|---|---|
| 每次都用 docker exec 执行 true | 300 | 16.7 | 16.7 | 59.260 | 66.979 | 70.383 |
| 长连接,空请求回包 | 5000 | 10,175.9 | 10,175.9 | 0.094 | 0.113 | 0.132 |
| 长连接,逐条执行 true | 5000 | 909.1 | 909.1 | 1.097 | 1.201 | 1.321 |
| 长连接,连续下发空请求 | 5000 | 486.6 | 24,801.6 | 2.046 | 2.243 | 2.491 |
| 长连接,连续下发 true | 5000 | 19.6 | 1,246.3 | 51.827 | 60.464 | 66.579 |
说明:“按真实总耗时折算”更适合看系统整体一秒能处理多少条;“中位等待、95% 等待、99% 等待”更适合看单条命令要等多久。
Host 网络:box-ba
| 方式 | 次数 | 按单条延迟折算 | 按真实总耗时折算 | 中位等待 | 95% 等待 | 99% 等待 |
|---|---|---|---|---|---|---|
| 每次都用 docker exec 执行 true | 300 | 15.8 | 15.8 | 62.760 | 71.164 | 74.573 |
| 长连接,空请求回包 | 300 | 9,661.9 | 9,661.9 | 0.102 | 0.123 | 0.192 |
| 长连接,逐条执行 true | 300 | 934.0 | 934.0 | 1.088 | 1.155 | 1.190 |
| 长连接,连续下发空请求 | 300 | 745.2 | 5,624.0 | 1.084 | 1.813 | 2.019 |
| 长连接,连续下发 true | 300 | 32.3 | 1,027.0 | 29.345 | 56.690 | 69.523 |
判断
- 这次命令用的是
true,它本身几乎不耗时。所以慢点主要不在命令本身,而在“每次都重新通过 Docker 进去执行”这套固定流程。 - 长连接的价值是:只在启动时付一次进入容器的成本,后面一直复用这条通道。
- 如果只是轻量控制动作,最好直接写在常驻服务里完成,不要每次再启动新命令。
- 如果必须执行外部命令,连续下发能提高整体处理量,但单条命令等待时间会变长。
多个容器同时跑会怎样
又补了一组测试:同时启动 1、2、3 个容器内常驻服务,外部同时给它们下发命令。
每个容器各执行 3000 次 true;同时也测了一组“空请求回包”,用来观察长连接本身的压力。
| 方式 | 总次数 | 整体每秒处理 | 单条中位等待 | 95% 等待 | 99% 等待 |
|---|---|---|---|---|---|
| 1 个容器,空请求回包 | 3000 | 21,887 次/秒 | 2.036 ms | 2.146 ms | 2.268 ms |
| 2 个容器,空请求回包 | 6000 | 25,685 次/秒 | 3.944 ms | 5.425 ms | 6.546 ms |
| 3 个容器,空请求回包 | 9000 | 29,008 次/秒 | 5.336 ms | 8.974 ms | 10.392 ms |
| 1 个容器,连续下发 true | 3000 | 1,443 次/秒 | 40.822 ms | 56.776 ms | 67.186 ms |
| 2 个容器,连续下发 true | 6000 | 2,016 次/秒 | 63.213 ms | 68.909 ms | 81.627 ms |
| 3 个容器,连续下发 true | 9000 | 3,383 次/秒 | 55.534 ms | 59.819 ms | 72.853 ms |
结论:增加容器数量后,整体吞吐没有衰减,反而会上升;但不是完全线性上升。
例如执行 true 时,1 个容器约 1443 次/秒,2 个容器约 2016 次/秒,3 个容器约 3383 次/秒。
也就是说,总处理量提高了,但每个容器平均分到的速度会比单独跑时低一些。
原因也比较直观:多个容器同时跑时,宿主机 CPU、进程创建、网络收发、Python 调度都会一起竞争。
对空请求来说,单条等待从约 2 ms 增加到约 5 ms;对执行 true 来说,单条等待基本在 40-65 ms 区间。
所以如果目标是“总量更高”,多容器有帮助;如果目标是“单条命令尽快返回”,容器数量和同时下发数量都要控制。
复跑命令
uv run --with websockets python ws_daemon_bench.py \
--container box-bh \
--host 0.0.0.0 \
--connect-host [内网地址已隐藏] \
--count 5000 \
--window 64 \
--command true \
--skip-docker-exec
uv run --with websockets python multi_daemon_bench.py \
--containers box-bh,box-bos,box-ba \
--count-per-container 3000 \
--window 64 \
--command true \
--verbose
后续如果要做成正式方案,建议补上身份校验、请求编号、超时、取消、同时处理数量限制、命令输出实时返回,以及容器重启后的自动重连。