0xAA55 发表于 2024-2-23 10:32:24

【Python】写了个脚本,避免 群晖 NAS 里的机械硬盘进入睡眠状态

## 1. 起因
小程序上线后,偶尔会出现无法读取群晖挂盘的问题。一直以来找不到问题的所在。后来笔者自购一台四盘位群晖家用,在使用过程中发现其中的硬盘工作时会发出 **响声** 。
硬盘启动需要 **花费相当长的时间** 。若要描述硬盘启动的噪声,凭感觉听的话,像是磁头部位先发出咔哒咔哒的响声,然后盘片开始呼呼呼地转起来,最后等盘片的转速稳定了,磁头就又发出咔哒咔哒的响声,它就启动好了,整个过程要持续至少一分多钟甚至几分钟(粗略估计)。
我在某一次使用我的家用群晖服务器时,由于之前长时间没有使用共享盘,所有的硬盘都休眠了。当我试图在电脑上进入我的共享盘的时候,我能听到硬盘在一个个启动,但是还没等所有硬盘完成启动,Windows 便会 **报错:「无法访问共享盘」** ,并且 **要求我输入共享盘的账号密码** 。这是因为 Windows 里的 **网盘相关软件的设计者** 认为一块网络硬盘的响应时间不应该那么久。实际上,等我听到所有硬盘都启动完成了,我再次尝试打开共享盘就能很快打开了。
在机房配置群晖服务器时它的响声被 **其它服务器的巨大噪声** 盖过去了,因此谁都没有察觉到硬盘启停的过程,所以没有找出共享盘挂盘访问失败的原因(之一)。

## 2. 造成的问题
小程序在运行的时候需要访问群晖 NAS 盘里的文件,并且需要及时取得文件内容来确保稳定提供服务。但如果群规共享盘里的硬盘处于休眠状态, **则会由于硬盘的启动时间太长导致小程序获取文件超时** ,无法提供服务并且报错。

## 3. 解决方案
因为硬盘的休眠行为无法受群晖 NAS 控制, **事实上硬盘的休眠行为无法受任何系统、软件控制** ,甚至笔记本、台式机电脑内置的机械硬盘也会在闲置时自动休眠,笔者便想出了一个方法:在 **内部管理专用的服务器上编写一段脚本** ,每隔一段时间对群晖共享盘里的文件进行一次读取写入操作,来 **强制保持硬盘处于运行状态,避免其休眠** 。这样可以确保小程序需要使用群晖共享盘文件的时候,任何时候硬盘都处于活跃状态,可以稳定提供服务。

```
#!/usr/bin/env python3
# -*- coding: utf-8 -*

# 838816058@qq.com
# This script is used to keep NAS harddisk spinning, called by crond.
# For more information, ask the author.

import os
import math
import time
import random
from datetime import datetime
today = datetime.today()

WORDINGS = [ # 狗血文案
        'Memory is a bridge leading to the prison of loneliness.',
        'The stability of small cities is enviable, and the neon of big cities is also hesitant. We always yearn for stability in our hearts, but we are not mediocre in our bones.',
        'When everything is gone with the wind, when all those special moments into eternity.',
        'Time has changed everything, everything has changed us; originally disliked, now used to; once wanted, now do not need; at first very persistent, later very free and easy.',
        'Last night the stars and stars, there are always some people who did not return.',
        'Some show off, some search frantically, some forget who they are, some are hysterical and hopeless!',
        'Growth is a dilemma between compromise and persistence.',
        'Sometimes a person really will stare at a place in a daze for a long time, back to find that you are not looking at the scenery.',
        "Lonely, you can drink, you can play games, you can cry, but don't miss a person who doesn't love you.",
        'The world, unspeakable regret, it hid in tears, fell in the years, lost in the stubborn; Then, disappear into a sea of people.',
        'The most afraid of is: more rain and more love more far away.',
        'Too many things, slowly can not do; Too many people, gradually disappeared; Originally, growth is destined to be a lost process.',
        'We keep turning over the memories, but can not find the back of their own.',
        'Helpless is: language this thing, in the expression of love so weak, in the expression of injury, but so sharp.',
        'Summer will go round and round, and those who should meet will meet again.',
        "Will be lost, will be sad, will think more, but don't ask or say.",
        "Seriously forget me, really don't remember me and don't hurt me."
        'I told you more than once in my heart that it was a pleasure to meet you.'
]

# 配置信息
FILENAME = 'Poked.txt'
FILEPATH = f'/mnt/snnasls/{FILENAME}'
DATETIME = today.strftime('%Y-%m-%d %H:%M:%S')
KEEPTIME = 28 * 86400 # 文件保留时长
FCT_FILE = os.path.join(os.path.dirname(__file__), '.pokenas_fct') # 用于记录文件创建日期的隐藏文件
daystart = datetime(year=today.year, month=today.month, day=today.day, hour=0, second=0).timestamp()
histday = int(daystart / 86400) # 从今天到时间戳为零之间的历史天数

# 记录文件创建时间戳
def WriteFCTFile(fct):
        with open(FCT_FILE, 'w', encoding='utf-8') as f:
                f.write(str(fct))

# 获取文件创建时间戳
def GetFCT():
        # 先读 FCT (a.k.a. File Creation Time) 文件,读到了就直接转浮点数返回,没读到就瞎编一个。
        # 转浮点数错误的话手动处理这个问题(任其报错)。
        try:
                with open(FCT_FILE, 'r', encoding='utf-8') as f:
                        return float(f.read())
        except FileNotFoundError:
                pass
        retFCT = 0
        try:
                # 读取文件的创建时间。实际上,只在 Windows 的文件系统上可以读出创建时间
                # Linux/Unix 只有文件的修改时间,使用 getctime 会获取到这个时间值
                # 此处暂时就拿这个时间值当作文件的创建时间
                retFCT = os.path.getctime(FILEPATH)
        except FileNotFoundError:
                pass
        # 将取得的 FCT 写入文件
        WriteFCTFile(retFCT)
        return retFCT

if os.path.exists(FILEPATH):
        filetime = GetFCT()
        livetime = daystart - filetime
        if livetime >= KEEPTIME or livetime < 0:
                os.remove(FILEPATH)
                print(f'Removed "{FILEPATH}".')

if os.path.exists(FILEPATH):
        # 计算文件的剩余保留小时数
        ttl = int(math.ceil((KEEPTIME - livetime) / 3600))
        if ttl <= 48:
                ttl = f'{ttl} hrs'
        else: # 大于 48 小时则表示天数
                ttl = f'{int(math.ceil(ttl / 24))} days'
        edittime = os.path.getmtime(FILEPATH)
        with open(FILEPATH, 'a', encoding='utf-8') as f:
                f.write(f'[{DATETIME}] NAS poked. This file will be recreated in about {ttl}.')
                editday = datetime.fromtimestamp(edittime).day
                # 每次编辑文件,判断是不是今日的第一次编辑,如果是,则插入一句狗血文案。
                if editday != today.day:
                        random.seed(today.day)
                        f.write(f' {random.choice(WORDINGS)}.\n')
                else: # 否则插入随机数量个星号
                        random.seed(today.hour)
                        f.write(f' {"".join(["*"] * random.randrange(80))}\n')
        print(f'The file "{FILEPATH}" will be recreated in about {ttl}.')
else:
        # 文件不存在或者已删除,创建文件然后写入狗血文案。
        WriteFCTFile(daystart)
        random.seed(histday)
        with open(FILEPATH, 'w', encoding='utf-8') as f:
                f.write(f'[{DATETIME}] This file was automatically generated by a script to prevent NAS HDD entering sleep mode.\n')
                f.write(f'[{DATETIME}] {random.choice(WORDINGS)}.\n')
        print(f'Recreated "{FILEPATH}".')
```

## 4. 还可以继续改进的地方
由于脚本生成的 `Poked.txt` 文件太小,可能只存储在 NAS 磁盘阵列中的某单一的一块盘上,这个脚本可能只能阻止某一块硬盘的休眠,而不能真正确保所有的盘都被唤醒,可能会导致别的问题。
继续改进的思路则是找出一个 **「足够大但是又足够小」** 的文件,大到它的内容足够散列在每一块阵列盘里,小到拷贝该文件的时间成本足够小,目测估计大概是个 100 MB 左右的大小的文件。然后当 pokenas.sh 执行时,它重复传输该文件: **如果文件在本地则移动它到共享盘;如果文件在共享盘则移动它回到本地。** 这样可以更加确保所有的硬盘都不休眠,但是会在运行的时候略微占用共享盘带宽,并增加其中的硬盘的寿命损耗。

## 5. 永久性解决方案 *(瞎写的)*
*(注意本文写于 2024/2/20)*
- 全部阵列盘换成 SSD。
- 经济成本较大,换下来的机械盘无处可用。
- 现在没有容量那么大的 SSD 可买。
- 若冗余保护失效,SSD 无法进行数据找回。
- 寻找市面上不会休眠的机械硬盘。
- 现在的机械硬盘都会休眠。
- 「企业级服务器硬盘」可能不会休眠,但是价格是「企业级价格」, **一块「企业级」的硬盘价格可能顶十块我们的「民用级」硬盘价格** ,不适合我们使用。
- 把曙光服务器改造成共享盘,它自带了阵列卡,可以用 SSD 组阵列,软件方面安装 SAMBA 和阵列卡驱动即可,硬件方面需要采购新的阵列卡(现有阵列卡不够用)。
- 这个方案同样非常需要相当大的经济成本。
- 改造过程容易翻车。
- 需要联系曙光售后购买阵列卡,让曙光售后给曙光服务器安装新的阵列卡。
- 同样因为买不到足够大的 SSD,组阵列得到的容量有限。
- 若冗余保护失效,SSD 无法进行数据找回。

## 结论
亲自实践是非常重要的。

AyalaRs 发表于 2024-2-24 00:17:15

本帖最后由 AyalaRs 于 2024-2-24 01:29 编辑

用freenas,这么多年也没出过问题,挂rsync任务,和自动快照任务,不过freenas可以设置专门的缓存盘,就算机械盘休眠也没事,windows的电源计划里是可以禁止硬盘休眠的,linux,freebsd,都有类似的选项
freenas里

misakarinkon 发表于 2024-2-24 22:46:01

AyalaRs 发表于 2024-2-24 00:17
用freenas,这么多年也没出过问题,挂rsync任务,和自动快照任务,不过freenas可以设置专门的缓存盘,就算机 ...

这里的 群晖 是软硬件一体的 (
换 freenas 得自己买硬件组,维护成本也不低

AyalaRs 发表于 2024-2-25 09:49:23

misakarinkon 发表于 2024-2-24 22:46
这里的 群晖 是软硬件一体的 (
换 freenas 得自己买硬件组,维护成本也不低...

我用过群晖,就是各种不方便,性能也不行就放弃了
页: [1]
查看完整版本: 【Python】写了个脚本,避免 群晖 NAS 里的机械硬盘进入睡眠状态