版本: v1.0 日期: 2026-03-17 状态: 草案 目标受众: 产品经理、技术架构师、开发团队、投资者
NAS(网络附加存储)市场长期存在严重的厂商锁定问题。用户一旦选择某个品牌(如群晖Synology、威联通QNAP等),其数据就会被封装在专有的文件系统、RAID格式和元数据结构中。当用户希望更换品牌时,面临的选择往往是:
本文档定义 MigrateNAS 产品的完整技术方案和商业规划,旨在实现:
"硬盘即插即用,数据零迁移" —— 让用户真正拥有数据的自由迁移权
| 术语 | 解释 |
|---|---|
| SHR | Synology Hybrid RAID,群晖专利RAID技术 |
| Btrfs | B-tree文件系统,群晖默认文件系统 |
| LVM | Logical Volume Manager,逻辑卷管理 |
| MD RAID | Linux Multi-Device RAID,Linux软RAID |
| Live USB | 可启动的USB系统,无需安装 |
| FUSE | Filesystem in Userspace,用户空间文件系统 |
| 指标 | 数据 | 来源 |
|---|---|---|
| 全球NAS市场规模 (2025) | $38.2B | Grand View Research |
| 年复合增长率 (CAGR) | 15.8% | 2024-2030预测 |
| 群晖市场份额 | ~35% | IDC报告 |
| 潜在迁移需求用户 | ~500万 | 估算(5年+老用户) |
| 竞品/方案 | 优点 | 缺点 | 我们的优势 |
|---|---|---|---|
| 群晖Hyper Backup | 官方支持,可靠 | 仅群晖间迁移 | 跨品牌支持 |
| rsync手动拷贝 | 通用性强 | 需网络传输,慢 | 直接读取硬盘,快 |
| UFS Explorer | 专业数据恢复 | 贵($100+),只读 | 性价比高,支持写入 |
| RAID Reconstructor | 支持多种RAID | 仅Windows,无NAS集成 | Linux原生,NAS集成 |
成为NAS领域的"数据自由"倡导者,打破厂商锁定,让存储回归用户
| 价值维度 | 描述 | 用户收益 |
|---|---|---|
| 数据主权 | 用户完全掌控自己的数据 | 不再被厂商绑架 |
| 零停机迁移 | 硬盘插拔即用,无需网络拷贝 | 节省数天时间 |
| 元数据完整 | 保留权限、快照、共享设置 | 无需重新配置 |
| 成本节约 | 避免购买同品牌高价设备 | 硬件选择更自由 |
MigrateNAS ├── 软件产品 │ ├── Live USB系统(个人用户) │ ├── NAS厂商授权SDK(B2B) │ └── 命令行工具包(技术用户) ├── 服务产品 │ ├── 在线迁移服务(远程协助) │ └── 企业级数据迁移咨询 └── 开源项目 ├── 社区版驱动 └── 文档与Wiki
┌─────────────────────────────────────────────────────────────────┐ │ MigrateNAS 技术架构 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 用户交互层 (UI Layer) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ │ │ Web管理界面 │ │ 命令行工具 │ │ REST API │ │ │ │ │ │ (React/Vue) │ │ (CLI) │ │ (FastAPI) │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────────────────┼───────────────────────────────┐ │ │ │ 业务逻辑层 (Service Layer) │ │ │ │ ┌─────────────┐ ┌────────┴────┐ ┌─────────────────┐ │ │ │ │ │ 迁移引擎 │ │ 元数据转换器 │ │ 阵列重组引擎 │ │ │ │ │ │ Migration │ │ Metadata │ │ RAID Rebuilder │ │ │ │ │ │ Engine │ │ Converter │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────────────────┼───────────────────────────────┐ │ │ │ 兼容驱动层 (Driver Layer) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ │ │ SHR解码器 │ │ Btrfs映射器 │ │ 加密层处理器 │ │ │ │ │ │ SHR Decoder │ │ Btrfs │ │ Encryption Handler │ │ │ │ │ │ │ │ Mapper │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ │ │ 分区解析器 │ │ LVM激活器 │ │ 快照管理器 │ │ │ │ │ │ Partition │ │ LVM │ │ Snapshot Manager │ │ │ │ │ │ Parser │ │ Activator │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────────────────┼───────────────────────────────┐ │ │ │ 内核层 (Kernel Layer) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ │ │ MD RAID驱动 │ │ Btrfs驱动 │ │ LVM2驱动 │ │ │ │ │ │ dm-mod │ │ btrfs │ │ device-mapper │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ │ │ FUSE驱动 │ │ 块设备层 │ │ 文件系统抽象层 │ │ │ │ │ │ fuse │ │ Block Layer │ │ VFS │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────────────────┴───────────────────────────────┐ │ │ │ 硬件层 (Hardware Layer) │ │ │ │ 原群晖硬盘 ─┬─ 硬盘1 (SHR-1成员) │ │ │ │ ├─ 硬盘2 (SHR-1成员) │ │ │ │ ├─ 硬盘3 (Basic单盘) │ │ │ │ └─ 硬盘4 (SSD缓存) │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
技术原理:
SHR(Synology Hybrid RAID)是群晖的专利技术,本质上是LVM + MD RAID的组合。通过逆向工程,我们发现:
SHR磁盘布局示例(4TB硬盘): ┌─────────────────────────────────────────────────────────────┐ │ MBR/GPT分区表 │ ├─────────────────────────────────────────────────────────────┤ │ 分区1: /dev/sda1 (2GB, Linux RAID) ───┐ │ │ 分区2: /dev/sda2 (3.6TB, Linux RAID) ─┼──┐ │ │ │ │ │ ├──────────────────────────────────────┘ │ │ │ │ │ │ MD设备层: │ │ │ /dev/md0 (RAID1, 系统分区) ◄───────────┘ │ │ /dev/md1 (RAID5, 数据分区) ◄───────────────┐ │ │ │ │ ├─────────────────────────────────────────────┘ │ │ │ │ LVM层: │ │ VG: vg1 (卷组) │ │ ├─ LV: syno_vg1_volume1 (逻辑卷 - 实际数据) │ │ └─ LV: syno_vg1_docker (Docker存储) │ │ │ ├─────────────────────────────────────────────────────────────┤ │ Btrfs文件系统 │ │ ├─ @syno (根子卷) │ │ ├─ @syno@snapshot (快照子卷) │ │ ├─ @docker (Docker子卷) │ │ └─ @iSCSI (iSCSI LUN子卷) │ └─────────────────────────────────────────────────────────────┘
关键代码逻辑:
python# syno_shr_decoder.py
import struct
import subprocess
from dataclasses import dataclass
from typing import List, Optional, Dict
from enum import Enum
class SHRRaidLevel(Enum):
"""SHR支持的RAID级别"""
BASIC = "basic" # 单盘
RAID0 = "raid0" # 条带
RAID1 = "raid1" # 镜像
RAID5 = "raid5" # 分布式奇偶校验
RAID6 = "raid6" # 双奇偶校验
RAID10 = "raid10" # 镜像+条带
SHR1 = "shr1" # SHR 1盘容错
SHR2 = "shr2" # SHR 2盘容错
@dataclass
class DiskInfo:
"""群晖磁盘信息"""
device_path: str
serial_number: str
model: str
size_bytes: int
partitions: List['PartitionInfo']
is_synology_disk: bool = False
raid_role: Optional[str] = None
disk_order: int = -1
@dataclass
class PartitionInfo:
"""分区信息"""
number: int
start_sector: int
end_sector: int
size_bytes: int
partition_type: str
filesystem: Optional[str] = None
md_superblock: Optional[Dict] = None
class SHRDecoder:
"""
SHR (Synology Hybrid RAID) 解码器
功能:
1. 识别群晖磁盘特有的分区签名
2. 解析MD RAID超级块
3. 提取LVM配置
4. 重组SHR阵列
"""
# 群晖特有的魔术数字和UUID前缀
SYNO_MAGIC = b'Syno'
SYNO_UUID_PREFIX = '25534852' # "SHR"的十六进制
MD_SB_OFFSET_V0 = -8 * 1024 * 1024 # 版本0超级块偏移
MD_SB_OFFSET_V1 = -4 * 1024 # 版本1超级块偏移
def __init__(self):
self.logger = self._setup_logger()
self.detected_arrays: Dict[str, List[DiskInfo]] = {}
def scan_all_disks(self) -> List[DiskInfo]:
"""
扫描所有连接的磁盘,识别群晖磁盘
Returns:
List[DiskInfo]: 检测到的群晖磁盘列表
"""
syno_disks = []
# 获取所有块设备
block_devices = self._get_block_devices()
for device in block_devices:
disk_info = self._analyze_disk(device)
if disk_info and disk_info.is_synology_disk:
syno_disks.append(disk_info)
self.logger.info(f"检测到群晖磁盘: {device} ({disk_info.model})")
return syno_disks
def _analyze_disk(self, device_path: str) -> Optional[DiskInfo]:
"""
分析单个磁盘,检测是否为群晖格式
Args:
device_path: 设备路径,如 /dev/sda
Returns:
DiskInfo: 如果识别为群晖磁盘,返回详细信息
"""
# 1. 获取磁盘基本信息
disk_info = self._get_disk_info(device_path)
# 2. 扫描分区表
partitions = self._scan_partitions(device_path)
disk_info.partitions = partitions
# 3. 检测群晖签名
is_syno = self._check_synology_signature(device_path, partitions)
disk_info.is_synology_disk = is_syno
if is_syno:
# 4. 如果是群晖盘,解析RAID信息
self._parse_raid_info(disk_info)
return disk_info
def _check_synology_signature(self, device: str, partitions: List[PartitionInfo]) -> bool:
"""
检测群晖特有的签名
检测点:
1. 分区类型是否为 Linux RAID (0xFD)
2. 分区标签是否包含 "syno" 或 "SYNO"
3. 特定偏移位置是否有群晖魔术数字
"""
try:
# 检查第一个分区的标签
with open(device, 'rb') as f:
# 读取GPT分区表头部
f.seek(512) # 跳过保护MBR
gpt_header = f.read(512)
# 检查GPT签名
if gpt_header[0:8] != b'EFI PART':
# 可能是MBR分区表,检查MBR
f.seek(446)
mbr_part = f.read(64)
# 检查分区类型
if any(mbr_part[i*16+4] == 0xFD for i in range(4)):
return self._verify_synology_mbr(device)
else:
# GPT分区表,检查分区名称
return self._verify_synology_gpt(device, gpt_header)
except Exception as e:
self.logger.error(f"检查签名失败 {device}: {e}")
return False
def _parse_raid_info(self, disk_info: DiskInfo):
"""
解析RAID超级块信息
"""
for part in disk_info.partitions:
if part.partition_type == 'fd': # Linux RAID
md_info = self._read_md_superblock(
disk_info.device_path,
part.number
)
if md_info:
part.md_superblock = md_info
disk_info.raid_role = md_info.get('role')
disk_info.disk_order = md_info.get('disk_number', -1)
def _read_md_superblock(self, device: str, partition_num: int) -> Optional[Dict]:
"""
读取MD RAID超级块
MD超级块存储在:
- 版本0.90: 分区末尾8MB处
- 版本1.x: 分区开头4KB或末尾4KB
"""
partition = f"{device}{partition_num}"
try:
# 使用mdadm查询(如果可用)
result = subprocess.run(
['mdadm', '--examine', partition],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return self._parse_mdadm_output(result.stdout)
# 如果mdadm不可用,手动解析
return self._manual_parse_md_sb(partition)
except Exception as e:
self.logger.warning(f"读取MD超级块失败 {partition}: {e}")
return None
def assemble_shr_array(self, disks: List[DiskInfo]) -> Dict:
"""
组装SHR阵列
流程:
1. 按array_uuid分组磁盘
2. 使用mdadm --assemble组装MD设备
3. 激活LVM卷组
4. 扫描并返回逻辑卷列表
"""
if not disks:
raise ValueError("没有提供磁盘")
# 按阵列UUID分组
arrays = self._group_by_array(disks)
assembled = {}
for array_uuid, array_disks in arrays.items():
self.logger.info(f"组装阵列: {array_uuid}")
# 1. 组装MD RAID
md_devices = self._assemble_md_arrays(array_disks)
# 2. 激活LVM
vg_name = self._detect_volume_group(array_disks)
if vg_name:
self._activate_volume_group(vg_name)
# 3. 获取逻辑卷
logical_volumes = self._scan_logical_volumes(vg_name)
assembled[array_uuid] = {
'uuid': array_uuid,
'md_devices': md_devices,
'volume_group': vg_name,
'logical_volumes': logical_volumes,
'disks': [d.device_path for d in array_disks]
}
return assembled
def _activate_volume_group(self, vg_name: str):
"""
激活LVM卷组
"""
try:
# 扫描卷组
subprocess.run(
['vgscan'],
check=True,
capture_output=True
)
# 激活卷组
subprocess.run(
['vgchange', '-ay', vg_name],
check=True,
capture_output=True
)
self.logger.info(f"已激活卷组: {vg_name}")
except subprocess.CalledProcessError as e:
self.logger.error(f"激活卷组失败 {vg_name}: {e}")
raise
python# btrfs_mapper.py
import subprocess
import os
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class BtrfsSubvolume:
"""Btrfs子卷信息"""
id: int
parent_id: int
path: str
name: str
readonly: bool = False
received_uuid: Optional[str] = None
class SynoBtrfsMapper:
"""
群晖Btrfs子卷映射器
群晖使用特定的子卷命名约定:
- @syno: 根数据子卷
- @syno@snapshot: 快照存储
- @docker: Docker镜像和容器数据
- @iSCSI: iSCSI LUN存储
"""
SYNO_SUBVOLUME_PREFIXES = {
'@syno': 'data',
'@syno@snapshot': 'snapshots',
'@docker': 'docker',
'@iSCSI': 'iscsi',
'@swap': 'swap',
'@syno@ea': 'extended_attributes'
}
def __init__(self, mount_point: str):
self.mount_point = mount_point
self.subvolumes: List[BtrfsSubvolume] = []
self.syno_structure: Dict[str, any] = {}
def scan_subvolumes(self) -> List[BtrfsSubvolume]:
"""
扫描所有Btrfs子卷
"""
try:
result = subprocess.run(
['btrfs', 'subvolume', 'list', '-p', '-u', self.mount_point],
capture_output=True,
text=True,
check=True
)
self.subvolumes = self._parse_subvolume_list(result.stdout)
self._analyze_syno_structure()
return self.subvolumes
except subprocess.CalledProcessError as e:
raise RuntimeError(f"扫描子卷失败: {e.stderr}")
def _analyze_syno_structure(self):
"""
分析群晖特有的子卷结构
"""
structure = {
'root_data': None,
'snapshots': [],
'shares': {},
'docker': None,
'iscsi': None
}
for sv in self.subvolumes:
path = sv.path
# 识别各类子卷
if path == '@syno':
structure['root_data'] = sv
elif path.startswith('@syno@snapshot'):
structure['snapshots'].append(sv)
elif path.startswith('@docker'):
structure['docker'] = sv
elif path.startswith('@iSCSI'):
structure['iscsi'] = sv
elif path.startswith('@syno/'):
# 共享文件夹通常映射为 @syno/photo, @syno/video 等
share_name = path.replace('@syno/', '')
structure['shares'][share_name] = sv
self.syno_structure = structure
def create_user_friendly_mount(self, target_base: str) -> Dict[str, str]:
"""
创建用户友好的挂载结构
将群晖的子卷结构转换为用户友好的目录结构:
- @syno/photo → /mnt/migrate/Photos
- @syno/video → /mnt/migrate/Videos
- @syno/homes → /mnt/migrate/Homes
"""
mount_map = {}
# 确保目标目录存在
os.makedirs(target_base, exist_ok=True)
# 挂载根数据子卷
if self.syno_structure['root_data']:
root_mount = os.path.join(target_base, 'Data')
self._mount_subvolume(self.syno_structure['root_data'], root_mount)
mount_map['root'] = root_mount
# 为每个共享文件夹创建独立挂载点
for share_name, subvol in self.syno_structure['shares'].items():
# 转换为友好的显示名称
friendly_name = self._convert_share_name(share_name)
share_mount = os.path.join(target_base, friendly_name)
self._mount_subvolume(subvol, share_mount)
mount_map[share_name] = share_mount
return mount_map
def _convert_share_name(self, syno_name: str) -> str:
"""
转换群晖共享文件夹名称为用户友好名称
"""
name_map = {
'photo': 'Photos',
'video': 'Videos',
'music': 'Music',
'home': 'Home',
'homes': 'Homes',
'docker': 'Docker',
'web': 'Web',
'surveillance': 'Surveillance'
}
return name_map.get(syno_name.lower(), syno_name.title())
def migrate_snapshots(self, target_path: str) -> List[str]:
"""
迁移Btrfs快照到新系统
策略:
1. 只读发送快照到目标位置
2. 在新系统创建对应的快照结构
"""
migrated = []
for snapshot in self.syno_structure['snapshots']:
try:
# 使用btrfs send/receive迁移快照
snapshot_name = os.path.basename(snapshot.path)
target_snapshot = os.path.join(target_path, snapshot_name)
self._send_receive_snapshot(snapshot, target_snapshot)
migrated.append(target_snapshot)
except Exception as e:
self.logger.warning(f"迁移快照失败 {snapshot.path}: {e}")
return migrated
def _mount_subvolume(self, subvolume: BtrfsSubvolume, mount_point: str):
"""
挂载指定的子卷
"""
os.makedirs(mount_point, exist_ok=True)
# 构建挂载选项
mount_opts = f"subvolid={subvolume.id},noatime"
subprocess.run(
['mount', '-o', mount_opts, self.mount_point, mount_point],
check=True
)
python# metadata_migrator.py
import json
import sqlite3
import xml.etree.ElementTree as ET
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class UserAccount:
"""用户账户信息"""
uid: int
username: str
description: str
email: Optional[str]
password_hash: str # 可能需要重新加密
groups: List[str]
home_directory: str
quota_bytes: Optional[int] = None
@dataclass
class SharedFolder:
"""共享文件夹配置"""
name: str
path: str
description: str
enable_recycle_bin: bool
enable_encryption: bool
hide_unreadable: bool
acl_entries: List[Dict] # 访问控制列表
@dataclass
class SynoMetadata:
"""完整的群晖元数据结构"""
system_info: Dict
users: List[UserAccount]
shared_folders: List[SharedFolder]
network_config: Dict
application_settings: Dict
snapshots: List[Dict]
class SynoMetadataExtractor:
"""
群晖元数据提取器
群晖将配置存储在:
- /etc/synoinfo.conf (系统配置)
- /etc/passwd, /etc/shadow (用户信息)
- /usr/syno/etc/acl/ (ACL配置)
- /usr/syno/etc/packages/ (应用配置)
- /var/lib/synoindex/ (索引数据库)
"""
def __init__(self, config_partition_path: str):
self.config_path = Path(config_partition_path)
self.metadata = SynoMetadata(
system_info={},
users=[],
shared_folders=[],
network_config={},
application_settings={},
snapshots=[]
)
def extract_all(self) -> SynoMetadata:
"""
提取所有元数据
"""
self.metadata.system_info = self._extract_system_info()
self.metadata.users = self._extract_users()
self.metadata.shared_folders = self._extract_shared_folders()
self.metadata.network_config = self._extract_network_config()
self.metadata.application_settings = self._extract_app_settings()
self.metadata.snapshots = self._extract_snapshot_info()
return self.metadata
def _extract_system_info(self) -> Dict:
"""
提取系统基本信息
"""
synoinfo_path = self.config_path / 'etc' / 'synoinfo.conf'
info = {}
if synoinfo_path.exists():
with open(synoinfo_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
# 解析 key="value" 格式
if '=' in line:
key, value = line.split('=', 1)
info[key] = value.strip('"\'')
return info
def _extract_users(self) -> List[UserAccount]:
"""
提取用户账户信息
"""
users = []
passwd_path = self.config_path / 'etc' / 'passwd'
shadow_path = self.config_path / 'etc' / 'shadow'
if not passwd_path.exists():
return users
# 读取passwd
with open(passwd_path, 'r') as f:
for line in f:
parts = line.strip().split(':')
if len(parts) >= 6:
username = parts[0]
uid = int(parts[2])
gid = int(parts[3])
home = parts[5]
shell = parts[6] if len(parts) > 6 else ''
# 跳过系统账户
if uid < 1000 and username not in ['admin', 'guest']:
continue
user = UserAccount(
uid=uid,
username=username,
description='',
email=None,
password_hash='*',
groups=[],
home_directory=home
)
users.append(user)
# 读取shadow获取密码哈希
if shadow_path.exists():
shadow_data = {}
with open(shadow_path, 'r') as f:
for line in f:
parts = line.strip().split(':')
if len(parts) >= 2:
shadow_data[parts[0]] = parts[1]
for user in users:
if user.username in shadow_data:
user.password_hash = shadow_data[user.username]
# 读取额外的用户信息(描述、邮箱等)
self._enrich_user_info(users)
return users
def _extract_shared_folders(self) -> List[SharedFolder]:
"""
提取共享文件夹配置
"""
shares = []
# 群晖共享文件夹配置在多个位置
share_conf_paths = [
self.config_path / 'usr' / 'syno' / 'etc' / 'share_right.map',
self.config_path / 'usr' / 'syno' / 'etc' / 'shared' / 'settings'
]
for conf_path in share_conf_paths:
if conf_path.exists():
shares.extend(self._parse_share_config(conf_path))
# 提取ACL信息
self._enrich_acl_info(shares)
return shares
def _enrich_acl_info(self, shares: List[SharedFolder]):
"""
为共享文件夹添加ACL信息
"""
acl_path = self.config_path / 'usr' / 'syno' / 'etc' / 'acl'
for share in shares:
acl_file = acl_path / f"{share.name}.acl"
if acl_file.exists():
share.acl_entries = self._parse_acl_file(acl_file)
class MetadataConverter:
"""
元数据转换器基类
"""
def convert(self, syno_metadata: SynoMetadata) -> Dict:
"""
将群晖元数据转换为目标格式
"""
raise NotImplementedError
class QNAPConverter(MetadataConverter):
"""
转换为威联通QNAP格式
"""
def convert(self, syno_metadata: SynoMetadata) -> Dict:
qnap_config = {
'system': self._convert_system_info(syno_metadata.system_info),
'users': self._convert_users(syno_metadata.users),
'shares': self._convert_shares(syno_metadata.shared_folders),
'network': self._convert_network(syno_metadata.network_config)
}
return qnap_config
def _convert_users(self, users: List[UserAccount]) -> List[Dict]:
"""
转换用户格式
QNAP使用不同的密码加密方式,需要重新设置密码
"""
converted = []
for user in users:
converted.append({
'username': user.username,
'description': user.description,
'email': user.email or '',
'password': '__RESET_REQUIRED__', # 标记需要重置
'uid': user.uid,
'home': f"/share/homes/{user.username}",
'groups': user.groups
})
return converted
class TrueNASConverter(MetadataConverter):
"""
转换为TrueNAS格式
"""
def convert(self, syno_metadata: SynoMetadata) -> Dict:
# TrueNAS使用数据库存储配置
return {
'database_inserts': self._generate_sql_inserts(syno_metadata),
'middleware_calls': self._generate_middleware_calls(syno_metadata)
}
yaml# live_usb_config.yaml
system:
name: MigrateNAS Live
version: "1.0.0"
base: Debian 12 (Bookworm)
kernel: 6.1.x with custom modules
boot_modes:
- name: GUI Mode
description: 图形界面向导模式
default: true
- name: CLI Mode
description: 命令行专家模式
- name: Rescue Mode
description: 数据救援模式(只读)
included_packages:
# RAID支持
- mdadm
- dmraid
# LVM支持
- lvm2
- thin-provisioning-tools
# 文件系统
- btrfs-progs
- xfsprogs
- e2fsprogs
- zfsutils-linux
# 网络工具
- rsync
- openssh-server
- nfs-common
- cifs-utils
# 诊断工具
- smartmontools
- hdparam
- nvme-cli
- sdparm
# UI依赖
- xorg
- openbox
- chromium
- python3-tk
custom_modules:
- syno_shr_decoder.ko
- syno_btrfs_helper.ko
- disk_identify.ko
auto_start_services:
- migrate_nas_daemon
- web_ui_server
- disk_monitor
partition_layout:
boot:
size: 512MB
type: FAT32
flags: [boot, esp]
root:
size: 4GB
type: ext4
compressed: true
persistence:
size: 2GB
type: ext4
label: MIGRATE_DATA
# 用于保存配置和日志
MigrateNAS SDK 架构 ├── 内核模块层 │ ├── syno_compat.ko # 核心兼容驱动 │ ├── shr_decoder.ko # SHR解码 │ └── btrfs_ext.ko # Btrfs扩展 │ ├── 用户空间库 │ ├── libmigrate.so # 主库 │ ├── libshr.so # SHR处理 │ └── libsyno_meta.so # 元数据处理 │ ├── 头文件 │ ├── migrate_nas.h # 主API头文件 │ ├── shr_types.h # SHR类型定义 │ └── meta_types.h # 元数据类型 │ ├── 示例代码 │ ├── example_basic.c # 基础使用示例 │ ├── example_advanced.c # 高级功能示例 │ └── example_qnap.cpp # QNAP集成示例 │ └── 文档 ├── API_REFERENCE.md ├── INTEGRATION_GUIDE.md └── TROUBLESHOOTING.md
| 功能模块 | 功能点 | 社区版 | 专业版 | 企业版 | 优先级 |
|---|---|---|---|---|---|
| 磁盘识别 | 自动检测群晖硬盘 | ✓ | ✓ | ✓ | P0 |
| 显示磁盘健康状态 | ✓ | ✓ | ✓ | P0 | |
| 支持SATA/SAS/NVMe | ✓ | ✓ | ✓ | P0 | |
| RAID支持 | Basic单盘只读 | ✓ | ✓ | ✓ | P0 |
| Basic单盘读写 | - | ✓ | ✓ | P0 | |
| 标准RAID 0/1/5/6/10 | ✓ | ✓ | ✓ | P0 | |
| SHR-1阵列只读 | - | ✓ | ✓ | P1 | |
| SHR-1阵列读写 | - | ✓ | ✓ | P1 | |
| SHR-2阵列支持 | - | ✓ | ✓ | P2 | |
| SSD缓存识别 | - | ✓ | ✓ | P2 | |
| 文件系统 | Btrfs标准功能 | ✓ | ✓ | ✓ | P0 |
| Btrfs快照读取 | - | ✓ | ✓ | P1 | |
| Btrfs快照迁移 | - | ✓ | ✓ | P2 | |
| ext4支持 | ✓ | ✓ | ✓ | P0 | |
| 加密磁盘解密 | - | ✓ | ✓ | P2 | |
| 元数据 | 共享文件夹结构 | - | ✓ | ✓ | P1 |
| 用户账户迁移 | - | ✓ | ✓ | P1 | |
| 权限ACL转换 | - | ✓ | ✓ | P2 | |
| 快照历史保留 | - | - | ✓ | P3 | |
| 应用数据 | Docker容器迁移 | - | - | ✓ | P3 |
| 虚拟机磁盘转换 | - | - | ✓ | P3 | |
| iSCSI LUN支持 | - | - | ✓ | P3 | |
| 目标平台 | TrueNAS CORE | ✓ | ✓ | ✓ | P0 |
| TrueNAS SCALE | ✓ | ✓ | ✓ | P0 | |
| QNAP QTS | - | ✓ | ✓ | P1 | |
| ASUSTOR ADM | - | ✓ | ✓ | P2 | |
| Unraid | ✓ | ✓ | ✓ | P1 | |
| 通用Linux | ✓ | ✓ | ✓ | P0 |
| 指标 | 目标值 | 测试方法 |
|---|---|---|
| 磁盘识别时间 | < 5秒/盘 | 插入后自动检测 |
| RAID组装时间 | < 30秒 | SHR-1 4盘位测试 |
| 顺序读取速度 | > 90% 原生 | 与群晖本机对比 |
| 顺序写入速度 | > 85% 原生 | 与群晖本机对比 |
| 随机IOPS | > 80% 原生 | fio测试 |
| 内存占用 | < 512MB | Live USB运行时 |
| 系统启动时间 | < 60秒 | 从USB启动到可操作 |
支持的群晖系统版本:
支持的存储池类型:
支持的文件系统:
限制条件:
用户: 小张,想从群晖DS218+换到TrueNAS 数据: 1块8TB硬盘,Basic单盘,存储家庭照片 操作流程: ┌─────────────────────────────────────────────────────────────┐ │ 1. 准备工作 │ │ □ 下载 MigrateNAS Live USB 镜像 │ │ □ 使用 Rufus/balenaEtcher 制作启动盘 │ │ □ 关闭群晖NAS电源,拔出硬盘 │ │ □ 将硬盘插入新NAS设备(任意盘位) │ │ □ 插入USB启动盘,开机 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. 自动检测 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ MigrateNAS 启动中... │ │ │ │ │ │ │ │ [==========] 正在扫描磁盘... │ │ │ │ │ │ │ │ ✓ 检测到 1 块群晖硬盘 │ │ │ │ - /dev/sda: 8.0 TB, Basic, 状态: 健康 │ │ │ │ │ │ │ │ [继续] [查看详情] [取消] │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 3. 选择迁移模式 │ │ ○ 模式A: 只读挂载(安全预览,推荐初次使用) │ │ ◉ 模式B: 完全导入(将数据融入新系统) │ │ ○ 模式C: 数据导出(复制到其他存储) │ │ │ │ [选中模式B] │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 模式B 详情: │ │ │ │ - 保留原有数据不动 │ │ │ │ - 在新系统创建指向原盘的链接 │ │ │ │ - 可以读写,但拔下后群晖仍可识别 │ │ │ │ - 部分高级功能可能受限 │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 4. 配置目标系统 │ │ 检测到目标系统: TrueNAS SCALE │ │ │ │ □ 保留共享文件夹名称 (Photo → Photo) │ │ □ 创建对应的数据集结构 │ │ □ 迁移用户权限配置(如适用) │ │ │ │ 预计耗时: < 5分钟 │ │ [开始迁移] │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 5. 完成 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ ✓ 迁移完成! │ │ │ │ │ │ │ │ 您的数据现在可以在以下位置访问: │ │ │ │ - SMB: \\truenas\Photo │ │ │ │ - NFS: /mnt/tank/Photo │ │ │ │ - WebUI: http://truenas.local │ │ │ │ │ │ │ │ [重启到TrueNAS] [关机] [返回主菜单] │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
用户: IT管理员,公司群晖RS1221+,需迁移到TrueNAS 数据: 4块8TB硬盘,SHR-1阵列,含虚拟机、数据库、共享文件 特殊要求: 零停机或最小停机时间 操作流程: 1. 使用 MigrateNAS 评估模式(只读)验证阵列可识别 2. 准备新NAS设备,安装目标系统(TrueNAS) 3. 使用MigrateNAS Live USB启动,选择"热迁移"模式 4. 配置增量同步策略 5. 在维护窗口执行最终切换
bash# MigrateNAS CLI 命令参考
# 扫描磁盘
migrate-nas scan
# 输出:
# [INFO] Scanning all block devices...
# [FOUND] /dev/sda: Synology Disk, 8TB, Model: ST8000VN004
# [FOUND] /dev/sdb: Synology Disk, 8TB, Model: ST8000VN004
# [INFO] Detected SHR-1 array with 2 disks
# 检查阵列状态
migrate-nas check --disks /dev/sda,/dev/sdb
# 输出详细的阵列信息和健康状态
# 组装阵列(只读)
migrate-nas assemble --disks /dev/sda,/dev/sdb --readonly
# 组装阵列(读写)
migrate-nas assemble --disks /dev/sda,/dev/sdb --read-write
# 导出元数据
migrate-nas export-meta --source /dev/md1 --output ./syno_meta.json
# 转换配置
migrate-nas convert --input ./syno_meta.json --target truenas --output ./truenas_config.json
# 迁移数据(rsync包装)
migrate-nas sync --source /mnt/syno --destination /mnt/truenas --progress
# 完整迁移(自动化流程)
migrate-nas migrate \
--disks /dev/sda,/dev/sdb \
--target-system truenas \
--mode hot-migrate \
--config ./migration.conf
gantt
title MigrateNAS 开发路线图
dateFormat YYYY-MM
section Phase 1: 基础
技术调研与原型 :done, p1_1, 2024-01, 2024-02
Btrfs Basic支持 :active, p1_2, after p1_1, 2024-03
Live USB基础镜像 :p1_3, after p1_2, 2024-04
CLI工具开发 :p1_4, after p1_2, 2024-04
section Phase 2: 核心
SHR-1解码器 :p2_1, after p1_4, 2024-05
标准RAID支持 :p2_2, after p1_4, 2024-05
GUI向导界面 :p2_3, after p2_1, 2024-06
元数据提取器 :p2_4, after p2_2, 2024-06
section Phase 3: 扩展
SHR-2支持 :p3_1, after p2_3, 2024-07
快照迁移 :p3_2, after p2_4, 2024-07
QNAP适配器 :p3_3, after p3_1, 2024-08
ASUSTOR适配器 :p3_4, after p3_3, 2024-09
section Phase 4: 商业化
SDK开发 :p4_1, after p3_4, 2024-10
企业支持服务 :p4_2, after p4_1, 2024-11
市场推广 :p4_3, after p4_2, 2024-12
| 阶段 | 时间 | 里程碑 | 交付物 |
|---|---|---|---|
| MVP | Q1 2026 | 基础可读 | CLI工具,Basic盘只读支持 |
| v1.0 | Q2 2026 | 核心功能 | GUI界面,SHR-1读写支持 |
| v1.5 | Q3 2026 | 多平台 | QNAP/ASUSTOR适配 |
| v2.0 | Q4 2026 | 商业化 | SDK,企业服务,市场推广 |
| 角色 | 人数 | 主要职责 |
|---|---|---|
| 技术负责人 | 1 | 架构设计,技术决策 |
| Linux内核工程师 | 2 | 驱动开发,文件系统 |
| 后端工程师 | 2 | 服务开发,API设计 |
| 前端工程师 | 1 | GUI界面开发 |
| QA工程师 | 1 | 测试,兼容性验证 |
| 产品经理 | 1 | 需求分析,项目管理 |
| 技术支持 | 1 | 文档,社区支持 |
| 风险 | 可能性 | 影响 | 缓解策略 |
|---|---|---|---|
| SHR专利纠纷 | 中 | 高 | 基于开源MD+LVM实现,避免专利侵权;法律咨询 |
| 数据损坏 | 低 | 极高 | 默认只读模式;完整测试;数据校验 |
| 兼容性问题 | 高 | 中 | 建立测试矩阵;社区反馈机制;快速迭代 |
| 群晖系统更新导致不兼容 | 中 | 中 | 持续监控DSM更新;自动化测试 |
| 风险 | 可能性 | 影响 | 缓解策略 |
|---|---|---|---|
| 群晖官方对抗 | 中 | 高 | 保持技术中立;开源社区支持;法律合规 |
| 市场需求不足 | 低 | 高 | 前期市场调研;MVP验证;灵活转型 |
| 竞争对手复制 | 高 | 中 | 建立技术壁垒;快速迭代;社区生态 |
安全设计原则: ┌─────────────────────────────────────────────────────────────┐ │ 1. 默认只读 │ │ - 所有操作默认以只读模式执行 │ │ - 明确用户确认后才允许写操作 │ │ │ │ 2. 数据完整性校验 │ │ - Btrfs校验和验证 │ │ - 定期数据一致性检查 │ │ │ │ 3. 操作审计日志 │ │ - 记录所有修改操作 │ │ - 支持操作回滚 │ │ │ │ 4. 加密安全 │ │ - 密码/密钥不上传云端 │ │ - 内存中不保留敏感信息 │ │ │ │ 5. 沙箱测试 │ │ - 提供虚拟化测试环境 │ │ - 建议用户先测试再正式迁移 │ └─────────────────────────────────────────────────────────────┘
收入流 ├── 软件销售 │ ├── 专业版许可证 ($49/设备) │ ├── 企业版订阅 ($99/年/节点) │ └── 批量许可证折扣 │ ├── B2B授权 │ ├── NAS厂商SDK授权费 │ ├── 按设备抽成 ($5-10/台) │ └── 技术支持年费 │ ├── 服务收入 │ ├── 远程迁移服务 ($299/次起) │ ├── 企业级数据恢复 │ └── 培训与咨询 │ └── 开源生态 ├── 社区版(免费,品牌曝光) ├── 企业支持合同 └── 定制开发服务
| 版本 | 价格 | 目标用户 | 包含内容 |
|---|---|---|---|
| 社区版 | 免费 | 技术爱好者 | 基础只读功能,社区支持 |
| 专业版 | $49/设备 | 个人/小型工作室 | 完整功能,邮件支持 |
| 企业版 | $999/年 | 企业IT部门 | 无限设备,优先支持,SLA |
| 厂商SDK | 面议 | NAS制造商 | 源码授权,定制开发 |
技术社区渗透
内容营销
合作伙伴
群晖技术文档:
Linux存储技术:
相关开源项目:
| 群晖设备 | DSM版本 | 测试配置 |
|---|---|---|
| DS218+ | 7.2 | 2x4TB SHR-1 |
| DS920+ | 7.2 | 4x8TB SHR-1 |
| DS1621+ | 7.2 | 6x4TB RAID5 |
| RS1221+ | 7.1 | 8x8TB SHR-2 |
| DS118 | 7.2 | 1x4TB Basic |
Q: 使用MigrateNAS会 void 群晖保修吗? A: 不会。MigrateNAS只读取磁盘数据,不对群晖硬件做任何修改。
Q: 迁移后还能插回群晖使用吗? A: 只读模式可以;读写模式(模式B)理论上也可以,但建议先备份。
Q: 支持加密的群晖磁盘吗? A: 支持,但需要您提供正确的密码或密钥文件。
Q: 数据会丢失吗? A: 社区版/只读模式不会修改任何数据。写操作前会明确提示并建议备份。
Q: 支持M.2 SSD缓存盘吗? A: 可以识别,但缓存数据通常不需要迁移。
| 版本 | 日期 | 作者 | 变更说明 |
|---|---|---|---|
| 0.1 | 2026-03-17 | Claude | 初始版本,创建完整PRD |
本文档为产品规划文档,内容可能随产品迭代而更新。
本文作者:oyph
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!