Mac下Docker不同文件挂载方式的性能差异

众所周知 Mac 跑 Docker是有一层 VM 的,最早 Mac 的 Docker Desktop 的共享文件系统非常烂,基于 osxfs,执行一个 npm install ,Linux 下 20 秒,Mac 下可能要几分钟。而如今情况据说已经大不相同,基于 VirtioFS,性能已有大幅提升。

绑定目录(Bind Mount)Docker Volume(命名卷) 这两种挂载方式是 Docker 两种完全不同的存储机制,其中在 Mac 下,bind mount 会经过 VirtioFS,而卷挂载时数据则属于 VM 内部。

测试方案

分别用 绑定路径(bind mount) 的方式与 卷命名(volume) 的方式创建 Postgres18 的 docker 容器,再加入 Mac 下 brew 安装的原生 Postgres18 作为对比。

建表:

CREATE TABLE test (
    id BIGSERIAL PRIMARY KEY,
    value TEXT
);

执行测试,在 psql 下:

\timing

然后执行:

INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,1000000);

最后查看统计数据即可。

Docker Bind Mount

100 万数据插入:

postgres=# \timing
启用计时功能.
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,1000000);
INSERT 0 1000000
时间:3804.413 ms (00:03.804)
postgres=#

在此基础上 1000 万数据插入:

postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,10000000);
INSERT 0 10000000
时间:48126.862 ms (00:48.127)
postgres=#

在此基础上 10 次 10 万数据插入:

postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:396.869 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:412.232 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:382.937 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:428.471 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:426.206 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:439.906 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:416.487 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:443.733 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:453.728 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:443.960 ms
postgres=#

Docker Volume

100 万数据插入:

postgres=# \timing
启用计时功能.
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,1000000);
INSERT 0 1000000
时间:2169.361 ms (00:02.169)
postgres=#

在此基础上 1000 万数据插入:

postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,10000000);
INSERT 0 10000000
时间:21762.814 ms (00:21.763)
postgres=#

在此基础上 10 次 10 万数据插入:

postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:213.439 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:233.843 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:217.458 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:219.334 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:212.566 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:216.081 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:211.386 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:206.213 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:202.603 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:211.573 ms
postgres=#

Mac 原生

100 万数据插入:

postgres=# \timing
启用计时功能.
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,1000000);
INSERT 0 1000000
时间:2850.996 ms (00:02.851)
postgres=#

在此基础上 1000 万数据插入:

postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,10000000);
INSERT 0 10000000
时间:28794.815 ms (00:28.795)
postgres=#

在此基础上 10 次 10 万数据插入:

postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:302.238 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:272.471 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:264.486 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:284.120 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:287.495 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:295.402 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:272.715 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:277.457 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:298.809 ms
postgres=# INSERT INTO test(value)
SELECT md5(random()::text)
FROM generate_series(1,100000);
INSERT 0 100000
时间:290.244 ms
postgres=#

数据对比汇总

以下耗时单位均为 ms,越小越好。

100 万数据单次插入

方案耗时相对 Mac 原生用时
Docker Bind Mount3804.413133.4%
Docker Volume2169.36176.1%
Mac 原生2850.996100%

继续 1000 万数据单次插入

方案耗时相对 Mac 原生用时
Docker Bind Mount48126.862167.1%
Docker Volume21762.81475.6%
Mac 原生28794.815100%

继续 10 次 10 万数据插入

方案平均耗时相对 Mac 原生平均用时最快最慢
Docker Bind Mount424.453149.2%382.937453.728
Docker Volume214.45075.4%202.603233.843
Mac 原生284.544100%264.486302.238

从这组测试结果看,Docker Volume 在 Mac 下三组数据里都是最快;Mac 原生 基本稳定排在第二;Bind Mount 始终最慢,而且在大批量写入时差距会进一步拉大。

如果容器里跑的是数据库、索引、队列这类高频写入服务,数据目录更适合放在 Docker VolumeBind Mount 更适合代码、配置、静态资源这类需要和宿主机实时编辑同步的内容。

另外其实 Mac 和 Windows 的 Docker 场景非常类似:

平台
macOS + Docker Desktop 数据挂载方式Docker VolumeBind Mount(共享 macOS 目录)
Windows + WSL2 数据目录位置WSL2 内部文件系统Windows C:\ 等目录

但话又说回来,Mac 作为开发机器,自然是怎么方便怎么来,哪怕是最慢的映射方式,绝大部分情况还没有到影响开发的地步,甚至很多时候作为开发端是无感的。