基于对极空间 ZOS 的逆向分析 + 通用工程实践 目标:从零设计一套跨平台、可维护的 NAS 软件体系
本篇是系列的收尾:把前面几篇拆解出来的结论,反过来变成一份"如果自己做 NAS 系统该怎么设计"的方案。
一、最重要的决策:选对技术栈
在开始编码之前,有四个决策会决定整个项目的命运。
决策 1:Base OS — Buildroot 还是 Debian?
这是第一个分岔口,选错后面都得还债。
| Buildroot/Yocto | Debian/Ubuntu | |
|---|---|---|
| 镜像体积 | 极小(几十 MB) | 大(几百 MB) |
| 开发效率 | 低(交叉编译、依赖地狱) | 高(apt install 一把梭) |
| 硬件适配 | 需要写 board support package | 有内核就能跑 |
| 安全更新 | 自己维护 | 上游社区支持 |
| 定制程度 | 完全控制 | 受限于发行版 |
极空间的选择:Debian 11 打底,没用 Buildroot。这是对的。
建议:用 Debian 或 Ubuntu Server 做 base,走极空间的路。理由:
- 初期团队小,没精力维护 BSP
- Go 应用层不需要 Buildroot 的裁剪能力
- Debian 的 arm64 支持已经很成熟
- 安全更新由社区负责
如果你需要极小的镜像体积(IoT 场景),用 Buildroot 构建一个最小 rootfs,但只放内核 + systemd + ssh + docker/containerd。所有应用层用 Go 静态编译,不依赖发行版的包管理器。
决策 2:编程语言 — Go 几乎不用想
极空间 90%+ 用 Go,这不是偶然。
| 需求 | Go | Rust | C/C++ | Python |
|---|---|---|---|---|
| 交叉编译 ARM/x86 | 原生支持 | 支持 | 工具链复杂 | 不需要编译 |
| 静态链接 | 是(CGO_ENABLED=0) | 是 | 是 | 不适用 |
| 内存安全 | GC | 编译期 | 手动 | GC |
| 开发速度 | 快 | 慢(学习曲线) | 慢 | 快 |
| 运行时开销 | 低 | 极低 | 极低 | 高 |
| 生态(网络/存储) | 极好 | 好 | 分散 | 好 |
结论:Go 是唯一同时满足「交叉编译简单 + 静态链接 + 开发快 + 生态好」的语言。
少数场景例外:
- 内核模块(zfuse 等文件系统组件)→ C
- 高性能转码 → C/C++ + FFmpeg,Go 通过 CGO 或子进程调用
- AI 推理 → C++(直接绑 ONNX Runtime 或 RKNN),Go 通过 subprocess 调用
决策 3:Monorepo 还是多仓库?
极空间是 Monorepo。对于 5-20 人的团队,Monorepo 利大于弊:
- 统一的构建工具链(一个 Go 版本、一套 CI 配置)
- 跨服务重构容易(改 API 协议时一起改)
- 版本管理简单(一个 commit 对应所有服务的状态)
当团队超过 50 人、服务超过 100 个时再考虑拆分。
决策 4:要不要 Docker?
极空间的选择:不用 Docker 跑应用,用原生 Go 二进制 + systemd。
这是嵌入式场景的务实选择。但如果你的目标用户需要自定义扩展(像群晖的 Docker 套件),你必须支持 Docker。
建议:分两层——
- 平台服务(API、存储、文件共享):Go 原生二进制 + systemd
- 用户应用:Docker Compose,通过 Portainer 或自研面板管理
这样你既保证了核心系统的轻量和可控,又给了用户扩展能力。
二、存储层设计
这是 NAS 的命根子。自研文件系统(zfuse)这种玩法风险太大——极空间有钱养内核团队,你没有。
建议方案:标准化组件
物理磁盘
│
▼
Linux md (mdadm) — 软件 RAID
│
▼
LVM — 逻辑卷管理(可选,灵活调整分区大小)
│
▼
btrfs / ext4 / xfs — 成熟文件系统
│
▼
mergerfs — 多盘池化(UnionFS 风格)
│
▼
应用层(Samba / NFS / WebDAV 直接读 mergerfs 挂载点)
为什么不用 ZFS?
ZFS 的 Linux 支持(OpenZFS)确实成熟,但:
- 许可证问题(CDDL vs GPL),不能随内核分发
- 内存消耗大(ARC 缓存建议 1GB+)
- ARM 平台支持不如 x86
- 内核升级时需要手动跟进 ZFS 模块版本
建议:入门设备用 btrfs + mergerfs,高端设备可选 ZFS。
mergerfs 是关键
# 把多个硬盘合并成一个逻辑池
mergerfs -o defaults,allow_other,use_ino,category.create=epmfs \
/mnt/disk1:/mnt/disk2:/mnt/disk3 \
/pool/data
- 每个盘仍是独立文件系统(坏了不会全丢)
- 文件不拆分,单盘损坏只丢该盘数据
- 支持热添加/移除磁盘
- 配合 SnapRAID 可以做冗余(非实时但够用)
三、服务架构设计
核心原则:API Gateway + 微服务 + 共享基础设施
不是每个服务都要独立进程。按调用频率和数据依赖来拆分:
┌─────────────────┐
│ OpenResty/Nginx │
│ (API Gateway) │
│ :80 :443 │
└────────┬────────┘
│
┌──────────────┬───────────┼───────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ core-api│ │ file │ │ media │ │ remote │ │ user │
│ 用户/权限│ │ 文件管理│ │ 转码/ │ │ 远程 │ │ apps │
│ 设备管理 │ │ 共享/ │ │ 缩略图 │ │ 访问 │ │ Docker │
│ :8050 │ │ 搜索 │ │ :8060 │ │ :22345 │ │ :9123 │
└────┬────┘ └────┬────┘ └─────────┘ └─────────┘ └─────────┘
│ │
┌────┴────┬─────────┴──────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│MySQL │ │Redis │ │SQLite│
│PostgreSQL│ │ │ (嵌入式)│
└──────┘ └──────┘ └──────┘
基础设施选择
| 组件 | 选择 | 理由 |
|---|---|---|
| API 网关 | OpenResty (Nginx+Lua) | Lua 脚本做 token 改写、限流、动态路由 |
| 关系数据库 | PostgreSQL | 比 MySQL 更适合 Go(更好的 JSON 支持、更严格的 SQL) |
| 缓存 | Redis | 标准选择,session 和热点数据 |
| 消息队列 | NATS 或 Redis Streams | 轻量,不需要 Kafka 的复杂度 |
| 对象存储 | MinIO | S3 兼容,Go 原生 |
| 搜索引擎 | Bleve (Go 库) 或 ZincSearch | 嵌入 Go 进程,不需要 Elasticsearch |
| 文件共享 | Samba + NFS + WebDAV | 直接调用系统服务 |
为什么不要过度拆分
极空间有 95 个服务目录,但很多是历史遗留 + 重复(go-baseset 和 go-basesetAdv 应该合并,zbasic 和 webapi 功能重叠)。
初期建议 5-8 个核心服务:
- core:用户、认证、设备管理(对极空间的 go-baseset)
- files:文件 CRUD、分享、搜索
- storage:磁盘管理、RAID、存储池
- media:转码、缩略图、AI 索引
- remote:P2P 穿透、relay、DDNS
- apps:Docker 容器管理、应用商店
四、远程访问:最复杂的模块
这是区分 NAS 产品水平的核心能力。建议分三个阶段实现。
阶段一(MVP):frp 反向代理
最快能跑起来的方案:
- 部署 frps 到自己的云服务器
- 设备端集成 frpc 客户端,开机自动注册隧道
- 用 frp 的 admin API 动态管理隧道
- 给每个用户分配独立子域名
优点:一周能上线。缺点:所有流量经你的服务器,带宽成本高。
阶段二:STUN + P2P 直连
核心是 NAT 穿透:
┌─────────────────────────────────────┐
│ 你的云服务 │
│ │
│ STUN Server (coturn) │
│ Signaling Server (自研 WebSocket) │
│ Relay Server (TURN/自研 TCP relay) │
│ │
└──────────┬──────────────────────────┘
│
┌──────▼──────┐
│ NAS 设备 │
│ │
│ P2P Manager│ ← 自研 Go 服务
│ (类似zrps) │
│ │
│ 1. UPnP 端口映射 │
│ 2. STUN 获取公网地址 │
│ 3. UDP/TCP 打洞 │
│ 4. 成功后通知信令服务器 │
└─────────────┘
核心技术选型:
| 功能 | 开源方案 | 建议 |
|---|---|---|
| STUN/TURN | coturn | 直接用,成熟稳定 |
| UDP 打洞 | pion/webrtc 或自己实现 | 自己实现 UDP hole punching 不复杂 |
| TCP 打洞 | 自己实现或 TCP simultaneous open | 支持率低,优先 UDP |
| 信令 | WebSocket + Protobuf | 自研,Go 写很容易 |
| Relay | TCP 透明代理 | 自研,类似 zrps 的 8041 端口 |
P2P 打通率预估:
| NAT 类型组合 | P2P 可行? | 方案 |
|---|---|---|
| Full Cone ↔ 任意 | 直连 | 直接 IP:Port |
| Restricted Cone ↔ Restricted Cone | UDP 打洞 | 双方同时发包 |
| Symmetric ↔ 非 Symmetric | UDP 打洞(端口预测) | 成功率中等 |
| Symmetric ↔ Symmetric | 不可行 | Relay 中转 |
| CGNAT(移动 4G/5G) | 不可行 | Relay 中转 |
国内运营商现状:家用宽带大多是 Full Cone 或 Restricted Cone,P2P 成功率在 70-80%。移动端 4G/5G (CGNAT) 必须走 Relay。
阶段三:设备间互联 + 智能路由
极空间的 zconn 协议是这个阶段的标杆——用户连一台设备就能访问所有设备。
实现思路:
// 设备注册到信令服务器时上报自己的 P2P 地址和 Relay 地址
type DeviceInfo struct {
ID string
P2PAddr string // UDP 打洞后的公网地址
RelayAddr string // 通过中继的地址
}
// 信令服务器存储设备拓扑
// 连接时返回最佳路径:直连 > P2P > Relay > frp
func GetBestPath(from, to DeviceInfo) ConnectionPath {
if canDirectConnect(from, to) {
return DirectPath{addr: to.P2PAddr}
}
if canP2P(from, to) {
return P2PPath{addr: to.P2PAddr, requiresPunching: true}
}
return RelayPath{relayAddr: config.RelayServer}
}
Nginx 层面的实现(借鉴极空间 5056 配置):
# zconn 协议 — 设备间路由
location /zconn/ {
# Lua 解析目标设备 ID
# 本地请求 → proxy_pass 到本地服务
# 远程请求 → proxy_pass 到 relay proxy (:8041)
set_by_lua_block $target {
local target_id = parse_uri(ngx.var.uri)
if target_id == self_device_id then
return "127.0.0.1:5055"
else
return "127.0.0.1:8041" -- relay proxy
end
}
proxy_pass http://$target;
}
五、构建与交付体系
CI/CD 流水线
Git Push
│
▼
Jenkins / GitHub Actions
│
├── 1. Lint + Test (所有服务)
│
├── 2. 交叉编译
│ ├── GOOS=linux GOARCH=arm64 → ARM 设备
│ └── GOOS=linux GOARCH=amd64 → x86 设备
│
├── 3. 完整性校验
│ └── 生成 filehash.json (MD5/SHA256)
│
├── 4. 打包
│ └── <service>/<version>/<arch>.tar.gz
│
└── 5. 发布到 OTA 服务器
└── 设备端定期检查更新、增量更新
目录结构建议
nas-os/
├── base/ # 系统根文件系统
│ ├── rootfs/ # Debian 最小 rootfs + 定制
│ └── kernel/ # 内核配置和补丁
│
├── platform/ # 平台服务(Go monorepo)
│ ├── core/ # 核心 API
│ ├── files/ # 文件管理
│ ├── storage/ # 存储管理
│ ├── media/ # 媒体处理
│ ├── remote/ # 远程访问
│ │ ├── p2p/ # P2P 穿透管理器
│ │ ├── relay/ # Relay 代理
│ │ └── ddns/ # DDNS 客户端
│ ├── apps/ # 应用商店
│ ├── api-gateway/ # OpenResty 配置 + Lua 模块
│ └── shared/ # 共享库(proto 定义、工具函数)
│
├── apps/ # 用户应用
│ ├── photos/ # 相册
│ ├── videos/ # 视频
│ └── music/ # 音乐
│
├── web/ # Web 前端
│ └── admin/ # 管理后台 (React/Vue)
│
├── tools/ # 构建和部署工具
│ ├── builder/ # 统一构建脚本
│ └── toolchain/ # ARM 交叉编译工具链
│
├── deploy/ # 部署配置
│ ├── systemd/ # systemd unit 模板
│ └── ota/ # OTA 升级配置
│
└── docs/ # 文档
构建脚本统一化
极空间每个服务复制一份 build.sh 是反面教材。正确做法:
// tools/builder/main.go — 统一构建工具
// builder build --service=files --arch=arm64 --output=./dist
// builder build --all --arch=amd64
// builder package --service=files --version=1.2.3
或者至少用一个 Makefile 或 Taskfile,不要每个服务拷一份。
六、跨平台策略
三层跨平台
┌─────────────────────────────────────┐
│ Layer 3: Go 应用层 │
│ GOOS=linux GOARCH=arm64/amd64 │
│ 90% 代码共享 │
├─────────────────────────────────────┤
│ Layer 2: 架构特定依赖 │
│ arm64/* vs amd64/* 目录 │
│ FFmpeg, AI 模型, 硬件编解码 SDK │
│ Git LFS 管理 │
├─────────────────────────────────────┤
│ Layer 1: 内核 + 驱动 │
│ 每平台独立编译 │
│ RK3568/3588/Intel 各一套 defconfig │
└─────────────────────────────────────┘
关键点:不要让平台差异渗透到应用层。Go 代码不应该出现 if runtime.GOARCH == "arm64"。平台差异全部封装在 Layer 2 的目录结构里,构建时选择。
七、实施路线图
Phase 1:MVP(2-3 人,3 个月)
目标:能在 ARM 开发板上跑起来的 NAS Demo
- Debian rootfs + 内核裁剪
- 存储管理(mdadm + btrfs + mergerfs)
- Samba + WebDAV 基础文件共享
- core-api(用户注册/登录/权限)
- Web 管理后台(基础版)
- frp 远程访问(阶段一)
不做的:App 生态、P2P 穿透、媒体处理、多设备互联
Phase 2:可用产品(4-6 人,6 个月)
目标:能卖的最小产品
- P2P 穿透(STUN + UDP 打洞)
- 相册 + 视频基础功能
- 文件搜索(Bleve)
- OTA 升级系统
- x86 平台适配
- 手机 App(Flutter,一套代码 iOS/Android)
Phase 3:完整系统(8-12 人,12 个月)
目标:对标极空间
- Relay 中继 + 设备间互联
- Docker 应用管理
- AI 相册(人脸识别、场景分类)
- 视频转码 + 硬解(RKMPP/VAAPI)
- 应用商店
- 同步/备份(Syncthing 集成或自研)
- 监控录像(ONVIF/RTSP 接入)
八、需要自研的核心模块(按难度排序)
必须自研
| 模块 | 为什么不能只用开源 | 工作量 |
|---|---|---|
| P2P Manager | 没有现成的 NAS 专用 P2P 方案 | 高(2-3 人月) |
| API Gateway 配置 | 需要 + 业务逻辑深度绑定 | 中 |
| OTA 升级 | 与分区布局/回滚强耦合 | 中 |
| Web 管理后台 | 用户体验差异化的关键 | 中 |
可以先用开源
| 模块 | 开源方案 |
|---|---|
| STUN/TURN | coturn |
| frp 兜底 | frp |
| 文件共享 | Samba + NFS + WebDAV(配置管理) |
| 搜索 | Bleve(Go 嵌入) |
| 数据库 | PostgreSQL + Redis |
| 对象存储 | MinIO |
不建议自研
| 模块 | 理由 |
|---|---|
| FUSE/内核文件系统 | 极空间自研 zfuse 是它们的核心技术,但没有内核团队极难做好,bug 会丢数据 |
| RAID 引擎 | 直接用 Linux md,社区维护了几十年 |
| 数据库 | 无意义 |
| 视频编解码器 | 直接绑 FFmpeg |
九、云服务基础设施
远程访问依赖你的云服务。最小需要的服务:
你的云服务器(建议 3 台,国内多地域)
├── STUN/TURN (coturn) → 端口 3478
├── frps → 端口 10050
├── Signaling Server → WebSocket (自研)
├── Relay Server → TCP 代理 (自研)
├── OTA 更新服务器 → HTTP + CDN
├── API 服务器 (用户账户) → 与设备端 core-api 同一套代码
└── Nginx (SSL 终止 + 域名路由)
云服务可以用 Go 写,和设备端共享代码。用户账户系统是跨设备的,需要在云端有一份。
十、总结
如果只记住一件事:Go monorepo + Debian base + 渐进式远程访问。
具体说:
- 用 Debian 做 rootfs,别碰 Buildroot
- 所有应用层 Go,用一个仓库
- 存储层用成熟方案(mdadm + btrfs + mergerfs),别自研文件系统
- 远程访问三阶段:先上 frp,再做 P2P,最后搞设备互联
- 初期不要超过 5 个微服务,后期按需拆分
- 代码和构建工程统一化,别每个服务拷一份 build.sh
- 把精力投在 P2P 穿透上,这是和开源 NAS 拉开差距的地方