知识广场
按学科筛选:计算机科学 / 操作系统
«计算机科学 / 操作系统» 分类下共 20 篇帖子
`tmpfiles.d(5)` 能解决大多数自动清理需求,但偶尔我们需要更"剧本化"的清理逻辑: 按业务规则保留某些目录、清完后通知 Prometheus pushgateway,或者顺便压缩归档。 这时候手写一个 `oneshot` 服务比改 `tmpfiles.d` 干净得多。 ## 1. 单元文件 ```ini # /etc/systemd/system/tmp-purge.service [Unit] Description=Purge stale /tmp entries older than 24h After=network.target [Service] Type=oneshot ExecStart=/usr/local/sbin/tmp-purge.sh Nice=10 IOSchedulingClass=idle ProtectSystem=strict ProtectHome=true ReadWritePaths=/tmp NoNewPrivileges=true ``` `ReadWritePaths=/tmp` 是关键 —— `ProtectSystem=strict` 之后整个 `/` 默认只读, 不显式开白名单会让脚本写不了任何东西。 ## 2. 脚本 ```bash #!/usr/bin/env bash # /usr/local/sbin/tmp-purge.sh set -euo pipefail PROTECTED='^/tmp/(systemd-private-|\.X11-unix|\.ICE-unix)' find /tmp -mindepth 1 -maxdepth 1 \ -mmin +1440 \ ! -regex "$PROTECTED" \ -exec rm -rf -- {} + logger -t tmp-purge "completed at $(date -Iseconds)" ``` `-mmin +1440` 是 24 小时;用 `+10080` 改为一周。`-regex` 那条把 `systemd-private-*` 和 X session 套接字目录排除掉,否则容易踩坑。 ## 3. timer ```ini # /etc/systemd/system/tmp-purge.timer [Unit] Description=Daily /tmp purge [Timer] OnCalendar=*-*-* 03:30:00 Persistent=true RandomizedDelaySec=15m [Install] WantedBy=timers.target ``` `Persistent=true` 在机器关机过那一时刻后会补跑一次。`RandomizedDelaySec` 让多 台机器错峰,避免 NFS / 备份目标在同一时刻被打爆。 ## 4. 启用 + 校验 ```bash chmod +x /usr/local/sbin/tmp-purge.sh systemctl daemon-reload systemctl enable --now tmp-purge.timer # 手动跑一次确认 systemctl start tmp-purge.service journalctl -u tmp-purge.service -n 20 --no-pager # 看下一次触发时间 systemctl list-timers tmp-purge.timer ``` ## 踩过的坑 - 早期版本忘记 `Type=oneshot`,systemd 默认 simple 模式会把"脚本退出"判为 "服务挂了",反复重启把磁盘 I/O 拉爆。一定要写 oneshot。 - 在很老的 systemd(< 235)上 `ReadWritePaths=/tmp` 会被 `PrivateTmp=true` 覆盖;如果你的 base unit 继承了 `PrivateTmp`,记得显式写 `PrivateTmp=false`。 - `find -delete` 不会递归删除非空目录,所以这里用 `-exec rm -rf -- {} +`。
## 起因 家里 NAS 上有 4TB 照片 + 项目数据,本地做了 ZFS RAID1(同一台机器 两块盘镜像)。但火灾 / 水患 / 误删 ZFS 仓库 → 一晚上全没。 "异地备份"是真灾备。 需求:每天把 NAS 上某个 dataset 增量同步到一台远程 VPS(10GB 起价 便宜的 storage VPS)。要求:增量、加密传输、自动管理 snapshot 链、 失败告警。 手写 `zfs send | ssh ... | zfs receive` 能行,但要自己管"上一次成功 的 base snapshot 是哪个"非常烦。`syncoid`(sanoid 项目的一部分)自动 处理整套流程。 ## 解决方案 ### 装 NAS + VPS 都要装: ```bash # Debian / Ubuntu sudo apt install sanoid syncoid --version ``` ### 配 SSH 密钥(NAS → VPS 单向) NAS: ```bash sudo ssh-keygen -t ed25519 -f /root/.ssh/syncoid -C 'syncoid' sudo cat /root/.ssh/syncoid.pub ``` VPS: ```bash sudo useradd -m -s /bin/bash zfsbackup sudo mkdir -p /home/zfsbackup/.ssh sudo tee -a /home/zfsbackup/.ssh/authorized_keys <<'EOF' command="zfs receive *",no-pty,no-port-forwarding ssh-ed25519 AAAA... EOF sudo chown -R zfsbackup:zfsbackup /home/zfsbackup/.ssh sudo chmod 700 /home/zfsbackup/.ssh sudo chmod 600 /home/zfsbackup/.ssh/authorized_keys ``` `command="zfs receive *"` 限定这把 key 只能跑 zfs receive, 即使泄露也不能登 shell。 ### 远程仓库 VPS 建 ZFS pool(数据盘,比如 `/dev/sdb`): ```bash sudo zpool create -O compression=zstd backup-pool /dev/sdb sudo zfs create backup-pool/nas sudo zfs allow zfsbackup create,mount,receive,destroy,snapshot \ backup-pool/nas ``` `zfs allow` 让 zfsbackup 用户能 receive 到这个 dataset 下。 ### 测试一次同步 NAS: ```bash sudo syncoid -i /root/.ssh/syncoid \ tank/photos [email protected]:backup-pool/nas/photos ``` 第一次跑会全量传 4TB(取决带宽,几小时-几天)。后续增量只传差异。 `syncoid` 自动: - 在源端创建一个 `syncoid_*` 快照 - `zfs send -i <last-common> <new>` 增量发到远端 - 远端 `zfs receive` - 删除源端不再需要的老 syncoid 快照(保留最近两个用于下次增量) ### sanoid 管本地 snapshot retention NAS `/etc/sanoid/sanoid.conf`: ```ini [tank/photos] use_template = production recursive = yes [template_production] hourly = 36 daily = 30 monthly = 12 yearly = 3 autosnap = yes autoprune = yes ``` cron / timer 跑: ```bash sudo systemctl enable --now sanoid.timer # 默认每 15 分钟检查 ``` 每小时打 snapshot,保留 36 小时;每天保留 30 天;每月保留 12 个月。 本地 snapshot 给"刚才误删了文件,立刻回来"用;syncoid 异地同步给"机器 没了"用。 ### 自动化 syncoid `/etc/systemd/system/syncoid-nas.service`: ```ini [Unit] Description=ZFS replicate tank/photos to vps After=network-online.target [Service] Type=oneshot ExecStart=/usr/sbin/syncoid \ --quiet \ -i /root/.ssh/syncoid \ tank/photos \ [email protected]:backup-pool/nas/photos ExecStartPost=-/usr/local/sbin/notify-success.sh syncoid [email protected] ``` `/etc/systemd/system/syncoid-nas.timer`: ```ini [Timer] OnCalendar=*-*-* 02:30:00 RandomizedDelaySec=30m Persistent=true [Install] WantedBy=timers.target ``` ```bash sudo systemctl enable --now syncoid-nas.timer ``` ### 校验异地数据 VPS: ```bash sudo zfs list -t snapshot backup-pool/nas/photos | tail -5 # 应当看到最近 syncoid_* snapshot sudo zpool scrub backup-pool # 每月一次校验 ``` ### 还原演练(重要) 最佳实践:每季度做一次"模拟 NAS 没了"演练。 从 VPS 拉回最近 snapshot: ```bash ssh nas sudo zfs send -R zfsbackup@vps:backup-pool/nas/photos@syncoid_2026_05_20 \ | sudo zfs receive -F tank/photos-restored ``` 确认能用 + 文件完整 + 权限对。**演练过的备份才是真备份**。 ## 效果 - 4TB 数据每天增量同步,平均增量 100-500 MB(取决于使用量) - 上传跑半小时-1 小时(看带宽),完全在低峰期 02:30 - 本地误删 → ZFS snapshot 秒级恢复 - NAS 全挂 → VPS 拉回完整数据,零数据丢失 - VPS 端 zstd 压缩让 4TB 实际占 ~2.4 TB(10 美元 / 月的 storage VPS 够用) ## 踩过的坑 1. **第一次全量同步卡 ssh 限速**:default OpenSSH 在某些 distro 上有 ChaCha20 加密的 CPU 瓶颈。改 `[email protected]` 加密算法 能快 2-3 倍:`syncoid --sshcipher [email protected]`。 2. **远端 dataset 没存在**:syncoid 不会自动建 parent dataset。 先手动 `zfs create backup-pool/nas` 建好。 3. **snapshot 过多**:sanoid 没配 retention 时 snapshot 几千个, `zfs list` 都慢。一定配 `autoprune = yes`。 4. **clock skew**:源端 / 目标端时间差大于 1 分钟,syncoid 偶尔报 "common ancestor" 找错。两端配 chrony 同 NTP。 5. **加密 dataset 同步**:源是 encrypted dataset,要加 `--sendoptions=w` 送 raw encrypted stream,远端不需要密码就能存。
rsync 是最稳的小型备份方案:传输层 SSH 已加密,增量算法只传 delta, 配合 `--link-dest` 可以在目标侧做"硬链接快照"——每个快照看起来像 完整目录,磁盘占用却只算改动量。 适用场景:单机或少量机器的备份目标是另一台 Linux 主机 / NAS。 量大或对 dedup 要求高时换 Restic / Borg。 ## 备份脚本 ```bash #!/usr/bin/env bash # /usr/local/sbin/snap-backup.sh set -euo pipefail SRC="/srv/app /var/log /etc" DEST_HOST="[email protected]" DEST_BASE="/volume1/backups/$(hostname -s)" STAMP="$(date +%Y%m%d-%H%M%S)" LATEST="${DEST_BASE}/latest" NEW="${DEST_BASE}/${STAMP}" ssh "${DEST_HOST}" "mkdir -p '${DEST_BASE}'" rsync -aAXH --delete --numeric-ids \ --link-dest="${LATEST}" \ --rsync-path="ionice -c 3 rsync" \ --info=stats2,progress2 \ ${SRC} "${DEST_HOST}:${NEW}/" ssh "${DEST_HOST}" "rm -f '${LATEST}' && ln -sfn '${STAMP}' '${LATEST}'" # 保留最近 14 个快照 ssh "${DEST_HOST}" " cd '${DEST_BASE}' \ && ls -1 | grep -E '^[0-9]{8}-[0-9]{6}\$' | sort | head -n -14 \ | xargs -r -I{} rm -rf -- {} " ``` 关键参数: - `-aAXH`:等价于 `-rlptgoD` + 保留 ACL、xattr、硬链接关系 - `--numeric-ids`:用数字 UID/GID 而不是名字,跨机器一致性更好 - `--link-dest`:与上一次快照做硬链接,未改的文件几乎不占空间 - `--rsync-path="ionice -c 3 rsync"`:远端 rsync 进程降到 idle I/O, 备份时段不影响其它服务 ## 校验 ```bash # 远端校验 checksum 是否匹配(慢,每月一次足够) rsync -aAXH --dry-run --checksum --itemize-changes \ ${SRC} "${DEST_HOST}:${LATEST}/" | head -20 # 任何输出都意味着源 vs 备份不一致,需要排查 ``` ## SSH 配置 避免脚本里硬编码密钥路径,统一在 SSH config: ``` # ~/.ssh/config(或 /root/.ssh/config) Host nas.example.com User backup IdentityFile ~/.ssh/id_backup_ed25519 IdentitiesOnly yes ``` NAS 上限制这个 key 只能跑 rsync: ``` # 远端 ~backup/.ssh/authorized_keys command="rsync --server -vlogDtprAXe.iLsfxC --numeric-ids --delete --link-dest=* . /volume1/backups/*",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... ``` ## systemd timer ```ini # /etc/systemd/system/snap-backup.timer [Timer] OnCalendar=*-*-* 02:00:00 RandomizedDelaySec=30m Persistent=true [Install] WantedBy=timers.target ``` ## 踩过的坑 - `--link-dest` 路径必须是 **目标侧** 的绝对路径,而且当时已存在 上一次快照目录;第一次跑没快照时 link-dest 自然失效,rsync 会回退到 完整拷贝,正常。 - 备份 `/etc` 不开 `-H` 时,sudoers / cron / 各种 symlink 容易脱钩, 恢复时一头雾水。 - `--delete` 是双刃剑:源被误删后下次备份会同步删掉目标。所以快照轮转 保留多个版本很重要。
## 起因 之前文章分别介绍过 borg 和 restic。一个朋友问"我应该选哪个?" 干脆做一个对比实测:同样数据集 + 同样备份策略,跑出真实数字。 三个候选: - **borg**:Python 写的,2014 起,老牌 - **restic**:Go 写的,2015 起,现代 - **kopia**:Go 写的,2018 起,最年轻,UI 最强 三者都支持:客户端加密、去重、增量、压缩、多 backend。 ## 测试 setup - 数据:500 GB(混合:1M 个文件 + 几个大 VM disk) - 机器:8 core / 32 GB / NVMe SSD - 后端:本地磁盘(避免网络变量) - 跑 5 次取均值 ## 1. 首次全量备份 | | 时间 | 备份大小 | CPU 峰值 | RAM 峰值 | |---|---|---|---|---| | borg | 38 min | 195 GB | 800% (8 core) | 1.2 GB | | restic | 31 min | 198 GB | 700% | 1.8 GB | | kopia | 24 min | 180 GB | 850% | 2.4 GB | kopia 最快 + 最小(zstd 默认压缩级别更激进)。 borg / restic 接近。 ## 2. 增量备份(改动 5GB) | | 时间 | 新写入 | |---|---|---| | borg | 1m 50s | 1.8 GB | | restic | 1m 20s | 2.0 GB | | kopia | 0m 55s | 1.5 GB | kopia 增量也最快。 ## 3. 还原性能(取 10 个文件) | | 时间 | |---|---| | borg | 4s | | restic | 2s | | kopia | 2s | borg 单文件还原稍慢(Python 启动开销)。 ## 4. 还原全量 | | 时间 | |---|---| | borg | 32 min | | restic | 25 min | | kopia | 22 min | 跟备份时间类似的趋势。 ## 5. 仓库 check(校验完整性) | | 时间 | |---|---| | borg check | 8 min | | restic check --read-data | 18 min | | kopia maintenance | 6 min | borg / kopia 校验快。restic 默认 check 不读 data;带 `--read-data` 慢。 ## 6. 大量小文件场景(100 万个 1KB 文件) | | 备份时间 | |---|---| | borg | 14 min | | restic | 12 min | | kopia | 9 min | kopia 处理 small files 最好。 ## 7. UI / 易用性 ### borg:CLI only ```bash borg init -e repokey /backup/repo borg create /backup/repo::$(date +%F) /home /etc borg list /backup/repo borg extract /backup/repo::2024-05-24 home/me/important.txt ``` `borgmatic` 是它的 YAML wrapper。无原生 GUI。 ### restic:CLI + fuse mount ```bash restic init -r /backup/repo restic backup -r /backup/repo /home /etc restic snapshots restic mount /tmp/r # FUSE 挂载浏览 ``` 无 GUI 但 fuse mount 让"浏览历史快照" 很方便。 ### kopia:CLI + Web UI + 跨平台 GUI ```bash kopia repository create filesystem --path=/backup/repo kopia snapshot create /home /etc kopia snapshot list kopia server start --address=0.0.0.0:51515 --insecure # Web UI ``` Web UI 浏览快照 / 还原 / 管理 policy 都可视化。 还有 Windows / Mac 客户端 GUI。 ## 8. 多客户端共享仓库 多机器备份到同一仓库(共享 dedup 池): | | 支持 | 体验 | |---|---|---| | borg | ❌ 单写者(一次一客户端) | 复杂:需 borg serve + lock | | restic | ✅ 多客户端 | 简单 | | kopia | ✅ 多客户端 | 简单(甚至有 P2P 模式) | 家庭多设备 / 公司多服务器场景 restic / kopia 友好。 ## 9. 远程后端支持 | | 本地 | SSH | S3 | B2 | GCS | Azure | rclone | |---|---|---|---|---|---|---|---| | borg | ✅ | ✅ | rclone 代理 | rclone | rclone | rclone | ✅ | | restic | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | kopia | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | webdav | restic / kopia 原生支持云存储更全。 ## 10. 安全 / 加密 三者都 AES + 客户端加密 + 服务端只看到加密块。 | | 加密算法 | 密钥管理 | |---|---|---| | borg | AES-256-CTR + HMAC-SHA256 | keyfile / repokey | | restic | AES-256-CTR + Poly1305-AES | 内置 config | | kopia | AES-256-GCM | 多 key 支持 | 都安全。kopia 用 AEAD 更现代。 ## 11. 资源占用对比 长期运行的 inactive 仓库: | | 元数据大小 | 看快照速度 | |---|---|---| | borg | 中(chunks index) | 中 | | restic | 中 | 慢(大 repo 时) | | kopia | 小 | 快 | restic 在 TB 级仓库的"列出快照"操作会慢,是个已知问题。 ## 选择决策 | 场景 | 推荐 | |---|---| | 单机 / Linux 老手 / 已经在用 | borg(稳定 + 成熟) | | 多机器 + 云后端 + 团队 | restic(生态最广) | | 喜欢 GUI + 跨平台 + 多设备共享 | **kopia**(最现代) | | 不想折腾 | restic | | 极致性能 | kopia | 我个人现在新项目首选 kopia,遗留项目继续 restic / borg。 ## 共同 best practice 不管选哪个: 1. **测试还原**:每季度真的还原一次到不同机器,验证流程能通 2. **3-2-1 规则**:3 份数据,2 种媒介,1 份异地 3. **加密 key 单独备份**:repo 备份了但 key 丢了 = 数据死透了 4. **自动 prune 策略**:保留 7d / 4w / 12m 之类 5. **monitor 告警**:备份失败要立刻知道(systemd OnFailure / healthchecks.io) ## 配置示例:kopia + B2 ```bash # 第一次 kopia repository create b2 \ --bucket=my-backups \ --key-id=00abc... \ --key=K00... \ --password=very-strong-password # 定 policy kopia policy set --global \ --keep-latest 10 \ --keep-hourly 24 \ --keep-daily 30 \ --keep-weekly 12 \ --keep-monthly 24 \ --keep-annual 5 \ --compression=zstd # 备份 kopia snapshot create /etc /home/me/important /srv # 定时 # systemd timer 每小时 ``` ## 踩过的坑 1. **borg 单写锁卡死**:多机器同时备份 → 第二个等待 / 失败。 解决:每机一独立 repo,或者错峰备份。 2. **restic prune 慢**:TB 级仓库 prune 几小时。改 `forget --keep-* --prune-max-unused 5%` 增量 prune。 3. **kopia metadata 损坏**:仓库被 unsafe shutdown → 偶尔 corruption。 `kopia maintenance --safety=full` 修复。 4. **三者都 case-sensitive**:Windows / Mac 用户备份大小写不敏感 文件系统,恢复到 Linux 时可能冲突。 5. **加密密码丢了**:**没有 recovery**。所有这类工具都设计成"你忘 密码 = 数据死透了"。一定要写在 password manager + 离线纸质备份。
cron 用了二十多年但有几个老问题:失败没有 retry、错过的任务(机器关机) 不补跑、日志要自己重定向、表达式只到分钟、找不到任务跑慢了的根因。 systemd timer 把这些一站式解决。 ## 一个完整例子 需求:每小时把 PostgreSQL 备份压缩传到远端,失败重试 3 次。 ### service 单元 ```ini # /etc/systemd/system/pgdump.service [Unit] Description=Hourly pg_dump + ship to backup host Wants=network-online.target After=network-online.target [Service] Type=oneshot User=postgres Group=postgres # 失败重试:5s, 15s, 45s 三次 Restart=on-failure RestartSec=5s StartLimitIntervalSec=120 StartLimitBurst=4 # 运行约束 Nice=10 IOSchedulingClass=idle TimeoutStartSec=30m # 命令本身 ExecStart=/usr/local/sbin/pgdump-ship.sh # 资源限制(避免 OOM 把别的进程踩死) MemoryMax=2G TasksMax=64 ``` `Restart=on-failure` 是关键 —— 脚本非零退出就触发重启。`StartLimitBurst=4` 意味着 120s 内最多触发 4 次(即 3 次重试),超过就 systemd 放弃并把单元 标记为 `failed`,配合 `OnFailure=` 还能进一步触发告警。 ### timer 单元 ```ini # /etc/systemd/system/pgdump.timer [Unit] Description=Hourly pg_dump [Timer] # 每小时第 5 分钟(避开 nginx logrotate 等整点任务) OnCalendar=*:05:00 # 错过的执行(如机器关机时)启动时补一次 Persistent=true # 多机器错峰 RandomizedDelaySec=2m # 防止上一次还没跑完又起新的 Unit=pgdump.service [Install] WantedBy=timers.target ``` ### 启用 ```bash sudo systemctl daemon-reload sudo systemctl enable --now pgdump.timer systemctl list-timers pgdump.timer # NEXT LEFT LAST PASSED UNIT ACTIVATES # Sat 2026-05-23 14:05:00 CST 22min left Sat 2026-05-23 13:05:00 CST 38min pgdump.timer pgdump.service ``` ## OnCalendar 表达式速查 | 表达式 | 含义 | |---|---| | `*-*-* 03:00:00` | 每天 03:00 | | `Mon..Fri 09:00` | 工作日 09:00 | | `*:0/15` | 每 15 分钟 | | `*:0/30:00` | 每 30 分钟(秒级精度) | | `2026-05-23 12:00` | 一次性 | | `weekly` | 每周一 00:00(别名) | 校验表达式: ```bash systemd-analyze calendar 'Mon..Fri 09:30' # Next elapse: Mon 2026-05-26 09:30:00 CST ``` ## OnUnitActiveSec / OnBootSec:相对触发 不基于墙钟,基于上次激活时间: ```ini [Timer] OnBootSec=15min # 开机后 15 分钟触发首次 OnUnitActiveSec=1h # 之后每隔 1 小时 ``` 适合"开机后 N 分钟做一次健康检查 + 之后周期跑"的场景。 ## 失败时告警 ```ini # 在 pgdump.service 里加 [Unit] [email protected] ``` ```ini # /etc/systemd/system/[email protected] [Service] Type=oneshot ExecStart=/usr/local/sbin/notify.sh "%i failed: $(journalctl -u %i -n 30 --no-pager)" ``` `%i` 是模板实例名,这里就是 `pgdump`。`notify.sh` 调用 Slack webhook / 邮件 / 任意。 ## 日志:journal 自动归档 不需要 `>> /var/log/...`。所有 stdout / stderr 自动进 journal: ```bash journalctl -u pgdump.service --since '1 day ago' journalctl -u pgdump.service -p err # 只看 error 级 ``` ## 一次性手动触发 ```bash sudo systemctl start pgdump.service # 立刻跑一次,不影响 timer ``` ## 踩过的坑 - `Type=oneshot` 漏写:systemd 默认 simple,脚本退出后认为"挂了"会触发 restart 死循环。所有 cron-style 任务必须 `Type=oneshot`。 - `Wants=network-online.target` 但脚本仍然连不上:很多 ISP 给 IPv6 默认路由 比 IPv4 慢;加 `ExecStartPre=/bin/sleep 5` 简单粗暴。 - `Persistent=true` 在频繁开关机的笔记本上会变成"开机就跑一堆任务"风暴, 可以加 `RandomizedDelaySec=10m` 散开。 - 两个 timer 同时触发同一个 service 时只跑一次 —— 这是 feature 不是 bug (服务的"活跃中"状态不会被重复启动)。
应用启动卡了 30 秒、跑命令显示半天没动静、systemd 报 timeout —— 不知道 卡在哪步时,strace 能直接告诉你进程在做什么 syscall。 90% 的"卡"是某个 syscall 在等待:DNS / 网络连接 / 文件锁 / 互斥锁 / fsync。 ## 基本用法 ```bash # 跑一个新进程,观察所有 syscall strace -f -tt -o /tmp/trace.log ./myapp # attach 到正在运行的进程 strace -f -tt -p <PID> -o /tmp/trace.log # Ctrl-C 停(被 attach 的进程继续跑) ``` 关键参数: - `-f`:跟踪 fork 出来的子进程(多线程 / 多进程应用必选) - `-tt`:每行加微秒时间戳(看延迟必备) - `-o file`:写文件而不是混进 stdout - `-T`:每个 syscall 的耗时(找慢调用神器) - `-e trace=...`:只看某类 syscall 常见过滤: ```bash # 只看网络相关 strace -f -e trace=network -p <PID> # 只看文件 I/O strace -f -e trace=file -p <PID> # 只看慢的(> 100ms) strace -f -T -p <PID> 2>&1 | awk '/<[0-9]+\.[0-9]+>/{ split($NF, a, "<|>"); if (a[2]+0 > 0.1) print }' ``` ## 案例 1:应用启动卡 30 秒 ```bash strace -f -tt -T -o /tmp/start.log ./myapp # ... 30 秒后启动完成 sort -k1 /tmp/start.log | grep -E '\<[0-9]\.[0-9]+\>' | sort -k2 -t'<' -nr | head ``` 或者直接看时间戳跳变: ```bash awk '{print $1, $0}' /tmp/start.log | awk ' NR==1{prev=$1; next} { diff=$1-prev if (diff > 1) print "*** gap " diff "s at " $0 prev=$1 }' /tmp/start.log ``` 常见根因: - `connect(... 192.168.x.x:53 ...)` 后 timeout 几秒 → DNS 配置错或者 resolv.conf 第一个 server 不可达 - `openat(... /etc/...locked) = -1 EAGAIN` 反复重试 → 文件锁等其它进程 - `futex(... FUTEX_WAIT ...)` 长时间不返回 → 库内部 mutex 死锁 - `read(socket, ...)` 长时间阻塞 → 下游响应慢 ## 案例 2:DNS 慢 ```bash strace -f -e trace=network -tt curl http://api.example.com/ 2>&1 | head -30 # 11:23:45.001 socket(AF_INET, SOCK_DGRAM, 0) = 5 # 11:23:45.002 connect(5, {sa_family=AF_INET, sin_addr=8.8.8.8, ...}, 16) = 0 # 11:23:45.003 sendto(5, ...) = 32 # DNS query # 11:23:50.003 sendto(5, ...) = 32 # 5 秒后重试,第一次超时了 ``` 5 秒间隙说明第一个 nameserver 不响应。改 `/etc/resolv.conf`: ``` nameserver 1.1.1.1 nameserver 8.8.8.8 options timeout:1 attempts:2 ``` `timeout:1` 把单次 DNS 超时从默认 5 秒降到 1 秒。 ## 案例 3:找文件被打开了多少次 ```bash strace -f -e trace=openat -o /tmp/o.log ./myapp grep -oE '"[^"]+"' /tmp/o.log | sort | uniq -c | sort -rn | head # 832 "/etc/ld.so.cache" # 412 "/usr/lib/x86_64-linux-gnu/libc.so.6" # 158 "/proc/self/maps" # ... ``` 发现某个文件被 open 几百上千次 → 可能是配置文件没缓存、或者插件加载死循环。 ## strace vs ltrace vs perf | 工具 | 看什么 | 典型场景 | |---|---|---| | strace | syscalls | I/O 慢、卡 syscall、文件路径错 | | ltrace | 动态库函数调用 | 怀疑某个 libc 函数 / glib 函数行为 | | perf | CPU 采样 | CPU 100% 找热点函数 | | bpftrace | 几乎一切 | 复杂的内核态行为 | ## 性能影响 strace 会 **大幅** 拖慢被跟踪进程(每个 syscall 加上 ptrace 上下文切换)。 高并发服务上用 `-e ...` 严格限制范围,或者用 `bpftrace` 这种基于 eBPF 的"无停顿"工具。 ## 高级:只在某行附近抓 ```bash # 进程已经卡 5 分钟了,attach 抓 30 秒 strace -f -tt -p <PID> -o /tmp/probe.log & sleep 30 kill %1 ``` ## 踩过的坑 - 容器内可能没装 strace;`apt install strace` 在精简镜像装不上时, 从宿主 `nsenter` 进容器跑也行: ```bash sudo nsenter -t <PID> -p -m -n strace -f -p <PID> ``` - strace 输出的字符串默认截断 32 字符,要看完整路径加 `-s 256`。 - 多线程进程 attach 时 `-f` 会顺带跟所有线程,量大可能淹掉日志, 缩小范围用 `-e trace=...`。 - 不要在生产数据库上 attach strace —— 可能把 IO 慢到触发副节点切换 / 超时回滚。先看 perf / eBPF。
## 起因 服务挂了 systemd `Restart=on-failure` 能自动重启。但 "进程没崩,却卡死 不响应" 这种情况 systemd 看不出来——TCP 连接还在,进程还活,只是不 处理请求了。 `WatchdogSec` 让服务定期向 systemd 发"我还活"信号;超时不发就被 systemd 当死了重启。 ## 工作原理 ``` service 进程 ↓ 每 N 秒 sd_notify(WATCHDOG=1) systemd watchdog timer ↓ 收到 → reset timer ↓ 没收到 → SIGTERM + restart ``` 时间预算(WatchdogSec=30s)= 服务必须在 30s 内"喂狗"一次。 ## 配置 systemd 单元 `/etc/systemd/system/myapp.service`: ```ini [Service] Type=notify WatchdogSec=30 NotifyAccess=main ExecStart=/usr/local/bin/myapp Restart=on-watchdog RestartSec=5 ``` - `Type=notify`:systemd 期待服务在启动好后发 `READY=1` - `WatchdogSec=30`:30 秒内没"喂狗"就重启 - `Restart=on-watchdog`:watchdog 超时也重启(默认 on-failure 不含) - `NotifyAccess=main`:只允许主进程发 notify ## 应用代码:发 WATCHDOG ### Python ```python import os import socket import threading import time def notify(message): """向 systemd notify socket 发消息""" sock_path = os.environ.get('NOTIFY_SOCKET') if not sock_path: return s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) if sock_path.startswith('@'): sock_path = '\0' + sock_path[1:] try: s.sendto(message.encode(), sock_path) finally: s.close() def watchdog_loop(): interval = int(os.environ.get('WATCHDOG_USEC', '30000000')) // 1_000_000 // 2 while True: # 在这里 check 应用真"活"——不只是循环跑 if app_is_healthy(): notify('WATCHDOG=1') time.sleep(interval) # 启动时 notify('READY=1') # 后台喂狗 threading.Thread(target=watchdog_loop, daemon=True).start() # 业务主循环 serve_forever() ``` `WATCHDOG_USEC` 是 systemd 自动设的环境变量(微秒)。喂狗间隔通常 设为 WatchdogSec 的 1/2 给容错。 `app_is_healthy()` 是你的健康检查: ```python def app_is_healthy(): # check DB / Redis / 关键资源 try: db.ping(timeout=2) # 检查事件循环没死锁 / queue 不爆 return queue.qsize() < 10000 except Exception: return False ``` 如果 health check 失败,不要喂狗 → systemd 杀进程重启。 ### Go ```go import ( "os" "time" "github.com/coreos/go-systemd/v22/daemon" ) func main() { // ... 初始化 daemon.SdNotify(false, daemon.SdNotifyReady) // 启动 watchdog goroutine interval, err := daemon.SdWatchdogEnabled(false) if err == nil && interval > 0 { go func() { tick := time.NewTicker(interval / 2) for range tick.C { if appHealthy() { daemon.SdNotify(false, daemon.SdNotifyWatchdog) } } }() } serve() } ``` ### Rust ```rust use systemd::daemon; use std::thread; use std::time::Duration; fn main() { daemon::notify(false, [(daemon::STATE_READY, "1")].iter()).unwrap(); thread::spawn(|| { let interval = Duration::from_secs(15); loop { if app_healthy() { daemon::notify(false, [(daemon::STATE_WATCHDOG, "1")].iter()).unwrap(); } thread::sleep(interval); } }); serve(); } ``` ### 不会修改源码的服务 跑现成 binary 没 sd_notify 支持?用 `systemd-notify` wrapper: ```ini [Service] ExecStart=/path/to/wrapper.sh ``` ```bash #!/bin/bash # wrapper.sh your-app & APP_PID=$! while kill -0 $APP_PID 2>/dev/null; do if curl -sf http://localhost:8080/health > /dev/null; then systemd-notify WATCHDOG=1 fi sleep 15 done ``` 但这种方式增加复杂度;建议优先改应用代码。 ## 实测 ```bash sudo systemctl start myapp sudo systemctl status myapp # 启动后看 status: active (running) # 状态行有 "Watchdog: 30s" # 模拟"应用卡死" —— 让 app_is_healthy 返 False # 30 秒后 systemd 会: # 1. 发 SIGTERM # 2. 等待终止 # 3. 按 Restart=on-watchdog 重启 journalctl -u myapp -f # 看到 "Watchdog timeout (limit 30s)!" + restart ``` ## 与"被动监控 + 外部重启" 对比 外部脚本检测 + 重启: ```bash # cron */1 * * * * if ! curl -sf http://localhost:8080/health; then systemctl restart myapp fi ``` 优点:简单,不改应用代码。 缺点: - 间隔最小 1 分钟(cron) - 健康检查走 HTTP(增加耦合) - restart 流程慢(systemctl 命令 + 进程退出 + 重启) WatchdogSec 优点: - 毫秒级触发 - 应用内部直接判断"我健康吗" - systemd 一站式管 生产推荐 WatchdogSec。 ## 几个调参 ### WatchdogSec 太短 `WatchdogSec=5`:网络抖动 / GC 暂停 / 大事务 commit → 误杀健康进程。 通常 30-120 秒是合理范围。 ### 启动期不喂狗 启动加载大模型几分钟: ```ini TimeoutStartSec=10min WatchdogSec=30 ``` 启动期内由 `TimeoutStartSec` 控制;READY 发出后才进入 WatchdogSec 监管。 应用代码:模型 load 完才 `notify('READY=1')` + 启动 watchdog 线程。 ### RuntimeMaxSec:周期性重启 ```ini WatchdogSec=30 RuntimeMaxSec=24h # 跑超过 24h 强制重启 ``` 防内存泄漏类问题:每天自动重启一次,泄漏不可能跑到 OOM。 ## 实战:FastAPI + uvicorn + WatchdogSec uvicorn 不原生支持 sd_notify。 解决方案 1:用 hypercorn 或 gunicorn (`UvicornWorker`),自己写 hook。 或者用 ASGI lifespan event: ```python # app/main.py import asyncio, socket, os from fastapi import FastAPI from contextlib import asynccontextmanager def notify(msg): sock_path = os.environ.get('NOTIFY_SOCKET') if not sock_path: return s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) try: s.sendto(msg.encode(), sock_path) finally: s.close() async def watchdog(): interval = int(os.environ.get('WATCHDOG_USEC', '30000000')) / 2_000_000 while True: if await check_health_async(): notify('WATCHDOG=1') await asyncio.sleep(interval) @asynccontextmanager async def lifespan(app: FastAPI): notify('READY=1') task = asyncio.create_task(watchdog()) yield task.cancel() app = FastAPI(lifespan=lifespan) # health check 是真检查事件循环 + DB async def check_health_async(): try: await asyncio.wait_for(db.execute('SELECT 1'), timeout=2) return True except Exception: return False ``` systemd unit: ```ini [Service] Type=notify WatchdogSec=60 ExecStart=/srv/app/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=on-watchdog ``` 效果:DB 卡死 → health check 不通过 → 不喂狗 → 60s 后 systemd 杀重启。 ## 监控 systemd journal 里 watchdog 触发显示: ``` systemd[1]: myapp.service: Watchdog timeout (limit 30s)! systemd[1]: myapp.service: Killing process 12345 with signal SIGABRT. ``` Prometheus node_exporter 自动收 systemd `*_restart_count` 指标。 告警:`rate(systemd_unit_restart_total[1h]) > 5` → 服务持续不健康。 ## 踩过的坑 1. **`Type=simple` 不 work**:必须 `Type=notify` watchdog 才生效。 2. **fork 后子进程喂狗** `NotifyAccess` 默认 main:子进程发的被忽略。 要么改成 `NotifyAccess=all`,要么主进程负责。 3. **测试期间设置太短**:`WatchdogSec=5` 测试是因为快,生产忘改回 常态 → 高负载时 GC 一下被误杀。 4. **app 用 thread + GIL**:watchdog 线程被 GIL 卡住 → 整个 Python 进程都不喂狗 → 误杀。用 async 或 multiprocessing 把 watchdog 独立。 5. **WATCHDOG_USEC 没读到**:环境变量是 systemd 启动子进程时注入。 如果你的 service 用 shell wrapper 启动,要 `exec` 替换或显式 `env` 传过去。
## 起因 我们的备份服务器收 rsync from 多台业务机器。问题: - rsync 跑到一半,业务机继续写新文件 → 备份目录里出现"part-A from 12:00, part-B from 12:15" 时间错乱的版本 - 备份过程中如果客户端机器挂了 → 备份目录是半新半旧 - 想"备份这一刻的整盘快照" 而不是"几小时内分散的快照" 如果备份目标是 ZFS 文件系统,可以利用 ZFS snapshot 做"事务性"备份: 1. rsync 完整跑完 2. 跑完后立即 zfs snapshot 3. 之后业务继续 rsync 上新数据 4. snapshot 是这一刻完整一致的副本 ## 完整流程 ### setup 备份服务器 NAS 上: ```bash # 创建一个 ZFS dataset 收备份 sudo zfs create backuptank/clients/server-foo sudo zfs set compression=zstd backuptank/clients/server-foo sudo zfs set atime=off backuptank/clients/server-foo # 给 rsync 用户写权限 sudo zfs allow rsync-user create,destroy,snapshot,mount backuptank/clients/server-foo ``` ### 业务机推送 + snapshot 脚本 ```bash #!/usr/bin/env bash # /usr/local/sbin/snap-rsync.sh set -euo pipefail REMOTE_USER=rsync-user REMOTE_HOST=nas.local REMOTE_DATASET=backuptank/clients/server-foo REMOTE_MOUNT=/backuptank/clients/server-foo SRC="/etc /home /srv" HOSTNAME=$(hostname -s) TS=$(date +%Y-%m-%dT%H-%M-%S) # 1. 同步过去(增量;--delete 保镜像) rsync -aAXH --numeric-ids --delete --info=stats2,progress2 \ -e ssh \ $SRC "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_MOUNT}/${HOSTNAME}/" # 2. 完成后远程 zfs snapshot 标记这一刻 ssh "${REMOTE_USER}@${REMOTE_HOST}" \ "sudo zfs snapshot ${REMOTE_DATASET}@${HOSTNAME}-${TS}" echo "snapshot: ${REMOTE_DATASET}@${HOSTNAME}-${TS}" # 3. retention:保留最近 30 个 snapshot ssh "${REMOTE_USER}@${REMOTE_HOST}" " sudo zfs list -H -o name -t snapshot ${REMOTE_DATASET} \ | grep '@${HOSTNAME}-' \ | sort -r \ | tail -n +31 \ | xargs -r -n 1 sudo zfs destroy " ``` 每天 cron / timer 跑这个。 ### 浏览 / 还原历史快照 NAS 上 ZFS snapshot 默认可访问: ```bash ls /backuptank/clients/server-foo/.zfs/snapshot/ # server-foo-2024-05-24T03-00-00 # server-foo-2024-05-23T03-00-00 # ... # 还原某个文件 cp /backuptank/clients/server-foo/.zfs/snapshot/server-foo-2024-05-24T03-00-00/etc/nginx/nginx.conf /tmp/ ``` 或者 `zfs clone` 把 snapshot 挂成可写副本: ```bash sudo zfs clone backuptank/clients/server-foo@server-foo-2024-05-24T03-00-00 \ backuptank/restore/server-foo-2024-05-24 mount | grep restore ``` 任意时间点的完整目录树都能挂载浏览。 ## ZFS dedup(可选) 如果多机器备份内容重叠多(OS 文件大体相同): ```bash sudo zfs set dedup=on backuptank/clients ``` 代价:每 1 TB dedup 数据约需 5 GB RAM 维护 dedup table。 所以只在高 dedup ratio + 充裕 RAM 时开。 测: ```bash sudo zdb -S backuptank # 输出 estimated dedup ratio:1.5x 以上才值得开 ``` ## ZFS send:异地同步备份 ```bash # 第一次全量 → 异地 NAS sudo zfs send backuptank/clients/server-foo@latest \ | ssh remote-nas "sudo zfs receive backuptank-mirror/server-foo" # 后续增量 sudo zfs send -i @prev-snap backuptank/clients/server-foo@new-snap \ | ssh remote-nas "sudo zfs receive backuptank-mirror/server-foo" ``` 或者 syncoid 自动: ```bash syncoid backuptank/clients/server-foo remote-nas:backuptank-mirror/server-foo ``` 每天本地 rsync + snapshot;每周 syncoid 异地。**两层保护**。 ## 与"直接备到 ZFS snapshot" 的对比 替代方案:客户端不 rsync,业务机自己 ZFS snapshot + zfs send 给 NAS。 ```bash # 业务机 sudo zfs snapshot tank/data@$(date +%F) sudo zfs send -i @prev tank/data@new \ | ssh nas "sudo zfs receive backuptank/clients/foo" ``` 优点: - 原生 ZFS 一致性快照(毫秒级 frozen) - 增量 send 只传变化的 block(比 rsync 比 file 快很多) - 文件系统级,不漏 metadata / xattr / hardlink 要求:业务机文件系统是 ZFS。 如果业务机是 ext4 / xfs → rsync + 备份目标 ZFS snapshot 是 fallback。 ## monitoring ```bash # 看 snapshot 数 + 大小 zfs list -t snapshot backuptank/clients/server-foo # Prometheus exporter (node_exporter zfs collector) # 暴露:node_zfs_zpool_state{pool="backuptank"} = 1 (ONLINE) # node_zfs_zfs_dataset_used_bytes # node_zfs_zfs_dataset_available_bytes ``` 仪表盘看: - snapshot 数量是否正常增长 - 各客户端的备份目录大小变化 - 最新 snapshot 时间戳(确认 cron 跑了) 告警:snapshot 超过 26 小时没新 = 备份链断了。 ## retention 策略:sanoid 风格 手写 `tail -n +31` 简单但只按计数。sanoid 风格按时间精细: ```bash # 保留: # - 最近 7 个 hourly # - 最近 30 个 daily # - 最近 12 个 monthly snapshots=$(ssh nas "sudo zfs list -H -o name -t snapshot backuptank/clients/foo") # 分组按时间删 # (实际写起来复杂;建议用 sanoid 现成工具) ``` sanoid 配 retention policy + 跨多 dataset 统一管。装好后写一个 `sanoid.conf` 完事。 ## 实战效果 我们 20 台业务机每天备份到一台 NAS: | | rsync only | rsync + ZFS snapshot | |---|---|---| | 单次备份耗时 | 30 min | 30 min | | 一致性 | 半天的飘移 | 时刻精确 | | 历史版本 | 没有 | 30 天日级 | | 占用空间 | 增量 | 增量 + dedup 后接近 | | 还原历史文件 | 困难 | `cp /.zfs/...` | ZFS snapshot 几乎免费给增量备份"加时间维度"。 ## 与商业方案对比 | | rsync + ZFS | Duplicati | restic to S3 | Veeam | |---|---|---|---|---| | 价格 | 免费 | 免费 | 免费 + 存储费 | 商业 | | 客户端加密 | 否(依靠传输 ssh) | ✅ | ✅ | ✅ | | 时间点恢复 | ✅(snapshot) | ✅ | ✅ | ✅ | | Web UI | 无 | 有 | 第三方 | ✅ | | 学习曲线 | 中 | 低 | 低 | 中 | 家用 / 小公司 + 有 ZFS NAS → 这套最便宜 + 最快。 ## 踩过的坑 1. **`sudo zfs snapshot` 没权限**:rsync 用户没 zfs allow。 `sudo zfs allow rsync-user create,destroy,snapshot ...`。 2. **snapshot 名字有冲突**:同一秒两次备份生成同名 snapshot 失败。 用毫秒级或随机 suffix。 3. **`--delete` 删了重要文件**:业务机误删 → rsync 同步删 → 但 ZFS snapshot 保住前一刻状态。**snapshot 是真正救命**。 4. **未控制保留 → snapshot 爆**:1 年 = 365 个 daily snapshot, 每 snapshot 元数据几 MB → 几 GB 元数据。 sanoid 策略限制数量。 5. **NAS 空间满 → 客户端 rsync 失败**:监控 ZFS pool 利用率, 超过 80% 告警。
Restic 是单文件 Go 二进制,做客户端加密 + 内容寻址 dedup + 增量备份。 对比 rsync 优势: - 加密在客户端完成,存储端只能看到加密块 - 强 dedup:跨快照 / 跨主机 / 跨文件位置都生效 - 任何 S3 兼容存储(AWS S3 / Backblaze B2 / Wasabi / minio / Cloudflare R2)都支持 - 单二进制零运行时依赖 ## 安装 ```bash # Debian / Ubuntu 自带的版本一般偏旧,建议直接装官方二进制 RESTIC_VER=0.17.3 ARCH=$(dpkg --print-architecture) # amd64 / arm64 curl -fsSL "https://github.com/restic/restic/releases/download/v${RESTIC_VER}/restic_${RESTIC_VER}_linux_${ARCH}.bz2" \ | bunzip2 > /usr/local/bin/restic sudo chmod +x /usr/local/bin/restic restic version ``` ## 初始化仓库(一次) 以 Backblaze B2 为例(最便宜的 S3-API 兼容方案之一): ```bash export B2_ACCOUNT_ID=... export B2_ACCOUNT_KEY=... export RESTIC_REPOSITORY=b2:my-bucket:/host-foo export RESTIC_PASSWORD=$(openssl rand -base64 32 | tr -d '\n') echo "$RESTIC_PASSWORD" | sudo tee /etc/restic.pw && sudo chmod 600 /etc/restic.pw restic init ``` **`RESTIC_PASSWORD` 丢了仓库就回不来了**,没有任何官方 recover —— 一定要 把它存到一个独立的 password manager / 离线纸质备份。 ## 备份脚本 ```bash #!/usr/bin/env bash # /usr/local/sbin/restic-backup.sh set -euo pipefail export B2_ACCOUNT_ID=... export B2_ACCOUNT_KEY=... export RESTIC_REPOSITORY=b2:my-bucket:/host-$(hostname -s) export RESTIC_PASSWORD_FILE=/etc/restic.pw restic backup \ --tag daily \ --exclude-file /etc/restic.exclude \ /etc /srv /home/yourname/projects # 保留策略:每日 7、周 4、月 12、年 5 restic forget --prune \ --keep-daily 7 --keep-weekly 4 \ --keep-monthly 12 --keep-yearly 5 # 完整性校验(采样 10%,全量太慢) restic check --read-data-subset=10% ``` `/etc/restic.exclude` 例: ``` node_modules __pycache__ *.pyc .cache .venv venv target build dist ``` ## systemd timer ```ini # /etc/systemd/system/restic-backup.timer [Timer] OnCalendar=*-*-* 02:30:00 RandomizedDelaySec=1h Persistent=true [Install] WantedBy=timers.target ``` ## 还原演练(重要——做一次) ```bash # 列出所有快照 restic snapshots # 列某个快照的文件 restic ls latest /etc # 还原某文件到临时目录 restic restore latest --include /etc/nginx --target /tmp/restore-test # 直接挂载为只读 FUSE mkdir /tmp/rmount restic mount /tmp/rmount & ls /tmp/rmount/snapshots/ fusermount -u /tmp/rmount ``` **至少每季度做一次完整还原演练**到一台不同的机器,确认凭据和流程都对。 未经演练的备份等于没有备份。 ## 监控 加在脚本末尾: ```bash # 通知 healthchecks.io 跑完了 curl -fsSL --retry 3 "https://hc-ping.com/<uuid>" # 或者推 Prometheus pushgateway cat <<EOF | curl --data-binary @- http://pushgw:9091/metrics/job/restic/host/$(hostname) restic_backup_last_success_time $(date +%s) restic_backup_last_size_bytes $(restic stats latest --mode raw-data --json | jq .total_size) EOF ``` ## 踩过的坑 - 第一次备份会很慢(全量上传),后续增量秒级;不要担心。 - `restic prune` 在大仓库(TB 级)上非常慢且 I/O 密集;改成 `restic forget --keep-* --prune-max-unused 5%` 限制每次 prune 范围。 - 仓库锁残留:客户端被 kill 后会留 `lock-*` 文件,下次报"repo is locked"。 确认没有其他进程后 `restic unlock`。 - B2 / R2 的 API 调用收费在小文件场景容易超出预期,配 `restic backup --pack-size 32` 增加 pack 大小(默认 16 MB)能减少调用次数。
## 起因 家用 NAS 16GB RAM,跑 ZFS + 几个 Docker 容器(Plex / Photoview / Syncthing)。`free -h` 显示 `used 15.2G`,容器频繁因 OOM 被重启。 但 `htop` 加起来所有进程才用 5GB。剩下 10GB 是谁吃的? 答案:ZFS ARC(Adaptive Replacement Cache,磁盘 cache)。 ZFS 默认抢半数物理内存做 cache,且不像 page cache 那样有 application 请求时立刻让出来。 ## 诊断 ### 看 ARC 实际占用 ```bash arc_summary | head -30 # 或: cat /proc/spl/kstat/zfs/arcstats | grep -E '^(c|c_max|c_min|size|hits|misses)' ``` 输出例: ``` size = 9.8 GB c = 10.4 GB c_max = 10.4 GB # arc 上限(默认 RAM 一半) c_min = 660 MB hits = 24351234 misses = 234567 # hit rate ~ 99% ``` `size` 是当前 ARC 占用。`c_max` 是上限——ZFS 不会让 ARC 超过这个值, 但会贪婪地用到这个值。 ### 看分类 ```bash arc_summary | grep -A 20 'ARC size' # ARC size (current): # MFU: 6.2 GB # MRU: 3.5 GB # Anon: 50 MB ``` MFU = most frequently used;MRU = most recently used。 ### `free` 看 ARC 在哪一栏 ```bash free -h # total used free shared buff/cache available # Mem: 15Gi 5.0Gi 0.2Gi 320Mi 9.8Gi 4.5Gi ``` ARC 主要算在 `used` 而非 `buff/cache`(与一般 page cache 不同)。 `available` 列才反映"应用真正可拿到多少内存"——这个 4.5GB 还行, 但 ZFS 释放 ARC 给 application 不是瞬间的,高内存压力时仍可能 OOM。 ## 解决方案 ### 1. 调小 ARC 上限 临时(重启失效): ```bash # 限到 4GB echo 4294967296 | sudo tee /sys/module/zfs/parameters/zfs_arc_max ``` 持久(modprobe 配置 + 重新生成 initramfs): ```bash echo 'options zfs zfs_arc_max=4294967296' | sudo tee /etc/modprobe.d/zfs.conf echo 'options zfs zfs_arc_min=1073741824' | sudo tee -a /etc/modprobe.d/zfs.conf sudo update-initramfs -u sudo reboot ``` `zfs_arc_max` 单位 byte。4GB = `4 * 1024^3 = 4294967296`。 ### 2. 设 ARC 收缩门槛(让 ARC 更快让位) ```bash # 系统 free memory < 256MB 时立刻收缩 ARC echo 268435456 | sudo tee /sys/module/zfs/parameters/zfs_arc_sys_free ``` 默认 ZFS 收缩很慢,对突发负载不友好。这条让 ARC 在内存紧张时更积极 释放。 ### 3. 实际数据评估"够不够" ```bash arc_summary | grep -A 5 'Cache hits' # Cache hits: # Total hits: 24.3M # Cache miss ratio: 1.0% ``` > 99% hit rate 说明现有 ARC 大小 OK,缩小一点不会明显影响性能。 < 90% 说明 ARC 缺,缩小会让磁盘 IO 增加。 ### 4. 应用专门的 prefetch 调优 ```bash # 预读(顺序读多 sample) echo 0 | sudo tee /sys/module/zfs/parameters/zfs_prefetch_disable # = 0 启用 prefetch;某些场景关掉省内存 # L2ARC:用 SSD 作二级缓存(家用通常没必要) sudo zpool add tank cache /dev/nvme0n1p3 ``` L2ARC 把"放不下 RAM 的 ARC 内容"扩展到 SSD。代价是 RAM 里要存 L2ARC 的元数据(约每 1GB L2ARC 占 25MB RAM)。 ### 5. 给容器 / 重要服务设 cgroup 内存保证 ```ini # /etc/systemd/system/docker.service.d/memory.conf [Service] MemoryHigh=10G MemoryLow=2G # 内存紧张时优先保住 docker 这 2GB ``` `MemoryLow` 是"低水位保护"——内核回收内存时不会减这个 cgroup 低于 2GB(除非全系统真没内存)。 ```bash sudo systemctl daemon-reload sudo systemctl restart docker ``` ## 效果 我家 NAS 调整后: - 设 `zfs_arc_max=4G`,ARC 从 10GB 缩到 4GB - 释放 6GB 给 Docker 容器,OOM 消失 - ZFS cache hit rate 从 99.2% → 96.5%(轻微下降) - 磁盘 IO 从 5 MB/s 平均 → 12 MB/s(更频繁回原盘读) - 但容器响应延迟稳定,体验明显改善 **结论**:家用 NAS 16GB / 32GB 内存,ARC 给 1/4 物理内存合适。 ZFS 默认 1/2 是为"纯 NAS"设计的,混合负载机器要调。 ## 何时不调 ARC 服务器是 dedicated ZFS file server(不跑别的)→ ARC 越大越好, hit rate 决定性能。 服务器跑 PostgreSQL / 任何强 I/O DB → DB 自己也想要 RAM 做 buffer pool。 两者抢内存。建议给 DB 60% RAM、ARC 30% RAM,剩 10% OS。 ## 看 cache 效果 ```bash # arcstat 实时 sudo apt install zfsutils-linux arcstat 5 # time read miss miss% dmis dm% pmis pm% mmis mm% arcsz c # 10:00 1.5K 2 0 2 0 0 0 1 0 4.0G 4.0G # ... # 第一列 read 是每秒读次数;miss% 越低越好 ``` ## 踩过的坑 1. **改 zfs.conf 后没 update-initramfs**:ZFS 模块在 initramfs 里 被加载,外面 conf 不生效。`update-initramfs -u` + reboot。 2. **`free` 误判**:很多人看到 ARC 占内存以为是 leak,去 kill 进程 也没用。`arc_summary` 才说清楚。 3. **L2ARC 放 HDD 上**:完全没意义,L2 比主磁盘还慢。L2ARC 必须 SSD 或 NVMe。 4. **ZIL(SLOG)vs L2ARC 混淆**:ZIL 是同步写日志加速(SSD 推荐), L2ARC 是读缓存扩展。不一样。 5. **swap 不要放 zvol 上**:ZFS 的写时复制 + 内存压力时需要 swap → 死锁。如果用 ZFS root,swap 单独建普通分区。
## 起因 新装 NAS 4 块 4TB 硬盘。选 file system 时纠结 ZFS vs btrfs。 都支持:snapshot / compression / dedup / RAID / checksum。 但底层设计 + 成熟度 + 稳定性差异不小。下面是我做完功课的对比。 ## 共同特性 - 文件系统级 checksum(侦测 bit rot) - transparent compression(zstd / lz4 / gzip) - snapshot + send/receive 增量 - pool / volume / dataset 抽象 - 多盘 RAID-like 配置 - copy-on-write 设计 ## 主要差异 ### 1. RAID **ZFS**: - mirror / raidz (raid5) / raidz2 (raid6) / raidz3 - pool 一旦创建后**不能扩容 vdev 内的盘**(v2024 改了,但有限制) - 加更多盘要新 vdev(stripe over vdev) **btrfs**: - single / dup / raid0 / raid1 / raid10 / raid5 / raid6 - 极灵活:随时加盘 / 移盘 / 改 RAID profile(balance 操作) - **raid5/6 仍然 NOT production ready**(这是 btrfs 最大坑) 家用 NAS 常见配置: - 4 块盘 + ZFS RAID-Z2:可挂 2 块(最佳容错) - 4 块盘 + btrfs RAID1:只挂 1 块;btrfs RAID1 是"每文件 2 副本" 而非"两组镜像",新颖但适配差 ### 2. 稳定性 / 数据安全 **ZFS**: - 2005 起 production,全球大企业用,**经受 20 年实战** - 数据完整性是设计第一原则 - bug 极少(且大多在 cutting-edge 功能) **btrfs**: - 2007 起,主线内核 2009+ - 单盘 / RAID0/1/10 稳,**RAID5/6 多年文档警告 not production** - 2024 ext4 maintainer 写过 "I still don't recommend btrfs raid5/6" - 多盘环境历史 bug 多于 ZFS 数据安全敏感 → ZFS。 ### 3. 内存需求 **ZFS**: - ARC (Adaptive Replacement Cache) 吃内存:1GB / 1TB rule of thumb - 16GB NAS + 16TB 数据:ARC 占 10GB + - 启用 dedup 更吃(每 1TB ~5GB RAM) - 可调整 `zfs_arc_max` **btrfs**: - 内存占用低很多 - 4GB RAM 也能稳定跑 8TB btrfs 老 NAS / 低 RAM 设备 → btrfs。 ### 4. 性能 随机读写:ZFS(with ARC)通常更快。 顺序读写:相近。 压缩:ZFS lz4 / zstd 性能略好。 但实际差距不大(除非 RAM 极充裕时 ZFS ARC 效果显著)。 ### 5. snapshot + send/receive 两者都有,用法相似: ```bash # ZFS zfs snapshot tank/data@daily-20240524 zfs send tank/data@yesterday tank/data@daily-20240524 | ssh remote 'zfs receive ...' # btrfs btrfs subvolume snapshot /mnt/data /mnt/data/.snapshots/daily-20240524 -r btrfs send -p /mnt/data/.snapshots/yesterday /mnt/data/.snapshots/daily-20240524 | ssh remote 'btrfs receive ...' ``` ZFS send 更成熟稳定。btrfs send/receive 有过历史 bug。 ### 6. 加密 **ZFS** 2.0+:内置 native encryption **btrfs**:依赖 LUKS 下层加密(不是 fs 级 native) ZFS 加密更灵活(per-dataset key)。 ### 7. 文件系统级 quota **ZFS**:quota 是基础特性,per-dataset / per-user **btrfs**:qgroup 复杂 + 历史 bug + 性能影响 需要 quota → ZFS。 ### 8. Linux kernel 集成 **ZFS**:不在 mainline kernel(CDDL vs GPL license 冲突)。 - 装 OpenZFS 模块(apt install zfsutils-linux) - 每次 kernel 升级需要 DKMS 重 build - Ubuntu 官方支持;其它 distro 看情况 **btrfs**:mainline kernel 内置,开箱即用 部署简单 → btrfs。 但 ZFS 装好后稳定,DKMS 自动重 build,不算大坑。 ### 9. 工具生态 | 任务 | ZFS | btrfs | |---|---|---| | GUI | Cockpit ZFS / TrueNAS | Cockpit btrfs / Snapper | | 备份 sync | syncoid + sanoid(极成熟) | btrbk | | 自动 snapshot | sanoid | snapper / btrbk | | pool 监控 | zpool status + zed | btrfs scrub status | ZFS 工具链更成熟(成熟得益于 Solaris 时代积累)。 ### 10. 修复 / 恢复 **ZFS**: - `zpool scrub` 自动修复有 redundancy 的损坏 - 严重时 `zpool import -F` 强制恢复 - 数据恢复工具:zdb(专家用) **btrfs**: - `btrfs scrub` 类似 - 严重时 `btrfs restore` 拉文件出来 - 但 RAID5/6 + 多盘损坏场景历史上有人 lose 数据 ZFS recover 工具更可靠。 ## 选择决策 | 场景 | 推荐 | |---|---| | 4+ 盘 + RAID5/6 + 关键数据 | **ZFS** (RAID-Z2) | | 2 盘 mirror | 都行(btrfs 略简单) | | 单盘大容量 | btrfs(CoW + snapshot) | | 内存紧张(< 4GB) | btrfs | | 内存充裕(> 16GB) | ZFS | | 笔记本 + Fedora | btrfs(mainline) | | 严格 quota / multi-tenant | ZFS | | 想随时改 RAID profile | btrfs | | 远程异地复制 | ZFS(syncoid 成熟) | ## 我的实际选择 家用 NAS(4 × 4TB):**ZFS RAID-Z2 + zstd 压缩** - 容忍 2 盘挂 - 16GB RAM 给 ARC 用 - 用 sanoid + syncoid 自动 snapshot + 远程异地 笔记本 Linux 根分区:**btrfs** - mainline,无 DKMS 折腾 - snapshot 频繁,Fedora 系自动 snapper ## 安装 ### ZFS on Ubuntu 22.04+ ```bash sudo apt install -y zfsutils-linux # 4 盘 RAID-Z2 + 4K aligned + lz4 默认 sudo zpool create -o ashift=12 \ -O compression=lz4 -O atime=off -O xattr=sa -O dnodesize=auto \ tank raidz2 \ /dev/disk/by-id/scsi-... \ /dev/disk/by-id/scsi-... \ /dev/disk/by-id/scsi-... \ /dev/disk/by-id/scsi-... ``` ### btrfs on Ubuntu / Fedora ```bash sudo apt install -y btrfs-progs # 4 盘 RAID10 sudo mkfs.btrfs -L data -m raid10 -d raid10 /dev/sdb /dev/sdc /dev/sdd /dev/sde sudo mkdir /mnt/data sudo mount -o compress=zstd:3,space_cache=v2 /dev/sdb /mnt/data ``` ## 常见操作对照 ```bash # 看状态 zpool status -v # ZFS btrfs filesystem usage /mnt/data # btrfs # 校验所有数据 zpool scrub tank btrfs scrub start /mnt/data # 加盘扩容 zpool add tank /dev/new-disk # 新 vdev (stripe) btrfs device add /dev/new-disk /mnt/data && btrfs balance start /mnt/data # snapshot zfs snapshot tank/data@snap btrfs subvolume snapshot /mnt/data /mnt/data/.snap/snap # 删除 snapshot zfs destroy tank/data@snap btrfs subvolume delete /mnt/data/.snap/snap # 看压缩比 zfs get compressratio tank compsize /mnt/data ``` ## 踩过的坑 ### ZFS 1. **ARC 吃光内存** → 容器 OOM。`zfs_arc_max` 调小。 2. **DKMS 升级 kernel 慢** → reboot 后 zpool 找不到。等 DKMS build 完 再 reboot。 3. **pool import 失败** → 用 disk by-id 不要 /dev/sdX(顺序会变)。 4. **dedup 慎用** → 5GB RAM / 1TB 数据,开了多数情况后悔。 ### btrfs 1. **RAID5/6 不要上生产**——文档明确警告。 2. **subvolume 嵌套乱** → snapshot 时易踩坑。清晰目录结构。 3. **balance 时机器抖** → 大量 IO 影响业务。低峰跑 + 限速。 4. **qgroup 启用后慢 5-10 倍** → 不需要 quota 就别开。 ## 多年使用感受 我个人 5+ 年家用 NAS 跑 ZFS: - 多次硬盘故障,RAID-Z2 透明处理 + 在线替换 - 月度 scrub 偶尔修复 bit rot(每次几 KB;硬盘老化的标志) - snapshot 救命过若干次:误删文件 / VM 改坏直接回滚 btrfs 笔记本 3 年: - snapshot 救过几次升级失败 - 偶尔 scrub 报错(无 redundancy 时无法 fix),重要数据靠云备份 - 性能 / 稳定够日常用 两个都好。选符合你 use case 的不犯错。
时间不准的服务器会引发各种诡异 bug: - TLS 证书"还没生效"或"已过期" - Kerberos 认证失败 - 分布式日志时间错乱,没法 trace - 数据库主从复制时间戳混乱 `systemd-timesyncd` 是 Ubuntu 默认的 SNTP 客户端,能用但功能很基础 (只读 NTP,不会被别人查询,调整精度一般)。生产用 chrony, 小内存占用 + 快收敛 + 网络中断后的快速恢复。 ## 安装 + 切换 ```bash sudo systemctl disable --now systemd-timesyncd sudo apt install -y chrony sudo systemctl enable --now chrony # 校验当前用的是哪个 timedatectl # System clock synchronized: yes # NTP service: active ``` ## 配置 `/etc/chrony/chrony.conf` 主流 distro 自带的默认就能跑。 生产建议改: ```conf # 优先用 pool 而不是 server —— 自动负载均衡到多台 pool 2.debian.pool.ntp.org iburst maxsources 4 # 国内服务器换国内源 # pool cn.pool.ntp.org iburst maxsources 4 # server ntp.aliyun.com iburst # server ntp.tencent.com iburst # 启动后的前 10 个样本只要测量到偏差就立即跳调 makestep 1.0 10 # 系统时钟同步到 RTC 硬件时钟(关机后保留) rtcsync # 允许哪些客户端查询(如果本机也是 NTP server) # allow 192.168.0.0/16 # 默认拒绝,注释掉它就 client-only # 数据存储 driftfile /var/lib/chrony/chrony.drift makestep 1 3 keyfile /etc/chrony/chrony.keys ntsdumpdir /var/lib/chrony # 日志 logdir /var/log/chrony log measurements statistics tracking ``` `iburst` 是关键:启动时连发 8 个查询,秒级完成初始同步(默认每 64 秒一次 会慢得让人无奈)。 ## 校验 ```bash # 当前选用的服务器 + 偏差 chronyc tracking # Reference ID : C0A87B0F (ntp1.aliyun.com) # Stratum : 3 # Ref time (UTC) : Sat May 23 09:00:12 2026 # System time : 0.000001234 seconds slow of NTP time # Last offset : +0.000045678 seconds # RMS offset : 0.000123456 seconds # Frequency : 12.345 ppm slow # ... # 看所有 source 状态 chronyc sources # MS Name/IP address Stratum Poll Reach LastRx Last sample # =============================================================== # ^* 100.100.5.1 2 7 377 45 +12us[+15us] +/- 1567us # ^* 是当前在用;^+ 是候选;^? 是不可达;^x 不一致;^- 被算法排除 # 各 source 的详细测量 chronyc sourcestats ``` `System time` 在毫秒级即合格;几十微秒到几百微秒是 Internet NTP 的正常范围。 ## NTS(NTP over TLS) 新硬件 / 新版 chrony 支持 NTS,给 NTP 流量加密 + 鉴权(防中间人篡改时间): ```conf server time.cloudflare.com iburst nts server nts.netnod.se iburst nts ``` 需要 `nts` 关键字 + chrony >= 4.0 + 客户端能解析 nts 服务器的证书链。 ## 给本机当 NTP server ```conf # /etc/chrony/chrony.conf allow 192.168.0.0/16 # 内网客户端 allow 10.0.0.0/8 # 监听 IPv4 / IPv6(默认开) # bindaddress 0.0.0.0 ``` ```bash sudo systemctl restart chrony sudo ufw allow 123/udp comment 'NTP' ``` 客户端 chrony.conf 写 `server <你这台>.example.com iburst`。 ## 强制立即同步 ```bash sudo chronyc -a 'burst 4/4' sudo chronyc -a makestep # 之前都不行的话直接: sudo chronyc -a 'manual on' && sudo chronyc settime ... ``` ## 监控 ```bash # Prometheus node_exporter 自带 chrony collector # node_chrony_system_time_offset_seconds 是要报警的指标 # 阈值 > 0.01 秒就告警 # 或直接定时脚本检查偏移 chronyc tracking | awk '/System time/ {print $4}' | xargs -I {} \ python3 -c "import sys; v=abs(float('{}')); sys.exit(0 if v<0.01 else 1)" ``` ## 踩过的坑 - 在云上(AWS / GCP / Azure)建议用 **云供应商提供的内网 NTP** 而不是公网 pool:低延迟 + 不出 VPC + 通常更稳定(AWS: `169.254.169.123`)。 - 容器里运行的程序看到的时间是宿主的;容器里跑 chrony 多此一举,反而可能 和宿主冲突。 - 虚拟机长时间挂起后 wakeup,时间漂移可能很大,触发 `makestep` 跳变。 如果业务对时间不能跳(金融 / log 单调),考虑把 makestep 关掉接受 slewing(缓慢调整)。 - 同步上游用 IP 而不用域名时,AWS / 云上 169.254.169.123 这类 link-local 地址不要经过 DNS(永远查不到)。
ZFS 的两个杀手特性: 1. **快照即时 + 几乎零成本**:copy-on-write,秒级、不占额外空间 2. **zfs send / receive**:增量传输到另一台机器,做异地备份 下面在 Ubuntu 上装 ZFS、建池、配快照轮转、做远程增量备份。 ## 1. 装 ZFS(Ubuntu 已带) ```bash sudo apt install -y zfsutils-linux zfs version ``` CentOS / RHEL:用 `zfs-fuse` 不行,要装 native(zfsonlinux 仓库)。 ## 2. 建池 假设有两块裸盘 `/dev/sdb` `/dev/sdc`: ```bash # 镜像(mirror = RAID1) sudo zpool create tank mirror /dev/sdb /dev/sdc # 单盘 # sudo zpool create tank /dev/sdb # RAIDZ1 (RAID5 类似,需 ≥ 3 盘) # sudo zpool create tank raidz /dev/sdb /dev/sdc /dev/sdd sudo zpool status tank sudo zpool list sudo zfs list ``` `tank` 是池名。建议用块设备的 `/dev/disk/by-id/...` 而不是 `/dev/sdb`(重启后顺序变)。 ## 3. 创建文件系统(dataset) ```bash sudo zfs create tank/data sudo zfs create tank/data/users sudo zfs create tank/data/projects sudo zfs list # tank/data 96K 3.5T ... # tank/data/users 96K 3.5T ... # tank/data/projects 96K 3.5T ... ``` dataset 像目录但每个都是独立挂载 + 独立 properties。 ## 4. 重要 properties ```bash sudo zfs set compression=lz4 tank # 透明压缩,几乎免费 sudo zfs set atime=off tank # 关 atime,性能 + sudo zfs set xattr=sa tank # 高效 xattr 存储 sudo zfs set recordsize=1M tank/data # 大文件场景调大 sudo zfs set quota=500G tank/data/users # 限制使用空间 sudo zfs set reservation=100G tank/data/projects # 保留 100G ``` `compression=lz4` 是 ZFS 最值得开的:CPU 开销几乎为零,能压缩出 30-50% 的额外空间。 ## 5. 快照 ```bash sudo zfs snapshot tank/data@$(date +%F-%H%M%S) sudo zfs list -t snapshot # tank/data@2026-05-23-090000 0B ... ``` `@` 后面是快照名。 恢复某个文件: ```bash ls /tank/data/.zfs/snapshot/2026-05-23-090000/ # 像普通目录,cp 出来即可 ``` 整个 dataset 回滚到某快照: ```bash sudo zfs rollback tank/data@2026-05-23-090000 # 注意:会丢掉快照之后的所有改动! ``` 删快照: ```bash sudo zfs destroy tank/data@2026-05-23-090000 ``` ## 6. 自动快照轮转:zfs-auto-snapshot ```bash sudo apt install -y zfs-auto-snapshot ``` 自动每 15 分钟 / 小时 / 天 / 周 / 月做快照并轮转: ```bash ls /etc/cron.*/zfs-auto-snapshot # /etc/cron.d/zfs-auto-snapshot (15min) # /etc/cron.hourly/zfs-auto-snapshot # /etc/cron.daily/... # ... # 给特定 dataset 关掉某频率 sudo zfs set com.sun:auto-snapshot:frequent=false tank/data/projects sudo zfs set com.sun:auto-snapshot:hourly=true tank/data/projects ``` 默认保留:96 frequent / 24 hourly / 31 daily / 8 weekly / 12 monthly。 ## 7. zfs send:远程增量备份 第一次全量: ```bash # 源机 sudo zfs snapshot tank/data@base sudo zfs send tank/data@base | ssh backup@remote 'zfs receive -F tank-backup/data' # 远端 zfs list # tank-backup/data ``` 之后增量: ```bash # 源机:在前一个快照基础上做新快照 sudo zfs snapshot tank/data@2026-05-23 # 增量 send:只传 base → 2026-05-23 的差异 sudo zfs send -i tank/data@base tank/data@2026-05-23 \ | ssh backup@remote 'zfs receive tank-backup/data' # 完成后 base 可以删掉(远端也跟着删),用新快照做下一次的 base ``` 实际生产用 [syncoid](https://github.com/jimsalterjrs/sanoid) 或 [zrepl](https://zrepl.github.io/) 包装,自动管 base 和 retention: ```bash sudo apt install -y sanoid # /etc/sanoid/sanoid.conf 配置 dataset + retention # /etc/sanoid/syncoid.conf 配置 replication syncoid tank/data backup@remote:tank-backup/data ``` ## 8. 加密 ```bash # 建 dataset 时启用加密 sudo zfs create -o encryption=on -o keyformat=passphrase \ tank/data/sensitive # 之后挂载要解锁 sudo zfs load-key tank/data/sensitive # 输入密码 sudo zfs mount tank/data/sensitive ``` zfs send / receive 加 `-w` 直接发加密 stream(远端不需要密码, 压缩 + dedup 在加密状态下做)。 ## 9. 校验 + scrub ```bash sudo zpool scrub tank sudo zpool status tank # scan: scrub in progress since ... # 100M scanned out of 50G at 200M/s ``` scrub 读全部数据 + 校验 checksum,发现损坏自动从镜像 / parity 修复。 推荐每月跑一次: ```cron 0 3 1 * * /sbin/zpool scrub tank ``` ## 10. 容量监控 ```bash sudo zpool list -v tank # NAME SIZE ALLOC FREE ... # tank 4T 1.2T 2.8T ... # mirror 4T 1.2T 2.8T # sdb - - - # sdc - - - sudo zfs list -o name,used,avail,refer,mountpoint ``` ZFS 超过 80% 使用率性能急剧下降;保持在 80% 以下。 ## 踩过的坑 - 用 `/dev/sdb` 直接建池,机器重启 sdb 变成 sdc → 池找不到。永远用 `/dev/disk/by-id/` 路径。 - ZFS 内存吃得多:1GB / 1TB 数据是经验值。8GB 机器跑 32TB 池可能要 调 `zfs_arc_max` 限制 ARC 缓存。 - `zfs destroy` 没确认就执行,整个 dataset + 所有快照消失。养成 `-n` 模拟先看的习惯。 - snapshot 是 read-only,但占空间 = 这个快照后所有被修改的数据。 长期保留快照 + 高频改动 = 池快速胀满。retention 不要太长。
应用 CPU 跑满,不知道卡在哪个函数?`perf` 是 Linux 性能分析的"瑞士军刀": 低开销采样,看到 CPU 时间花在每个函数 / 每行指令上。 配合 FlameGraph 出火焰图,一眼看清谁吃 CPU。 ## 1. 装 perf ```bash sudo apt install -y linux-tools-common linux-tools-$(uname -r) sudo perf --version ``` CentOS / RHEL:`sudo yum install perf`。 ## 2. 系统级采样 ```bash # 采样全系统 10 秒 sudo perf record -F 99 -a -g -- sleep 10 # -F 99: 每秒 99 次采样(避免和定时任务整数倍重合) # -a: 全系统 # -g: 抓 call graph ls perf.data # 几 MB sudo perf report ``` `perf report` TUI 里: - `+/-` 展开 / 折叠调用栈 - `a` 显示汇编 - `/` 搜函数名 按 CPU 百分比从高到低排: ``` 17.32% nginx libc-2.31.so [.] __memcpy_avx_unaligned 12.45% nginx nginx [.] ngx_http_parse_header_line 8.21% nginx libssl.so.3 [.] ssl3_read_bytes ... ``` ## 3. 单进程采样 ```bash sudo perf record -F 99 -p <PID> -g -- sleep 30 sudo perf report ``` `sleep 30` 是采样时长。`-p <PID>` 限定单进程。 ## 4. 火焰图(一图胜千言) ```bash # 装 FlameGraph 脚本 git clone https://github.com/brendangregg/FlameGraph ~/FlameGraph # 采样 sudo perf record -F 99 -p <PID> -g -- sleep 30 sudo perf script > /tmp/out.perf # 生成 SVG ~/FlameGraph/stackcollapse-perf.pl /tmp/out.perf > /tmp/out.folded ~/FlameGraph/flamegraph.pl /tmp/out.folded > /tmp/flame.svg # 浏览器打开 /tmp/flame.svg xdg-open /tmp/flame.svg ``` 火焰图怎么读: - **X 轴**:CPU 占用(宽度 = 占用比例,与时间无关) - **Y 轴**:调用栈深度(栈顶在上) - **宽块**:耗 CPU 的函数(从顶往下看,找最宽的就是热点) ## 5. 看具体函数的汇编 / 源码 ```bash sudo perf report --stdio # 文本输出 sudo perf annotate function_name # 看汇编 + 哪一行最热 ``` 需要程序带 debug symbols(`-g` 编译,或装 `*-dbg` 包): ```bash sudo apt install -y nginx-dbg postgresql-16-dbgsym ``` ## 6. 上下文切换 / 系统调用 / 缓存命中 ```bash # 看进程的上下文切换 / 缺页 / cache miss sudo perf stat -p <PID> -- sleep 10 # Performance counter stats for process id 12345: # 3,532.18 msec task-clock (10 cpus utilized) # 48,927 context-switches # 13,200 cpu-migrations # 1,234 page-faults # 9,876,543,210 cycles # 4,321,098,765 instructions (0.44 insn per cycle) # 789,012,345 branches # 34,567,890 branch-misses (4.38% of all branches) # 看具体的硬件事件 sudo perf stat -e cache-misses,cache-references,L1-dcache-load-misses -p <PID> -- sleep 10 ``` `branch-misses` 高(> 5%)通常意味着分支预测不友好的代码。 `instructions per cycle` < 1 是性能差的信号。 ## 7. live 模式(top 风格) ```bash sudo perf top -p <PID> # 实时看哪些函数当前最热 ``` 排查"刚才一瞬间 CPU 高了"很有用。 ## 8. trace syscall(替代 strace 的高性能版) ```bash sudo perf trace -p <PID> # 实时显示进程的所有 syscall(比 strace 开销小 10x) sudo perf trace -s -p <PID> -- sleep 10 # 统计 10 秒内的 syscall 次数 + 耗时 ``` ## 9. Python / Node / Java 特殊处理 perf 默认看不到解释器 / VM 里的函数名,需要特殊 hook: ```bash # Python: 用 py-spy 替代 perf pip install py-spy sudo py-spy record -o profile.svg --pid <PID> # Node.js: 用 0x(npm install -g 0x)或 node --perf-basic-prof node --perf-basic-prof app.js # 然后 perf record 会读 /tmp/perf-<PID>.map 解析 JS 函数名 # Java: 用 async-profiler java -agentpath:/path/to/libasyncProfiler.so=start,event=cpu,file=profile.html ... ``` ## 10. 远程 / 容器内 ```bash # 容器内的进程:在 host 上跑 perf,看到的是 host 视角的 PID sudo perf record -F 99 -p $(pgrep -f my-container-process) -g -- sleep 10 # 如果容器内 / 进程内没 debug symbols,host 上的 perf 看到的也是 [unknown] # 解决:把容器内的 /usr/lib/debug 也挂到 host,或在容器里跑 perf ``` ## 11. 实战案例:Python web 应用慢 ```bash # 看哪个进程在烧 CPU top -H -p $(pgrep gunicorn | head -1) # 多个线程?PID 列里的 TID sudo py-spy top --pid <TID> # 实时看每个 Python 函数 CPU 占比 # 或者出火焰图 sudo py-spy record -o /tmp/flame.svg --pid <PID> --duration 30 ``` ## 12. 注意采样精度 `-F 99` 是每秒 99 次。意味着: - 持续 100ms 的函数:约 10 个样本,足够看见 - 持续 1ms 的函数:约 0.1 样本,看不到 短函数 + 高频调用看 `-F 999` 或 `-F 4999`(CPU 开销变大但更精细)。 ## 踩过的坑 - 没装 debug symbols:火焰图全是 `[unknown]` 一片黑。装 `*-dbg` / 重编译加 `-g`。 - 容器 + minimal image:image 里没 debug symbols,分析复合 image 把 dbg 包打进去。 - `perf record` 写盘飞快(GB 级 perf.data),磁盘满:缩短采样时间, 或者 perf script 后立刻删 perf.data。 - root 才能 perf:调 `sysctl kernel.perf_event_paranoid=-1` 让普通用户 可以采样(生产环境慎用,有信息泄露风险)。
## 起因 一台服务器同时跑应用 + cron 任务 + 数据库。某个 cron 任务(数据 处理脚本)偶尔吃 90% CPU + 8GB 内存,把主应用挤到 OOM kill。 "给 cron 任务限定最大资源"是基础隔离。 cgroup(control group)是 Linux 内核功能,systemd 在 v2 之后用得很直接。 ## 解决方案 ### 1. 系统当前 cgroup 状态 ```bash # 看 cgroup 树 systemd-cgls # 看每个 unit 的资源占用 systemd-cgtop # 当前 cgroup 版本(v2 推荐,现代 distro 默认) cat /sys/fs/cgroup/cgroup.controllers # memory cpu io pids ... ``` ### 2. 给单个 service 限资源 ```ini # /etc/systemd/system/data-pipeline.service [Service] ExecStart=/usr/local/bin/process-data.sh # CPU:最多用 2 个核(200% = 2 * 100%) CPUQuota=200% CPUWeight=50 # 相对优先级(默认 100,调低让出 CPU) # 内存:硬上限 4GB,超过 OOM kill 这个 service(不影响别的) MemoryMax=4G MemoryHigh=3.5G # 软上限,超过后系统 throttle 这个进程的分配 # IO:限速 50MB/s 读 + 50MB/s 写(针对某块磁盘) IOReadBandwidthMax=/dev/sda 50M IOWriteBandwidthMax=/dev/sda 50M # tasks(线程 / 进程数) TasksMax=64 ``` `sudo systemctl daemon-reload && sudo systemctl restart data-pipeline`。 之后这个 service 超过 4GB 内存就被 OOM killed(service 重启), 不会影响系统其它进程。 ### 3. 实时调整(不重启 service) ```bash sudo systemctl set-property data-pipeline.service \ MemoryMax=2G CPUQuota=150% # 校验 systemctl show data-pipeline.service -p MemoryMax,CPUQuota ``` `set-property` 立刻生效 + 写到 `/etc/systemd/system.control/`,重启 保留。 ### 4. 把多个 service 归一组:slice ```ini # /etc/systemd/system/background.slice [Unit] Description=Background batch jobs [Slice] CPUWeight=10 # 后台任务低优先级 CPUQuota=400% # 整组合计最多 4 核 MemoryMax=8G # 整组合计最多 8GB IOWeight=10 ``` ```ini # data-pipeline.service [Service] Slice=background.slice # 不需要再单独设 CPUQuota / MemoryMax,由 slice 限制 # log-cleaner.service [Service] Slice=background.slice # image-resizer.service [Service] Slice=background.slice ``` `background.slice` 整体限到 4 核 + 8GB,里面 3 个 service 自动竞争分配。 主应用(不在这个 slice 里)不受影响。 ### 5. 给用户 / 会话限资源 `/etc/systemd/system/user-1001.slice`(用户 ID 1001 的所有进程): ```ini [Slice] CPUQuota=200% MemoryMax=4G ``` 或编辑用户的: ```bash sudo systemctl edit user-1001.slice ``` 防"某个用户跑了一个内存炸弹把整机器 OOM"。 ### 6. 临时跑一个命令带限制 ```bash systemd-run --scope --slice=background.slice -p MemoryMax=1G -p CPUQuota=50% \ ./bigjob.sh ``` 不需要建 service unit,临时挂在 background.slice 下跑。 ### 7. 看 cgroup 实际占用 ```bash # 某个 service 当前 RAM cat /sys/fs/cgroup/system.slice/data-pipeline.service/memory.current # 上限 cat /sys/fs/cgroup/system.slice/data-pipeline.service/memory.max # CPU 时间累计 cat /sys/fs/cgroup/system.slice/data-pipeline.service/cpu.stat ``` `systemctl status data-pipeline` 也会显示 Tasks / Memory / CPU。 或更直观: ```bash systemd-cgtop -d 1 # 实时刷新每 cgroup 资源占用 ``` ### 8. 监控 OOM kill ```bash # 查 dmesg 里的 oom_kill sudo dmesg | grep -i 'killed process' # journal 里看是哪个 cgroup 触发 OOM sudo journalctl -k --since '1 day ago' | grep oom ``` `MemoryMax` 触发的 OOM 只 kill 那个 cgroup 里的进程,不影响其它。 进程死了 systemd 按 `Restart=on-failure` 重启它(或留死状态等告警)。 ## 几个常用 hardening 选项 ```ini # 文件系统 ProtectSystem=strict # / 全只读 ProtectHome=true # /home 不可见 ReadWritePaths=/var/log/myapp # 例外可写 # 内核能力 NoNewPrivileges=true # exec 后不能升 cap PrivateTmp=true # /tmp 私有 PrivateDevices=true # /dev 极简 PrivateNetwork=true # 网络命名空间隔离(注意:不能访问外网) # user 隔离 User=trio Group=trio DynamicUser=true # 动态分配 UID(最安全,但要求 service 真不需要持久 user) # 资源 LimitNOFILE=65536 # 文件描述符 TasksMax=1024 ``` `systemd-analyze security <unit>` 给每个 service 评一个 0-10 分的 安全分,按提示加 hardening。 ## 效果 - 后台 batch job 不再挤死主应用 - 一个 cron 任务跑飞了只 OOM 自己 cgroup,systemd 重启它 - 多个低优先级服务自动让 CPU 给业务 - 资源审计:每个 service / slice 用了多少一目了然 ## 与 Docker / K8s 对比 容器(docker / k8s)其实就是 cgroup + namespace + 镜像分发。 单机服务用 systemd cgroup 已经够,不需要为了"资源隔离"就上 Docker。 ```bash # 等价物 docker run --cpus 2 --memory 4g ... # vs systemctl set-property myservice.service CPUQuota=200% MemoryMax=4G ``` ## 踩过的坑 1. **cgroup v1 vs v2**:老 distro / docker 默认 v1,systemd unified hierarchy 用 v2。混用导致某些 unit option 不生效。 `systemd.unified_cgroup_hierarchy=1` 内核参数强制 v2。 2. **MemoryMax 太严格**:服务正常工作时偶尔 spike → 频繁 OOM 重启 雪崩。给 50% buffer。 3. **PrivateNetwork=true 但服务要访问 DB**:隔离过头,service 起不来 连不上 DB。这个选项只给完全离线的工具用。 4. **CPUQuota=100%(一个核)但服务是多线程**:会被严格限速到 1 核 throughput,多线程优势没了。看实际负载决定。 5. **DynamicUser + 文件持久化**:DynamicUser 每次启动 UID 可能不同, 服务写到 `/var/lib/myservice/` 的文件下次 UID 变了读不了。 `StateDirectory=myservice` 让 systemd 管这个目录的权限。