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服务来取代 sshfs9p 存储挂载,虽然Colima没有内置支持这种NFS简洁方便的配置,但是可以采用 provision 脚本来实现通用型NFS挂载,来替代默认但性能底下的 mountType

快速起步

备注

本段设置方法是我的实践最终汇总,已经验证成功

  • 配置 /etc/nfs.conf 性能优化配置

性能优化的NFS v3 /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 目录
/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
使用NFS存储方式测试编译Sphinx文档的耗时(启用resvport参数,hard)
real    7m24.930s
user    5m36.570s
sys     0m10.935s

不过,在没有启用 resvport 参数的 hard 挂载同样测试性能差一些,完成时间减慢到将近12分钟:

可以看到完成时间惊人地缩短到 7m25s ,是原本使用 sshfs 时间的 1/5 不到,已经接近物理主机的的编译性能

使用NFS存储方式测试编译Sphinx文档的耗时(去除resvport参数,hard)
real    11m38.909s
user    7m32.720s
sys     0m43.471s

备注

macOS系统 NFS v4 服务 实践没有成功

异常排查

备注

以下是我的一次折腾之旅的记录,仅供参考。正确的方法则见本文前半部分!!!

  • 我最初采用的NFS v4配置

/etc/nfs.conf :

配置启用NFSv4
# 强制开启 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 工具来检测服务端口可达性:

执行端口2049检查
nc -zv -w 5 192.168.5.2 2049

输出显示端口是通的:

执行端口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主机一样:

在Colima VM中观察 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
在macOS Host主机上观察 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 端口的请求。这里看到的 portmapperstatus 但完全没有 nfsmountd 的输出,实际上是QEMU进程自己伪造并返回给虚拟机的应答。

解决方法

macOS内核提供了 仅限主机(Host-Only)的虚拟二层交换机(Virtual Switch)网络 :

  • macOS 内核原生的 vmnet.framework (苹果官方虚拟化网络框架)会在系统底层虚拟出一块物理网卡(通常叫 fnetworkvmnetX ),并直接分配给 Colima 虚拟机作为副卡。

  • 虚拟机和 Mac 宿主机变成了同一个局域网内的两台独立主机。它们之间的通信直接走 macOS 内核的二层转发,QEMU 再也没有机会在半路拦截 111 或 858 端口。

  • 192.168.106.x 网段是 Colima 底层依赖的开源网络组件 Lima: Linux Machines (Linux virtual machines on macOS)在源码里硬编码死锁的默认保留网段。

  • 方法一: 在命令行启用 address 网络

colima命令行启用address网络
colima start --network address
  • 方法二: 修订 ~/.colima/_template/default.yaml 启用 address 网络:

修订Colima虚拟机模板配置 ~/.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个网卡 :

在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服务:

检查address网络上的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中测试挂载

命令行挂载NFS v3
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 可以看到:

默认挂载HOME目录,在VM内部检查
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会看到一个特殊的 缓存挂载 :

当使用了定制挂载,Colima会挂载一个系统级的 缓存挂载
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内部的挂载会提示报错:

当主机休眠后恢复soft挂载会提示I/O错误
df: /Users/admin/docs: Input/output error

不过系统不会挂死,此时需要手工umount并重新mount一次进行恢复。

为了能够自动完成恢复,gemini提供了一段脚本定期检查和恢复,参考如下

通过定时任务来检查soft挂载是否异常,并自动重新挂载
# =========================================================================
# 基础设施拓扑:强开 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暂停以后缓存机制失效而死机。

我对比了 hardsoft 两种挂载方式:

  • 只要没有 /Users/admin/Library/Caches/colima/Users/admin (NFS挂载) 的挂载冲突,那么不管 hardsoft NFS挂载都不会导致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
  • 理论上 softhard 的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 link down/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:

持续写入NFS文件看能否保活
> $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不挂死:

定时每分钟写一次nfs_watchdog文件来保持NFS keepalive
        # 设置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% :

使用NFS存储方式测试编译Sphinx文档的耗时( 启用 resvport参数,hard)
real    7m24.930s
user    5m36.570s
sys     0m10.935s

不过,在没有启用 resvport 参数的 hard 挂载同样测试性能差一些,完成时间减慢到将近12分钟:

使用NFS存储方式测试编译Sphinx文档的耗时( 去除 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 命令差看不到,而需要通过以下命令检查客户端端口:

检查NFS客户端端口
# 过滤出所有连接到 Mac 宿主机(192.168.106.1)NFS 端口(2049)的本地套接字
ss -antp | grep 192.168.106.1:2049

这里可以看到客户端本地端口是 812