0%

上周发生的一件小事,因为过程还比较顺利,感觉也还比较有参考性,就顺便记录下来。

起因是偶然发现运行hentai@home 的机器出现了cpu吃满的情况。这个情况算是有一点异样,因为根据以往经验,这个类pcdn的应用属于IO密集型,不太可能会占用这么高的cpu。于是我试着杀掉进程后重启。重启后新进程的cpu使用率基本维持在10%上下,从实际运行状况看也没有什么异常。

结果过了几个小时后,发现又飙到了100%。这下看起来可能是触发了什么异常状况了。第一时间尝试升级应用到最新版本,问题依旧。清理掉应用缓存重新配置?这个又代价太大了,怕是要个把月才能完全恢复过来。

于是开始考虑跟进去查一下问题,hentai@home 是一个单体的java应用,因此先考虑用常规的java应用调试方法追查。

  1. 首先用top命令查到cpu占用异常的进程(典型的输出如下)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    red@red2:/cantos/app/hath$ top
    top - 05:02:02 up 36 days, 20:26, 2 users, load average: 6.08, 6.54, 6.37
    Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie
    %Cpu(s): 61.3 us, 38.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
    MiB Mem : 964.6 total, 88.7 free, 291.5 used, 584.3 buff/cache
    MiB Swap: 1962.0 total, 1909.7 free, 52.2 used. 488.0 avail Mem

    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

    3419086 red 20 0 2776900 148548 15160 S 199.0 15.0 1884:33 java
  2. 拿到这个PID后进一步查一下该进程下的线程信息,命令为ps -mp <PID> -o THREAD,tid,time

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    red@red2:/cantos/app/hath$ ps -mp 3419086 -o THREAD,tid,time

    USER %CPU PRI SCNT WCHAN USER SYSTEM TID TIME
    red 183 - - - - - - 1-07:21:27
    red 0.0 19 - futex_ - - 3419086 00:00:00
    red 0.0 19 - futex_ - - 3419087 00:00:00
    red 0.3 19 - futex_ - - 3419088 00:03:22
    red 0.0 19 - futex_ - - 3419089 00:00:02
    red 0.0 19 - futex_ - - 3419090 00:00:00
    red 0.0 19 - futex_ - - 3419091 00:00:00
    red 0.0 19 - futex_ - - 3419092 00:00:00
    red 0.0 19 - futex_ - - 3419093 00:00:18
    red 0.0 19 - futex_ - - 3419094 00:00:01
    red 0.0 19 - futex_ - - 3419095 00:00:00
    red 0.0 19 - futex_ - - 3419097 00:00:04
    red 0.0 19 - futex_ - - 3419098 00:00:14
    red 0.0 19 - futex_ - - 3419099 00:00:00
    red 0.0 19 - futex_ - - 3419100 00:00:01
    red 0.1 19 - poll_s - - 3419101 00:01:50
    red 50.5 19 - - - - 3448406 08:25:33
    red 43.8 19 - - - - 3599194 06:23:30
    red 41.2 19 - - - - 3661814 05:42:25
    red 39.7 19 - - - - 3748094 05:08:05
    red 37.6 19 - - - - 3989393 03:49:56
    red 0.0 19 - skb_wa - - 100601 00:00:00
    red 31.8 19 - - - - 300264 00:14:09
    red 0.0 19 - inet_c - - 347998 00:00:00
    red 0.0 19 - futex_ - - 354844 00:00:00
    red 0.0 19 - poll_s - - 354860 00:00:00
    red 0.0 19 - futex_ - - 354886 00:00:00
    red 0.0 19 - futex_ - - 354940 00:00:00
    red 0.0 19 - futex_ - - 354973 00:00:00
    red 0.0 19 - poll_s - - 354991 00:00:00
    red 0.0 19 - poll_s - - 355005 00:00:00
    red 0.0 19 - poll_s - - 355006 00:00:00

    到这一步已经能查到具体线程的情况了,可以看到有6个线程的执行时间特别长,且占用很高,可以选一个具体的TID继续追查。

    不过这里的TID是10进制格式,与jvm输出的还有一些区别,因此选择再多一步将格式转成十六进制。

  3. TID转十进制方法有很多, 命令行转换如下

    1
    2
    red@red2:~$ printf '%x' 3448406
    349e56
  4. 拿到TID后,下一步使用的工具是JDK中的jstack,用于查看堆栈信息

    1
    2
    red@red2:~$ jstack 3419086 | grep 349e56
    "Thread-29288" #29309 prio=5 os_prio=0 cpu=24966847.50ms elapsed=45068.84s tid=0x00007fabbc017000 nid=0x349e56 runnable [0x00007fabf96e0000]

    如果对这个java应用的源码比较熟悉的话,打印完整的堆栈信息会更利于排查,类似如下

    1
    2
    3
    4
    5
    6
    "Thread-5858353" #5859010 prio=5 os_prio=0 cpu=26.86ms elapsed=16.80s tid=0x00007f813404f000 nid=0x1efd22 waiting on condition  [0x00007f813a8e9000]
    java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(java.base@11.0.20.1/Native Method)
    at hath.base.HTTPResponseProcessorProxy.getPreparedTCPBuffer(HTTPResponseProcessorProxy.java:64)
    at hath.base.HTTPSession.run(HTTPSession.java:198)
    at java.lang.Thread.run(java.base@11.0.20.1/Thread.java:829)

    打印几段,知道代码都在哪儿打转,基本也就知道情况了。不过我对hentai@home这个项目并不熟,所以多做一步,打开debug模式连上去再查一下具体信息。

  5. 以debug模式启动应用

    java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005 HentaiAtHome.jar

    这个命令会开启5005端口,这样就可以用集成开发工具连接来做debug。

  6. 这里以vscode为例,添加如下debug配置

    1
    2
    3
    4
    5
    6
    7
    {
    "type": "java",
    "name": "Remote Debug h@h",
    "request": "attach",
    "hostName": "192.168.2.223", // Java 进程所在的主机名或 IP 地址
    "port": 5005 // 远程调试端口,与你的 Java 进程配置一致
    }

    然后在vscode里启动调试就能连到目标进程了,找到上面jstack打印的Thread-5858353,简单暂停一下然后单步调试,恩,问题一目了然,陷入死循环了……

    简单查了几个变量值,查到出现问题的文件,发现是文件大小为0时的临界处理有问题。

    猜测空文件大概是非正常退出时导致的未写入,最后简单把这些大小为0的文件删掉,重新启动,问题解决。有空的话,再顺便去e-hentai论坛上反馈一下这个问题吧。

前言

趁着 6 月份的年中大促,我升级了家里的 pc 配置。原因和上一次升级出奇的一致,又是因为 DNF 这个游戏….不过本篇的主角并不是新升级的电脑。从定位上来说,这台电脑将是完全的游戏 PC,也没有什么需要多说的。本篇的主角依旧是我 4 年前购买的这台电脑

这台电脑已经服役 4 年多,但是在网络游戏以外,其他各方面的表现都让我满意(可能不包括功耗)。所以淘汰下来以后,我预备用来搭一台新的服务器。相比之前使用 HPE Gen10 plus 搭建的 homeserver,对于这台服务器,我期望的升级点包括:

  1. 更大的内存,能支撑更多 vm 和 docker 应用
  2. 更高性能的 cpu
  3. 全闪存存储,无机械硬盘
  4. 接入 2.5~10G 网络

在使用 HPE Gen10 plus 一年有余的时间以后,我在 home server 上运行的应用逐渐达到了这台服务器能承载的极限。本以为,下一台搭建的服务器,应该是 arm 架构+全固态硬盘的组合。遗憾的是,前者目前并没有成熟的方案,后者倒是有机会尝试。因此这一次姑且是算是介于两者之间的一次半代升级。

不过这次半代升级也并非一时兴起, 至少有一半也是从真实需求出发。一方面,千兆网络环境下的 nas 确实在大文件的移动归档上体验有所不足,比如有时候我会把上百 G 的视频文件从桌面电脑上拷贝到 nas 上备份,整个过程耗时可能长达数十分钟。如果说这还不是致命的,那机械硬盘羸弱的 4k 读写性能,则是存在致命的缺陷。在升级之前我还不是很确定,不过在将 hentai@home 的缓存数据迁移到固态硬盘之后,证实了这里的读写瓶颈确实限制了 h@h 的点击率。

硬件更替

于是,在新的游戏 PC 搭建完毕开始正式服役之后,我就开始着手这台旧 PC 的改装。也是趁着 6 月份做了一点简单的采购,内容如下:

部件 名称
硬盘 西数 SN640 7.68T(U2 接口, 带 m2 转接)
网卡 mellanox CX341A
交换机 磊科 GS6(4 口 2.5G 电 2 口 10G 光)

除了硬盘以外, 其他部件价格都意外的便宜。其中网卡是淘宝买的拆机件,加上带光模块的光纤线材也不过百元出头。交换机本来想先买个全2.5G做过渡,意外发现这个新款方案带2个万兆光口居然不到300元,就干脆也换掉了。

系统

上一台nas的底层系统用的是esxi,虽然用下来也算是一切安好,不过这次我还是决定换成pve。原因有二,一是考虑到硬件用的是普通PC的配置,后期硬件的更替也会相对灵活,pve相比esxi在硬件的适配上应该算是有优势的;第二点则是我用下来对esxi最大的不满,备份相对不方便。对于企业用户来说,在一整套的vmware体系下,这些需求大概都不算事。不过对于单个虚拟机的使用,还要一整套的系统来做这些备份的事情,就太麻烦了。相对来说,pve在这一点上就很简单。设置好计划任务,往挂载的网络硬盘上写备份就行了。

nas系统

这几年里用truenas作为底层的存储系统也算是一切安好。不过这次我换成了omv,倒也没有什么特别的原因,姑且算是用两套不同的系统来降低风险吧。硬要说好处的话,omv没有使用zfs,所以这套系统不需要分配太多内存,可以省出更多的内存来跑应用。不过话说回来,哪怕这次再装truenas,我可能也不会分配很多内存给存储系统。底层都用ssd了,真的还有必要为存储分配大量的内存吗?不过这次没有用truenas,所以也没有往下深究。

硬件直通

其实我对于硬件直通没有太多要求,也许完全不直通也是可以的。因此也就只是尝鲜性质的用了用,把主板上的m2接口直通给了omv,还算是一切顺利。

另外一个可选的玩法是将网卡做半虚拟化的拆分,这大概也算是硬件直通?优势是能分担cpu承载虚拟网卡的负担,不过暂时我也用不到这么高性能的虚拟网卡,所以也没有折腾。

应用迁移

考虑到平稳过渡,旧的nas并没有一次性下线,而是依旧延用作为存储服务,起到本地二次备份的作用。如此以来,数据3、2、1备份策略也算是完整实施了。

剩余的应用的迁移,也不费事。我的绝大部分都是以docker-compose模式部署,原则上我甚至不需要做完整的虚拟机迁移,只要把docker-compose配置文件,和外部存储的数据文件拷贝到新的虚拟机上,就可以接着使用。不过虚拟机实例的迁移算起来还是相对更简单一点,因此最终还是选择了虚拟机实例平迁。旧的esxi虚拟机导出后再导入到pve中,然后停掉旧实例,启用新的即可。另外我还有一套做开发使用的arch虚拟机环境,这套环境从最早的windows pc上的hyper开始,先是迁到了esxi,这次又迁移到了pve继续使用,倒也是十分符合当初我把开发环境虚拟化的初衷。只要这套arch环境在未来的滚动更新中不会因为什么原因挂掉的话,我应该可以无视任何硬件的更迭,继续使用下去。

P2V

在准备将这台PC重装为PVE之前,我考虑了一下该怎么给原来的系统做备份。原则上我是可以把所有需要的文件拷贝走备份到nas上。不过这次我选择用了一个更粗暴的方式,将原来的系统整个打包成虚拟机镜像。某种意义上来说,这个旧系统也算是永久保留了下来。虽然未来我应该也不会再使用了,权且作为一个纪念吧。

新系统的感想

这套系统的理论性能要比原来的nas(HPE gen10 plus)强不少,这里面有一部分cpu、内存等核心部件性能的提升,不过主要的体验变化还是来自于纯ssd的存储配置。固态带来的读写性能的飞跃,让很多重IO的应用都有了质的提升。下一步我应该也会基于新机器的性能特点,寻找更多的应用场景,毕竟很多过去可能难以想象的实现方式,现在都成了可能。

虽说只算是一次半代升级,不过我对这次的升级成果相当满意。

序言

最近几个月chatgpt很火, 虽然我在去年12月份刚出的时候就开始试用了, 不过写篇感想什么的, 倒是确确实实是最近才想到的. 这个时候再回过头谈一谈看法什么的, 有一种蹭热度来晚了的感觉. 因此本篇我只谈使用感想, 不谈看法心得.

去年12月份的时候, 因为没有稳定的API, 所以基本只是在openai的网站上使用, 老实说还是挺受限的, 还有各种各样的问题, 回答缓慢甚至是说到一半中断了之类的. 之前Stable Diffusion开源模型没有出来之前, 我也不是没有逆向过novelai的api, 把画图的功能加到Yunzai-Bot这个聊天机器人上. 不过鉴于chatgpt热度太高, openai和逆向开发者道高一尺魔高一丈的斗智斗勇, 我实在没兴趣参与, 于是就摆烂等openai正式的api出来. 3月初有了正式的api之后, 就顺势做了一个聊天机器人用的插件. 不得不说, 接入聊天工具以后, 机器人的功能才算是真的好用了起来.

实用案例

资料检索

聊天工具也不是没有做过集成搜索引擎的事情, 依稀记得某几个版本的qq似乎就做过这样的事情, 在发出去的聊天消息里会加上搜索引擎的超链接, 点击后就会跳出搜索界面. 当然这种交互方式自然是比不上直接问机器人, “xxx是什么”, “为什么yyy”, 然后机器人直接告诉你答案来的自然和直接. 曾经, 搜索引擎也有过一个叫做”手气不错”的功能, 可能在搜索引擎的场景下, 这种直接给出答案的模式成功率并不高, 如果第一个链接不对, 你就只能顺着链接一个一个点开看了. 而带上下文的聊天机器人则可以在第一个答案的基础上, 一步步修正来逼近你预期的目标.

TLDR

作为IT工作者, 我更常用的模式是用它来取代阅读一些命令行工具的说明文档. 比如使用rsync将文件同步到目标ssh服务器命令该怎么写, 因为chatgpt可以理解上下文, 甚至可以接着问, 我想在同步的时候删除目标服务器上多余的文件同步的时候把进度信息写入到日志文件把这个命令写成sh脚本, 并创建crontab任务, 每天早上2点运行一次. 这些事情虽然自己看手册、查文档也能完成, 不过由AI来写, 确实省了不少时间. 毕竟我不是天天和这些命令打交道, 不太会把rsync每个参数都记下来的程度.

几年前我在mac上装过一个alfred插件, 叫tldr. tldr的本意是太长不看, 一般是指发帖或者文档内容太多, 让人不想读下去. 而这个插件的意义省去你看文档的时间, 把每个命令行工具最常用的用法的命令给你列出来. 不过实际使用上, 这个插件的体验还是有很多不足, 很多时候你对典型的命令用法会有一些小调整, 这个时候这个插件就无能为力了, 只要有一个你需要的参数它没有解释, 你就只能重新去翻手册. 在这一点上, 自然语言模式的交流, 确实要远强于说明文档. 最近几年各类弱智的问答机器人其实流行度很高(比如各类购物网站的客服、聊天工具上的机器人等等), 虽然很蠢, 但是又不是完全没用. 说明这种聊天问答的模式, 对比枯燥无味的手册, 确实更能让人接受.

生成代码

这部分功能和上一节TLDR部分说的有一些重合, 一些基础性质的代码生成确实很好用, 这里用我实际用过的一个例子来说明, 这部分内容包含大量代码, 不熟悉的读者可以选择跳过.

因为最近我开始大量使用openai的whisper模型(语音转文本), 所以我要编写一个脚本来读取音频文件, 这里面可以分解成几个子任务, 如下:

  1. 读取一个目录下所有指定后缀的文件(在我这个场景下, 是后缀为m4a的音频文件)
  2. 读取同一个目录下已经生成的文本文件(在我这个场景下, 是后缀为srt的字幕文件)
  3. 计算两个列表的差集, 即真正需要调用api翻译成文本的音频文件.
  4. 对于大于25MB的音频文件, 需要拆分成小于25MB的文件, 分批调用api, 再将api响应的文本做合并

这其中, 1、2两条很简单也很通用,于是我让chatgpt为我生成一段python代码来生成文件,生成的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def find_files_with_suffix(path, suffix):
"""
遍历指定目录下的所有文件,查找指定后缀名的文件
:param path: 指定的目录
:param suffix: 指定的后缀名,必须包含"."
:return: 返回满足条件的文件路径列表
"""
file_list = []
for root, _, files in os.walk(path):
for file in files:
if file.endswith(suffix):
file_path = os.path.join(root, file)
file_list.append(file_path)
return file_list

chatgpt生成的代码不仅可用, 而且还有完备注释, 确实不错.

然后对于第3条子任务, 可以简化为计算两个集合的差集, 我的预期不高, 但是也试着让chatgpt生成代码

1
untranslate_list = [value for value in audio_files if value not in srt_files]

我不是一个正经的python主义者(pythonism), 让我写的话, 可能会写成传统的for循环, 所以这个答案多少有一点让我惊艳, 以至于后面我在写代码的过程中, 时常会尝试性的问chatgpt, 我写的这段代码还能不能更精简一点…

对于第四条, 相对比较困难, 我尝试把其中做文件拆分的部分让chatgpt来写, 很遗憾这部分的答案不太令人满意, chatgpt生成的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pydub import AudioSegment
import math

# 定义音频文件大小的最大值为20MB
MAX_FILE_SIZE = 20 * 1024 * 1024

# 定义分割音频的函数
def split_audio(filename):
# 加载音频文件
audio = AudioSegment.from_file(filename)

# 计算音频文件的总长度(单位:毫秒)
total_length = len(audio)

# 计算每个小于20MB的音频文件的长度(单位:毫秒)
segment_length = math.ceil(MAX_FILE_SIZE / (total_length / 1000))

# 定义分割音频的起始时间和结束时间
start_time = 0
end_time = segment_length

# 分割音频并保存为多个小于20MB的文件
for i, segment in enumerate(audio[::segment_length]):
# 找到当前分段音频最后一秒的静默部分,避免将正在说话的部分切断
last_second = segment[-1000:]
silent_threshold = last_second.rms - 10
silence = last_second.detect_silence(silence_threshold=silent_threshold, min_silence_len=500, seek_step=1)

if silence:
end_time = start_time + (silence[0][0] + silence[0][1]) * 1000
else:
end_time = start_time + segment_length

# 保存当前分段音频为新的音频文件
new_audio = audio[start_time:end_time]
new_filename = f"output_{i}.mp3"
new_audio.export(new_filename, format="mp3")

# 更新下一段分割音频的起始时间
start_time = end_time

# 调用分割音频的函数
split_audio("input.mp3")

这一段代码乍一看有模有样, 但是很遗憾, 它并不能达成拆分文件的目标, 这里chatgpt无法理解其中的业务逻辑(比如为了正常拆分, 我需要音频文件在静默时间段拆分, 而不是强制按照特定的时间间隔拆分, 此外在拆分的时候, 应该尽量保证每个拆分片段足够长). 更遗憾的是, 我甚至没办法在chatgpt生成的代码基础上继续改, 因为这里我完全无法理解生成的代码的内在逻辑…

另外, 根据我的理解, gpt3.5作为通用语言模型, 应该是不具备生成代码这种需要精准的思维逻辑的能力(这一点从chatgpt甚至无法顺利完成复杂的四则运算就能看出来), 生成代码的能力应该是一个独立模型完成的. 这一点我在openai的官网上查了一下, 在gpt3.5之前, openai确实有另一个专用于自然语言生成代码的模型Codex. 因此我猜想gpt3.5中应该是延用了Codex的能力, 专门训练了通过自然语言生成代码的能力.

角色扮演

这类玩法网上说的很多, 因为接入了聊天机器人, 所以我也简单试过几个场景, 比如让chatgpt扮演旁白, 玩文字冒险游戏之类. 这里面我觉得主要的不足在于, 因为缺乏逻辑思维, 所以chatgpt并不具备长线编排故事的能力, 所以故事的情节显得比较无趣. 倒是可以先安排好故事大纲, 让chatgpt添加细节描写. 因为不是DND玩家, 这一块就不发表深度意见了.

不过有一点倒是需要一提, 目前的对话模型(包括最新发布的GPT4), 上下文能力还是非常有限的. 虽然说和过去几乎没有上下文能力的AI(比如Siri、小爱同学等等)相比, 已经好了很多. 限于目前gpt能接受的文字内容限制(gpt3.5是4k token, gpt4宣传是扩大到了3万, 但是目前使用chatgpt plus依旧用不到这么高), 另外也是这类模型(Generative Pre-trained Transformer)的特点, 文本越长, 越早的内容权重会越低, 所以就容易出现比如一开始给chatgpt一个设定, 但是随着对话的进行, 这个设定就失效了.

yet another TLDR

上文也提到了, 我有在大量使用whisper这个语音转文本的模型, 我经常在一些参与感不高的会议上用手机录音, 然后事后用这个模型来做语音转文本. 不过语音转出来的文本量通常非常巨大, 而且对于没有提前准备发言稿的会议发言来说, 阅读体验是非常差的, 因为这种场景下的文字经常不连贯, 有片段的重复之类, 让正常人类来阅读这些文字真的挺折磨的. 不过我意外的发现这件事情chatgpt非常适合做这件事情, 即使存在一些不通顺的句子, 这个模型也能顺利对全文进行总结摘要.

总结

本篇只是例举了一些我实际用到的AI对话场景, 具体对于这个模型的一些看法的内容, 以后有机会再聊吧.

花絮

本篇博客在写作之前, 我脑洞大开, 原本期望可以由chatgpt为我生成一篇内容, 再稍作润色修正, 遗憾的是生成的效果并不让人满意. 简单的加一些promote页并没有改善, 个人感觉对于文字风格的把控, 在缺少训练素材的情况下, 还是比较难实现的. 或许等以后openai开放模型定制后会好一些. 下文是基于gpt-3.5-turbo模型生成的效果, 一并记录在这里.

ChatGPT试用评测

ChatGPT是一个基于GPT-3的自然语言处理模型,可以根据输入的摘要信息生成完整的文章。在我的试用过程中,我发现ChatGPT非常好用,可以根据自己的需求输入文章的摘要信息,也可以让ChatGPT生成指令和代码,生成的代码简洁实用。

在我进行指令和代码生成的过程中,我发现ChatGPT的工作效率非常高,生成出的代码符合我的预期,而且代码的可读性也非常好。但是,正如许多人所指出的那样,ChatGPT的记忆量非常有限,目前只能保留4k token的上下文,因此记忆量很有限。

虽然ChatGPT能够用于生成代码,但是我并不认为它能够引发编程革命,因为ChatGPT本身并没有对代码做抽象的能力。因此,在目前的技术水平下,ChatGPT并不能让代码变得更少。出于这种原因,它在编程领域的应用还是受到了一些限制。

然而,从我的试用经验来看,ChatGPT大大提高了我的工作效率,尤其是在文章的生成过程中。它可以根据我的需要快速生成文章的摘要和关键信息,使我更快地完成撰写工作。

总的来说,ChatGPT是一个非常好用的自然语言处理模型,它可以帮助我们快速生成文章、指令和代码,提高工作效率。虽然它有一些限制,但在我们需要更快地完成工作的时候,ChatGPT会成为我们非常好的助手。

前言其一

本篇博客写作之时,本站的域名已经从 https://blog.goatman.me 切换到 https://blog.xdsoar.net,原因是为了将站点迁移到国内,只好更换了可以在国内备案的域名。原域名已经做了301重定向处理,不过还是建议看到这里的读者更新一下地址。

使用国内的对象存储服务来做静态站点托管有一个奇怪的矛盾。做静态站点托管,我需要有一个已备案的域名;要备案域名,我需要购买云主机;如果我购买了云主机,那么为什么我还需要对象存储做静态站点托管?

对象存储做静态站点是我特别喜欢的公有云服务,价格低廉又安全可靠,但为此单独买一台云主机着实让我受不了。直到最近发现阿里云购买函数服务包也可以享受代备案,此事才算了解。

前言其二

这个博客的最早的几篇博文,写的就是当初在s3上部署的流程,在那之后的几个实验项目我也都是按照当时博文里写的流程来做发布。不过最近几年里,我也在思考能不能更进一步,让整个流程以更顺畅的方式进行。CI工具已经打通了从代码到制品的全过程,代码到服务的最后一公里,或许就要靠IaC工具来实现了。

去年我花了不少时间研究aws的CloudFormation,老实说看的我云里雾里。折腾了半天也没搞明白,想了想这东西可能真的不适合个人使用。直到熟悉IaC的朋友给我推荐了Terraform才算找到了条可行的路子。

本身迁站是个很繁琐的事情,结合Terraform来做,本是可以省不少事的。不过实际使用下来,terraform与阿里云的衔接还是有不少坑,所以实际并没有那么顺利。好在这种事情折腾一次,后面的项目都可以复用,还算是比较便利。

关于Terraform

Terraform是HashiCorp创建的IaC工具,在IaC领域目前姑且算是较为成熟的产品。从我短暂的使用体验上来说,算不上优秀,只能说是够用的程度,当然这可能也和我使用的是阿里云的provider有关。因为阿里云的配套设施对比aws确实算不上成熟,一些配置项在控制台页面可以配置,但是在terraform中无法配置。我本来以为是provider没有实现,想着实在不行就提pr加一下,都拉了代码准备动手了,结果阿里云的API本身就没有提供这块的配置项……

因为目前我没有使用Terraform来管理aws,所以aws的provider姑且不评价。不过主观评估,这类第三方工具通常是很难做到完全覆盖第一方产品的功能的。

从动机上考虑,云服务厂商恐怕更希望提供独有的无可替代的服务。而目前除了aws s3成了对象存储的事实标准以外,各类云服务其实并没有特别统一的标准。因此即使Terraform声称支持众多云服务提供商,也不可能做到一次编写,到处运行的效果,想要把已有的aws上的服务,改一改provider就迁移到阿里云上,依旧是做不到的。

那么IaC的作用是什么呢?我个人认为是为基础设施提供更好的运维方式。特别是采用声明式的管理方式,可以确保基础设施的状态是受控的,可感知的。当然前提是能够完全通过IaC工具进行基础设施管理,使它成为单一事实来源。

Terraform的官方文档比较冗长,为了快速上手,我参考的是阿里云的文档)。下面我以本次博客部署为例,讲述使用Terraform管理基础设施的全过程。

开始之前

开始之前,需要做如下准备:

  • 安装Terraform命令行客户端
  • 创建阿里云用户对应的access key,并在RAM中赋权。
  • 购买一个域名

其中域名或许也可以通过Terraform创建,不过域名毕竟不像其他资源那么随意,所以我没有使用Terraform管理。第二步原则应该创建一个IaC专用的用户并设计权限范围,个人使用并没有那么讲究,我就直接使用了实名用户。

准备好之后就可以使用Terraform进行资源创建了。这里再啰嗦两句,使用Terraform的过程中会产生一些记录资源状态的文件(tfstate),又或者是创建的用户密钥文件,如果有团队协作的需求,使用对象存储是个好办法,别忘了注意访问权限。个人使用来说,放到一个私人的git仓库当然也是可以的,同样要注意不能放到公开仓库里

至于一些可复用的配置模板或者是分享给别人用的参考配置之类的,可能gist是个不错的选择。

配置对象存储与授权

首先厘清一个静态博客站点需要用到哪些云服务

  • 一个oss bucket用于存储静态站点的文件
  • 一个dns记录,作为博客域名
  • 一个用户,以及对应的RAM,用于CI部署制品到OSS
  • (可选)一个用于加速访问的CDN
  • (可选)一个SSL证书,支持https访问

前三步基础配置其实很简单,参考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
provider "alicloud" {
alias = "hz-prod"
region = "cn-hangzhou"
}

resource "alicloud_oss_bucket" "bucket-blog-log" {
provider = alicloud.hz-prod
bucket = <bucket_name_for_log>
acl = "private"
force_destroy = true
}

resource "alicloud_oss_bucket" "bucket-blog" {
provider = alicloud.hz-prod

bucket = <bucket_name_for_blog>
acl = "public-read"
force_destroy = true

# 配置CDN加速
transfer_acceleration {
enabled = true
}
# 静态网站的默认首页和404页面
website {
index_document = "index.html"
error_document = "error.html"
}
# 访问日志的存储路径
logging {
target_bucket = alicloud_oss_bucket.bucket-blog-log.id
target_prefix = "log/"
}

# 防盗链设置
referer_config {
allow_empty = true
referers = ["http://<blog_url>","https://<blog_url>"]
}
}


resource "alicloud_ram_user" "blog_user" {
name = "blog_deploy"
display_name = "blog_deploy"
mobile = "86-18688866888"
comments = "devops user for blog"
force = true
}

resource "alicloud_ram_access_key" "ak" {
user_name = alicloud_ram_user.blog_user.name
secret_file = "<storage_path>/accesskey.txt"
# 保存AccessKey的文件名
}

resource "alicloud_ram_policy" "blog_policy" {
policy_name = "blog_oss_policy"
policy_document = <<EOF
{
"Statement": [
{
"Action": [
"oss:*"
],
"Effect": "Allow",
"Resource": [
"acs:oss:*:*:<bucket_name_for_blog>",
"acs:oss:*:*:<bucket_name_for_blog>/*",
"acs:oss:*:*:<bucket_name_for_log>",
"acs:oss:*:*:<bucket_name_for_log>/*"
]
}
],
"Version": "1"
}
EOF
description = "privilege for deploy to blog bucket"
force = true
}

resource "alicloud_ram_user_policy_attachment" "blog_attach" {
policy_name = alicloud_ram_policy.blog_policy.policy_name
policy_type = alicloud_ram_policy.blog_policy.type
user_name = alicloud_ram_user.blog_user.name
}

# 如果不需要使用独立cdn, 则将dns解析到oss专用的加速域名上
resource "alicloud_alidns_record" "blog_record" {
domain_name = "<domain>"
rr = "<sub_domain>"
type = "CNAME"
value = "${alicloud_oss_bucket.bucket-blog.id}.oss-accelerate.aliyuncs.com"
remark = "blog domain"
status = "ENABLE"
}

整个文件中需要使用者自己定义的参数都以<custom parameter>的方式标出,应该说还是挺通用的。如果想新建一个别的站点,只要把整个文件复制一份,并填写相应的参数就好了,然后一条terraform apply就可以创建出来。

不过很遗憾的是,做完上面这些步骤之后,还有两项设置,只能在控制台进行。分别是设置bucket可以访问的域名,以及设置子页面的首页,即访问aaa.com/bb/时展示aaa.com/bb/index.html的内容。由于这两项配置甚至连阿里云的API都没有提供,目前看来是很难实现了。我能想到的就是在hcl中声明,当bucket被创建的时候,打印一条消息,提醒需要手动执行的这两个步骤了……

除了这两项做不了的配置,整体过程应该说还是很顺畅的,配置完成后,本地也有了访问对象存储的密钥,先本地做一趟实验把命令跑通,再修改博客对应的流水线脚本,整个迁移过程就算完成了。

配置SSL证书

其实按上面的流程走完,OSS内建的加速方案也已经开启了,原则上就只有配置SSL证书这一件事情要单独做,偏偏这件事情做起来又不是那么方便。经过前面对OSS相关API的摸排,我确信虽然控制台上提供了配置SSL证书的步骤,但是这个过程无法通过Terraform完成。如果觉得麻烦,这步也可以在控制台完成。

这一环节的另一复杂性在于,如果不想用阿里云的免费证书,使用诸如acme.sh之类的工具申请的免费证书只有3个月的有效期,为了能让证书自动更新,那么还需要定时作业来更新证书。简而言之,不是很有必要,而且很复杂,如果真的想干,那么就接着往下吧……

首先是再创建一个用户,并赋予更新dns record权限,用于申请证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
provider "alicloud" {
alias = "hz-prod"
region = "cn-hangzhou"
}


resource "alicloud_ram_user" "ssl_user" {
name = "sslUser"
display_name = "sslUser"
mobile = "86-18688866888"
comments = "for get ssl cert"
force = true
}

resource "alicloud_ram_access_key" "ak" {
user_name = alicloud_ram_user.ssl_user.name
secret_file = "accesskey.txt" # 保存AccessKey的文件名
}

resource "alicloud_ram_policy" "ssl_policy" {
policy_name = "ssl_policy"
policy_document = <<EOF
{
"Statement": [
{
"Action": "alidns:*",
"Resource": "*",
"Effect": "Allow"
}
],
"Version": "1"
}
EOF
description = "privilege for ssl"
force = true
}

resource "alicloud_ram_user_policy_attachment" "ssl_attach" {
policy_name = alicloud_ram_policy.ssl_policy.policy_name
policy_type = alicloud_ram_policy.ssl_policy.type
user_name = alicloud_ram_user.ssl_user.name
}

上面的配置里没有一个变量,属于拿来即用的简单配置。不过这里我分配的权限偏大了一点,给予了账户下DNS解析服务的所有权限,追求权限分配最小化的话可以更细化一点。

关于acme.sh的使用这里就不展开叙述了,网上相关资料很多,用acme.sh alidns做关键词就能搜到很多,比如这篇),你会发现这篇文章里占了较大篇幅的用户权限配置部分,在用了Terraform以后可以完全被上面的配置实现,有没有感受到一点IaC带来的便利?

这样就拿到了免费的证书,可以开始正式配置了。

由于CDN的配置中可以指定,所以可以通过重新配置CDN并指定SSL证书实现https。如果要进行以下配置,记得先在之前配置bucket的文件中,去掉关于dns记录的配置,并再次terraform apply,当然如果你想使用一个不一样的域名用于CDN的话,也是可以的。具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
provider "alicloud" {
alias = "hz-prod"
region = "cn-hangzhou"
}

resource "alicloud_cdn_domain_new" "domain" {
domain_name = "<cdn_blog_domain>"
cdn_type = "web"
scope = "global"
sources {
content = "<oss_bucket_url>"
type = "oss"
}
certificate_config {
server_certificate = <ssl_cert>
private_key = <ssl_key>
cert_name = "terra_ssl_cert"
cert_type = "upload"
}
}

resource "alicloud_alidns_record" "blog_record" {
domain_name = "<domain>"
rr = "<sub_domain>"
type = "CNAME"
value = "${alicloud_cdn_domain_new.domain.cname}"
remark = "blog domain"
status = "ENABLE"
}

这里有一个小细节,这个参数理论上可以从上一个关于oss bucket的配置中拿到。不过这个CDN的配置我单独拆了一个文件,原因是这个CDN的实现并不理想。如果按照上面的配置做一次terraform apply是没有问题,但是如果要做修改的话(其实相当于destroy再create),失败的可能性相当大。这主要是因为CDN这个资源的一致性较差,destroy后需要等待一段时间后才能使用重新创建。

而因为SSL证书有效期的问题,每隔几个月,就要手动destroy,等待一段时间后再apply。当然因为整个流程其实就是几个命令的事情,加一个cron任务,自动执行也不是多难的事情,倒也还算可以接受。另一个瑕疵点在于,删除了dns记录和cdn后,一段时间内这个域名就无法访问了。从可用性角度考虑,更新时采用AB两个CDN实例进行蓝绿切换会更合适。不过我这只是一个简单的博客站点,就不额外考虑这么多了。

后续

前面写了好几篇关于家用服务器(home server)的内容,但其实公有云也有很多实用又实惠的用法。但是记录过程要一页一页的截图,一些实验性质的操作做完再重复一遍也很麻烦,有了IaC工具以后确实方便了很多。后续我会把这些有趣的尝试记录下来,也算是一个系列吧。

背景

我有一个服役超过7年的android平板Sony z3 tablet compact,上面运行着号称android核心竞争力的应用:ehviewer。

长久以来,我都有每天登录ehviewer刷新一下订阅并下载新的漫画本子的习惯。之前我就有想过这个流程能不能优化,一方面每天手动刷新下载比较繁琐,并且若是不及时下载,一些资源在上传之后又会被删除。此外由于ehentai的资源限制,ehviewer下载原图并不方便,通常下载的还是重采样后的缩放图片,在平板上阅览虽然影响不大,若是作为仓鼠收藏则有点过意不去。

而且在长年使用之后,ehviewer中的下载列表变的很长,在翻阅上也有一些不便,大量漫画的管理并不是ehviewer的核心功能,基础的检索、收藏功能也是依托ehentai提供的api实现的。ehentai在功能上虽然没什么可以挑剔的,不过也曾面临过关站风险,而且部分版权物也会被删除,如果有办法做本地管理作为替代和补足,自然也更好。

订阅下载

使用到的软件:RSSHub、qBittorrent

本来以为整套流程都需要自己处理,结果调研的时候发现RSSHub+qBittorrent就可以把整个流程完美的解耦后串联了起来。

ehentai原站提供的RSS功能比较单一,无法满足个性化订阅的需要,不过有了RSSHub就比较方便了,这里主要用到的是RSSHub中ehentai的搜索路由。路由参考RSSHub官网的说明,格式为 /ehentai/search/:params?/:page?/:routeParams?。可以先在ehentai网站上搜索一次,复制网址后面的搜索参数。这里额外补充一点,其中页码参数page是从0开始而非1。以及因为要用于后续下载使用,所以还需要把获取种子地址参数打开。

例如我会订阅所有评分为5分的汉化漫画和同人志,最后的路由为:

/ehentai/search/f_cats=1017&f_search=language%3AChinese&advsearch=1&f_sname=on&f_stags=on&f_sr=on&f_srdd=5/0/bittorrent=1

qBittorrent在server端需要额外安装qBittorrent-nox来提供web管理功能,稍微需要注意,qBittorrent-nox的版本不能太低,否则是没有rss订阅功能的。把订阅地址扔到qBittorrent的RSS订阅里,配置好自动下载,整个流程就算完成了。本来还想着对于没有提供种子的漫画需要额外处理一下走http下载,搜了一下近半年的汉化漫画都提供种子文件,决定暂时先不折腾这块了。

当然这样一来下载的漫画就不方便在ehviewer里看了,需要额外的阅读工具,这部分在下面详述。

额外的EX

部分资源只在里站exhentai有,所以最好还是配置从里站下载。不过这比表站就要多几步折腾。

RSSHub本身支持检索exhentai,只需要在环境变量中添加EH_IPB_MEMBER_ID, EH_IPB_PASS_HASH, EH_SK, EH_IGNEOUS这四个参数,就会自动改成从里站爬取信息。这里有一个小坑,添加上述变量后,访问ehentai和exhentai,会采用用户的个性化配置,可能会对网页元素造成影响,影响爬取。可以在网站的个人设置中,添加一个默认的profile供RSSHub使用。

另外一个麻烦的问题是,exhentai只能在登录后访问,所以输出的exhentai的种子文件地址,在qBittorrent中是无法下载的。搜了一圈qBittorrent好像也没有提供在下载种子文件的时候配置自定义header这么偏门的功能。

RSSHub ehentai路由的作者也许在其他场景下碰到了这个问题,因此提供了一个EH_IMG_PROXY参数,用于替换生成的RSS文本中图像的链接为代理服务器地址,从而在RSS阅读器中可以看到exhentai上的封面,不过他可能没想过把这个参数应用在种子下载地址上……

这个思路倒是可行,在代理服务中手动配置Cookie到header中,应该可以解决exhentai无法访问的问题,遂尝试在nginx上添加一项代理配置

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name exhentai.<HOMELAB_DOMAIN>;
location / {
proxy_pass https://exhentai.org;
proxy_set_header Host exhentai.org;
proxy_ssl_server_name on;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie <EXHENTAI_COOKIE>;
}
}

测试了一下使用代理地址下载种子文件,没有问题,看来方法可行。

理论上可以把server_name直接配置成exhentai.org,然后在hosts或者自定义的dns中把这个域名指向homelab的内网ip,这样不需要修改RSS输出结果就能让qBittorrent通过反向代理下载到种子文件。不过exhentai的下载地址使用了https,虽然不清楚qBittorrent和ehentai在保障https通信安全上做的如何,使用自签证书或许可以绕过去,不过我也不太想干这种左右互搏的事情。

所以为了让qBittorrent订阅到的地址指向代理服务器,本来最后一步应该是给RSSHub提个PR,把代理地址应用到替换种子地址上,不过这样多少要费些时日。临时性的又想了个法子,反正是要做反向代理的,索性在nginx的反向代理里加一项配置,把RSSHub的响应体中的exhentai域名直接替换成代理的域名。修改的配置也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;
server_name rsshub.<HOMELAB_DOMAIN>;
location / {
proxy_pass http://<HOMELAB_DOMAIN>:<RSSHUB_PORT>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
sub_filter_once off;
sub_filter_types application/xml;
sub_filter "https://exhentai.org/" "http://exhentai.<HOMELAB_DOMAIN>/";
}
}

管理阅览

我之前试过用calibre来做管理,calibre也有从ehentai[获取元数据的插件](yingziwu/doujinshi_metadata_plugins: the calibre metadata plugins for doujinshi (github.com)),把ehviewer上的漫画导进去之后,先编辑好基础的名称和作者信息,然后用插件从e站获取余下的tag等信息。整个管理流程是没有问题,不过体验也说不上来有多好,另外calibre基本只能解决管理的问题,阅览的需求需要另外想办法。

本以为没有更好的方案就只能把这两步分开,结果意外发现Lanraragi用起来还不错。在管理功能上,比起calibre更适合做漫画和同人志的管理,也有插件从ehentai获取元数据。

此外在阅览方面也做的不错,网页端的功能算不上很完善但是够用,而且非常难得的有tachiyomi插件,移动端的体验也比较好。

因为Lanraragi和tachiyomi插件都是最近才开始用,暂时还没有什么大问题,后续折腾的时候碰到什么问题再单独更新吧。因为元数据都在本地,后期即使要迁移到其他管理方案上,也具有一定可行性。

唯一比较麻烦的是这个应用的后端是Perl写的,看起来有点头疼,希望后续不会有需要钻进去改代码的时候。

使用中的一个小插曲,下载的漫画我是放在nas上通过smb挂载到运行Lanraragi的虚拟机上,本想着数据库也直接存在nas上方便备份,结果应用一启动硬盘吵的像破锣似的,但是又没有大量读写。我本以为是因为频繁需要扫描文件变更来做更新,感叹以后漫画本子只能存ssd了,后来看了日志发现是因为smb权限问题导致Redis AOF持久化失败了疯狂重试导致的,算是给我刚服役不久的两块机械盘留了条活路。

这篇说一下homelab相关的内网访问配置,本来想写一些资源管理的内容,提纲列到一半发现存在一些依赖,只好先把网络配置的部分说了。

基本需求

在硬件部署完成以后,剩下的步骤其实都可以通过web端进行,而我大部分捣腾这台机器的时间也不是在家里,这样一来就需要能通过外部网络环境访问到家里的局域网。

出于安全考虑,需要尽可能少的暴露端口到公网上。此外,国内的家用宽带常规是不允许提供互联网服务的,暴露homelab上的web服务端口也可能给自己带来不必要的麻烦。

方案设计

动态域名解析

远程访问需要解决的第一个问题是获取家里宽带的ip。家用宽带的IP每次拨号都会发生变化,且存在固定时间间隔强制断开重新拨号的情况,从便利性上来说这一步也是必要的。不过方案也是现成的,很多路由器或者nas系统都提供了动态域名解析功能,一般是使用品牌商提供的子域名用于解析,直接使用即可。

不过因为我有几个托管在aws上的域名,就索性定义了一个二级域名,用一个定时脚本结合aws-cli工具来做更新,脚本是网上找的,因为aws查询IP的服务器地址是在国外,调用时走代理路线导致拿到代理的IP,于是换用了国内的ip查询服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/bash

#Variable Declaration - Change These
HOSTED_ZONE_ID=<HOST_ZONE_ID>
NAME=<DOMAIN_NAME>
TYPE="A"
TTL=60

#get current IP address
#IP=$(curl http://checkip.amazonaws.com/)
res=$(curl myip.ipip.net)
res_sub=${res: 6}
IP=${res_sub%% *}
#validate IP address (makes sure Route 53 doesn't get updated with a malformed payload)
if [[ ! $IP =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
exit 1
fi

#get current

/usr/local/bin/aws route53 list-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID | \
jq -r '.ResourceRecordSets[] | select (.Name == "'"$NAME"'") | select (.Type == "'"$TYPE"'") | .ResourceRecords[0].Value' > /tmp/current_route53_value

cat /tmp/current_route53_value

#check if IP is different from Route 53
if grep -Fxq "$IP" /tmp/current_route53_value; then
echo "IP Has Not Changed, Exiting"
exit 1
fi


echo "IP Changed, Updating Records"

#prepare route 53 payload
cat > /tmp/route53_changes.json << EOF
{
"Comment":"Updated From DDNS Shell Script",
"Changes":[
{
"Action":"UPSERT",
"ResourceRecordSet":{
"ResourceRecords":[
{
"Value":"$IP"
}
],
"Name":"$NAME",
"Type":"$TYPE",
"TTL":$TTL
}
}
]
}
EOF

#update records
/usr/local/bin/aws route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch file:///tmp/route53_changes.json

代理服务

这里的实现方式和科学上网相似,只要把服务端部署在家里的homelab上即可。部署VPN也是可以的,不过我个人感觉这种方式有点重,安全性上来说我觉得应该相差不大,都算的上是经过考验的。

最终暴露在公网上的端口只有两个,一个是http代理服务端口,另一个是方便登录维护的ssh端口。当然这两者都建议映射到五位数以上的端口上,不是说这样就能确保安全,至少能减少一些无聊的嗅探也算节约一些资源吧……

安全性上,ssh服务建议关闭密码登录,或者是使用Fail2Ban这类软件防止恶意爆破。

以上方案适用于家庭网络至少具有公网IP的情况。如果没有的话,则需要使用一些内网穿透的方案,像是frp、ngrok之类。不过这类方案需要一台vps转发,有公网IP的情况下我就不折腾这个浪费流量了。

另外我也准备了zerotier这条备用路线以备不时之需。据说因为是基于udp打洞的方式,在网络质量上由于运营商qos的缘故要比tcp差一些,我实测下来在跨网络运营商的情况下确实差一些,暂时只做备用。

反向代理

这一步不是必要的,不过个人觉得做了以后可以一定程度上优化访问体验。

由于在同一台服务器上部署了大量web服务,访问时通过端口区分,所以浏览器上会有很多类似192.168.x.x:xxxx这样的地址。一方面不方便自己记忆,另外似乎浏览器的密码管理对这种情况处理的也不太好。因此我加了一步用nginx作为反向的代理,根据不同的域名代理不同的后端服务。因为走的是内网代理路线,这个nginx是不需要暴露端口到公网的,也不存在安全性的问题。

nginx的配置比较简单,为了便于维护,核心的nginx配置我只做了基本的日志配置和引入配置目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;
server_tokens off;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;

include /etc/nginx/mime.types;
default_type application/octet-stream;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
}

每个服务的反向代理配置分拆为单独的配置文件,内容也很简单,以qbittorrent为例

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name <qbit_domain>;
location / {
proxy_pass http://<qbit_domain>:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

这里需要解决一个小问题就是将各个服务配置的域名都指向homelab使用的局域网ip。域名配置有两种做法,可以在hosts文件或者路由器的dns配置里按需配置,将域名指向homelab使用的局域网ip。这么做比较灵活,域名想配成什么都行。

另一个做法是使用公开域名做泛域名解析。比如配置一条A记录为*.home.xxxx.com指向192.168.1.100。这个方案的好处是只要配一次就好了,不需要逐个设备配置,也不需要每加一个服务就逐个设备改一遍hosts。缺点就是域名设置上没那么灵活了。

需要注意一点,我在用openwrt做软路由的时候使用了dnsmasq服务,结果发现域名解析无法指向私网地址,解决办法是在防火墙设置中打开IP动态伪装。

如果代理路线不能保证通信安全的话,还可以在nginx上配置ssl证书,通过https线路访问,来起到加密通信的作用。

前言

这个系列的第一篇先来说一说数据备份。毕竟很多人(包括我)组建nas的第一需求,就是用于数据备份,保护自己的数据安全。

我也是趁着这次重新组建的机会,重新整理了自己的数据备份需求。

开始之前

在规划一切方案之前,有一件我一直觉得很重要的事情,就是先要识别自己有哪些数据资产。这件事情并不简单,甚至可以说很难,使用过的设备越多,在互联网上留下的足迹越多,这些数据可能就越散乱。有些数据可能就在哪部电脑或者手机里装着,而有些则可能只存在于某个供应商提供的网盘里,甚至有些数据,服务商觉得并不属于你,你只是被授权访问了…

识别有效的数据资产是件很繁杂的事情,这里就不展开讲了。总之,在开始之前,我已经把所有我觉得需要做备份的数据都标识好了,这是一切的起点。

基本策略

数据备份有个被称为“3-2-1”的原则。即一份数据包含三份副本,保存在两种不同的介质上,并保持有一份数据存放在异地。

这个原则对于个人用户来说,最简单的实践方案是这样

  1. 在原始设备上(比如手机、电脑)存一份
  2. 定期将数据备份到移动硬盘或NAS
  3. 再存一份数据到云盘

我个人并不反感云存储,所以这个策略对于多数数据备份场景都是合适的。很多人会提到对于网盘,需要区分”同步“和”备份“。原因是”同步盘“无法防止误操作,如果本地误删除了某个重要文件,”同步盘”会在云端同步删除文件。

对于这一点,我个人觉得很难界定。一些同步网盘也会提供如历史版本、近期删除的文件等功能,来弥补“同步”的缺陷。但是回过头来说,很多“同步盘”都提供了像“文件随选”这样的功能,文件只在需要的时候才会下载到本地来,这时候就要注意了。

其实对于云盘,多数时候我并不担心数据丢失的风险,这本是供应商会考虑的问题。但是从近几年的情况来看,云盘或者说云存储的商业模式并不稳定,这反倒是需要担心的。

对我而言,还有另外一种情况需要关注,就是对于一些大文件(比如照片、视频),单一设备可能存不下的时候,可能本地只会在nas上留存了一份,这时候就需要有额外的方案了。

备份需求

当然,也不是所有数据我都会遵从“3-2-1”原则来处理,实际上我大概把数据分了三类。

  1. 第一类是不希望丢失的重要数据,会严格遵从“3-2-1”模式。
  2. 第二类是在一定程度上可容忍丢失的数据。比如手机、电脑如果故障坏了,能够完全恢复当然好,但是只要圈定的重要数据不丢,重新装一遍系统、应用,做一遍配置之类的,也不是不能接受。这类数据我会挑最简单的备份方案来执行,比如手机、平板就用厂商提供的云服务,实在恢复不了就算了。
  3. 第三类主要是网上下载的各类资源,这类数据定期记录一下目录信息就好,丢了的话再下一遍就好了。

商业上在容灾方案中,经常会提到RTO、RPO指标,通俗来讲,这两个分别指故障发生后,恢复需要花费的时间,以及,能够容忍的丢失数据时长。

对个人用户来说,前者通常是一个相对宽裕的值。假如我电脑坏了,需要买台新的,耽搁几天再正常不过,没什么不能接受的。

后者就需要自己好好评估了,如果觉得丢失一天数据都无所谓,那每天晚上做一次备份就足够了。如果丢失一两个小时的数据都不能接受,那可能就需要在使用的工具软件上做好方案,能够支持更及时的备份。

不过其实我提到这个,是我觉得需要结合这两个指标评估好自己能管理好的数据范围。比如我NAS上管理的数据如果有50TB,假如有一天硬盘全挂了,我需要重新买一套硬盘再把数据从网盘下回来,结果发现网速太慢,光是下载数据就要花6个月…这也是不能接受的。

工具与实施

明确了策略和需求以后,实际实施起来就没有多复杂。主要用的工具包括:

  • syncthing,用于实时同步文档,当然这里的同步并不是备份。
  • borgbackup,用于本地备份,同时兼顾增量备份、保留历史等需求。
  • rclone、BaiduPCS-Go,用于将文件备份到云盘。

其中syncthing会安装在nas和常用的电脑上,将变更的文件实时同步回来。

另外其他如相机上的照片、视频,或是电脑上录制的视频,定期拷到nas上以后,原设备上会删除,需要额外备份一份。这里我暂时没想到特别好的方案,只是刚好旧nas的硬盘(已经使用6~7年)并没有故障,所以就先拿来做备份副本使用,用borg backup制作备份副本的同时进行加密,也解决了上传云盘(主要是百度网盘)的安全问题。

云盘的备份我现在同时在用百度网盘和onedrive。前者单价更低,但是即使文件已经做了加密,我依旧担心会因为各种原因导致文件在云端不可用,因为office365赠送了tb级别的onedrive空间,所以姑且先拿来用。使用rclone可以直接将原始文件加密备份到onedrive上。不过因为onedrive的空间较小,而且分散在多个用户下,使用上还是有一些不便。另外云厂商的提供的对象存储也是可以考虑使用的,可靠性比云盘可能会略好一些,不过价格也略高,对可靠性要求高,数据量不是特别大的话可以考虑使用。

本地的备份前后零零散散大概花了一周左右做完,之后上传两个网盘则在半个月左右(总共4t左右数据)。随着直播行业的热捧,国内宽带的上行带宽略有好转,百兆以上宽带基本提供了30兆左右的上行速度,额外加钱也有套餐可以提升到百兆的上行带宽。另外也有临时的方案,电信提供了为期1天的加速包,可以临时将上行速度提升到百兆,在跑满的情况下,一天可以上传700G左右的文件,我在处理数据备份的这几天使用,很快就完成了全部文件的上传备份。

重构

这篇博客在两个月前写完后,我一直觉得有点不满意,内容上虽然已经把现在做完的东西都讲了一遍,感觉没有重点,而且对于homelab提供的软件服务部分,本身就是在一直迭代更替的,全都更新在一篇里面感觉也不合适。思来想去,决定重构一番,本篇内容只集中在系统搭建上,对于软件的部分,将分篇章逐篇论述。

底层系统

本着所有系统都虚拟化了方便管理备份的原则,这次底层采用的是esxi。不过这只是凭着过去几年的刻板印象选的,事前没有做过很完善的调研,事后想想,也许PVE会更合适。

gen10 plus的唯一的pcie卡槽我用来接了m2转接卡来使用ssd作为系统盘,esxi本身是否放在ssd上并不重要,这块盘更主要的作用是存储虚拟机系统。不过因为sata控制器计划是直通给上层的nas虚拟机使用,所以底层系统自然是需要有独立的介质存储的,能存在机器里面多少还是比插u盘方便一些。整个pcie槽位只用来接一块ssd其实有点浪费,理论上来说分拆成2~4个m2接口或者提供2个2.5G网口都是可行的。不过支持这些功能的转接卡价格会高很多,同时和机器本身也可能存在这样那样的适配性问题。因为暂时来说我的需求只要接一块ssd就够了,所以就姑且先只用单接口的转接卡就行。

由于这次的iLO与Gen8存在一些差别,底层系统的安装比起gen8稍微要波折一点,设置gen10plus的iLO配置需要先找一台显示器接上,在BIOS中设置好iLO使用共享网口的相关配置,配好以后就可以把机器插上网线塞到柜子里去了,后面的环节都可以通过iLO远程配置。

安装esxi的过程很快,底层系统安装完成后,后面的系统安装都可以在esxi的web端进行。

truenas(文件服务)

首先是安装truenas系统,在esxi中上传系统镜像并创建虚拟机,再把sata控制器直通给truenas即可。出于稳妥考虑,这里我安装的系统是truenas core,因为是基于freeBSD的系统,从便利性上来讲可能会差一些。如果需要额外的功能,就需要用其他虚拟机来支援了。

不过事后发现这么做带来的一个好处。由于sata控制器被直通给了truenas,整个truenas系统要做备份就变得比较困难,把整个存储系统做的比较简单,复杂的软件服务集中在独立的虚拟机上,单独对这些虚拟机做备份就比较简单了。

在使用上,这套系统只用于提供存储服务使用,存储池定义,用户ACL设置,设置smb、nfs、afp、webdav等文件服务,其余功能一概不用。这样一来,整套系统除了存储的数据以外,就只有配置信息了。只要定期备份配置(这些配置变动其实也是比较少的,所以备份的频次也会很低),就能防备系统发生故障后能够顺利恢复。

硬盘数据的备份,是个很复杂的方案,这个以后再具体展开讲。

ubuntu server(docker宿主)

因为除了基础的文件服务,通常我还会用到很多额外的软件服务,所以需要至少一套额外的虚拟机来充当软件服务器,之所以说是至少一套是因为部分软件如果限定了特定平台(比如windows),那么单独一套linux server是搞不定的,不过目前这种情况没有,所以我就先只装了一套linux系统,后面有需要再按需安装。

这里选什么系统倒是并不重要,大部分server软件都会以docker方式运行,所以并没有区别。选ubuntu一方面是延续原有的使用习惯,另外是考虑到部分软件或命令行工具会以原生方式安装,选个保有量大的版本很多时候会更方便(和买车一个道理)。

openwrt(软路由)

这个系统只是实验性质,我对软路由的主要需求是流量控管与监控以及透明代理。目前尝试下来效果还不太令人满意,就不展开讲了。只能说折腾网络设备确实是最麻烦的,一个没配置好导致断网就很麻烦。即便作为尝试,我已经是只拿这套系统做旁路由使用,但是在底层esxi部署好以后,大部分时间我都是在外面通过代理方式访问家里的局域网设备,所以一旦中断,可能这天都干不了什么了…

vinchin(系统备份)

因为ubuntu server上做了很多配置和安装的活计,此外除了server,我也还有其他几套用途的虚拟机需要备份。查了一下感觉vmware的备份方案都挺重的(这也是让我觉得一开始如果用的是PVE可能就没那么多事了)。相对而言vinchin(云祺)的这套方案还算不错。唯一的问题是付费模式主要面向企业用户,不太适合个人用户。好在有永久的免费授权可以一直使用,只是有三台虚拟机的限制。

结语

最后整理一下,系统整体结构图如下,还算清晰吧。

AIO.drawio

系统的搭建到这里基本告一段落了。可能还会再尝试一些软路由系统,或是出于软件需要装个windows server之类的小变更,也没必要展开说了。

后续的重点会放在软件服务的构建上,这些就逐篇展开再慢慢说吧。

起因

我客厅的电视柜边角上,放着一台HP Gen8,我把它用做网络存储,一直以来都工作的很好,也不算太突兀,很多来家里的客人都不觉得这是一台电脑,也许以为是音箱或是电视机顶盒之类的东西。作为一台有四个3.5寸硬盘位的电脑,我本来也觉得这个体积已经算是极限了,在迈入全固态存储时代前,我是没有办法把这样一台设备像ps5或是路由器一样塞到柜子里面的,直到去年我的Gen8电源开始出现问题,然后我在搜索后续替代品的时候看到了下一代产品Gen10 Plus。

同样的四盘位机箱,相比Gen8,仅仅少了光驱和用不了几次的硬盘快捷插拔前面板,但是高度却缩减了整整一半,真的变成了一台像路由器一样的设备,考虑到Gen8我已经使用了6年以上,升级换代的理由已经很充分了。

购买之前我也考虑过,如果Gen8再撑几年的话,理想的下一代NAS产品该是什么样子。我脑子里想到的两条我最关心的是:1、arm架构;2、纯SSD存储。

想到这两条并非是我的什么特殊偏好,而是因为我发现我对NAS产品最大的需求,就是体积,而实现这两点都可以让NAS产品的体积有比较显著的下降,从而在一个近乎“隐身”的状态下提供尽可能多的存储容量。

不过考虑到这两点短期内要实现确实不太现实,因此Gen10 Plus有理由成为在完全实现这以目标前的一代过渡产品。

当然,往更长远考虑的话,完全的云存储在未来也是可能的,只是这里的不确定性,更多取决于未来云存储是否能有合理的商业模式,以及作为基础的网络设施能到什么程度了,这就不展开说了。

购买

Gen10 Plus在国内并没有像Gen8当时那样有正常的购买渠道,对比了海淘和其他国内代购商家后,我选择了从computeruniverse网站海淘。这或许并不是一个正确的选择,因为去年的疫情原因,最终这台电脑在下单两个多月的时间之后才配送到了我手里。不过考虑到其他硬盘等配件我也是等到双十一才购买的,所以这段等待时间倒也不算是什么问题。

最终完整的采购部件包括这些东西

部件
Gen10 Plus Xeon E2224 16GB RAM
Gen10 Plus iLO5 kit共享卡
佳翼 NVMe转接卡 PCIE转M.2
西数 SN750 1TB
东芝 14TB MG08ACA14TE *2

硬盘理论上可以配满4块,不过我准备还是一步一步来,先用两块把原来的数据转移过来再说。大容量机械硬盘很多人吐槽噪音会比较大,因此先买两块也是出于先扫雷的想法。SSD用了Gen10 Plus唯一的PCIE插槽,是有一点浪费。一些扩展卡支持转接更多的SSD,或者是附带高速的网络接口。不过因为暂时没有需求,也就先用着了。

gen8_gen10p_compare

硬件组装

刚拿到机器的时候,我开机看了一下,意识到这次需要折腾的会比Gen8要少很多。原因是当时Gen8的风扇实在太吵了,于是花了不少功夫去魔改电源和风扇。

因此这次就是简单的把所有的零件插上,拧上螺丝就好了。唯一没想到的是硬盘的固定方式,虽然一开始知道不像前代Gen8那么简单,不过螺丝固定的方式还是有点超乎我的预期,在网上找了几篇评测才找到怎么固定的说明。

装完后整个扔到电视机柜里,没有额外研究怎么把前指示灯关掉,整体存在感还算比较低,就先不折腾了,至此算是开始正常服役了。

gen10p_deploy

系统搭建

上一代Gen8我使用的主机系统是Windows server2012,对于其他系统的需求则通过Hyper-V安装虚拟机解决。

我细想了一下过去几年里主机操作系统的选择是否给我带来了多少便利或是不便,从纯粹的网络存储设备角度上来说,Windows的频繁更新重启带来的只有不便,从应用软件的角度看,我也并不强依赖于Windows生态下的软件。唯一值得一提的可能是硬盘监控软件Disk Sentinel,它确实让我在使用过程中,特别是硬盘过保后超期服役期间,更加安心了。

这当然并不足以成为我把平台留在Windows的理由,最终我安装了Exsi作为宿主系统,一个Truenas虚拟机提供基础的存储服务,并把SATA控制器直通给这个虚拟机。对于其他软件的需求(由于Truenas并非linux系统,不支持docker,这类情况恐怕还不少),再通过其他虚拟机挂载网络存储后对外提供服务。

我并没有组建raid,Truenas和ZFS我了解不多,我也并不打算依靠raid来保障数据安全,对我来说,这两块硬盘仅仅就是两块硬盘而已。对于数据安全的问题,就留给下次说明吧。

AdventofCode这个网站是我在几年前浏览Hackernews时看到的。网站的作者在每年的十二月份,会在每天给出一道编程题,包含一二两部分问题,答对一题可以累积1颗星,直到12月25号圣诞节这天为止。通常一天的题目中,第一部分会简单一些,第二部分是在第一部分的基础上有一些延伸,当然通常难度差别不会很大。从网站给出的数据也可以看出来,只回答一道题的人数通常在1%~10%这个区间,只在最后几天里会有比较大的起伏,比如今年最后一天的题目,在答出第一题的基础上,只有一般人回答了第二题。

第一次看到这个网站的时候是在十二月中旬,做了几题之后,因为感觉落下太多就没有跟着做,之后的几年不是想起来太晚就是忘记了……直到今年,算是在十二月到来前就想起了这件事,于是记着在一号的时候开始跟着做。跟题之前我还特地挑去年比较难的题目试了一下,从难度上来说,都是属于可以做出来的程度。所以理论上今年跟完全程应该是可行的。

或许跟着做题才会发现这些 puzzle hunter 的狂热程度。前几天的题目都不算难,我提交上去的时间大概是在题目开放后20分钟左右,然后发现最快的选手2分钟就已经做完了……

于是第二天开始为了能做的更快一点,我就没有继续用传统的编程方式来写,而是用juypter notebook这种交互式的编程界面,能快速拿到响应的方式来做。不过从之后的经验来看,这种方式虽然在一些小题目上能快速的响应,但是面对复杂的问题时,反而是种负担。

再简单说说感想吧。在前面的一周半的时间里,整个解谜过程还算是比较轻松的,基本上属于上班的时候抽出一点时间就能做完。十二月中旬的时候出去旅游了一趟,落下了几天的题目没做,然后接下来又是出差,作息规律上受了一些影响,导致在后面的一段时间里,我花了不少时间来追赶进度,差不多每天追两三题的样子。

而从中旬开始,偏偏题目的难度也有所上升,属于比较耗时耗精力的事情了。于是整个过程就变成了不是饭后娱乐那么简单的事情。说实话追题的过程还是比较辛苦的,很多题目从阅读理解上就比较耗费精力,更别提完整做完了。诚然这些谜题都还属于可以解开的范畴,做到第19、20天的题目的时候,我评估了一下完整做完需要投入的时间精力,感觉并不算小,而相对来说十二月和接下来的一月事情都还比较多,于是决定终止继续追题。

总体来说,解谜的过程算的上是有趣。如果有心思思考的话,也确实可以磨练一下编程技巧,不知道是不是已经过了能够一门心思投入在一件事情上的年纪了。完整地跟完解谜的全程对于我而言都已经是接近奢侈的事情了,更别提进一步的反思总结了。

我姑且把今年的解谜过程记录下来,如果来年还有机会参与的话,不妨作为对比。