规划
NAS 上配备了 4 块 HDD,每块的容量为 7.3 TB
。
我们可以简单的将 NAS 中存储的数据根据重要性归类为:
- 重要数据
data
,如代码、实验数据、照片等; - 不重要数据
resource
,如电影等可重下载资源;
这两种数据的特征为:
- 重要数据:必须要备份;主要是随机访问;对容量要求、读写速度要求不高;
- 不重要数据:不必备份;主要是顺序访问;对容量要求、读写速度要求高;
此外,该 NAS 将会被多个用户使用:
- 每个用户需要有自己的私有目录,同时能够访问到公共目录;
- 用户存储空间的公平性不太重要(你多存一点我少存一点都无所谓,反正空间大);
- 但可扩展性比较重要(未来可能加入新用户);
所以比较合理的设计为:
- 将四块硬盘中的一块分配给重要数据,将剩下三块全部分配给不重要数据;
- 定期将重要数据增量备份到不重要数据的硬盘中;
- 三块硬盘并行写入,增加数据写入速度;
- 不去为每个 NAS 用户预先分配固定的空间,而是让每个用户都认为自己独占所有存储空间(类似于虚拟内存);
- 通过文件共享系统的权限系统实现权限管理;
我们将在接下来的步骤中依次实现这些设计。
创建物理硬盘
首先 lsblk
看看现有的硬盘:
lsblk
# sda 8:0 0 7.3T 0 disk
# sdb 8:16 0 7.3T 0 disk
# sdc 8:32 0 7.3T 0 disk
# sdd 8:48 0 7.3T 0 disk
可以看到四块硬盘分别名为 sd{a,b,c,d}
,对应的路径自然也就为 /dev/sd{a,b,c,d}
。
将硬盘上现有的 signature
全部擦除掉(当然,重要数据先备份):
wipefs --all /dev/sd{a,b,c,d}
# /dev/sda:8 个字节已擦除,位置偏移为 ...
随后就可以创建物理磁盘了:
pvcreate /dev/sd{a,b,c,d}
# Physical volume "/dev/sda" successfully created...
通过 pvs
(或者更详细的 pvdisplay
)查看创建情况:
pvs
# PV VG Fmt Attr PSize PFree
# /dev/sda data lvm2 a-- <7.28t <3.27t
# /dev/sdb resource lvm2 a-- <7.28t <3.94t
# /dev/sdc resource lvm2 a-- <7.28t 3.94t
# /dev/sdd resource lvm2 a-- <7.28t <3.94t
挂载点设计
我们可以在根目录下创建一个专门的目录 /samba
作为挂载点:
mkdir /samba
我们期望的目录结构为:
/samba
├── public
├── .backup
| ├── backup.sh
| └── <USER1>
└── <USER1>
| ├── public -> /samba/public
| └── resource
└── <USER2>
├── ...
其中:
/samba/public
:公用目录,属于resource
;- 该目录会被符号链接到每个用户目录的
public
上;
- 该目录会被符号链接到每个用户目录的
/samba/.backup
:备份目录,用于当data
中数据损坏时恢复数据,属于resource
;/samba/<USERNAME>
:用户私有目录;- 该文件夹下的内容默认为重要数据,属于
data
, 会进行备份; resource
:用户私有不重要数据,属于resource
, 这些数据不会被备份;public
:用户公用不重要数据,属于resource
;
- 该文件夹下的内容默认为重要数据,属于
数据盘管理
接下来我们开始使用 LVM 创建相应的磁盘。
我们知道 LVM 分为物理磁盘 PV,磁盘组 VG 和逻辑磁盘 LV 三个抽象层次(详见 LVM 介绍)。LV 必须依附于某个 VG,为此需要首先为 data
和 resource
分别创建 VG。
使用 vgcreate
创建名为 data
的 VG (设计1):
vgcreate data /dev/sda
lvcreate -c 64K -L 4T -T data/pool
-c 64K
将分块大小设置为相对较小的64KB
,否则会提示Pool zeroing and 512,00 KiB large chunk size slows down thin provisioning.
;-T data/pool
指明该 pool 创建在data
上,名为pool
;
资源盘管理
创建名为 resource
的 VG Group:
vgcreate resource /dev/sd{b,c,d}
创建 thin pool:
lvcreate --stripes 3 --stripesize 128 -c 128K -L 10T -T resource/pool
其中:
--stripes 3 --stripesize 128
指定使用 3 个条带设备读写,条带大小128 KB
(设计3);-L 10T
指定了初始大小为10 TB
。Thin pool 是在物理空间中创建的,所以必须指定一个大小;但由于 thin pool 有自动扩容机制,所以初始大小无所谓;
随后即可在对应的 pool
上创建任意容量大小的 LV。先创建名为 public
LV 作为公共空间:
lvcreate -V 21T -T resource/pool -n public
其中:
-V 21T
指定虚拟大小为21TB
,远大于所在pool
的容量;
然后在磁盘上创建文件系统,使用 ext4
即可:
mkfs.ext4 /dev/resource/public
最后将磁盘挂载到指定挂载点:
mount /dev/resource/public /samba/public
大功告成。此时 /samba/public
目录就已经准备就绪了。创建 backup
或其他磁盘同理。
Thin Pool 自动扩容
Thin pool 容量被占满通常不是什么好事,所以在 /etc/lvm/lvm.conf
中,确保如下两项没有被注释掉:
thin_pool_autoextend_threshold = 80
thin_pool_autoextend_percent = 20
该两项设置代表了当 thin pool 的容量占用达到了 80%时,将自动扩容 20%。
即便如此,物理硬盘的空间仍然是有限的,所以还是需要定期检查存储空间剩余容量。
数据备份
切换到 root 账户,通过:
contab -e
打开 crontab 的编辑界面,添加如下任务:
0 0 */1 * * /samba/.backup/backup.sh
即每天执行一次 /samba/.backup/backup.sh
脚本,脚本内容为:
#!/bin/bash
# backup.sh
SMB_DIR=/samba
BACKUP_DIR=$SMB_DIR/.backup
rsync -av \
--exclude '.*' \
--exclude 'public' \
--exclude '*/resource' \
$SMB_DIR $BACKUP_DIR
也即将所有非 resource
的数据增量同步到 .backup
中(设计2)。
共享文件夹权限管理
由于 public
文件夹是所有用户的公共文件夹,因此需要做一些特殊的权限处理,期望能够做到:
- 所有用户均能够向其中写入数据并读取其中的数据;
- 用户只能删除自己写入的数据,而不能操作其他数据;
这实际上与 /tmp
目录是十分相似的,因此我们可以使用 Sticky Bit 权限位实现。当某个目录的Sticky Bit 被设置后,其中的文件对所有用户可读(当然,前提是你正确设置了其他权限位),但只能够被目录所有者、文件所有者和 root 用户修改和删除。
通过如下命令设置用户可对 public
目录完全管理,同时带有 Sticky Bit:
chmod 1777 /samba/public
# 777代表读、写和执行权限,前面的1代表 Sticky Bit
# 也可以通过 chmod +t /samba/public 单独添加 Sticky Bit
同时为了防止非 root 用户持有 public
目录,将所有权转交给 root:
chown root /samba/public
设置完目录后,在 /etc/samba/conf.d/
中新增 public.conf
:
# public.conf
[public]
path = /samba/public # 挂载路径
available = yes
browseable = yes
writable = yes
delete readonly = yes
create mask = 0666
directory mask = 1777 # 新建目录权限为1777
force user = nobody # 强制匿名访问,避免使用特定用户的权限问题
Samba 用户管理
很莫名其妙的,smb.conf
并不支持以通配符来包含外部配置,因此采用动态生成 的方式来创建一个 includes.conf
文件,然后将这个文件包含到 smb.conf
中。
首先新建一个配置文件夹:
mkdir /etc/samba/conf.d
用于存储额外的配置文件,如 public
目录对应的配置文件就可以是 /etc/samba/conf.d/public.conf
。
假设该目录现在非空了,在 /etc/samba
下执行如下命令生成 includes.conf
:
ls conf.d/* | sed -e 's/^/include = /' > includes.conf
生成出来的文件内容应该类似于:
# includes.conf
include = /etc/samba/conf.d/public.conf
// ...
然后修改 smb.conf
,在 [global]
下新增:
# smb.conf
[global]
include = /etc/samba/includes.conf
最后通过 testparm
检查,看看 conf.d
中的内容是否确实被包含了进去:
testparm -s
# 应该能够在打印出的内容中找到 conf.d 中包含的内容
每个用户对应的配置文件形如:
[<USERNAME>]
path = /samba/<USERNAME>
这完成了设计5。
添加新用户
我们当然不希望每添加一个新用户就重复一遍上述流程,所以可以通过脚本实现上述流程。
持久化挂载
附录 1:Samba 硬盘挂载
Ubuntu 玩家
先安装必要的软件:
apt-get install cifs-utils smbclient
随后创建挂载点,一般可以挂载到 /mnt/<USERNAME>
上:
mkdir /mnt/<USERNAME>
然后就可以挂载了,把文件系统类型指定为 cifs
:
mount \
-t cifs \
//<NAS_IP>/<USERNAME> \
/mnt/<USERNAME> \
-o username=<USERNAME>,password=<USER_PASSWD>
将如上的 NAS_IP
,USERNAME
和 USER_PASSWD
根据实际情况进行替换。
注意 samba url 前面需要以两个斜杠 //
开头,形如 //192.168.0.1
。
挂载完成后就可以像本地文件一样访问到 samba 目录了。
Windows 玩家
打开文件管理器,在侧边栏选择“网络”,然后(如果必要)点击“启用网络发现和文件共享”-“否,使已连接到的网络成为专用网络”:
然后在路径栏中输入 \\<NAS_IP>\<USERNAME>
,不出意外的话会要求输出凭据:
输入完成后即可连接。
为了方便,可以将连接后的图标拖到上面去,方便再次访问:
MacOS 玩家
访达-前往-连接服务器,输入 smb://<USERNAME>@<NAS_IP>/
,选择自己的宗卷进行挂载:
值得多提一句的是,这个残废的系统不会给你自动保存网络设备,所以每次重启之后你就会发现之前挂载的 samba 设备不见了,又得重新进行一遍上述步骤😡。
参考 v2ex
,有人说可以把宗卷拖拽到设置-启动项里面,从而开机自动挂载😅:
- 你的家族有精神病史吗?
- 我有个叔叔买了MacBook。
附录 2:脚本
/usr/local/bin/samba-user.sh
#!/bin/bash
set -e
if [ -z "$1" ]; then
echo "Usage: $0 <username>"
exit 1
fi
USERNAME=$1
INITIAL_PASSWORD=$(openssl rand -base64 12)
if [[ ! "$USERNAME" =~ ^[a-zA-Z0-9_]+$ || $USERNAME == "public" ]]; then
echo "Invalid username. Only alphanumeric characters and underscores (exclude 'public') are allowed."
exit 1
fi
echo "Validating username: $USERNAME - OK"
if id "$USERNAME" &>/dev/null; then
echo "User $USERNAME already exists, skipping creation."
else
echo "User $USERNAME does not exist - Proceeding to create user."
useradd -M "$USERNAME"
echo "$USERNAME:$INITIAL_PASSWORD" | chpasswd
fi
create_logical_volumes() {
local VG_NAME=$1
local MOUNT_POINT=$2
LV_NAME=$USERNAME
LV_PATH=/dev/$VG_NAME/$LV_NAME
POOLNAME=$VG_NAME/pool
if [ -e "/dev/mapper/$VG_NAME-$LV_NAME" ]; then
echo "Logical volume $LV_NAME already exists, skipping creation."
return
fi
lvcreate -V 20T -T $POOLNAME -n $LV_NAME
mkfs.ext4 $LV_PATH
mkdir -p "$MOUNT_POINT" && mount $LV_PATH "$MOUNT_POINT"
echo "$MOUNT_POINT initialized."
}
SAMBA_DIR=/samba
USER_DIR=$SAMBA_DIR/$USERNAME
RESOURCE_DIR=$USER_DIR/resource
create_logical_volumes "data" "$USER_DIR"
create_logical_volumes "resource" "$RESOURCE_DIR"
if [ ! -e $USER_DIR/public ]; then
ln -s /samba/public $USER_DIR/public
fi
echo "Setting ownership and permissions for $USER_DIR..."
chown -R "$USERNAME:$USERNAME" "$USER_DIR"
chmod -R 700 "$USER_DIR"
echo "Creating Samba configuration..."
SMB_DIR=/etc/samba
SMB_CONF_DIR=$SMB_DIR/conf.d
USER_CONF=$SMB_CONF_DIR/${USERNAME}.conf
if [ -f $USER_CONF ]; then
echo "Samba configuration for $USERNAME already exists. Skipping."
else
(echo "$INITIAL_PASSWORD"; echo "$INITIAL_PASSWORD") | smbpasswd -a "$USERNAME"
echo "Initial Password: $INITIAL_PASSWORD" > $USER_DIR/hello.txt
cat $USER_DIR/hello.txt
cat <<EOL > $USER_CONF
[$USERNAME]
path = $USER_DIR
available = yes
browseable = yes
writable = yes
valid users = $USERNAME
EOL
INCLUDES_CONF=$SMB_DIR/includes.conf
echo "Updating $INCLUDES_CONF to include user configuration..."
ls $SMB_CONF_DIR/* | sed -e 's/^/include = /' > $INCLUDES_CONF
SMB_CONF=$SMB_DIR/smb.conf
testparm $SMB_CONF
systemctl restart smbd
echo "Samba configuration for $USERNAME created successfully."
fi
echo "User $USERNAME created successfully."
- 请根据实际情况替换
SAMBA_DIR
对应的路径;
[!解释(by Deepseek)]-
- 脚本初始化和参数检查:
set -e
:设置脚本在遇到错误时立即退出。- 检查是否提供了用户名参数
$1
,如果没有,输出使用说明并退出。- 用户名验证:
- 检查用户名是否只包含字母、数字和下划线,并且不能是
public
。如果不符合要求,输出错误信息并退出。- 用户创建:
- 生成一个随机的初始密码。
- 检查用户是否已存在,如果存在则跳过创建,否则创建用户并设置初始密码。
- 创建逻辑卷:
- 定义一个函数
create_logical_volumes
,用于创建逻辑卷并挂载到指定目录。- 检查逻辑卷是否已存在,如果不存在则创建并挂载。
- 配置用户目录:
- 创建用户的主目录和资源目录,并调用
create_logical_volumes
函数为其创建逻辑卷。- 创建一个符号链接,将
/samba/public
链接到用户目录下的public
。- 设置权限:
- 设置用户目录的所有者和权限。
- 配置Samba共享:
- 检查Samba配置文件是否已存在,如果不存在则创建。
- 使用
smbpasswd
为新用户设置Samba密码。- 生成Samba配置文件,并更新Samba的主配置文件以包含用户配置。
- 重启Samba服务以应用配置。
- 完成提示:
- 输出用户创建成功的提示信息。
/usr/local/bin/sync-fstab.sh
#!/usr/bin/env python3
import argparse
import os
import re
import shutil
import subprocess as sp
from pathlib import Path
BLKID_RE = re.compile(r'(/dev/mapper/\S+):\s+UUID="([^"]+)"')
FSTAB_PATH = Path('/etc/fstab')
def check_root():
if os.geteuid() != 0:
print("This script must be run as root")
exit(1)
def get_disk_to_mount_mapping():
# 执行df命令获取磁盘挂载信息
df_output = sp.check_output(["df", "--output=source,target"], text=True)
# 过滤/dev/mapper设备并创建映射
disk_to_mount = {}
for line in df_output.splitlines():
if "/dev/mapper" in line:
source, target = line.strip().split()
disk_to_mount[source] = target
return disk_to_mount
def get_disk_to_uuid_mapping():
# 执行blkid命令获取UUID信息
blkid_output = sp.check_output(["blkid", "-s", "UUID"], text=True)
# 解析blkid输出并创建映射
disk_to_uuid = {}
for line in blkid_output.splitlines():
if "/dev/mapper" in line:
# 使用正则表达式提取设备路径和UUID
match = re.match(BLKID_RE, line)
if match:
device, uuid = match.groups()
disk_to_uuid[device] = uuid
return disk_to_uuid
def read_fstab():
"""读取fstab文件并删除标记的行"""
lines = FSTAB_PATH.read_text().splitlines()
cleaned_lines = []
i = 0
while i < len(lines):
line = lines[i]
if line.strip().startswith("#sync"):
i += 2 # 跳过注释行和其下一行
else:
cleaned_lines.append(line)
i += 1
return "\n".join(cleaned_lines)
def sync_fstab(disk_to_mount, disk_to_uuid):
"""生成新的fstab条目"""
entries = []
for disk, mount in disk_to_mount.items():
if disk in disk_to_uuid:
uuid = disk_to_uuid[disk]
entries.extend(
(
"#sync - auto generated for {}\n".format(disk),
"UUID={} {} ext4 defaults 0 0\n".format(uuid, mount),
)
)
return "".join(entries)
def write_fstab(content):
"""写入新的fstab内容"""
shutil.copy(FSTAB_PATH, FSTAB_PATH.with_suffix(".backup"))
FSTAB_PATH.write_text("".join(content))
def recover_fstab():
"""从备份文件恢复fstab"""
backup_path = FSTAB_PATH.with_suffix(".backup")
if not backup_path.exists():
print("No backup file found")
exit(1)
shutil.copy(backup_path, FSTAB_PATH)
print("Recovered fstab from backup successfully")
def main():
check_root()
parser = argparse.ArgumentParser(description="Sync or clear fstab entries")
parser.add_argument(
"-c",
"--clear",
action="store_true",
help="Only clear sync entries without generating new ones",
)
parser.add_argument(
"-r",
"--recover",
action="store_true",
help="Recover fstab from backup file",
)
args = parser.parse_args()
if args.recover:
recover_fstab()
return
content = read_fstab()
if not args.clear:
disk_to_mount = get_disk_to_mount_mapping()
disk_to_uuid = get_disk_to_uuid_mapping()
sync_content = sync_fstab(disk_to_mount, disk_to_uuid)
content += f"\n{sync_content}"
print("Updated /etc/fstab successfully")
write_fstab(fstab)
print("Backup saved as /etc/fstab.backup")
if __name__ == "__main__":
main()
/samba/.backup/backup.sh
#!/bin/bash
SMB_DIR=/samba
BACKUP_DIR=$SMB_DIR/.backup
rsync -av \
--exclude '.*' \
--exclude 'public' \
--exclude '*/resource' \
$SMB_DIR $BACKUP_DIR
- 请根据实际情况替换
SMB_DIR
和BACKUP_DIR
的值;
附录 3:如何删除逻辑磁盘
首先卸载位于 /dev/mapper
的磁盘:
umount /dev/mapper/<VG_NAME>-<LV_NAME>
# 比如 resource/public对应的路径为 /dev/mapper/resource-public
若执行后报错“设备忙”等,说明有进程在使用该磁盘挂载点中包含的文件,可以通过 lsof
找出这些进程并合适的解决掉它们:
lsof +D <MOUNT_POINT>
然后即可删除逻辑磁盘:
lvremove <VG_NAME>/<LV_NAME> -y
如有必要,删除该逻辑磁盘的挂载点(不删除的话则该挂载点中的数据将默认迁移到其父目录所用磁盘中):
rm -rf <MOUNT_POINT>
附录 4:出现错误: Activation of logical volume is prohibited
重启 NAS 后发现有逻辑磁盘没有挂载,运行如下命令检查挂载情况:
lvscan
# INACTIVE '/dev/<VG_NAME>/pool'
# ...
发现这些磁盘当前的状态是 inactive
。
尝试将这些磁盘的状态变更为 active
,遇到如下报错:
sudo vgchange -ay data
# Activation of logical volume data/pool is prohibited while logical volume data/pool_tmeta is active.
# ...
参考 PVE论坛,可能是由于 thin_check
占用时间太长导致的(不是很懂什么意思🤣),解决方法为:
将 thin pool 的 tmeta
和 tdata
禁用:
lvchange -an <VG_NAME>/<TP_NAME>_tmeta
lvchange -an <VG_NAME>/<TP_NAME>_tdata
随后重新激活相应的 VG:
lvchange -ay <VG_NAME>
最后重新挂载磁盘:
mount -a