博客 · Infrastructure

如何搭建 Mac mini 集群

Mac mini 集群搭建实用指南:节点角色规划、用 Brewfile 和 Ansible 统一配置、SSH 密钥、Runner 标签、监控与分批更新。

Mac mini 集群好不好用,关键在于一致性。本文介绍如何搭建一个可以随时重建、扩容和更新的 Mac mini 集群。

先规划节点角色,再开始配置

先列出这些机器到底要做什么。大多数小型集群最终只有两三种角色,提前写清楚可以省去后期大量返工。

  • 构建节点:运行 CI 任务,包括 Xcode 构建、单元测试、模拟器 UI 测试、归档与签名。需要足够的磁盘空间存放 DerivedData 和模拟器运行时。
  • 工具节点:承载共享服务,例如依赖缓存、制品存储、监控采集。一台普通配置的机器通常就够了。
  • 推理或批处理节点:运行本地 LLM 服务、媒体处理等长时间任务,最看重内存。

按角色选择硬件。对很多 iOS 项目来说,16GB 的 Mac mini M4 或 M6 就是合适的构建节点。大型应用、并行模拟器和较重的 Swift 编译更适合 24GB 的 Mac mini M6 或 M5 Pro。如果某个角色需要大得多的内存,比如运行 30B 或 70B 级模型,那就该考虑 Mac Studio 集群了。

同一角色尽量使用相同配置。节点一致,调度就简单:任何任务都能跑在任何带对应标签的机器上。

节点命名与 SSH 访问

选一个能体现角色和序号的命名规则,并同时设置 macOS 的三个名称,避免工具之间不一致:

sudo scutil --set ComputerName  mac-ci-01
sudo scutil --set HostName      mac-ci-01
sudo scutil --set LocalHostName mac-ci-01

接着让管理机通过密钥登录每个节点。建议为集群管理单独生成一把密钥,不要复用个人密钥:

ssh-keygen -t ed25519 -f ~/.ssh/mac-cluster -C "cluster-admin"
ssh-copy-id -i ~/.ssh/mac-cluster.pub admin@mac-ci-01

然后在 ~/.ssh/config 中添加配置,让包括 Ansible 在内的所有工具使用同一套设置:

Host mac-ci-*
  User admin
  IdentityFile ~/.ssh/mac-cluster
  ServerAliveInterval 30

密钥登录确认可用后,在 /etc/ssh/sshd_config 中关闭密码登录(设置 PasswordAuthentication no),并保留至少一种备用访问方式,例如屏幕共享。如果你还不熟悉 macOS 远程访问,可以先看如何访问远程 Mac。

用同一种方式配置每个节点

Mac mini 集群里构建不稳定,最常见的原因是配置漂移:某个节点的 Xcode、Ruby 或 CocoaPods 版本和其他节点不同。解决办法是使用可以反复执行的声明式配置。

第一层:Brewfile

Homebrew Bundle 会安装 Brewfile 中列出的所有软件,已安装的会自动跳过。把它提交到所有节点都能读取的仓库:

# Brewfile
brew "git"
brew "jq"
brew "xcodes"
brew "swiftlint"
brew "node_exporter"
cask "temurin"
brew bundle --file=./Brewfile
brew bundle check --file=./Brewfile

brew bundle check 适合放进 CI 健康检查:节点一旦漂移,它会返回非零退出码。

第二层:Xcode 与系统设置

Xcode 不是普通的 Homebrew 软件包。可以用 xcodes 安装指定版本,再显式切换:

xcodes install 16.4
sudo xcode-select -s /Applications/Xcode-16.4.app
sudo xcodebuild -license accept
xcodebuild -runFirstLaunch

把电源和睡眠设置写进同一个脚本,避免构建途中节点进入睡眠:

sudo pmset -a sleep 0 displaysleep 0 disksleep 0

第三层:通过 SSH 使用 Ansible

机器超过两三台后,就应该从一个地方统一执行。Ansible 在 macOS 上很好用,因为目标机只需要 SSH 和 Python。一个最小的清单和 playbook:

# inventory.ini
[build]
mac-ci-01
mac-ci-02
mac-ci-03

[utility]
mac-util-01
# site.yml
- hosts: all
  tasks:
    - name: Copy Brewfile
      ansible.builtin.copy:
        src: Brewfile
        dest: /Users/admin/Brewfile
    - name: Install packages
      ansible.builtin.command: /opt/homebrew/bin/brew bundle --file=/Users/admin/Brewfile
      register: bundle
      changed_when: "'Installing' in bundle.stdout"
ansible-playbook -i inventory.ini site.yml --limit build

--limit 参数后面会很重要:分批更新就靠它把变更只推送到部分节点。

共享缓存,但不共享状态

缓存能显著加快集群速度,但共享可变状态会带来最难排查的问题。一条好用的原则:共享下载内容,构建产物留在本地。

  • DerivedData 留在各节点本地。通过网络共享既慢又容易损坏。
  • 依赖集中缓存。让 Swift Package Manager、CocoaPods、npm 或 Gradle 走工具节点上的缓存代理,或者使用 CI 系统自带、以 lockfile 为键的缓存。
  • 启用 macOS 内容缓存(系统设置 › 通用 › 共享),同一网络里的其他 Mac 可以从工具节点获取 Apple 软件更新。
  • 编译缓存:ccache 或 sccache 对 C、C++ 和 Rust 代码很有帮助,网络允许的话可以配共享后端。

节点之间能否通过内网互通,取决于托管方式。如果你在 Hatmac 租用并需要机器互联,请告诉我们你的需求。

在集群中分发任务

带标签的 CI Runner

CI 任务的调度交给 CI 系统。每个节点注册一个 Runner,用标签描述能力。以 GitHub Actions 自托管 Runner 为例:

./config.sh --url https://github.com/your-org/your-repo \
  --token YOUR_TOKEN --name mac-ci-01 \
  --labels macos,arm64,xcode-16.4 --unattended
./svc.sh install
./svc.sh start

工作流里写 runs-on: [self-hosted, macos, xcode-16.4] 即可。升级 Xcode 时,给已升级的节点加新标签,再逐步迁移工作流。GitLab Runner 的 tags 和 Jenkins agent 的 labels 也是同样的思路。详见 GitHub 自托管 Runner 官方文档,或我们的 GitHub Actions Mac Runner 页面。

分布式推理

把一个大模型拆到多台 Mac 上运行是可行的,但仍在发展中。llama.cpp 有 RPC 后端,MLX 也提供分布式原语,都能把计算分摊到多台机器。节点间的网络延迟会限制吞吐,也需要不少调优。对大多数团队来说,更简单的做法效果更好:每个节点运行一个完整模型,前面放一个负载均衡器,让每个请求只落在一台机器上。

监控基础

起步不需要庞大的监控栈。每个节点先关注四项:剩余磁盘、内存压力、CPU 负载、Runner 服务是否在线。

df -h /
memory_pressure | tail -1
uptime
launchctl list | grep actions.runner

需要看板时,上面 Brewfile 装好的 node_exporter 会暴露指标,可由工具节点上的 Prometheus 采集。最先要配的告警是磁盘空间不足:Mac 上的 CI 故障,多数是 DerivedData 和模拟器运行时把磁盘占满造成的。每周跑一次清理任务会很有帮助:

rm -rf ~/Library/Developer/Xcode/DerivedData/*
xcrun simctl delete unavailable

维护:逐个节点分批更新

不要一次更新所有节点。分批推进可以让集群在你验证变更时继续处理任务。

  1. 摘下一个节点。暂停它的 Runner 或去掉标签,不再接新任务。
  2. 更新它。先查看再安装:softwareupdate -l,然后 sudo softwareupdate -i -a -R。Xcode 变更通过 playbook 完成。
  3. 跑一次金丝雀任务。只在这个节点上触发一次真实的构建和测试。
  4. 逐步推进。使用 ansible-playbook --limit mac-ci-02 等命令,每次一到两个节点。
  5. 保留回退余地。新版本稳定运行几天之前,至少留一个节点在旧版本。

在清单中记录每个节点的 macOS 和 Xcode 版本,任何人都能一眼看清集群状态。

自建还是租用硬件

上述步骤在自有机器和租用机器上完全一样。租用省去了上架、供电和硬件故障处理。Hatmac 的每台机器都是独享的物理 Mac,不是虚拟机,拥有完整管理员权限,所以本文的配置步骤可以直接使用。Mac mini M4 和 M6 支持按月租用,租期越长月均越便宜,详见当前价格。想全面比较利弊,可以阅读租还是买 Mac mini。

常见问题

Mac mini 集群至少需要几台?

实际最少两台:一台摘下来更新时,另一台继续跑任务。CI 排队时间变长时再加节点。

用 Ansible 还是 shell 脚本?

两三台机器用脚本就够了。需要针对部分节点执行、并且能安全地重复执行变更时,Ansible 的优势就体现出来了。

一个集群里能混用不同型号的 Mac mini 吗?

可以。用 Runner 标签标明内存和芯片,让较重的任务落到配置更高的机器上。

mac mini clusteransiblehomebrewci runnersdevops