把时间往前推一年,我还在老老实实当一名“现代工具党”。家里一台吃灰多年的旧笔记本,被我翻出来塞了四五个自托管服务,觉得这样就叫“数字生活”。结果更新一次就坏一次,某个深夜我看着满屏的systemd报错,差点把键盘砸了。冷静之后决定彻底清算所有多余的“生态”:这东西不叫项目,不叫产品,我给它起名caveman,翻译过来就是“穴居人”——我没有追求先进的堆栈,而是主动退回最原始的方案,只靠系统的自带能力、纯文本和几条脚本,把个人数字地基重新搭了一遍。
caveman其实不是某个软件,更像一套“自我约束规范”:凡是需要单独维护数据库服务、独立进程管理器、一拉依赖就是几百MB的东西,一律不进家。系统自带什么就用什么,脚本必须能在任何一台普通Linux机器上直接看懂、直接跑起来。目标是让这台旧设备像穴居人的石斧一样,功能不花哨,但永远能使、永远能修、不怕断网、不怕工具下线。
这篇文章就把caveman从思路到落地完整拆开。适合谁看?适合那些被Docker、Kubernetes、各种微服务生态压得喘不过气、想回到“一眼看穿自己的服务器”状态的Linux用户;也适合手里有老旧电脑,想低成本搭个人笔记、备份、监控体系的玩家。我尽量把每一步都写成可以直接抄作业的命令和脚本,并把我踩过的几个大坑一并交代清楚。
1. 内容整体设计与思路拆解
1.1 项目定位:为什么要把一切退回“原始”
很多人问我,放着那么多现成工具不用,非要把自己搞成原始人,是不是有毛病。我的回答很简单:我要的不是功能多,而是省心。
过去那套“现代方案”里,笔记要同步数据库,备份要挂容器,监控要搭可视化平台,最后每个组件都有自己的更新节奏、配置格式和故障模式。表面上是自动化了,实际上我成了这些系统的“人肉管理员”。一旦某个服务死活起不来,排错链路长得能写一篇小说。caveman的思路是反过来的:一切链路从入口到出口都能在一屏之内说清楚。
这套方法对旧硬件尤其友好。我那台古董笔记本用的是普通机械硬盘,跑起容器群跟拖拉机一样,但跑caveman只需要占用几十MB内存。不卡顿,不发热,断电重启之后所有功能自动恢复。我需要的只是一个能跑bash、rsync、cron和SQLite的Linux系统,而这些工具在所有发行版里都是基础配置。
1.2 三原则:可解释、可恢复、可离线
caveman从设计上就把自己钉死在三根柱子上。
第一根是可解释性。任何一项功能,如果我一个月不碰它,再回来时必须能在5分钟内搞清楚它是干嘛的、改了哪里、日志在哪个文件。所以脚本里的注释不超过10行,变量命名用全拼,不用缩写不说黑话。宁可啰嗦,不能装酷。
第二根是可恢复性。系统要允许我用最原始的工具把它修好,而不是只能靠原作者。比如我写的备份脚本,别人哪怕没看过说明,只要顺着日志文件和固定路径就能找到错误点。我不设置只有我自己懂的魔法参数,所有可调变量集中在每个脚本顶部的配置区。
第三根是可离线性。我的所有核心数据都必须落地在本地,任何云端同步都是可选加分项,不是必要条件。原因很简单:网络不会永远稳定,服务商不会永远良心,我能控制的只有自己硬盘上的字节。所以笔记、密码库、备份快照全部默认本地存储,外网瘫痪也不影响我使用这套系统。
1.3 技术栈选型:全是几十年的“老家伙”
- bash:任何Linux发行版都自带,写起来零门槛,跨机器零依赖。
- cron:系统级调度器,没有常驻内存进程,配置是纯文本,行为几十年稳定。
- rsync:同步和备份的瑞士军刀,支持增量、断点续传、远程加密传输。
- SQLite:单文件数据库,不需要单独启动数据库服务,备份就是复制文件。
- 纯文本/Markdown:结构化程度低但最“透明”的格式,用grep就能查档。
选它们不为什么时髦,就因为一条朴素逻辑:工具越老越稳。你永远不会因为升级而突然发现某一天它无法启动,也不会因为使用的人少了而被迫迁移。我在项目里的态度就是:能用文本就别用数据库,能用系统命令就别装新软件。
2. 核心细节解析与实操要点
2.1 每个脚本都必须“幂等”:跑一百次结果也一样
caveman里几乎所有脚本都在执行前先检查前置状态,而不是上来就破坏式操作。这个习惯叫幂等性,也是我踩过大坑之后定下的铁律。
举个最简单的例子。备份脚本开头会先创建目标目录,但创建前一定判断一下是否已经存在:
backup_root=/backup/$(hostname) if [ ! -d "$backup_root" ]; then mkdir -p "$backup_root" fi为什么这么较真?因为我曾经写过一段“干净利落”的脚本,上来就rm -rf旧目录再重建。结果某次手动测试时变量没赋值,命令直接变成rm -rf /,吓得我冷汗冒了一晚。从那之后我规定:凡是涉及删除、覆盖、重建的代码,一律先用条件判断保护。宁可多写三行,不能赌变量一定不为空。
幂等还有一个额外的好处:脚本因为某一步中途失败,或者你手痒多执行了一遍,系统状态不会被搞坏。所有任务都可以放心地反复重跑,这在实际运维里非常重要,因为你是人,你就一定会误操作。
2.2 脚本开头的“三件套”:set、PATH、日志
我每一份脚本的头部都有一段固定的三件套,它是整个caveman脚本的命根子。
#!/usr/bin/env bash set -euo pipefail export PATH="/usr/local/bin:/usr/bin:/bin" LOG_FILE="/var/log/caveman/$(basename "$0").log" log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG_FILE" }严格模式自不必多解释,但我要重申的是PATH那一行。cron环境里PATH变量经常被精简成/bin:/usr/bin,很多安装在/usr/local/bin下的命令直接找不到。我至少因此修过十几次“手敲正常、定时任务报command not found”的诡异问题。统一在脚本里重置PATH之后,这类问题彻底消失。
日志为什么坚持带时间戳?因为排查故障时,没有时间的日志等于废纸。你根本不知道某条报错是哪年哪月冒出来的,和旁边那条记录有没有因果关系。统一用ISO标准时间,看着不洋气,但绝对不出错。
2.3 软链接配合rsync实现“无限历史快照”
caveman备份机制的核心是利用rsync的--link-dest参数。它能让每次同步产生的新快照,与上一次快照内容相同的文件共用物理存储块,用硬链接指向同一处。这样哪怕你每天全量备份,365天之后磁盘占用依然只有一份完整数据加少量差异。
这个思路的精妙之处在于不需要任何专门软件,纯粹靠文件系统的硬链接机制。实现时我会维护一个current软链接,始终指向最新快照目录,脚本的配合方式如下:
#!/usr/bin/env bash set -euo pipefail export PATH="/usr/local/bin:/usr/bin:/bin" LOG_FILE="/var/log/caveman/backup.log" log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG_FILE"; } SRC_DIR="/home/me/data/" BACKUP_BASE="/backup/$(hostname)" TODAY=$(date +%F) mkdir -p "$BACKUP_BASE/$TODAY" rsync -a --delete \ --link-dest="$BACKUP_BASE/current" \ "$SRC_DIR" "$BACKUP_BASE/$TODAY/" rm -f "$BACKUP_BASE/current" ln -s "$BACKUP_BASE/$TODAY" "$BACKUP_BASE/current" log "backup success: $TODAY"这里有个细节:源路径变量$SRC_DIR末尾的“/”绝对不能省。省掉它,rsync就会把源目录本身当作一个子目录放进目标目录,结果目标里套了一层同名目录。这是我给别人排查问题时见过最多的低级错误,几乎每个人都会踩一次。
2.4 调度配置规范:每一条cron都必须带注释
我用crontab写定时任务时,坚持每条任务上方都有中文注释说明用途。看起来挺傻,但三个月后你真会感谢那些注释。下面是我家caveman的crontab模板:
# 每天 07:00 执行数据备份 0 7 * * * /home/me/bin/backup.sh # 每天 12:00 检查磁盘空间与关键服务端口 0 12 * * * /home/me/bin/check_disk.sh # 每周日凌晨 03:00 清理临时目录和过期日志 0 3 * * 0 /home/me/bin/cleanup.sh特别提醒cron的星期和日期字段关系:0 3 * * 0表示的是“星期天”而不是“0号”。这是新手最容易混淆的语义,连见过好几次的我都差点在这上面翻车。
3. 实操过程与核心环节实现
3.1 笔记系统:一个函数加一个文件夹
caveman的笔记系统不依赖任何笔记软件,就是bash函数加固定目录。把下面代码写进~/.bashrc,重新加载后你就有了一套离线笔记系统:
note() { local base="$HOME/docs/$(date +%Y)" local file="$base/$(date +%Y-%m-%d-%H%M).md" mkdir -p "$base" if [ ! -f "$file" ]; then printf '# %s\n\n' "$(date '+%F %H:%M')" > "$file" fi ${EDITOR:-vim} "$file" }不吹不黑,这套方案有一堆“缺点”:不能自动同步,没有全文检索高亮,没有标签体系。但这些缺点它都以另一种方式补回来了:文件永远不会被某个公司停止维护,永远不会因为升级坏掉,备份它就是复制一个文件夹。配合grep命令,用关键词找笔记其实和大多数工具的搜索体验相差无几:
grep -rn "重要关键词" ~/docs/3.2 数据备份:从本地到异地一步步加固
备份是整个caveman系统的生死线。我不搞花活,就分三个层次:本地快照、同机增量、异地同步。
本地快照上面已经写好了,覆盖的是日常误删和文件覆盖。但硬盘本身也会坏,所以至少每周要把备份目录同步到另一块物理硬盘或局域网里的另一台机器。用到的命令同样是rsync:
rsync -a --delete /backup/ /mnt/disk2/caveman-backup/如果外面还有一台有公网地址的服务器,我会再加一层加密传输。由于我的旧笔记本不支持花哨的加密算法,实际上就是用rsync走ssh协议,也没额外安装任何客户端软件:
rsync -a --delete -e ssh /backup/ user@remote-server:/home/user/caveman-backup/这里要说句掏心窝的话:没有验证过的备份都不算备份。我每个月会随机抽一天执行一次“恢复演练”,从快照目录里取出几个文件,尝试从清空后的临时目录恢复它的权限和内容。很多人天天备份,可等到真要恢复那天才傻眼,发现备份目录里只有一堆残缺文件。定期演练是唯一能避免这种悲剧的办法。
3.3 状态监控:用SQLite保持“有据可查”
监控最怕的就是平台本身变成新的运维负担。caveman的做法是每天让cron往SQLite数据库里插入一条状态记录,后面需要看的时候随时查。我不装监控面板,因为用命令行查早够用了。
采集脚本核心部分如下:
DB=/var/lib/caveman/status.db sqlite3 "$DB" <<SQL CREATE TABLE IF NOT EXISTS status ( ts TEXT PRIMARY KEY, host TEXT, disk_pct INT, load_avg REAL, port_open_ssh INT ); SQL DISK_PCT=$(df / | awk 'NR==2 {gsub("%",""); print $5}') LOAD_AVG=$(cut -d' ' -f1 /proc/loadavg) SSH_OPEN=$(ss -tln | grep -c ':22 ' || true) sqlite3 "$DB" "INSERT INTO status VALUES ('$(date -Iseconds)','$(hostname)',$DISK_PCT,$LOAD_AVG,$SSH_OPEN);"查询最新记录只需要一行:
sqlite3 -header -column /var/lib/caveman/status.db \ "SELECT * FROM status ORDER BY ts DESC LIMIT 10;"有个坑必须提醒你:SSH_OPEN那行我加了“|| true”,因为ss命令查询有结果时退出码是0,没结果时grep退出码是1,而脚本前面set -e会在退出码非零时直接终止整段脚本。没有这个“|| true”,某些情况下这条采集命令会静默失败。我见过太多类似问题,都是在环境差异下才暴露出来。
3.4 日志与告警:让机器自己喊救命
caveman不做烦人的推送通知,只在出错时发一封极短邮件。系统自带mail命令足矣,我不再引入消息队列或IM通知接口。脚本异常时的处理逻辑通常是:
if ! rsync -a --delete --link-dest="$PREV" "$SRC/" "$DEST/"; then log "backup failed" echo "caveman backup failed on $(hostname) at $(date)" | \ s-nail -s "[caveman] backup failed" "$ADMIN_EMAIL" exit 1 fi告警最怕什么?怕误报一堆没人看,最后真正出问题时被淹没在噪音里。所以我坚持只在失败时通知,成功时多说一句废话都不写。人的注意力和耐心是有限资源,系统不该浪费它们。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
| 现象 | 可能原因 | 快速处理办法 |
|---|---|---|
| cron任务不执行 | PATH环境变量不对,脚本没有可执行权限 | 脚本顶部固定重置PATH;chmod +x;手动执行看退出码 |
| rsync备份后多了一层同名目录 | 源路径末尾忘了加“/” | 源目录写成$SRC/,目标目录不带尾斜杠 |
| --link-dest没生效,磁盘占用暴涨 | 上一次快照目录被手动删了,硬链接失去参照 | 不要手动删除current或快照目录,统一走清理脚本 |
| 脚本执行一半退出 | set -e遇到某条命令返回非零 | 逐个命令加 |
| 备份恢复后文件权限不对 | rsync未带-a参数,只用了-r | 同步必须用-a,它包含权限、时间戳、软链接属性 |
| 监控采集无数据 | 脚本里grep找不到结果,退出码非零 | 命令末尾加 |
4.2 避坑经验三则:我亲眼见过的最惨翻车
第一条关于内存盘。有阵子我图快,把临时备份目录放进了tmpfs内存盘。读写速度确实飞快,但某个周末家里断电,重启后薄薄一层RAM里的一切化为乌有。那一刻我才真正理解“掉电不丢数据”是存储系统的第一优先级。从那以后,我的所有备份目标要么是物理硬盘,要么是网络存储,绝不为了性能去赌供电稳定。
第二条关于时间戳。早期我的脚本只写日志内容不写时间,某次排查问题时对着十几行长得一模一样的记录干瞪眼,完全分不清先后顺序和下一次执行间隔。后来统一规定日志格式必须带ISO时间戳,排查效率才算真正起来。这种细节平时看不见,可一旦数据出了问题时,你恨不得穿越回去给当时的自己两拳。
第三条关于自动化边界。我曾经追求“全自动”,连续加了三个无人值守的清理任务。结果月底查日志时发现某个来源不明的动作在悄悄清理文件,因为时间太久,我已经记不清是哪一天写了哪条规则。从此我立下规矩:任何新增自动化任务必须连续手动执行一周,确认行为完全符合预期,并且添加详细注释,才允许交给cron调度。机器的自动化不该以主人的失控为代价。
4.3 排查方法论:怎么一步步找到故障点
排错的第一步永远是看日志。caveman里每个脚本都把日志写到/var/log/caveman/,文件名和脚本名一致。出问题先做三件事:
- 看日志文件最后20行,确认脚本到底执行到哪一步崩了。
- 手动在shell里逐条执行关键命令,直接看真实报错。
- 检查退出码,尤其关注rsync返回码和sqlite3返回码,它们是判断成功与否的硬指标。
这套顺序几乎没有失手过。原因也很朴素:当系统足够简单时,故障点数量是有限的,只要顺着链路一条条排除,总能在几分钟内定位。现代框架最大的问题恰恰是把故障藏在一层层抽象后面,让排错变成了考古。
写在最后:一点使用体会
caveman在我手里已经跑了将近一年,最大的收获不是省了多少时间,而是重新找回了“完全看懂自己机器”的安全感。现在每次重启后,我都会手动跑一次备份脚本,看到日志里最新一行输出success,心里就踏实了。它不聪明,不漂亮,不会自动炫耀自己,但它永远不会在我最需要它的时候背叛我。
如果你也正被一堆互相依赖的工具和服务搞到疲惫,我建议你从下周开始,只挑一个最常用的系统功能,试着把它精简到一条命令就能完成。坚持一个月,你会发现自己没那么需要那些“生态”了。我套用的基本测试只有三个:这个方案能不能用纯文本实现?能不能不新增依赖?半年后的我还能不能一眼看懂?只要三个答案都是“能”,就放心写进caveman。这三个问题,就是我这套“穴居人哲学”的全部核心。