基本概念

NAS:(Network Attached Storage,网络附加存储) 使用的是基于文件的通信协议,例如NFS或SMB/CIFS,计算机请求访问的是抽象文件的一段内容,而非对磁盘进行的块设备操作。

SAN:(storage area network,存储区域网络)是一种连接外接存储设备和服务器的架构。该架构的特点是,连接到服务器的存储设备,将被操作系统视为直接连接的存储设备。

NAS和SAN都是为了实现存储共享。不同的是NAS以文件为基础,客户端可以操作远程共享出的文件或文件夹。而SAN以更底层的磁盘块为基础,客户端可以像使用本地磁盘一样使用SAN共享出的磁盘块,可以在上面创建独立的文件系统。

SCSI:(Small Computer System Interface,小型计算机系统接口)是一种用于计算机及其周边设备之间(硬盘、软驱、光驱、打印机、扫描仪等)的通信协议。

iSCSI(Internet Small Computer System Interface,基于Internet的SCSI)又称为IP-SAN,利用了TCP/IP的port 860 和 3260 作为沟通的渠道。透过两部计算机之间利用iSCSI的协议来交换SCSI命令,让计算机可以透过Internet把SAN模拟成为本地的储存设备。

TrueNAS:一个操作系统,专门用于提供各种形式的存储共享服务(包括NAS和SAN)。家用场景主要有两个版本:TrueNAS Core基于FreeBSD,TrueNAS Scale基于Linux。当然也有企业版

为什么要这么做

如果你有一个Homelab,Nas系统肯定是必不可少的,因为肯定要有一个数据中心吧。既然是数据中心,我希望所有的数据都保存到Nas系统中,当我创建Redis服务的时候想到了这点,所以就想研究下能不能把Redis的持久化数据保存到Nas系统中。因为是用于服务器,使用基于文件的共享可能会有一些限制。为避免出现奇奇怪怪的问题,所以决定使用基于块的共享iSCSI,毕竟可以像本地磁盘一样进行操作。

环境介绍

Target(iscsi服务端):TrueNas Core 13.0

Initiator(iscsi客户端):Debian12

TrueNas创建iSCSI共享

  1. 首先在存储池中创建一个Zvol,将作为块设备通过iSCSI共享出去

    image-20240324225204293
  2. 配置iSCSI

    1. 服务器全局配置:Target Global Configuration标签页面,保持默认就行,可以忽略下图中我的配置

      image-20240324225520770
    2. 账号密码配置:客户端连接时的用户名密码。点击Authorized Access标签页,点击添加按钮

      image-20240324231028129

      群组ID:自定义一个数字

      用户:远程连接用户名

      秘密,确认:远程连接密码

    3. 门户配置:点击Portals标签页,点击添加按钮

      image-20240324231622579

      描述:自定义描述信息

      验证方法:CHAP

      验证组:上一步配置的群组ID

      IP地址:0.0.0.0

    4. 客户端组配置:Initiators Groups标签页,点击添加

      image-20240325092635603

      勾选允许所有启动器,并保存

    5. 共享块配置:选择Extends标签页,点击添加按钮

      image-20240324230336140

      名称:自定义一个名称

      设备:选择之前创建的Zvol

      LUN RPM:磁盘转速,根据自己的磁盘类型选择

    6. 配置目标:客户端要连接的target

      image-20240325093029283

      目标名称:自定义名称

      门户组ID:Portals中配置的ID

      身份验证方法:CHAP

      启动器组ID:Initiators Groups添加之后生成的ID,默认第一个为1

      身份验证组号:Authorized Access中添加的组ID

    7. 关联目标:Associated Targets,将目标和磁盘块进行关联

      image-20240325093510594

      目标:目标中配置的目标名称

      区块:Extends中配置的名称

  3. 到此服务端的配置就完成了,配置的过程中,有些配置项是有依赖的,所以需要按照上述步骤顺序进行。

Linux连接iSCSI

连接到iSCSI,并挂载磁盘

  1. 安装客户端工具:apt install open-iscsi

  2. 修改配置文件 /etc/iscsi/iscsid.conf,之后才能执行下面的命令

    discovery.sendtargets.auth.authmethod = CHAP
    discovery.sendtargets.auth.username = jdoe # 在TrueNAS中配置的用户名
    discovery.sendtargets.auth.password = YourSecurePwd1 # 在TrueNAS中配置的密码
    
    node.session.auth.authmethod = CHAP
    node.session.auth.username = jdoe # 在TrueNAS中配置的用户名
    node.session.auth.password = YourSecurePwd1 # 在TrueNAS中配置的密码
    
    node.startup = automatic
    
    node.session.timeo.replacement_timeout = -1  # 当连接出现故障会一直重试
  3. 发现iscsi target

    iscsiadm  --mode discovery --type sendtargets --portal truenas.home
    # truenas.home,你自己truenas的ip地址或域名,因为我局域网配置了小型dns服务器,所以我使用了自定义的域名

    执行结果如下

    image-20240325094455147
  4. 连接iscsi

    iscsiadm  --mode node  --targetname "iqn.2005-10.org.freenas.ctl:redis" --portal "truenas.home:3260" --login
    # iqn.2005-10.org.freenas.ctl:redis 上一步显示的结果,后面部分
  5. 验证连接

    iscsiadm --mode session --print=1

    最后三行显示如下,说明连接成功了

    image-20240325094805838
  6. 开机自动连接

    以上操作完之后,开机就会自动连接了,无需额外操作

  7. 挂载

    # 查看磁盘,正常情况下会多出/dev/sdb
    fdisk -l
    
    # 直接格式化磁盘(不用分区,更加方便扩容)
    mkfs.ext4 /dev/sdb
    
    # 查看uuid
    blkid
    
    #修改/etc/fstab,添加下面一行(替换成你自己的uuid)
    UUID=2009e83c-73a0-4774-a55c-5cf02fa50e37   /data      ext4    _netdev      0       0
    
    # 最后挂载
    mkdir /data
    systemctl daemon-reload
    mount -a

安装redis,将redis的持久化数据保存到/data

这一步主要是安装redis,修改redis的配置文件将数据保存到/data。redis可以换成任意服务。不是文章重点,就不多说了。

修改redis的systemd单元,让redis在iscsid之后启动

# 编辑:/lib/systemd/system/redis-server.service
# 修改After字段为:
After=network.target open-iscsi.service iscsid.service

还有更重要的事

经过上面的操作,Linux成功挂载了iSCSI磁盘,并且作为了redis服务的数据盘,可以正常使用了。实现了我们的初衷:可以将所有的数据保存到TrueNAS系统中。但还远远没有结束。

扩容

TrueNAS中,Zvol卷只能扩容,不能缩容,所以最开始要分配尽量小的空间

  1. TrueNAS中,编辑Zvol,直接更改容量就行

  2. 在linux中:

    1. 关闭redis服务(就是写/data的那个服务,保证磁盘在扩容过程中没有被占用)

    2. 卸载/data:umount /data

    3. 扩容操作:

      e2fsck -f /dev/sdb
      resize2fs /dev/sdb
    4. 重新挂载:

      mount -a
    5. 最后启动redis服务

  3. 结束,就是如此简单

重中之重:故障恢复

这一部分才是最难的,也是这篇文章最有价值的地方。上面的所有东西,只要玩过Homelab都应该能摸索出来,或者可以在网上搜索的到。但是下面要说的,在网上找了很久都没找到。

现在我们只是搭建了一个Redis服务(每个服务都是一个单独的虚拟机),在未来我肯定要部署很多服务,开很多虚拟机,每个虚拟机都使用iSCSI。在家庭环境中,我们的数据中心(TrueNAS系统)肯定会有升级、重启的时候。那么问题来了!1:如何保证TrueNAS不可用的时候,不影响其他依赖的虚拟机;2:如何保证TrueNAS恢复的时候,依赖的虚拟机也一起恢复。

经过不厌其烦的测试,发现iscsi客户端是可以做到故障自动恢复的,由于上面的配置文件中,我们修改了一行node.session.timeo.replacement_timeout = -1 ,所以检测到故障后会一直重试,当恢复的时候会自动连接。但是!如果在故障期间对iscsi磁盘进行了操作,iscsi连接恢复后,磁盘会变得只读,重新挂载会报错:写保护

所以要保证在iscsi连接故障期间,不能对磁盘进行任何操作。好在依赖iscsi的不是整个虚拟机,只是具体的服务(比如Redis),所以我们要解决的问题是:检测到iscsi不可用时停止具体的依赖服务(Redis)。检测到iscsi可用了,再启动具体的依赖服务。

所以问题的关键变成了关键的问题:如何在Linux中及时检测到iSCSI的连接状态。

咋一听好像挺难的,需要很深入的了解Linux和iSCSI,也确实挺难的,我也不会。所以想到一种简单的方法:监听日志。

在Linux上的服务出现故障肯定会有日志打印出来,而维护iSCSI连接的服务是iscsid.service,所以我们需要实时监听iscsid.service的日志,而在使用了systemd的Linux中,实时监听日志也变得非常简单:journalctl -u iscsid.service -f

image-20240325110722336

在上图中,红色划线部分(不是红色字体)分别是iSCSI不可用时打印的第一条日志,和iSCSI恢复时打印的第一条日志。所以只需要写个脚本实时监听日志,在出现上述字符串时分别关闭/启动redis服务。

脚本如下:/root/monitor_iscsid.sh

#!/bin/bash

# 定义消息模式
ERROR_PATTERN="Kernel reported iSCSI connection 1:0 error"
RECOVERY_PATTERN="connection1:0 is operational after recovery"

# 针对不同的服务,只需修改这个变量
depend_server="redis-server"

# 初始状态:所依赖的服务正在运行
depend_running=true

# 函数:停止服务
stop_depend() {
    if $depend_running; then
        echo "$(date): 检测到iscsid错误,正在停止${depend_server}服务..."
        systemctl stop ${depend_server}.service
        depend_running=false
        echo "$(date): ${depend_server}服务已停止."
        echo ""
    fi
}

# 函数:启动depend服务
start_depend() {
    if ! $depend_running; then
        echo "$(date): iscsid已恢复,正在启动${depend_server}服务..."
        systemctl start ${depend_server}.service
        depend_running=true
        echo "$(date): ${depend_server}服务已启动."
        echo ""
    fi
}

# 实时监控iscsid的日志
echo "$(date): 开始监控iscsid的日志..."
journalctl -u iscsid -f | while IFS= read -r line; do
    if echo "$line" | grep -q "$ERROR_PATTERN"; then
        stop_depend
    elif echo "$line" | grep -q "$RECOVERY_PATTERN"; then
        start_depend
    fi
done

加上可执行权限:chmod +x monitor_iscsid.sh

脚本使用systemd进行管理:/etc/systemd/system/monitor-iscsid.service

[Unit]
Description=Monitor iscsid and manage depend service
After=network.target iscsid.service
Requires=iscsid.service

[Service]
Type=simple
ExecStart=/root/monitor_iscsid.sh
Restart=on-failure
User=root
Group=root
StandardOutput=append:/root/monitor-iscsid.log
StandardError=append:/root/monitor-iscsid.err
WorkingDirectory=/root/

[Install]
WantedBy=multi-user.target

启动服务:

systemctl daemon-reload
systemctl enable monitor-iscsid.service --now

问题优化

在之前的操作中,我们在全局配置文件/etc/iscsi/iscsid.conf中修改了配置node.startup = automatic,当执行iscsiadm --mode discovery --type sendtargets --portal truenas.home之后如果发现了多个target,所有发现的target都会保存到/etc/iscsi/nodes/文件夹中。重启后会自动连接这个文件夹下的所有target。

  1. 我们并不需要自动连接所有的target

  2. 如果在TrueNAS端删除了某个target,linux的target文件夹中并不会自动删除。就会出现如下问题:

    • 当Linux关机时报错,并且一直无法关机

      image-20240329135834252
    • open-iscsi.service服务启动失败,因为会去自动连接一个不存在的target。

为解决这个问题,需要修改全局配置文件:node.startup = manual,然后重新执行:iscsiadm --mode discovery --type sendtargets --portal truenas.home,然后修改单独的target配置,操作步骤如下:

# 1. 修改/etc/iscsi/iscsid.conf
node.startup = manual

# 2. 重新发现target,执行:
iscsiadm  --mode discovery --type sendtargets --portal truenas.home

# 3. 修改具体的target配置文件(这里假如target为mysql):/etc/iscsi/nodes/iqn.2005-***.freenas.ctl:mysql/truenas.home,3260
node.startup = automatic

总结

通过监听日志的方式来检测故障,是我能想到的最简单的办法了,具体的可靠性还要经过验证。不确定是否可能出现这样一种情况:iscsi出现了故障,但是没有及时检测到,而对iscsi磁盘进行了操作。也就是可能出现对故障的检测并不及时的情况。