运维体系
站点各部分的实现方式与选择理由。
部署架构
/var/www/releases/<时间戳>-<标签>/ 不可变发布物,每个版本独立
/var/www/personal-site 软链接 → 当前生效版本
/var/www/releases/.current 当前版本指针
/var/www/releases/.history 版本历史(回滚依据)
用软链接而不是直接覆盖文件,因为覆盖是破坏性的:
切一半失败会得到半新半旧的状态。软链接切换是
原子的(mv -T 的 rename 语义),
要么全新要么全旧,没有中间态。
发布流程(9 步,每步失败都不留半成品)
| 步骤 | 动作 | 失败时 |
|---|---|---|
| 1 | 校验源目录(必须有 index.html) | 拒绝发布 |
| 2 | 记录回滚点 + 备份 nginx 配置 | 中止 |
| 3 | 创建新 release 目录(cp -a) | 中止 |
| 4 | 权限预检(以 nginx 用户试读) | 删除新目录,中止 |
| 5 | nginx -t 语法校验 | 删除新目录,中止 |
| 6 | 原子切换软链接 | — |
| 7 | reload(非 restart) | — |
| 8 | 健康检查 5 次 | 自动回滚 |
| 9 | 清理旧版本(保留 5 个) | 跳过生效版本 |
为什么是 reload 而不是 restart
| 方式 | 总请求 | 失败 | master 进程 |
|---|---|---|---|
reload | 25,496 | 0 | 保留 |
restart | 25,612 | 408 | 更换 |
restart 的失败数是 408 个(393 个 connection refused +
15 个 connection reset)。这一组是同时跑的对照:它说明压测方法本身
能检测到服务中断,因此 reload 那一组的零失败才可采信。
流量防护(按 20 GB/月 额度设计)
CDT 免费额度按地域区分:中国内地 20 GB/月, 非中国内地 200 GB/月。本机在青岛,所以额度是 20 GB ≈ 682.7 MB/天。
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req zone=general burst=20 nodelay;
limit_conn perip 20;
limit_req_status 429;
取值依据:真人浏览瞬时峰值约 10–20 请求/秒,自动化工具通常 100+ 请求/秒,
所以 10r/s + burst=20 只挡后者。
双向验证(只测"能拦住"是不完整的——过严的限流赶走真人更糟):
| 实验 | 负载 | 结果 |
|---|---|---|
| A 正常用户 | 2 请求/秒 × 10 | 10/10 成功,零误伤 |
| B 攻击负载 | 8 并发,2320 QPS | 11,547 个被 429 拦截 |
监控与告警
lib/traffic-monitor.py 日志分析引擎(纯标准库)
bin/traffic-check.sh 阈值判定 + 告警
/etc/cron.d/ops-lab-traffic 每小时巡检
/etc/cron.d/ops-lab-traffic-daily 每日存档(不发邮件)
logs/traffic/ 历史报告(自动保留 48 份)
logs/alerts/ 告警记录
bin/db/verify-backup.sh 每天 03:40 在一次性容器里试恢复最新备份
logs/db-backup/ 备份文件与每日校验日志
阈值来自额度换算而非拍脑袋:20GB ÷ 30 天 = 682.7 MB/天,
50% 警告、80% 严重。
测量局限(这点很重要):/proc/net/dev 的计数
包含内网流量——阿里云 agent 大量发往
100.64.0.0/10 内部服务网,不出机房、不计费。
实测站点出流量只占 eth0 发送量的 5.58%,真实计费数字要看控制台 CDT。
日常巡检
巡检的目的是在问题变成故障之前发现它。下面是每天实际执行的顺序, 从头做一遍就能确认整台服务器的状态。
第一步:跑一次全量快照
healthcheck.sh 把负载、内存、磁盘、inode、失败服务、监听端口、
占用最高的进程、网卡累计流量、fail2ban、备份数量、练习环境与已知偏差一次打全,
并落成一份带时间戳的快照:
cd /root/ops-lab
bash bin/healthcheck.sh
ls -t logs/health-*.txt | head -n 1
快照文件名是 health- 加年月日时分秒,历史快照留着用于对比。
第二步:看自动巡检的结果
不用手动跑也不会漏:每天 08:00 由 cron 触发
lib/daily-check.py,把 11 项检查跑一遍,结果写进日志。
读三个文件就能掌握现状:
cat logs/daily-check/latest.txt # 最近一次巡检全文
ls -t logs/alerts/ | head -n 5 # 最近的告警文件
tail -n 50 logs/daily-check/cron.log # 定时任务的执行记录
11 项检查依次是:磁盘、inode、服务、心跳、内存、负载、流量、数据库、Tomcat 代理、目录占用、脚本自洽。任何一项越界都会抬高整体等级。
告警不会重复轰炸:脚本把上一次的等级写在
logs/daily-check.state,只有等级发生变化时才发邮件。
所以邮箱里没有新信,不代表巡检没跑 —— 也可能是问题还在、等级没变。
第三步:核对阈值
判断越界用的是固定阈值,不是感觉:
磁盘使用率 80%
inode 使用率 80%
可用内存 200 MB
系统负载 4.0
心跳文件年龄 180 秒
流量占日均预算 50% 警告 / 80% 严重
数据库备份年龄 48 小时
这些值写成常量放在 lib/daily-check.py 顶部,要调只改那一处。
心跳那一项看的是 /var/log/quest02.log 的修改时间,而不是进程是否存在
—— 进程活着但不再干活,也是一种故障。
第四步:分项排查
整体巡检报出异常后,用对应的单项工具定位:
bash bin/f2b-status.sh # fail2ban 是否在跑、封了多少、增长快不快
bash bin/deviation-check.sh # 对照 docs/KNOWN-DEVIATIONS.md 审计已知偏差
python3 lib/daily-check.py --verbose # 显示 OK 项的完整巡检
--verbose 把通过的检查也打印出来,用来确认「没报错」是真的通过,
而不是检查项根本没被执行。
第五步:单独看流量口径
服务器按流量计费,所以流量单独看一眼。每小时与每天各跑一次
bin/traffic-check.sh,报告自动保留最近 48 份:
ls -t logs/traffic/traffic-*.txt | head -n 1 | xargs tail -n 20
报告里的「占日均预算」按 20 GB ÷ 30 天 = 682.7 MB/天 折算,
50% 警告、80% 严重。注意 /proc/net/dev 的计数包含内网流量,
真实计费口径要看阿里云控制台。
技术选型理由
| 选择 | 理由 |
|---|---|
| nginx 直跑静态文件 | 内存 14.8 MB;Node/Python 后端起步 50–100 MB |
| 不用 Docker 跑网站 | 多一层 overlayfs 与网络命名空间开销,2 GB 内存要留给其他练习 |
| 不用宝塔面板 | 本项目目的是理解各组件的手动配置过程。面板自动生成的配置不透明,出问题时需先弄清它的生成逻辑才能定位;这类工具更适合快速建站场景,与练习目的不同 |
| 自研 Markdown 转换器 | 服务器软件越少攻击面越小;且零依赖可审计 |
| CSS 内联进 HTML | 省一次 HTTP 请求,弱网下体感明显,也少一次被限流的可能 |
工具集
| 工具 | 用途 |
|---|---|
backup-config.sh | 改配置前自动备份(保留 30 份) |
healthcheck.sh | 一键系统巡检 |
deploy.sh | 原子发布(9 步 + 自动回滚) |
rollback.sh | 秒级回滚 |
traffic-check.sh | 流量巡检 + 分级告警 |
db/verify-backup.sh | 每天在一次性容器里试恢复最新数据库备份并校验 |
test-narrow-path.sh | 窄 PATH 下跑两个回滚脚本,缺命令必须拒绝执行 |
lib/loadtest.py | 标准库并发压测器(支持按时长) |
lib/md2html.py | Markdown 转换器(含 XSS 防护、图片与有序列表起始编号,61 项测试) |