望舒

人的一生注定会遇到两个人一个惊艳了时光,一个温柔了岁月

1. 引言

“日志不会骗人,但日志太多的时候,你根本不想看。”

备份管线做好后(#3),下一个自然该管的是——日志。数据有备份,但诊断数据本身呢?

一台 24/7 运行的 N100,跑着 Docker、pm2、systemd 全家桶,日志文件会悄悄膨胀到你想不到的程度。今天这篇就是我在 aicore 上做日志治理的真实记录。

先看现状:

1
2
3
4
5
$ journalctl --disk-usage
Archived and active journals take up 3.9G in the file system.

$ journalctl -p err --since "3 days ago" | wc -l
~4000 行

3.9GB。 对于一台 N100 来说,这不是一个小数字。而且——默认的 journald.conf 什么都没配。


2. 日志全景图

先画一下这台机器上的日志源:

日志源 位置 规模 关键程度
systemd journal journalctl 3.9GB ⭐⭐⭐
pm2 logs ~/.pm2/logs/ ~few MB ⭐⭐
Docker 容器日志 journald 驱动 包含在 3.9G 内 ⭐⭐⭐
Nginx (如有) /var/log/nginx/ N/A
kernel/dmesg journald 包含在上 ⭐⭐⭐

最大的问题是:所有日志都涌进 journald,而它没有上限配置。


3. 噪音分析 — 谁在刷屏

3.1 蓝牙重连狂魔

占 err 级别日志 80% 以上

1
2
Jun 21 17:46:24 aicore bluetoothd[780]: profiles/audio/avdtp.c:avdtp_connect_cb()
connect to 0C:FC:30:00:06:2A: Host is down (112)

这是 ZEALOT-S32 蓝牙音箱的连接尝试。音箱没有开机,但 systemd 在不断地尝试重连——每 30 秒一次,一天 2880 条。

3.2 PCIe AER 错误(零星但存在)

1
2
Jun 21 17:37:55 aicore kernel: pcieport 0000:00:1c.0: AER:
Error of this Agent is reported first

N100 平台上的常见现象,频率不高(一周几次)。单个事件信息量不大,但长期积累会暗示某个 PCIe 链路的不稳定。

3.3 pm2 日志(正常流量)

ai-portal 的 pm2 日志里全是正常的 HTTP 200/304——说明服务健康,但日志本身对故障诊断帮助不大。

1
2
0|ai-porta | GET /assets/index-CNdUHBv6.js 200 0.754 ms - 2425817
0|ai-porta | GET /api/status/public 200 56.428 ms - 144

4. journald 治理实战

4.1 加个容量上限

journald.conf 默认策略是 不限制。这意味着 journal 会一直长下去,直到你把磁盘占满。配置一个合理上限:

1
2
3
4
5
# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=100M
MaxRetentionSec=30day
  • SystemMaxUse=500M — journal 总大小上限 500MB
  • SystemMaxFileSize=100M — 单个文件上限
  • MaxRetentionSec=30day — 只保留 30 天

改动后重启 journald:

1
sudo systemctl restart systemd-journald

等等——重启 journald 不会丢日志吗?

答案是:重启时 active journal 会被 rotate,已持久化的日志不会丢。 你可以安全执行。

4.2 立刻缩减

配置改了,但现有的 3.9GB 还在。手动清理:

1
2
3
4
5
# 保留最近 7 天
sudo journalctl --vacuum-time=7d

# 或者保留最近 500MB
sudo journalctl --vacuum-size=500M

这个命令会删除旧日志文件,只保留满足条件的数据。注意:执行后 journal 会释放磁盘空间,但 journal 文件删除是文件级别操作,不会碎片化。

4.3 验证生效

1
2
$ journalctl --disk-usage
Archived and active journals take up 463.6M in the file system.

从 3.9GB → ~460MB,节省了 3.4GB。对于 N100 的 1TB 盘来说或许不算什么,但对于 /var/log/journal 可能位于系统分区的小盘机器来说,这个差异很关键。


5. 日志分析技巧

5.1 找出高频错误源

1
2
3
# 按服务/进程统计最近一周的 warn+ 日志
journalctl -p warn --since "7 days ago" --no-pager \
| awk '{print $5}' | sort | uniq -c | sort -rn | head -20

对 aicore 执行结果一目了然——蓝牙驱动独占了 TOP 1。

5.2 时间窗口分析

1
2
3
4
5
6
7
8
# 某个时间段内的 Kernel 错误
journalctl -k -p err --since "2026-06-18 00:00" --until "2026-06-19 00:00"

# Docker daemon 自己的日志
journalctl -u docker.service -p err --since "7 days ago"

# pm2 的错误输出(不是 access log)
journalctl -t pm2 -p err --since "today"

5.3 专注模式:只看真正需要关注的

如果你在排查某个容器的问题:

1
2
3
4
5
# 容器 stderr/stdout(如果 Docker 日志驱动是 journald)
journalctl CONTAINER_NAME=ai-portal -p err --since "1 hour ago"

# 或者直接用 Docker 的接口
docker logs --since 1h ai-portal 2>&1 | grep -i error

6. pm2 日志轮转

pm2 自己也有日志文件,默认在 ~/.pm2/logs/。pm2 内置了日志轮转模块:

1
2
3
4
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 100M
pm2 set pm2-logrotate:retain 3
pm2 set pm2-logrotate:compress true

这能防止 pm2 的应用日志无限增长。不过我实际检查了一下,ai-portal 的 pm2 日志目前只有几个 MB——得益于它只是提供 AI 对话界面,日志量很小。


7. 从日志到告警

日志治理不只是”别让它撑爆磁盘”。当关键事件发生时,日志应该变成告警。

目前的方案比较朴素——cron job 跑脚本检查 journal 中的关键模式:

1
2
3
4
5
6
7
8
#!/bin/bash
# /usr/local/bin/check-journal-alerts.sh
# 由 cron 每小时执行

if journalctl -p crit --since "1 hour ago" --quiet | grep -q .; then
# 发现 critical 级别日志,触发告警
echo "🚨 CRITICAL log detected in last hour" | mail -s "N100 Alert" michael@chenyun.org
fi

如果要更专业:journald 支持 ForwardToSyslog=yes 把日志转发到 rsyslog,然后在 rsyslog 做规则匹配和远程转发。对于家庭实验室来说,这个组合绰绰有余。


8. 真实事件复盘:蓝牙刷屏与 PCIe 错误

写这篇文章的契机,是这个 cron job 触发了检查时意外发现的”隐藏噪音”:

Bluetooth 误连(严重度:低)

ZEALOT-S32 音箱关机后,systemd 蓝牙服务每隔 30 秒尝试重新连接。这不是系统问题——但 2880 条/天的无效日志在浪费空间和注意力。

修复方案很简单:给蓝牙配置一个合理的连接超时,或者在不需要时禁用蓝牙模块。

PCIe AER(严重度:中)

1
pcieport 0000:00:1c.0: AER: Error of this Agent is reported first

频率很低(本周 3 次),但值得关注。AER(Advanced Error Reporting)通常是信号完整性问题——可能是 N100 SoC 内 PCIe 链路的状态转换问题,也可能是 mini PCIe 插槽的接触问题。如果频率增加(每天数十次),需要检查 dmesg 并考虑升级 BIOS/固件。


9. 最终配置清单

治理完成后,我的日志策略长这样:

1
2
3
4
5
✅ journald 上限 500MB,保留 30 天
✅ journalctl 按需清理至合理体积
✅ pm2-logrotate 安装并配置
✅ 关键日志告警脚本部署
✅ 已识别并记录噪音源(蓝牙/PCIe)

系统盘空间从 3.9GB 日志负担降至约 500MB。释放的 3.4GB 可以做更多有用的事——比如多存几张 Docker 镜像。


10. 下期预告

日志能告诉我们”发生了什么”,但性能数据告诉我们”跑得怎么样”。下一篇:

运维手记 #5: 性能调优 — N100 的性能边界在哪里?

涉及 CPU 调频策略、内存压缩、Swap 优化、I/O 调度和 Docker 资源限制。


本文所有数据来自 aicore(N100 / 32GB / 1TB NVMe / Ubuntu 24.04 / systemd 256)的真实运行环境。

1. 引言

“备份不做,运维白干。”

这句话在运维界能排进金句前三。但真正到了实操层面——“做”和”做好”之间隔着多远?

让我直接看一下现状:

  • 11 个 Docker 容器在跑(WordPress x2、Stalwart Mail、Qdrant、MySQL、Headscale……)
  • 21 个 Docker 卷,总共 1.178GB,83% 可回收
  • 21 个匿名卷挂在那里,来自那些我早就 docker compose down 的项目
  • 每日 3:00 AM cron 执行备份,GPG 加密,7 天循环覆盖
  • 已有 3.2GB 备份数据躺在本地,包括一个 3.4GB 的升级前全量存档

听起来挺像那么回事?拆开看,问题不少。

本文将记录 HanyanOS 的备份管线现状、踩过的坑,以及正在进行的优化方向——同样基于 N100 单机 + 上海网关的双节点架构。

2. 备份管线总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────┐      ┌──────────────┐      ┌───────────────┐
│ crontab │ ──→ │ 备份脚本 │ ──→ │ GPG 加密 │
│ (每日 03:00) │ │ (Bash 500行)│ │ (AES256) │
└─────────────┘ └──────────────┘ └──────┬────────┘

┌──────────────────────────────┘

┌──────────────────────┐
│ 本地留存 (7日滚动) │ ← ~/backups/weekly/{1..7}/
└──────────────────────┘
│ (可选)

┌──────────────────────┐
│ SGVPS 远程副本 │ ← 上海 Lightsail 网关
└──────────────────────┘

备份内容清单

备份脚本每天打包的内容:

模块 内容 说明
OpenClaw 核心 openclaw.json, MEMORY.md, SOUL.md, AGENTS.md, HEARTBEAT.md Agent 人格与配置
密钥 ~/.hanyanos/secrets/, ~/.ssh/ 全套 SSH 私钥 + 基础设施凭据
Nginx nginx.conf + sites-enabled/ 反向代理与域名路由
Docker 元数据 ps, images, volumes 列表 容器快照清单
FRP 配置 /etc/frp/ 内网穿透配置
系统快照 uptime, disk, memory, ports, tailscale 运行状态基线
数据库 mysqldump --all-databases | gzip MySQL 全量 SQL 导出
远程 SGVPS SSH 拉取 Docker/Nginx/FRP 快照 双节点覆盖

3. 脚本拆解:亮点与槽点

3.1 做得好的

加密意识在线。 所有备份文件用 GPG --symmetric --cipher-algo AES256 加密,密码通过环境变量 BACKUP_PASS 传入。备份到磁盘上是 .tar.gz.gpg,不是纯文本。

数据库凭据不暴露在进程列表。 MySQL 密码通过 /dev/stdin 传入 mysqldump,而不是 -p 参数——避免 /proc 泄漏。细节见脚本第 55-61 行:

1
2
3
4
MYSQL_CREDS=$(printf '[client]\npassword=%s\n' "${MYSQL_ROOT_PASSWORD}")
printf '%s' "${MYSQL_CREDS}" | docker exec -i hanyan-db mysqldump \
--defaults-extra-file=/dev/stdin --all-databases -uroot \
2>/dev/null | gzip > hanyan-db.sql.gz

这套模式值得保留——/dev/stdin 在容器内作为临时 my.cnf 注入。

7 日滚动机制简单有效。 date +%u 作为目录编号(1=周一,7=周日),不需要复杂逻辑,天然覆盖。

空间预警。 脚本检查 /tmp 剩余空间是否 >1GB,不足时输出警告。

trap EXIT 清理。 使用 trap 'rm -rf "${WK}"' EXIT 确保退出时清理临时目录——但 set -euo pipefail 配合 gpg 的错误处理需要小心(下文会讲)。

3.2 槽点 / 踩过的坑

1. SGVPS_HOST 环境变量依赖——失败即跳过,但无声

1
2
3
4
5
6
SG_HOST="${SGVPS_HOST:-}"
if [ -n "${SG_HOST}" ] && [ -f "${SG_KEY}" ]; then
...
else
echo " ⚠ SGVPS_HOST 未设置或密钥缺失,跳过上海网关备份" >&2
fi

如果环境变量丢了,远程备份静默跳过。没有告警。

2. 没有备份成功率监控

备份脚本跑完就完事了。stderr 重定向到 log 文件,但没有检查 exit code,没有 PagerDuty/Telegram/Webhook 通知。如果连续 3 天备份失败,没人知道。

实测发现 crontab 的 log 文件路径有问题:

1
0 3 * * * bash .../hanyan-weekly-backup.sh >> ~/backups/logs/weekly-backup.log 2>&1

但目录 ~/backups/logs/ 可能不存在或权限不对——刚才检查时该目录为空。这意味着备份错误可能一直在黑洞里。

3. set -euo pipefail 与 GPG 的错误处理不兼容

set -e 遇到任何非零退出码就终止脚本。如果 mysqldump 失败,gzip 拿到空输入,gpg 尝试加密空文件也可能失败——连锁反应。

推荐模式:对非关键步骤加 || true 或单独捕获:

1
mysqldump ... || { echo "DB dump failed, continuing"; touch hanyan-db.sql.gz; }

4. Docker 卷未纳入备份

备份脚本只记录 docker volume ls 的文本清单,但卷的物理数据没有备份。这意味着:

  • qdrant_storage → 向量数据库丢了很难重建
  • wordpress-docker_db_data → MySQL 已单独导出,但 wp_data 上传的文件没备份
  • hanyan-api_hanyan-data → API 应用数据也没备份

mysqldump 保了数据库,WordPress 的 wp-content/uploads/ 呢?它们是独立卷挂载的。

5. 匿名卷是定时炸弹

1
2
3
4
5
$ docker volume ls | wc -l
21

$ docker volume ls -f dangling=true | wc -l
17 ← 81% 是垃圾

21 个卷里 17 个是匿名卷——来自已经 docker compose down -v 忘了清理的旧项目。每个几十到几百 MB,半年后就是 5GB 死数据。而且 docker system prune 并不会自动清理未使用的匿名卷,需要 docker volume prune 明确执行。

4. 实时数据:N100 备份健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ uptime
18:02:07 up 11 days, 19:21, load average: 0.57, 0.50, 0.52

$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 41 21 24.97GB 19.93GB (79%)
Containers 22 11 166.8MB 101MB (60%)
Local Volumes 21 4 1.178GB 980MB (83%)

$ df -h /
/dev/nvme0n1p2 937G 240G 650G 27% /

$ sensors
Package id 0: +60.0°C (high = +105.0°C, crit = +105.0°C)

健康指标解读:

指标 评价
连续运行 11 天 19h ✅ 稳定
负载 0.57 ✅ 极低(N100 idle~30%)
温度 60°C ✅ 安全(无空调环境可接受)
磁盘使用 27% (240G/937G) ✅ 充裕
镜像回收潜力 79% (19.93GB) ⚠️ 可以 prune
卷回收潜力 83% (980MB) ⚠️ 17个匿名卷需要清理
备份本地占用 3.2GB ✅ 可接受
备份 log 空文件 ❌ 错误日志丢失

5. 下阶段改进计划

P0 — 修复现有漏洞

  1. Fix backup logging — 创建备份日志目录并确认 crontab 重定向正确
  2. 备份通知 — 在脚本末尾加 Telegram/webhook 通知,备份失败时告警
  3. set -e 保护 — 对非关键步骤加错误处理,DB dump 失败不阻塞其余备份

P1 — 扩展备份纵深

  1. Docker 卷增量备份 — 用 docker run --volumes-from 方式 tar 关键卷数据到临时目录再加密
  2. SGVPS 远程副本可靠性 — 添加 SSH 重试(3 tries + backoff),SGVPS 失败时队列到下次执行
  3. 备份验证 — 解密一次、校验 tarball 完整性(gpg --verifysha256sum

P2 — 长期架构

  1. 备份保留策略细化 — 日备份 → 周备份 → 月归档,而不是最简单的 7 天覆盖
  2. 备份恢复手册 — 在没有装 Docker 的机器上恢复一个容器的全量数据,流程写在 BACKUP_RESTORE.md
  3. 基准测试 — 备份耗时、加密开销、远程传输速度,作为后续优化的基线

6. 一条命令能干的事

备份做没做好,run 一下就知道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 看看备份目录状况
ls -ld ~/backups/weekly/{1..7}/ 2>/dev/null
for d in 1 2 3 4 5 6 7; do
f=~/backups/weekly/$d/n100.tar.gz.gpg
[ -f "$f" ] && echo "Day $d: $(ls -lh "$f" | awk '{print $5}') $(date -r "$f" '+%Y-%m-%d')" \
|| echo "Day $d: MISSING"
done

# 看看容器数据有多大(决定备份策略)
docker run --rm -v wordpress-docker_wp_data:/data alpine \
du -sh /data

# 清理匿名卷(慎重,先确认)
docker volume ls -qf dangling=true | wc -l
# docker volume prune # 先别跑,确认哪些可以删

7. 小结

HanyanOS 的备份管线已经有了正确的骨架:

  • ✅ 每日凌晨自动运行
  • ✅ 本地 + 远程双副本
  • ✅ GPG AES256 加密
  • ✅ MySQL 全量导出
  • ✅ 7 日滚动覆盖

但骨架还缺血肉:

  • ❌ 日志丢失 → 无法审计
  • ❌ 无通知 → 失败即静默
  • ❌ Docker 卷裸数据未备份
  • ❌ 无恢复手册 → 真出事可能手忙脚乱

运维手记系列的意义就在这里——不是展示一个”完美系统”,而是记录从能用到可靠的每一步。

下周目标:把 P0 修掉,把 P1 的 Docker 卷增量备份跑通一次。到时见。


之前系列:

0%