知识广场

按学科筛选:计算机科学 / 操作系统
清除筛选

«计算机科学 / 操作系统» 分类下共 20 篇帖子

Borg + Borgmatic 做本地 + 异地双备份(一份配置两个仓库)

Borg 是 Python 写的去重备份工具,自带客户端加密、压缩、增量、仓库 deduplication。功能上和 Restic 相近,但生态早成熟两年,老牌系统管理 员居多。 Borgmatic 是 Borg 的 YAML wrapper,把 "备份 → 检查 → 清理 → 通知" 四件事写在一个配置文件里。 ## 安装 ```bash sudo apt install -y borgbackup borgmatic # 也可以 pip install --user borgmatic 拿更新版本 ``` ## 仓库初始化 我们做两份:本地 NAS + 异地(rsync.net / hetzner storage box / 自建机)。 两次 init: ```bash # 本地 NAS(NFS / SMB 挂载到 /mnt/nas) borg init -e repokey /mnt/nas/borg/host-foo # 异地,通过 ssh+borg serve borg init -e repokey \ ssh://[email protected]:23/./borg/host-foo ``` 每次 init 会要求设置 passphrase;同样这两个密码 **丢了就完了**。 ## Borgmatic 配置 `/etc/borgmatic/config.yaml`: ```yaml location: source_directories: - /etc - /srv - /home/yourname/projects exclude_patterns: - '*/node_modules' - '*/__pycache__' - '*/.venv' - '*/target' - '*/.cache' exclude_caches: true exclude_if_present: - .nobackup repositories: - path: /mnt/nas/borg/host-foo label: nas - path: ssh://[email protected]:23/./borg/host-foo label: offsite storage: encryption_passcommand: 'cat /etc/borgmatic/passphrase' compression: zstd,3 archive_name_format: '{hostname}-{now:%Y%m%d-%H%M%S}' borg_keep_exclude_tags: true retention: keep_daily: 7 keep_weekly: 4 keep_monthly: 12 keep_yearly: 5 consistency: checks: - name: repository frequency: 2 weeks - name: archives frequency: 1 month hooks: before_backup: - echo "starting backup at $(date)" after_backup: - curl -fsSL --retry 3 'https://hc-ping.com/<uuid>' || true on_error: - curl -fsSL --retry 3 'https://hc-ping.com/<uuid>/fail' || true ``` `/etc/borgmatic/passphrase`: ``` my-very-long-random-passphrase ``` 权限:`sudo chmod 600 /etc/borgmatic/passphrase`。 ## 跑 ```bash sudo borgmatic --verbosity 1 # 等价于:create + prune + 周期性 check ``` 只跑某一步: ```bash sudo borgmatic create # 只备份 sudo borgmatic check # 只校验 sudo borgmatic prune # 只清理 sudo borgmatic list # 看快照列表 ``` ## systemd timer Borgmatic 包自带 unit: ```bash sudo systemctl enable --now borgmatic.timer systemctl list-timers borgmatic.timer ``` 或者自己写 timer 覆盖时间。 ## 还原 ```bash # 列快照 sudo borgmatic list --repository /mnt/nas/borg/host-foo # 或 sudo borg list /mnt/nas/borg/host-foo # 看快照内容 sudo borg list /mnt/nas/borg/host-foo::host-foo-20260520-024300 | head # 还原一个文件到当前目录 cd /tmp sudo borg extract /mnt/nas/borg/host-foo::host-foo-20260520-024300 etc/nginx/nginx.conf # 挂载快照为只读文件系统(很方便) sudo mkdir /mnt/snap sudo borg mount /mnt/nas/borg/host-foo::host-foo-20260520-024300 /mnt/snap ls /mnt/snap sudo borg umount /mnt/snap ``` ## 双仓库的取舍 - 本地仓库快但 fate-shared:火灾水患时一起没。 - 异地仓库慢但是真灾备;如果带宽紧张就只把 `/etc` `/srv/critical` 推异地。 可以拆配置: ```yaml repositories: - path: /mnt/nas/borg/host-foo label: nas # 仅在某些 source_directory 同步到异地,靠 borg patterns 文件 ``` 更简单的做法:写两份 borgmatic 配置 `local.yaml` / `offsite.yaml`, 各自有 source_directories,timer 在不同时间触发。 ## 踩过的坑 - Borg 仓库 **不能** 让两个 client 同时写。两台机器要往同一个仓库 备份必须串行或用 `--lock-wait`。 - 加密类型 `repokey` 把 key 放仓库里(密码加密),方便但密码弱时不安全; `keyfile` 把 key 放客户端 `~/.config/borg/keys/`,要单独备份。 - 大文件改动(虚拟机磁盘)每次 dedup 都要扫整个文件,慢。建议这种文件 排除,单独做内置工具的快照备份。 - 远端 ssh 用专门 key 加 `command="borg serve --restrict-to-repository ..."` 限制;别用你的常规 key 跑 borg。

systemd socket activation:让服务按需启动(替代 inetd)

## 起因 我有一台小 VPS 跑十几个偶尔被用的服务(小工具 API、自建图床、内部 Wiki 之类)。每个 24x7 常驻进程都吃 50-200 MB 内存。 "按需启动 + 闲置自动停"能省一大半内存。 `xinetd` 是老古董做法;systemd 的 socket activation 是现代等价物 + 完全集成系统单元 + 更灵活。 ## 工作原理 1. systemd 启动时只创建 listening socket(端口),不启服务进程 2. 第一个请求到达端口时,systemd 启动服务进程并把 socket fd 传给它 3. 服务正常工作,处理后续请求 4. 闲置一段时间后服务可以 exit,systemd 重新只监听端口 5. 下次请求重复 1-4 ## 解决方案 ### 例子:按需启动一个 Python 小服务 `my-tool.service`: ```ini # /etc/systemd/system/my-tool.service [Unit] Description=My Tool API After=network.target my-tool.socket Requires=my-tool.socket [Service] Type=notify # 配合 sd_notify,启动好通知 systemd User=trio Group=trio ExecStart=/srv/my-tool/.venv/bin/python /srv/my-tool/server.py StandardInput=socket # systemd 把 socket fd 传给进程 Restart=on-failure # 闲置 5 分钟自动退出 RuntimeMaxSec=infinity [Install] WantedBy=multi-user.target ``` `my-tool.socket`: ```ini # /etc/systemd/system/my-tool.socket [Unit] Description=My Tool socket [Socket] ListenStream=8088 Accept=no # systemd 不 accept,把 listening socket 传给服务 # 闲置多久关: NoDelay=true [Install] WantedBy=sockets.target ``` ```bash sudo systemctl daemon-reload # 只 enable + start socket,不启 service sudo systemctl enable --now my-tool.socket sudo systemctl status my-tool.socket # active (listening) sudo systemctl status my-tool.service # inactive (dead) ss -tlnp | grep 8088 # 看到是 systemd 在监听 # 第一次访问 curl http://localhost:8088/ # systemd 启动 my-tool.service,处理请求 sudo systemctl status my-tool.service # active (running) ``` ### 服务端代码(Python)拿 socket fd ```python # /srv/my-tool/server.py import os, socket from wsgiref.simple_server import make_server def app(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'hello from on-demand service'] # 检测是否被 systemd 启动 + socket activated LISTEN_FDS_START = 3 if os.environ.get('LISTEN_FDS') == '1': fd = LISTEN_FDS_START sock = socket.socket(fileno=fd) # 用现成的 listening socket 而非新建 sock.listen(128) # 配合 wsgiref: from wsgiref.simple_server import WSGIServer httpd = WSGIServer(('', 0), None, bind_and_activate=False) httpd.socket = sock httpd.set_app(app) print(f'serving on socket fd {fd}', flush=True) # 通知 systemd 启动完成(Type=notify 要求) notify_socket = os.environ.get('NOTIFY_SOCKET') if notify_socket: ns = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) ns.sendto(b'READY=1', notify_socket) httpd.serve_forever() else: httpd = make_server('', 8088, app) httpd.serve_forever() ``` 很多框架原生支持 socket activation: - **systemd-python** 库的 `daemon.listen_fds()` 简化拿 fd - **Gunicorn** 支持 `--bind fd://3`(systemd LISTEN_FDS 模式) - **Caddy / nginx** 都能 socket activate ### 让服务闲置后自动退出 服务端代码:accept 后开 N 秒计时,无请求就 sys.exit(0)。 或者用 `RuntimeMaxSec=10min` 强制最多跑 10 分钟(systemd 会 SIGTERM)。 ### accept-per-connection 模式(替代 inetd) 如果服务每个连接超轻量(如 TCP echo),可以 `Accept=yes`: ```ini # my-tool.socket [Socket] ListenStream=8088 Accept=yes ``` ```ini # [email protected] (注意 @) [Service] ExecStart=/srv/my-tool/handle.sh StandardInput=socket StandardOutput=socket ``` 每个连接 systemd accept 后 fork 一个 service 实例处理。 适合纯短任务,不适合 HTTP(每请求新进程开销大)。 ### 监控 ```bash # 看 socket 等待状态 sudo systemctl list-units --type=socket # 看 service 历史 sudo systemctl list-timers journalctl -u my-tool.service --since '1 day ago' ``` ## 效果 我的 VPS 上: | | 常驻 | socket activation | |---|---|---| | 12 个小服务总内存 | 1.6 GB | ~200 MB(只有正在用的几个) | | 冷启动延迟 | 0 | 100-500ms(首次请求) | | 闲置时 CPU | < 1% | 0% | 代价:用户每天第一次访问慢半秒。绝大多数内部工具能接受。 ## 与替代方案对比 | | systemd socket | inetd / xinetd | nginx fastcgi | k8s scale-to-zero | |---|---|---|---|---| | 复杂度 | 低 | 中 | 高 | 高 | | 现代 | ✅ | ❌(淘汰) | ✅ | ✅ | | 每连接 fork | 可选 | 是 | 否 | 否 | | 内存节省 | 大 | 大 | 中 | 大 | | 适合场景 | 偶用小服务 | 纯短任务 | Web 服务 | 微服务集群 | ## 踩过的坑 1. **`Type=notify` 但代码不发 READY**:systemd 默认 90 秒后认为启动 失败,把服务 kill 掉。改 `Type=simple` 或确认代码发 sd_notify。 2. **第一次冷启动慢得离谱**:venv 加载 / 模型加载 / DB 连接初始化 都在第一次请求时发生。考虑 `WatchdogSec=` + 预热请求。 3. **socket 端口冲突**:先确保对应端口没被别的进程占用。 `Type=simple` 的进程自己 bind 会冲突,必须用 socket activation 提供 的 fd。 4. **service 退出后端口短暂关闭**:闲置退出 → systemd 检测到 → 重新 开 socket。这之间几十 ms 可能拒绝连接。`KeepAlive=true` + 服务退出 前发 stop 信号给 systemd 让它先准备好。 5. **不适合高并发**:socket activation 设计给"偶尔用"。如果服务持续 有流量,常驻反而更高效(避免冷启动)。

bpftrace 一行 eBPF 脚本排查"看不见"的内核 / 用户态问题

eBPF 允许在内核里跑沙箱化的小程序,无侵入地观察系统行为。 bpftrace 是 awk 风格的高级语言,让你不用 C / libbcc 也能写 eBPF 脚本。一行就能解决很多用 strace / perf 都麻烦的问题。 ## 装 ```bash sudo apt install -y bpftrace bpftrace --version # 需要 >= 0.16,老版本功能少很多 # 内核 5.5+ / Debian 11+ / Ubuntu 20.04+ ``` ## 1. 一行解决的常见问题 ### 谁在打开 /etc/passwd ```bash sudo bpftrace -e ' tracepoint:syscalls:sys_enter_openat /str(args->filename) == "/etc/passwd"/ { printf("%s (%d) opened /etc/passwd\n", comm, pid); } ' ``` 任何进程 open /etc/passwd 时立即打印进程名 + PID。 ### 哪个进程在创建 socket ```bash sudo bpftrace -e ' tracepoint:syscalls:sys_enter_socket { printf("%s (%d) opened socket family=%d type=%d\n", comm, pid, args->family, args->type); } ' ``` ### 哪个 exec 系列 syscall 在被频繁调用 ```bash sudo bpftrace -e ' tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); } ' ``` 实时显示所有 exec —— 找出 shell 死循环 / 短生命周期进程喷。 ### 系统调用直方图 ```bash sudo bpftrace -e ' tracepoint:syscalls:sys_enter_read { @[comm] = count(); } interval:s:5 { print(@); clear(@); exit(); } ' # 5 秒内每个进程的 read 次数 ``` ### 哪个进程在 page fault ```bash sudo bpftrace -e ' software:major-faults:1 { @[comm] = count(); } ' # Ctrl-C 后看汇总 ``` major-fault = 必须从磁盘加载 page,对应 swap / mmap miss 等慢操作。 ## 2. 用户态 dynamic instrumentation (uprobe) 观察 nginx 调用某函数: ```bash # 列 nginx 二进制的可探测函数 sudo bpftrace -l 'uprobe:/usr/sbin/nginx:*' | head # 探测某个函数 sudo bpftrace -e ' uprobe:/usr/sbin/nginx:ngx_http_process_request { @[comm] = count(); } ' ``` 不需要重启 nginx、不需要改代码,实时统计某函数被调多少次。 ## 3. 时延直方图 ```bash sudo bpftrace -e ' tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; } tracepoint:syscalls:sys_exit_read /@start[tid]/ { @us = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); } interval:s:5 { print(@us); clear(@us); } ' ``` 输出每 5 秒一次 read syscall 的时延直方图(微秒): ``` @us: [0] 8 |@ | [1] 256 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | [2, 4) 384 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | [4, 8) 128 |@@@@@@@@@@@@@@@@ | [8, 16) 16 |@@ | [16, 32) 4 | | [1K, 2K) 1 | | ``` 立刻看出 read 大多 1-4 µs,偶尔有 1-2 ms 的慢——可能是磁盘 / 网络 IO 慢。 ## 4. 内置变量速查 - `pid` / `tid`:进程 / 线程 ID - `comm`:进程名(前 16 字符) - `nsecs`:当前时间戳(纳秒) - `cpu`:当前 CPU - `args`:tracepoint 参数(结构体) - `kstack` / `ustack`:内核 / 用户调用栈 ## 5. 输出 + 聚合 ``` @hist_name = hist(value) # 2 的幂直方图 @lhist_name = lhist(value, 0, 100, 10) # 线性直方图 @count[key] = count() # 计数 @sum[key] = sum(value) # 求和 @avg[key] = avg(value) # 平均 @stats[key] = stats(value) # min/max/avg/count/sum @top10 = bottom(10, count()) # Top-N ``` Map 自动按 key 分组。 ## 6. 实战:找出"哪个文件被读最多" ```bash sudo bpftrace -e ' tracepoint:syscalls:sys_enter_openat { @opens[str(args->filename)] = count(); } interval:s:30 { print(@opens, 10); clear(@opens); } ' # 每 30 秒输出 Top 10 被 open 的文件 ``` ## 7. 现成脚本库:bpftrace tools ```bash git clone https://github.com/iovisor/bpftrace ls bpftrace/tools # biolatency.bt biosnoop.bt execsnoop.bt funcslower.bt # opensnoop.bt runqlat.bt tcpaccept.bt tcpconnect.bt # ... ``` 例:磁盘 IO 时延直方图 ```bash sudo bpftrace bpftrace/tools/biolatency.bt ``` `execsnoop.bt` 实时显示新建进程,`tcpconnect.bt` 显示所有新建 TCP 连接, 都是诊断生产问题的利器。 ## 8. 跟 bcc 比 bcc 是用 C 写 eBPF + Python 包装,更强大但学习成本高。 bpftrace 是高级语言,限制更多但写起来快 10x。生产经验: - 简单监控 / 调试一次性脚本:bpftrace - 持续运行的复杂 agent / 工具:bcc ## 9. 性能影响 bpftrace 比 strace / perf 通常开销小 5-10x。但还是有开销: - tracepoint:几乎零(编译时插入的探测点) - kprobe / uprobe:每次触发都中断到 BPF 解释器,有一点开销 - 高频事件(每秒百万级)+ map 写:可能加 5-15% CPU 生产环境 attach 前先估算事件频率,从低开销 tracepoint 开始。 ## 10. CO-RE / libbpf-tools 最新的 BPF 工具用 CO-RE(Compile Once, Run Everywhere): ```bash sudo apt install -y libbpf-tools sudo execsnoop-bpfcc # bcc 版(需要 kernel-devel) sudo execsnoop # libbpf-tools 版(任意内核 5.4+) ``` CO-RE 工具不需要内核头文件,便携性最好。生产首选 libbpf-tools。 ## 踩过的坑 - 老内核(< 5.0)功能阉割严重;如果没法升内核,回退到 strace / perf。 - BTF(BPF Type Format)没启用的内核:很多 bpftrace 脚本直接报错。 `ls /sys/kernel/btf/vmlinux` 看是否有,没有就只能用 kprobe 而非 fentry。 - 一个 bpftrace 脚本里写 5+ 个 probe + 大 map → BPF verifier 拒绝 ("too many instructions" / "stack overflow")。拆成多个脚本。 - 容器内跑 bpftrace 没权限:需要 `--privileged` + `--cap-add SYS_ADMIN`, 或者直接在 host 上跑。

LVM 上扩 / 缩 ext4 分区(不停机)

LVM 比直接分区灵活得多:磁盘空间可以在线扩、跨多块物理盘组卷、做快照。 日常运维最常用的就是"分区满了,加块盘扩进去"。下面按真实流程走一遍。 前置:分区是 `ext4` on `LVM`,要扩或缩的是某个 LV。XFS 类似但不能缩。 ## 现场摸底 ```bash df -h /data # Filesystem Size Used Avail Use% Mounted on # /dev/mapper/vg0-data 100G 95G 5G 95% /data sudo lvs # LV VG Attr LSize # data vg0 -wi-ao 100.00g # root vg0 -wi-ao 50.00g sudo vgs # VG #PV #LV #SN Attr VSize VFree # vg0 1 2 0 wz--n- 200.00g 50.00g sudo pvs # PV VG Fmt Attr PSize PFree # /dev/sda2 vg0 lvm2 a-- 200.00g 50.00g ``` ## 场景 A:VG 有剩余空间,扩 LV + ext4 最简单情况。直接扩: ```bash # 加 30G 到 data LV,顺手扩 ext4 sudo lvextend -L +30G -r /dev/vg0/data # -r 等价于扩完后自动跑 resize2fs(ext4)或 xfs_growfs ``` 不带 `-r` 时分两步: ```bash sudo lvextend -L +30G /dev/vg0/data sudo resize2fs /dev/vg0/data ``` ext4 / XFS 都支持在线扩容,不需要 umount,业务无感知。 校验: ```bash df -h /data # /dev/mapper/vg0-data 130G 95G 31G 76% /data ``` ## 场景 B:VG 空间不够,先加物理盘 ```bash # 新磁盘 /dev/sdb 装上,整盘做 PV(生产建议先 fdisk 建分区给它) sudo pvcreate /dev/sdb sudo vgextend vg0 /dev/sdb sudo vgs # VG #PV #LV #SN Attr VSize VFree # vg0 2 2 0 wz--n- 700.00g 550.00g # 然后回到场景 A 扩 LV sudo lvextend -L +100G -r /dev/vg0/data ``` ## 场景 C:缩 LV(危险,要停机 + 备份) ext4 缩容必须先 umount,且 **没有 100% 安全的在线缩容方案**。 执行前 ===先做备份===。 ```bash # 1. umount sudo umount /data # 2. fsck(强制检查) sudo e2fsck -fy /dev/vg0/data # 3. 文件系统先缩到目标大小 sudo resize2fs /dev/vg0/data 60G # 4. LV 缩到同样或稍大的尺寸(建议 +1G 余量) sudo lvreduce -L 61G /dev/vg0/data # 5. 文件系统再扩满 LV sudo resize2fs /dev/vg0/data # 6. 重新 mount sudo mount /data ``` 步骤 3 < 步骤 4 的顺序不能颠倒!文件系统比块设备大会立即损坏。 ## 场景 D:从一块物理盘迁数据到另一块(替换坏盘) ```bash # 新盘加入 VG sudo pvcreate /dev/sdc sudo vgextend vg0 /dev/sdc # 把 sdb 上的所有 LV 迁到其它 PV(pvmove 是在线的) sudo pvmove /dev/sdb # 漫长过程,可以加 -b 后台跑,sudo pvmove -b /dev/sdb # 完成后从 VG 移除 sudo vgreduce vg0 /dev/sdb sudo pvremove /dev/sdb # 现在 sdb 可以物理移除 ``` ## 快照(紧急回滚) ```bash # 给 data 拍个快照,预留 10G 写时变化 sudo lvcreate -L 10G -s -n data-snap /dev/vg0/data # 操作系统看到的是冻结时刻的 data,挂载它能恢复文件 sudo mkdir /mnt/snap sudo mount /dev/vg0/data-snap /mnt/snap # 没问题就删掉快照(不删会占空间且影响写性能) sudo umount /mnt/snap sudo lvremove /dev/vg0/data-snap ``` 升级 / 大改动前给 root LV 拍快照,是 LVM 系统的"undo"能力。 ## 踩过的坑 - `lvextend -L 200G` 是 **设置** 总大小到 200G,`-L +100G` 是 **追加** 100G。 搞反了能把分区缩没掉。 - ext4 在 64-bit 模式(新版本默认)才支持超 16T;老分区扩到 16T+ 会 fail。 `tune2fs -O 64bit /dev/...` 升级,操作需要 fsck 时间。 - pvmove 时如果系统 reboot,会卡在半迁状态。重新启动后用 `pvmove --abort` 或继续 `pvmove`。 - XFS 不能缩 —— 想缩只能 dump + mkfs + restore。所以新分区如果有可能缩, 用 ext4。

用 mdadm 创建 RAID1 并模拟磁盘故障恢复

软 RAID(mdadm)相比硬 RAID 卡的优势是便宜 + 透明 + 跨硬件可迁移。 代价是占一点 CPU、组装比硬 RAID 复杂。家庭 NAS / 小型服务器很常用 RAID1 (镜像)做最基础的硬盘冗余。 下面用 `/dev/sdb` 和 `/dev/sdc` 两块同型号硬盘做 RAID1,挂到 `/srv/data`。 ## 1. 准备分区 ```bash # 看现状 lsblk sudo wipefs -a /dev/sdb /dev/sdc # 清残留 FS / 分区表签名 # 给两块盘都建一个 Linux RAID 类型的分区 for d in /dev/sdb /dev/sdc; do sudo parted -s "$d" mklabel gpt sudo parted -s "$d" mkpart primary 1MiB 100% sudo parted -s "$d" set 1 raid on done lsblk # sdb 8:16 0 4T 0 disk # └─sdb1 8:17 0 4T 0 part # sdc 8:32 0 4T 0 disk # └─sdc1 8:33 0 4T 0 part ``` ## 2. 创建 RAID1 ```bash sudo mdadm --create /dev/md0 \ --level=1 \ --raid-devices=2 \ --metadata=1.2 \ --name=md0 \ /dev/sdb1 /dev/sdc1 # Continue creating array? y ``` 初始化(resync)会在后台跑,过程中阵列可用但慢: ```bash cat /proc/mdstat # Personalities : [raid1] # md0 : active raid1 sdc1[1] sdb1[0] # 3906886976 blocks super 1.2 [2/2] [UU] # [>....................] resync = 3.6% (142336640/3906886976) finish=312.4min speed=200800K/sec watch -n 5 cat /proc/mdstat # 看进度 ``` 不想等 resync 也能立刻继续下面步骤(性能会差点)。 ## 3. 文件系统 + 挂载 ```bash sudo mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 -L data /dev/md0 sudo mkdir /srv/data sudo mount /dev/md0 /srv/data df -h /srv/data ``` 写入 fstab: ```bash echo "LABEL=data /srv/data ext4 defaults,nofail 0 2" \ | sudo tee -a /etc/fstab # nofail:万一阵列没起来,启动不会卡死 ``` ## 4. 保存 mdadm 配置(关键,否则 reboot 后阵列名变) ```bash sudo mdadm --detail --scan | sudo tee -a /etc/mdadm/mdadm.conf # ARRAY /dev/md/md0 metadata=1.2 name=hostname:md0 UUID=... sudo update-initramfs -u ``` ## 5. 模拟磁盘故障(演练,重要) 任何 RAID 都要做一次"假装坏一块"的演练,确认告警 + 替换流程通畅。 ```bash # 标记 sdb1 为 faulty sudo mdadm /dev/md0 --fail /dev/sdb1 cat /proc/mdstat # md0 : active raid1 sdc1[1] sdb1[0](F) # 3906886976 blocks super 1.2 [2/1] [_U] # ^^^^^^ 缺一块 ^^^ # 应用仍在跑(RAID1 可单盘工作) echo hello | sudo tee /srv/data/test.txt cat /srv/data/test.txt # 从阵列移除 faulty 盘 sudo mdadm /dev/md0 --remove /dev/sdb1 # 模拟换上新盘(这里直接加回来;现实里换新硬盘后重新分区然后加) sudo mdadm /dev/md0 --add /dev/sdb1 cat /proc/mdstat # [=>...................] recovery = 5.4% ... # 等几小时到几天,看盘大小 ``` resync 完成后 `[2/2] [UU]`,恢复完整冗余。 ## 6. 监控 + 告警 ```bash # 邮件告警(前提是本机能寄信,参考 unattended-upgrades 那篇) echo "MAILADDR [email protected]" | sudo tee -a /etc/mdadm/mdadm.conf sudo systemctl restart mdmonitor # 直接测试一封"假告警" sudo mdadm --monitor --test --oneshot /dev/md0 ``` 或 Prometheus node_exporter 已经导 `node_md_state` 指标,PromQL: ``` node_md_state{state="failed"} > 0 ``` ## 7. 扩 RAID1 容量(替换两块盘为更大的) ```bash # 1. 一块一块替换:fail + remove + 物理换新盘 + add sudo mdadm /dev/md0 --fail /dev/sdb1 --remove /dev/sdb1 # 物理换更大新盘,分区 raid on,mdadm --add,等同步完 sudo mdadm /dev/md0 --add /dev/sdb1 # wait resync # 2. 重复对 sdc1 # 3. 两块都换大盘后,扩 md 用满 sudo mdadm --grow /dev/md0 --size=max sudo resize2fs /dev/md0 ``` ## 踩过的坑 - 用不同型号 / 大小 / 转速的盘做 RAID1 不犯法但很糟:性能 = 慢盘, 寿命差异大容易"双故障"。 - 整盘做 RAID(不分区,直接 `/dev/sdb`)也行但碰到换不同型号盘时尺寸 不能完全匹配。建议永远先分区给它(且留 100MB 余量)。 - `nofail` 在 fstab 里很重要——RAID 同步还没完成或某块盘没认到, 没 nofail 会进 emergency mode。 - 软 RAID 替代不了备份。RAID 防硬件故障,不防误删 / 加密勒索 / FS 损坏。 独立做异地备份。