Colima使用NFS共享存储
备注
经过实践验证,采用NFS作为Colima的数据共享交换底层,在我的古旧的 MacBook Pro 15" Late 2013 替换 QEMU 默认使用的 sshfs ,能够获得大约 5倍 的 I/O 性能提升。虽然探索设置有些折腾,但是能够让十几年前的硬件焕发青春,还是有点意义的。
在尝试 Colima mountType 9p 优化存储性能之后,我发现虽然I/O性能有一定提升,但是也带来了很多不稳定的因素:
可能由于 使用OCLP(OpenCore Legacy Patcher)安装最新macOS 魔改的驱动插入和内核调用拦截改写,导致了需要降级为
qemu64类型的通用模拟CPU类型,导致性能和特性大幅下降在使用过程中,容器中编译的应用多次闪退,虽然通过降级GCC编译的目标二进制指令集来规避,但是依然存在偶然的闪退
和gemini讨论启发了我,考虑采用对小文件更优的NFS服务来取代 sshfs 和 9p 存储挂载,虽然Colima没有内置支持这种NFS简洁方便的配置,但是可以采用 provision 脚本来实现通用型NFS挂载,来替代默认但性能底下的 mountType 。
快速起步
备注
本段设置方法是我的实践最终汇总,已经验证成功
配置
/etc/nfs.conf性能优化配置
/etc/nfs.conf# =========================================================================
# macOS 内核 NFSv3 高性能基础设施调优(完全体)
# =========================================================================
# 1. 彻底关闭 NFSv4,让 nfsd 专心走熟练的 v3 状态机
nfs.server.v4 = 0
# 2. 核心放行安全锁:允许来自非特权端口(>1024)的挂载请求
# 配合虚拟机端的挂载参数,双重确保绝不被 Mac 内核丢包或 Reset
nfs.server.mount.require_resvport = 0
# 3. 狂暴多线程优化:将内核 nfsd 守护线程池从默认的 4 或 8 强行扩容至 124 个
# 完美解决多并发编译(如 make -j4、mise install、cargo build)高频读写海量小文件时因线程排队导致的 I/O 便秘
nfs.server.async = 1
nfs.server.threads = 124
# 4. 拓宽内核 TCP 读写泵血管道:将发送与接收队列缓冲区提升至 4MB (4194304 字节)
# 允许 Mac 内核在面对虚拟机并发灌入的 1MB 级全速数据块(rsize/wsize)时,拥有足够的蓄水池,杜绝 TCP 窗口收缩和重传
nfs.server.tcp.send_quota = 4194304
nfs.server.tcp.recv_quota = 4194304
配置
/etc/exports配置输出:
/Users/admin -mapall=501:20 -async -network 192.168.106.0 -mask 255.255.255.0
修订
~/.colima/_template/default.yaml配置,将sshfs修订 "伪挂载" ,并且独立挂载/Users/admin/docs:
# Number of CPUs to be allocated to the virtual machine.
# Default: 2
cpu: 3
# Size of the disk in GiB to be allocated to the virtual machine for container data.
# NOTE: value can only be increased after virtual machine has been created.
#
# Default: 100
disk: 100
# Size of the memory in GiB to be allocated to the virtual machine.
# Default: 2
memory: 6
# Architecture of the virtual machine (x86_64, aarch64, host).
#
# NOTE: value cannot be changed after virtual machine is created.
# Default: host
arch: host
# Container runtime to be used (docker, containerd).
#
# NOTE: value cannot be changed after virtual machine is created.
# Default: docker
runtime: containerd
# AI model runner (docker, ramalama).
# Both require krunkit VM type for GPU access.
# docker: Uses Docker Model Runner.
# ramalama: Uses Ramalama.
#
# Default: docker
modelRunner: docker
# Set custom hostname for the virtual machine.
# Default: colima
# colima-profile_name for other profiles
hostname: ""
# Kubernetes configuration for the virtual machine.
kubernetes:
# Enable kubernetes.
# Default: false
enabled: false
# Kubernetes version to use.
# This needs to exactly match a k3s version https://github.com/k3s-io/k3s/releases
# Default: latest stable release
version: v1.35.0+k3s1
# Additional args to pass to k3s https://docs.k3s.io/cli/server
# Default: traefik is disabled
k3sArgs:
- --disable=traefik
# Kubernetes port to listen on
# A common port is 6443, though left unbound to ensure no port conflicts
# Default: pick random unbound port
port: 0
# Auto-activate on the Host for client access.
# Setting to true does the following on startup
# - sets as active Docker context (for Docker runtime).
# - sets as active Kubernetes context (if Kubernetes is enabled).
# - sets as active Incus remote (for Incus runtime).
# Default: true
autoActivate: true
# Network configurations for the virtual machine.
network:
# Assign reachable IP address to the virtual machine.
# NOTE: this is currently macOS only and ignored on Linux.
# Default: false
address: true
# Network mode for the virtual machine (shared, bridged).
# NOTE: this is currently macOS only and ignored on Linux.
# Default: shared
mode: shared
# Network interface to use for bridged mode.
# This is only used when mode is set to bridged.
# NOTE: this is currently macOS only and ignored on Linux.
# Default: en0
interface: en0
# Use the assigned IP address as the preferred route for the VM.
# Note: this only has an effect when `address` is set to true.
# Default: false
preferredRoute: false
# Custom DNS resolvers for the virtual machine.
#
# EXAMPLE
# dns: [8.8.8.8, 1.1.1.1]
#
# Default: []
dns: []
# DNS hostnames to resolve to custom targets using the internal resolver.
# This setting has no effect if a custom DNS resolver list is supplied above.
# It does not configure the /etc/hosts files of any machine or container.
# The value can be an IP address or another host.
#
# EXAMPLE
# dnsHosts:
# example.com: 1.2.3.4
dnsHosts:
host.docker.internal: host.lima.internal
# Replicate host IP addresses in the VM. This enables port forwarding to specific
# host IP addresses.
# e.g. `docker run --port 10.0.1.2:8080:8080 alpine` would only forward to the
# specified IP address.
#
# Default: false
hostAddresses: false
# Custom gateway address for the virtual machine.
# The last octet needs to be 2.
#
# EXAMPLE
# gatewayAddress: 192.168.10.2
#
# Default: 192.168.5.2
gatewayAddress: 192.168.5.2
# ===================================================================== #
# ADVANCED CONFIGURATION
# ===================================================================== #
# Forward the host's SSH agent to the virtual machine.
# Default: false
forwardAgent: false
# Docker daemon configuration that maps directly to daemon.json.
# https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file.
# NOTE: some settings may affect Colima's ability to start docker. e.g. `hosts`.
#
# EXAMPLE - disable buildkit
# docker:
# features:
# buildkit: false
#
# EXAMPLE - add insecure registries
# docker:
# insecure-registries:
# - myregistry.com:5000
# - host.docker.internal:5000
#
# Colima default behaviour: buildkit enabled
# Default: {}
docker: {}
# Virtual Machine type (krunkit, qemu, vz)
# NOTE: this is macOS 13 only. For Linux and macOS <13.0, qemu is always used.
#
# vz is macOS virtualization framework and requires macOS 13.
# krunkit runs super‑light VMs on macOS/ARM64 with a focus on GPU access. It is experimental.
#
# NOTE: value cannot be changed after virtual machine is created.
# Default: qemu
vmType: qemu
# Port forwarder for the virtual machine (ssh, grpc, none).
# ssh is more stable but supports only TCP.
# grpc supports both TCP and UDP, but is experimental.
# none disables port forwarding.
#
# Default: ssh
portForwarder: ssh
# Utilise rosetta for amd64 emulation (requires m1 mac and vmType `vz`)
# Default: false
rosetta: false
# Enable foreign architecture emulation via binfmt (e.g. amd64 on arm64, arm64 on amd64)
# Default: true
binfmt: true
# Enable nested virtualization for the virtual machine (requires m3 mac and vmType `vz`)
# Default: false
nestedVirtualization: false
# Volume mount driver for the virtual machine (virtiofs, 9p, sshfs).
#
# virtiofs is limited to macOS and vmType `vz`. It is the fastest of the options.
#
# 9p is the recommended and the most stable option for vmType `qemu`.
#
# sshfs is faster than 9p but the least reliable of the options (when there are lots
# of concurrent reads or writes).
#
# NOTE: value cannot be changed after virtual machine is created.
# Default: virtiofs (for vz), sshfs (for qemu)
mountType: sshfs
# Propagate inotify file events to the VM.
# NOTE: this is experimental.
mountInotify: false
# The CPU type for the virtual machine (requires vmType `qemu`).
# Options available for host emulation can be checked with: `qemu-system-$(arch) -cpu help`.
# Instructions are also supported by appending to the cpu type e.g. "qemu64,+ssse3".
# Default: host
cpuType: host
# Custom provision scripts for the virtual machine.
# Provisioning scripts are executed on startup and therefore needs to be idempotent.
#
# EXAMPLE - script executed as root
# provision:
# - mode: system
# script: apt-get install htop vim
#
# EXAMPLE - script executed as user
# provision:
# - mode: user
# script: |
# [ -f ~/.provision ] && exit 0;
# echo provisioning as $USER...
# touch ~/.provision
#
# EXAMPLE - script executed after VM boot, before container runtimes start
# provision:
# - mode: after-boot
# script: echo "VM is up, containers not yet started"
#
# EXAMPLE - script executed after VM and container runtimes are ready
# provision:
# - mode: ready
# script: echo "everything is ready"
#
# Default: []
# provision: []
# ===================================================================== #
# ADVANCED CONFIGURATION
# ===================================================================== #
# Custom provision scripts for the virtual machine.
# Provisioning scripts are executed on startup and therefore needs to be idempotent.
provision:
# -----------------------------------------------------------------
# 任务一:系统底层工具链补齐(使用系统管理员 root 权限执行)
# -----------------------------------------------------------------
- mode: system
script: |
#!/bin/sh
echo "=== [Task 1/2] Updating package list and installing system tools ==="
# 1. 禁用交互式弹窗,防止 apt 阻塞开机流程
export DEBIAN_FRONTEND=noninteractive
# 2. 规范执行更新并同步安装你需要的工具(例如 htop, vim, rsync)
apt-get update -y
apt-get install -y htop vim rsync nfs-common cron
systemctl enable --now cron
echo "=== [Task 1/2] Infrastructure tools installed successfully ==="
# -----------------------------------------------------------------
# 任务二:NFS 挂载
# -----------------------------------------------------------------
- mode: system
script: |
#!/bin/bash
set -e
# 1. 确保虚拟机内部的本地挂载点物理存在
MOUNT_POINT="/Users/admin/docs"
mkdir -p ${MOUNT_POINT}
EXPORT_SRC="192.168.106.1:/Users/admin/docs"
# 使用 soft 模式,配合超短超时,防止手工和自动化脚本卡死
#MOUNT_OPTS="nfsvers=3,tcp,nolock,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,soft,timeo=5,retrans=2"
# 使用 hard 模式,获取更好性能
MOUNT_OPTS="nfsvers=3,tcp,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,hard,intr"
# 2. 检查当前是否已经挂载过,避免 colima start/stop 重复挂载导致内核死锁
if ! mountpoint -q ${MOUNT_POINT}; then
echo "=== [Colima Provision] 正在注入高性能 NFS 存储底座 ==="
# 核心精调参数释义:
# nfsvers=3,tcp ➔ 锁死 NFSv3 走全透明 TCP 通道
# resvport ➔ 强制使用 Linux 的特权端口(Privileged Port,即小于 1024 的端口)去连接,实践发现可能是导致休眠恢复后再次mount报错"mount.nfs: mount system call failed for /Users/admin/docs" 的原因,取消该参数
# nolock ➔ 禁用 NLM 锁,彻底避开被 QEMU/网络干扰的 111 端口查询,本地单机开发安全
# rsize/wsize=1048576 ➔ 顶满 Linux 客户端硬编码的 1MB 单次 I/O 块大小极限
# noatime,nodiratime ➔ 禁止更新文件和目录的访问时间,减少一半以上的磁盘元数据交互 RTT
# actimeo=3600 ➔ 缓存属性失效时间拉长到1小时,对海量小文件的 git status 速度很大的加速效果
# hard,intr ➔ 硬挂载模式,配合 intr 允许在宿主机卡死时通过 kill -9 强行中断,防止虚拟机内核僵死
# 3. NFS挂载采用NFS v3
# 192.168.106.1 是 Colima 默认address网络分配给Host主机网关的IP
#mount -t nfs -o nfsvers=3,tcp,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,hard,intr 192.168.106.1:/Users/admin/docs /Users/admin/docs
#mount -t nfs -o nfsvers=3,tcp,resvport,nolock,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,soft,timeo=10,retrans=3 192.168.106.1:/Users/admin/docs /Users/admin/docs
mount -t nfs -o $MOUNT_OPTS $EXPORT_SRC $MOUNT_POINT
sleep 1
# 需要持续对NFS读写以保持工作正常
# while true; do
# date > $MOUNT_POINT/nfs_watchdog
# sleep 10
# done
# 设置cron每分钟写一次nfw_watchdog文件时间戳来保持NFS
if ! crontab -l 2>/dev/null | grep -q "nfs_watchdog"; then
echo "* * * * * /usr/bin/date > $MOUNT_POINT/nfs_watchdog" | crontab -
fi
echo "=== [Colima Provision] NFS 挂载成功! ==="
else
echo "=== [Colima Provision] ${MOUNT_POINT} 已存在挂载,跳过 ==="
fi
# Modify ~/.ssh/config automatically to include a SSH config for the virtual machine.
# SSH config will still be generated in $COLIMA_HOME/ssh_config regardless.
# Default: true
sshConfig: true
# The port number for the SSH server for the virtual machine.
# When set to 0, a random available port is used.
#
# Default: 0
sshPort: 0
# Configure volume mounts for the virtual machine.
# Colima mounts user's home directory by default to provide a familiar
# user experience.
#
# EXAMPLE
# mounts:
# - location: ~/secrets
# writable: false
# - location: ~/projects
# writable: true
#
# Colima default behaviour: $HOME is mounted as writable.
# Default: []
# mounts: []
mounts:
- location: ~/.colima/empty_mount
mountPoint: /tmp/colima_empty_gate
writable: false
# Specify a custom disk image for the virtual machine.
# When not specified, Colima downloads an appropriate disk image from Github at
# https://github.com/abiosoft/colima-core/releases.
# The file path to a custom disk image can be specified to override the behaviour.
#
# Default: ""
diskImage: "https://cloud.debian.org/images/cloud/trixie/20260413-2447/debian-13-genericcloud-amd64-20260413-2447.qcow2"
# Size of the disk in GiB for the root filesystem of the virtual machine.
# This value is ignored if no runtime is in use. i.e. `none` runtime.
# Default: 20
rootDisk: 20
# Environment variables for the virtual machine.
#
# EXAMPLE
# env:
# KEY: value
# ANOTHER_KEY: another value
#
# Default: {}
env:
# curl / git 能够完美识别 http_proxy、https_proxy 或 ALL_PROXY,优先级是小写变量最高
# ALL_PROXY 是一个全协议兜底变量 它不仅管 HTTP(S),还管 ftp://、git://、甚至是内部未明确分类的其他 TCP 流量
http_proxy: socks5h://192.168.5.2:1080
https_proxy: socks5h://192.168.5.2:1080
all_proxy: socks5h://192.168.5.2:1080
no_proxy: localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,*.baidu.com
# 大写变量兜底防止某些应用忽略大小写
HTTP_PROXY: socks5h://192.168.5.2:1080
HTTPS_PROXY: socks5h://192.168.5.2:1080
ALL_PROXY: socks5h://192.168.5.2:1080
NO_PROXY: localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,*.baidu.com
性能测试: 和 Colima mountType 9p 采用相同的编译 Sphinx文档 方式
time make html
real 7m24.930s
user 5m36.570s
sys 0m10.935s
不过,在没有启用 resvport 参数的 hard 挂载同样测试性能差一些,完成时间减慢到将近12分钟:
可以看到完成时间惊人地缩短到 7m25s ,是原本使用 sshfs 时间的 1/5 不到,已经接近物理主机的的编译性能
real 11m38.909s
user 7m32.720s
sys 0m43.471s
备注
macOS系统 NFS v4 服务 实践没有成功
异常排查
备注
以下是我的一次折腾之旅的记录,仅供参考。正确的方法则见本文前半部分!!!
我最初采用的NFS v4配置
/etc/nfs.conf :
# 强制开启 NFS v4 服务端支持
nfs.server.v4 = 1
# 核心排毒:强制让 macOS 的 NFSv4 彻底脱离 rpcbind 依赖,只监听标准的 2049 端口
nfs.server.mount.require_resvport = 0
我最初采用的Colima VM的配置
~/.colima/_template/default.yaml:
provision 段落的NFS挂载脚本cpuType: host
vmType: qemu
# mountType必须设置,这里采用sshfs,但实际不挂载
mountType: sshfs
# 这里挂载一个空目录,欺骗Colima,这样就不会挂载 /home/admin 目录,留给定制的NFS挂载
mounts:
- location: ~/.colima/empty_mount
mountPoint: /tmp/colima_empty_gate
writable: false
# Custom provision scripts for the virtual machine.
# Provisioning scripts are executed on startup and therefore needs to be idempotent.
provision:
- mode: system
script: |
#!/bin/sh
echo "=== [Task 1/2] Updating package list and installing system tools ==="
# 1. 禁用交互式弹窗,防止 apt 阻塞开机流程
export DEBIAN_FRONTEND=noninteractive
# 2. 规范执行更新并同步安装你需要的工具(例如 htop, vim, rsync), 并且特别安装了NFS客户端工具nfs-common
apt-get update -y
apt-get install -y htop vim rsync nfs-common
echo "=== [Task 1/2] Infrastructure tools installed successfully ==="
- mode: system
script: |
#!/bin/bash
set -e
# 1. 确保虚拟机内部的本地挂载点物理存在
mkdir -p /home/admin
# 2. 检查当前是否已经挂载过,避免 colima start/stop 重复挂载导致内核死锁
if ! mountpoint -q /home/admin; then
echo "=== [Colima Provision] 正在注入高性能 NFS 存储底座 ==="
# 3. 强力泵血:执行最高性能的 NFSv4 挂载命令
# 192.168.5.2 是 Colima 默认指向 Mac 宿主机的网关 IP
mount -t nfs -o \
nfsvers=4.1,\
rsize=1048576,\
wsize=1048576,\
noatime,\
nodiratime,\
actimeo=3600,\
hard,\
intr,\
tcp \
192.168.5.2:/Users/admin /home/admin
echo "=== [Colima Provision] NFS 挂载成功! ==="
else
echo "=== [Colima Provision] /home/admin 已存在挂载,跳过 ==="
fi
我发现启动Colima VM之后,并没有如预想的那样自动成功挂载 /Users/admin 目录,这里可能有几个潜在的问题:
macOS的安全策略可能限制了
nfsd这样的服务进程全盘访问,例如禁止访问敏感目录(完整全量的HOME目录)Debian 客户端访问macOS的NFS v4协商失败
在 Colima VM ( Ubuntu Linux )中安装
netcat-openbsd软件包获取nc工具来检测服务端口可达性:
nc -zv -w 5 192.168.5.2 2049
输出显示端口是通的:
Connection to 192.168.5.2 2049 port [tcp/nfs] succeeded!
偶然发现,在Colima VM中采用
rpcinfo -p 192.168.5.2居然看到和macOS Host主机完全不同的内容,就好像 192.168.5.2 根本不是Host主机一样:
rpcinfo -p 192.168.5.2 program vers proto port service
100000 4 tcp 111 portmapper
100000 3 tcp 111 portmapper
100000 2 tcp 111 portmapper
100000 4 udp 111 portmapper
100000 3 udp 111 portmapper
100000 2 udp 111 portmapper
100024 1 udp 55958 status
100024 1 tcp 57397 status
rpcinfo -p program vers proto port
100000 2 udp 111 rpcbind
100000 3 udp 111 rpcbind
100000 4 udp 111 rpcbind
100000 2 tcp 111 rpcbind
100000 3 tcp 111 rpcbind
100000 4 tcp 111 rpcbind
100003 2 udp 2049 nfs
100003 3 udp 2049 nfs
100003 2 tcp 2049 nfs
100003 3 tcp 2049 nfs
100005 1 udp 893 mountd
100005 3 udp 893 mountd
100005 1 tcp 842 mountd
100005 3 tcp 842 mountd
100011 1 udp 853 rquotad
100011 2 udp 853 rquotad
100011 1 tcp 832 rquotad
100011 2 tcp 832 rquotad
可以看到两者完全不同: 即使在Colima VM中通过 nc 检查是能够访问Host主机的 2049 但是却看不到rpc信息
Gemini给出了一个可能的解释:
Colima 默认使用的 QEMU Slirp (用户态网络栈),为了在不需要 macOS 宿主机提供 root 权限和复杂桥接网卡的前提下实现虚拟机的 NAT 上网,它在虚拟机和宿主机之间实现了一个 轻量级的内置虚拟路由器(由 QEMU 进程模拟) :
执行
nc -zv 192.168.5.2 2049时,QEMU 的 Slirp 引擎发现 2049 是一个普通端口,它做了一次透明的端口转发(Port Forwarding),把流量老老实实地导向了 Mac 宿主机真正的 localhost:2049。所以 nc 看到的是真 Mac,提示成功。当执行
rpcinfo -p 192.168.5.2时,请求去往的是 111 端口。 QEMU 为了在虚拟网段内部提供基础的 RPC 发现,它自己内部实现了一个极简的、用户态的 RPCBind 服务(Portmapper) QEMU 虚拟路由器拦截了发往111端口的请求。这里看到的portmapper和status但完全没有nfs和mountd的输出,实际上是QEMU进程自己伪造并返回给虚拟机的应答。
解决方法
macOS内核提供了 仅限主机(Host-Only)的虚拟二层交换机(Virtual Switch)网络 :
macOS 内核原生的
vmnet.framework(苹果官方虚拟化网络框架)会在系统底层虚拟出一块物理网卡(通常叫fnetwork或vmnetX),并直接分配给 Colima 虚拟机作为副卡。虚拟机和 Mac 宿主机变成了同一个局域网内的两台独立主机。它们之间的通信直接走 macOS 内核的二层转发,QEMU 再也没有机会在半路拦截 111 或 858 端口。
192.168.106.x网段是 Colima 底层依赖的开源网络组件 Lima: Linux Machines (Linux virtual machines on macOS)在源码里硬编码死锁的默认保留网段。方法一: 在命令行启用
address网络
colima start --network address
方法二: 修订
~/.colima/_template/default.yaml启用address网络:
~/.colima/_template/default.yaml# Network configurations for the virtual machine.
network:
# Assign reachable IP address to the virtual machine.
# NOTE: this is currently macOS only and ignored on Linux.
# Default: false
address: true
在启用了 address 网络之后,通过 colima ssh 进入VM之后,检查 ip addr 可以看到VM中现在有 2个网卡 :
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:55:55:61:33:26 brd ff:ff:ff:ff:ff:ff
inet 192.168.5.1/24 metric 200 brd 192.168.5.255 scope global dynamic eth0
valid_lft 3103sec preferred_lft 3103sec
inet6 fe80::5055:55ff:fe61:3326/64 scope link
valid_lft forever preferred_lft forever
3: col0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:55:55:6d:f1:3a brd ff:ff:ff:ff:ff:ff
inet 192.168.106.2/24 metric 300 brd 192.168.106.255 scope global dynamic col0
valid_lft 3104sec preferred_lft 3104sec
inet6 fd6a:cbf9:1133:35b9:5055:55ff:fe6d:f13a/64 scope global dynamic mngtmpaddr noprefixroute
valid_lft 2591963sec preferred_lft 604763sec
inet6 fe80::5055:55ff:fe6d:f13a/64 scope link
valid_lft forever preferred_lft forever
现在用
rpcinfo -p 192.168.106.1检查就能够看到macOS Host主机上的NFS服务:
program vers proto port service
100000 2 udp 111 portmapper
100000 3 udp 111 portmapper
100000 4 udp 111 portmapper
100000 2 tcp 111 portmapper
100000 3 tcp 111 portmapper
100000 4 tcp 111 portmapper
100003 2 udp 2049 nfs
100003 3 udp 2049 nfs
100003 2 tcp 2049 nfs
100003 3 tcp 2049 nfs
100005 1 udp 893 mountd
100005 3 udp 893 mountd
100005 1 tcp 842 mountd
100005 3 tcp 842 mountd
100011 1 udp 853 rquotad
100011 2 udp 853 rquotad
100011 1 tcp 832 rquotad
100011 2 tcp 832 rquotad
然后在VM中测试挂载
sudo mount -t nfs -o nfsvers=3,tcp,resvport 192.168.106.1:/Users/admin /home/admin
挂载成功!!!
休眠问题
上述NFS挂载确实使得I/O性能得到极大提升,但是我发现当笔记本合上以后进入休眠,再次唤醒系统时会发现Colima虚拟机无法登录且容器无响应。
这个问题是NFS挂载 hard 模式相关,实际上我在阿里巴巴工作时,也曾经遇到过NAS故障导致依赖NFS挂载的应用服务器无法 df 以及应用挂死的故障。
警告
这段排查我实践下来已经排除,gemini提供的建议不准确
gemini推测(其实我觉得不准确):
由于我在NFS挂载的
/home/admin/目录下工作,当MacBook合盖休眠,vmnet虚拟网卡(192.168.106.1`)随之断电。此时虚拟机内还有工作进程(例如正在使用vim编辑该目录下文件)hard模式下,Linux内核会立即将该I/O进程挂起,并进入不可中断的睡眠状态(D状态, Uninterruptible Sleep)当电脑唤醒时,macOS的虚拟网卡需要几秒钟的握手时间,而此时虚拟机内部因为D状态进程无法被杀死,会迅速把系统的VFS(虚拟文件系统)控制块耗尽
负载处理
colima ssh登录的sshd守护进程在读取用户配置或日志时,一旦触碰到被锁死的VFS锁,它也会瞬间变成D状态挂起。最终现象就是虚拟机虽然在运行,但是整个I/O链路已经挂起导致任何命令都无法进行
解决方法尝试:
修订
default.yaml的挂载参数
将 hard 模式修订为待要高频超时重试的 soft,retrans 组合,并缩短超时周期
soft,retrans 组合挂载 # 3. 强力泵血:执行最高性能的 NFSv4 挂载命令
# 192.168.106.1 是 Colima 默认address网络分配给Host主机网关的IP
# 为防止休眠挂死,将hard挂载修订为 soft,retrans 组合
# mount -t nfs -o nfsvers=3,tcp,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,hard,intr 192.168.106.1:/Users/admin /Users/admin
mount -t nfs -o nfsvers=3,tcp,resvport,nolock,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,soft,timeo=10,retrans=3 192.168.106.1:/Users/admin /Users/admin
说明:
soft(软挂载): 如果网络断开(合盖休眠),向 Mac 发起的 I/O 请求在重试失败后, 直接向调用进程返回一个 EIO(I/O 错误)状态码,而不是让进程进入死等的 D 状态! 这样就彻底保护了系统的 sshd 等核心基础设施不会被连带锁死。timeo=10: 将单次 RPC 请求的超时时限缩短为 1.0 秒(单位是 0.1 秒)。默认是 60 秒,合盖时根本等不起。retrans=3: 当发生超时后,只尝试重试 3 次(总计 3 秒钟)。如果 3 秒内 Mac 没醒过来,立刻切断本次连接并报错返回。
样微调后,合盖时虚拟机内部的进程会抛错,而不会卡死系统。当开盖网卡恢复后,下一次 I/O 请求又会瞬间重新拉通。
其他调整(可选):
如果Mac 经常在后台跑长时间的编译任务,可以利用 macOS 內置的 pmset 电源管理工具,允许 Mac 在合盖时依然保持网络栈和虚拟机的微弱唤醒状态(不进入物理彻底断电):
# 当插着电源充电器时,合盖禁止系统进入全面断电休眠(显示器会熄灭,但内核和虚拟网卡依然活着)
sudo pmset -c disablesleep 1
休眠问题(再查)
我还发现一个Colima VM挂载的特性:
当没有配置
mounts:部分,也就是采用默认配置,Colima会自动挂载 HOME 目录,而且只挂载 HOME 目录。此时进入VM执行df -h可以看到:
Filesystem Size Used Avail Use% Mounted on
/dev/root 19G 1020M 18G 6% /
...
:/Users/admin 1.9T 126G 1.7T 7% /Users/admin
但是,当我配置了一个欺骗Colima的空目录挂载:
# mountType必须设置,这里采用sshfs,但实际不挂载
mountType: sshfs
# 这里挂载一个空目录,欺骗Colima,这样就不会挂载 /home/admin 目录,留给定制的NFS挂载
mounts:
- location: ~/.colima/empty_mount
mountPoint: /tmp/colima_empty_gate
writable: false
则此时进入VM会看到一个特殊的 缓存挂载 :
缓存挂载Filesystem Size Used Avail Use% Mounted on
/dev/root 19G 1020M 18G 6% /
...
:/Users/admin/Library/Caches/colima 1.9T 126G 1.7T 7% /Users/admin/Library/Caches/colima
:/Users/admin/.colima/empty_mount 1.9T 126G 1.7T 7% /tmp/colima_empty_gate
这就带来一个冲突隐患: 如果我再直接通过NFS挂载 /Users/admin 目录到VM内部,是否和这个缓存挂载目录冲图,是否会引起前面所说的挂起死机:
可以看到当停止默认的HOME挂载,Coliama会明确挂载一个缓存目录
/Users/admin/Library/Caches/colima,这个缓存目录原先是包含在HOME挂载中的,所以这个目录独立挂载以后,再通过NFS去直接挂载HOME目录存在冲图:NFS是最后挂载的,并且覆盖了缓存目录挂载,虽然理论上Colima依然可以通过NFS访问缓存文件,但是一旦主机从休眠中恢复,显然NFS服务没有这么块就能够就绪
缓存目录可能是Colima最早需要访问的数据,由于NFS覆盖了原本sshfs提供的目录挂载导致Colima内核暂时看不到缓存,此时可能会导致系统挂起
前述Gemini建议的NFS挂载修改为Soft模式并且快速重试应该能够改善NFS的挂起,但是根据我的经验,这种NFS挂载通常不会导致挂起,但是这里存在的问题是缓存也在NFS上,这个内核机制锁死了系统
综上,我 觉得 必须 放弃(或不建议)直接在NFS中输出完整的HOME目录,除非缓存目录能够从HOME目录中移出,以避免和HOME的NFS挂载冲突。
所以最终修订目录 /Users/admin 改为 /Users/admin/docs (挂载数据目录)
soft,retrans 组合挂载 /Users/admin/docs # 3. 强力泵血:执行最高性能的 NFSv4 挂载命令
# 192.168.106.1 是 Colima 默认address网络分配给Host主机网关的IP
# 为防止休眠挂死,将hard挂载修订为 soft,retrans 组合
# mount -t nfs -o nfsvers=3,tcp,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,hard,intr 192.168.106.1:/Users/admin /Users/admin
mount -t nfs -o nfsvers=3,tcp,resvport,nolock,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,soft,timeo=10,retrans=3 192.168.106.1:/Users/admin/docs /Users/admin/docs
soft挂载的自动恢复
在使用了 soft 挂载NFS之后,实践发现,当主机休眠以后,再恢复工作,此时VM内部的挂载会提示报错:
df: /Users/admin/docs: Input/output error
不过系统不会挂死,此时需要手工umount并重新mount一次进行恢复。
为了能够自动完成恢复,gemini提供了一段脚本定期检查和恢复,参考如下
# =========================================================================
# 基础设施拓扑:强开 vmnet 物理副卡
# =========================================================================
cpu: 4
memory: 8
disk: 60
network:
address: true
mounts:
- location: ~/.colima/empty_mount
mountPoint: /tmp/colima_empty_gate
writable: false
mountType: sshfs
# =========================================================================
# 自动化自愈系统:注入满血 NFSv3 挂载与定周期自愈看门狗
# =========================================================================
provision:
- mode: system
script: |
#!/bin/bash
set -e
MOUNT_POINT="/Users/admin/docs"
EXPORT_SRC="192.168.106.1:/Users/admin/docs"
MOUNT_OPTS="nfsvers=3,tcp,resvport,nolock,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,soft,timeo=10,retrans=3"
# 1. 确保虚拟机内部挂载点物理存在
mkdir -p "$MOUNT_POINT"
# 2. 封装核心挂载/修复函数
do_nfs_mount() {
# 如果检测到挂载点陷入 EIO 泥潭(Stale),先强制物理卸载
if mount | grep -q "$MOUNT_POINT"; then
echo "=== [NFS Watchdog] 检测到挂载点异常,正在强行剥离旧句柄... ==="
umount -f -l "$MOUNT_POINT" || true
fi
echo "=== [NFS Watchdog] 正在发起满血 NFSv3 挂载冲锋... ==="
mount -t nfs -o "$MOUNT_OPTS" "$EXPORT_SRC" "$MOUNT_POINT"
}
# 3. 首次开机保底挂载
if ! mountpoint -q "$MOUNT_POINT"; then
do_nfs_mount
fi
# =========================================================================
# 4. 绝杀:向系统注入守护看门狗脚本,每10秒检查一次,若发生 EIO 自动秒级复活
# =========================================================================
WATCHDOG_SCRIPT="/usr/local/bin/nfs_watchdog.sh"
cat << 'EOF' > "$WATCHDOG_SCRIPT"
#!/bin/bash
MOUNT_POINT="/Users/admin/docs"
EXPORT_SRC="192.168.106.1:/Users/admin/docs"
MOUNT_OPTS="nfsvers=3,tcp,resvport,nolock,rsize=1048576,wsize=1048576,noatime,nodiratime,actimeo=3600,soft,timeo=10,retrans=3"
# 核心判官:利用 ls 测试挂载点。如果吐出错误(包括 Input/output error),说明链路已断
if ! ls "$MOUNT_POINT" >/dev/null 2>&1; then
# 再次确认是否是真正的 NFS 失联
if mount | grep -q "$MOUNT_POINT" || [ ! -f "$MOUNT_POINT/.watchdog_gate" ]; then
umount -f -l "$MOUNT_POINT" >/dev/null 2>&1 || true
mount -t nfs -o "$MOUNT_OPTS" "$EXPORT_SRC" "$MOUNT_POINT" >/dev/null 2>&1
fi
fi
EOF
chmod +x "$WATCHDOG_SCRIPT"
# 5. 将看门狗挂载到 crontab 中(每分钟高频轮询检查)
if ! crontab -l 2>/dev/null | grep -q "nfs_watchdog.sh"; then
(crontab -l 2>/dev/null; echo "* * * * * $WATCHDOG_SCRIPT") | crontab -
fi
echo "=== [Colima Provision] NFSv3 高性能防休眠自愈看门狗布设成功! ==="
备注
这里的脚本仅供参考,但我实际方案改回了hard挂载,所以不再出现这里的soft挂载问题,方案就可以简化为不需要这段 cron_mount 修复
最终验证方案
如上所述,我最终验证确实如我推测,最核心的导致Colima VM在主机从休眠中恢复时挂起,其实就是cache缓存目录被NFS挂载HOME目录所覆盖,导致NFS暂停以后缓存机制失效而死机。
我对比了 hard 和 soft 两种挂载方式:
只要没有
/Users/admin/Library/Caches/colima和/Users/admin(NFS挂载) 的挂载冲突,那么不管hard和softNFS挂载都不会导致Colima VM挂起无响应问题但是
soft挂载和hard挂载表现不同的是,当macOS Host主机进入休眠,然后恢复时,hard模式下df -h会卡住任何输出,而soft则表现为能够输出所有和NFS无关的挂载输出,并且显示I/O错误:
soft NFS挂载显示I/O错误df: /Users/admin/docs: Input/output error
Filesystem Size Used Avail Use% Mounted on
/dev/root 19G 1.5G 17G 8% /
...
/dev/vdb1 98G 23G 71G 25% /mnt/lima-colima
:/Users/admin/Library/Caches/colima 1.9T 129G 1.7T 7% /Users/admin/Library/Caches/colima
:/Users/admin/.colima/empty_mount 1.9T 129G 1.7T 7% /tmp/colima_empty_gate
理论上
soft和hard的NFS挂载I/O性能应该是相同的,但是实践发现在编译sphinx文档,原本hard挂载的完成时间是7m25s,现在soft挂载则退化成大约12m。Gemini提示之前soft的参数timeo=5(表示0.5秒)太激进:编译 Sphinx 的 I/O 特征:Sphinx 在 make html 时,不仅有大文件的写入,更包含数万个小 .rst 和缓存文件的密集读写与元数据 lstat() 状态对齐。
延迟滚雪球:当虚拟机瞬间发起成百上千个小文件的 RPC 请求时,Mac 宿主机的物理磁盘或内核 nfsd 线程池即便再快,在并发洪峰下,个别 RPC 请求的响应时间也难免会超过 0.5 秒。
重传惩罚:一旦超过 0.5 秒,虚拟机内核判定触发 soft 超时。它会立刻中断当前的 TCP 传输,丢弃已经传输了一半的数据,重新发起重传(Retransmit)。这导致网络中充满了大量无效的、重复的重传风暴(Retransmission Storm)。
TCP 拥塞控制踩刹车:由于频繁触发超时,Linux 内核的 TCP 拥塞控制算法(如 Cubic)会误判定“网络发生了严重拥堵”,从而强行将 TCP 滑动窗口(Window Size)和慢启动阈值调整到最低。
我偶然发现,我在操作macOS Host主机,并且确认macOS没有休眠的情况下,Colima VM中的NFS挂载还是出现了上述NFS挂起无响应,出现
Input/output error,这说明:并非是macOS Host主机休眠导致的NFS Server无法响应或恢复缓慢导致Colima虚拟机NFS挂起
我最初怀疑是macOS的vmnet虚拟网卡在一段时间没有数据流量进入休眠导致了Colima VM到Host主机网络断开,但是通过
ip linkdown/up 并且确认网络正常情况下,NFS客户端挂载依然没有自动恢复,这和我之前在阿里的运维经验不同(NFS客户端应该在NAS恢复时自动恢复)。我通过持续的ping 192.168.106.1确认vmnet持续保持流量情况下,依然出现NFS挂起,这说明Colima NFS异常和vmnet没有直接关系
resvport可能是导致无法重新挂载NFS的元凶:resvport要求在1024端口以下对macOS服务端发起连接,这对于前一个NFS连接挂起(umount掉以后也可嫩更没有释放端口资源)再发起连接可能存在bug还是使用原先的resvport去掉这个
resvport参数以后,我验证发现至少soft挂载能够轻松地umount并且重新mount
我现在怀疑是Colima VM自身的NFS软件堆栈在长时间没有数据传输情况下进入了假死状态,这个问题可能和虚拟化有关
为了验证假设,我采用简单的脚本命令每10秒钟向NFS挂载目录写入一个时间戳,看看是否能够通过保持数据读写来keepalive:
> $MOUNT_POINT/nfs_watchdog.log;while true;do date >> $MOUNT_POINT/nfs_watchdog.log;sleep 10;done
经过验证,采用每10秒写入一次NFS能够keepalive保持Colima VM的挂载正常
还是恢复了 hard 挂载,而单纯采用 /Users/admin/docs 数据目录挂载,避免了目录冲图。这种 hard 挂载模式稳定性极佳。
最后,在 default.yaml 中采用了 crontab 方式每分钟写一次 /Users/admin/nfs_watchdog 文件的时间戳记录,这样就能够保证NFS不挂死:
# 设置cron每分钟写一次nfw_watchdog文件时间戳来保持NFS
if ! crontab -l 2>/dev/null | grep -q "nfs_watchdog"; then
echo "* * * * * /usr/bin/date > $MOUNT_POINT/nfs_watchdog" | crontab -
fi
性能参数
resvport
在 macOS 的NFS Clinet启用 resvport 对性能影响很大:
resvport(reserved port)指NFS客户单使用一个私有的低于1024"保留的"TCP/UDP源端口来连接服务器,这意味着客户端具备root权限(常规用户无法绑定低于1024的端口)。这个设计是NFS历史上用于验证请求是从私有的信任的客户端发起的。macOS 对
noresvport发起的请求会采用更为严格要求的安全沙箱进行隔离,这导致性能消耗严重
通过 Sphinx文档 的 time make html 对比,大致能够得出采用 resvport 的I/O性能大约比 noresvport 提升 36% :
启用 resvport参数,hard)real 7m24.930s
user 5m36.570s
sys 0m10.935s
不过,在没有启用 resvport 参数的 hard 挂载同样测试性能差一些,完成时间减慢到将近12分钟:
去除 resvport参数,hard)real 11m38.909s
user 7m32.720s
sys 0m43.471s
需要注意:
resvport是 “动作型参数” ,和通常通过-o传递给NFS client的 "状态型参数" (rsize,wsize,noatime,soft等)不同:状态型参数是在整个挂载生命周期内,文件西欧通难过如何表现,内核需要通过状态型参数永久记录在系统的
mount表中供调用者查询动作型参数(
resvport)本质上是一个 "临时的连接协商指令" ,内核的 RPC 客户端(sunrpc)在建立物理 TCP 三次握手的那一瞬间,看到resvport,就会启动特权端口分配逻辑(在0–1023之间挑一个绑死)。一旦 TCP 握手成功(ESTABLISHED),这个参数的使命就彻底完成了。 Linux 内核并不会把这个属于套接字(Socket)层面的临时握手标记,当成文件系统的属性保存在/proc/mounts里,所以使用mount命令差看不到,而需要通过以下命令检查客户端端口:
# 过滤出所有连接到 Mac 宿主机(192.168.106.1)NFS 端口(2049)的本地套接字
ss -antp | grep 192.168.106.1:2049
这里可以看到客户端本地端口是 812