番外:自研 NAS 系统设计方案

2026/05/01

基于对极空间 ZOS 的逆向分析 + 通用工程实践 目标:从零设计一套跨平台、可维护的 NAS 软件体系

本篇是系列的收尾:把前面几篇拆解出来的结论,反过来变成一份"如果自己做 NAS 系统该怎么设计"的方案。


一、最重要的决策:选对技术栈

在开始编码之前,有四个决策会决定整个项目的命运。

决策 1:Base OS — Buildroot 还是 Debian?

这是第一个分岔口,选错后面都得还债。

Buildroot/YoctoDebian/Ubuntu
镜像体积极小(几十 MB)大(几百 MB)
开发效率低(交叉编译、依赖地狱)高(apt install 一把梭)
硬件适配需要写 board support package有内核就能跑
安全更新自己维护上游社区支持
定制程度完全控制受限于发行版

极空间的选择:Debian 11 打底,没用 Buildroot。这是对的。

建议:用 Debian 或 Ubuntu Server 做 base,走极空间的路。理由:

如果你需要极小的镜像体积(IoT 场景),用 Buildroot 构建一个最小 rootfs,但只放内核 + systemd + ssh + docker/containerd。所有应用层用 Go 静态编译,不依赖发行版的包管理器。

决策 2:编程语言 — Go 几乎不用想

极空间 90%+ 用 Go,这不是偶然。

需求GoRustC/C++Python
交叉编译 ARM/x86原生支持支持工具链复杂不需要编译
静态链接是(CGO_ENABLED=0)是是不适用
内存安全GC编译期手动GC
开发速度快慢(学习曲线)慢快
运行时开销低极低极低高
生态(网络/存储)极好好分散好

结论:Go 是唯一同时满足「交叉编译简单 + 静态链接 + 开发快 + 生态好」的语言。

少数场景例外:

决策 3:Monorepo 还是多仓库?

极空间是 Monorepo。对于 5-20 人的团队,Monorepo 利大于弊:

当团队超过 50 人、服务超过 100 个时再考虑拆分。

决策 4:要不要 Docker?

极空间的选择:不用 Docker 跑应用,用原生 Go 二进制 + systemd。

这是嵌入式场景的务实选择。但如果你的目标用户需要自定义扩展(像群晖的 Docker 套件),你必须支持 Docker。

建议:分两层——

这样你既保证了核心系统的轻量和可控,又给了用户扩展能力。


二、存储层设计

这是 NAS 的命根子。自研文件系统(zfuse)这种玩法风险太大——极空间有钱养内核团队,你没有。

建议方案:标准化组件

物理磁盘
    │
    ▼
Linux md (mdadm) — 软件 RAID
    │
    ▼
LVM — 逻辑卷管理(可选,灵活调整分区大小)
    │
    ▼
btrfs / ext4 / xfs — 成熟文件系统
    │
    ▼
mergerfs — 多盘池化(UnionFS 风格)
    │
    ▼
应用层(Samba / NFS / WebDAV 直接读 mergerfs 挂载点)

为什么不用 ZFS?

ZFS 的 Linux 支持(OpenZFS)确实成熟,但:

建议:入门设备用 btrfs + mergerfs,高端设备可选 ZFS。

mergerfs 是关键

# 把多个硬盘合并成一个逻辑池
mergerfs -o defaults,allow_other,use_ino,category.create=epmfs \
  /mnt/disk1:/mnt/disk2:/mnt/disk3 \
  /pool/data

三、服务架构设计

核心原则: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 的复杂度
对象存储MinIOS3 兼容,Go 原生
搜索引擎Bleve (Go 库) 或 ZincSearch嵌入 Go 进程,不需要 Elasticsearch
文件共享Samba + NFS + WebDAV直接调用系统服务

为什么不要过度拆分

极空间有 95 个服务目录,但很多是历史遗留 + 重复(go-baseset 和 go-basesetAdv 应该合并,zbasic 和 webapi 功能重叠)。

初期建议 5-8 个核心服务:

  1. core:用户、认证、设备管理(对极空间的 go-baseset)
  2. files:文件 CRUD、分享、搜索
  3. storage:磁盘管理、RAID、存储池
  4. media:转码、缩略图、AI 索引
  5. remote:P2P 穿透、relay、DDNS
  6. apps:Docker 容器管理、应用商店

四、远程访问:最复杂的模块

这是区分 NAS 产品水平的核心能力。建议分三个阶段实现。

阶段一(MVP):frp 反向代理

最快能跑起来的方案:

优点:一周能上线。缺点:所有流量经你的服务器,带宽成本高。

阶段二: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/TURNcoturn直接用,成熟稳定
UDP 打洞pion/webrtc 或自己实现自己实现 UDP hole punching 不复杂
TCP 打洞自己实现或 TCP simultaneous open支持率低,优先 UDP
信令WebSocket + Protobuf自研,Go 写很容易
RelayTCP 透明代理自研,类似 zrps 的 8041 端口

P2P 打通率预估:

NAT 类型组合P2P 可行?方案
Full Cone ↔ 任意直连直接 IP:Port
Restricted Cone ↔ Restricted ConeUDP 打洞双方同时发包
Symmetric ↔ 非 SymmetricUDP 打洞(端口预测)成功率中等
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

不做的:App 生态、P2P 穿透、媒体处理、多设备互联

Phase 2:可用产品(4-6 人,6 个月)

目标:能卖的最小产品

Phase 3:完整系统(8-12 人,12 个月)

目标:对标极空间


八、需要自研的核心模块(按难度排序)

必须自研

模块为什么不能只用开源工作量
P2P Manager没有现成的 NAS 专用 P2P 方案高(2-3 人月)
API Gateway 配置需要 + 业务逻辑深度绑定中
OTA 升级与分区布局/回滚强耦合中
Web 管理后台用户体验差异化的关键中

可以先用开源

模块开源方案
STUN/TURNcoturn
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 + 渐进式远程访问。

具体说:

  1. 用 Debian 做 rootfs,别碰 Buildroot
  2. 所有应用层 Go,用一个仓库
  3. 存储层用成熟方案(mdadm + btrfs + mergerfs),别自研文件系统
  4. 远程访问三阶段:先上 frp,再做 P2P,最后搞设备互联
  5. 初期不要超过 5 个微服务,后期按需拆分
  6. 代码和构建工程统一化,别每个服务拷一份 build.sh
  7. 把精力投在 P2P 穿透上,这是和开源 NAS 拉开差距的地方