Java 后端兼管运维:用每周几小时把线上守住
白天写需求,晚上看告警。
发布只能挤在下班后,因为白天要开会、要改 bug、要联调。出了问题没有专职运维兜底,群里第一个被 @ 的是你。想系统学一遍运维,翻了两页《鸟哥的 Linux 私房菜》,第二周就放弃了。
这就是「兼顾」的状态。它和转岗是两回事——重点不是你要懂多少运维知识,是你能投入多少连续时间。
专职运维可以花两周搭一套 ELK,可以研究一周末的 K8s 网络模型。你不行。你的时间被切成一个个两小时的碎片,中间还要随时被打断。所以照搬运维教程的路径一定会失败:不是学不会,是排不进日程。
兼管模式下唯一的正确姿势是做减法:能买的不自建,能自动的不手动,能不做的先不做。用最少的维护面积,换最大的可用性。
下面按「先做减法 → 摸家底 → 管变更 → 建观测」的顺序写,最后给一份按小时排的 12 周节奏。
零、做减法:三条原则和三个不能省
原则一:能托管就别自建
这不是偷懒,是算账。
| 组件 | 自建一次性投入 | 自建持续成本 | 托管方案 |
|---|---|---|---|
| MySQL | 4 小时(安装 + 主从 + 参数) | 备份、升级、慢查询、磁盘告警,每月约 2 小时 | 云 RDS / PolarDB |
| Redis | 1 小时 | 内存、淘汰策略、持久化,每月约 0.5 小时 | 云 Redis |
| 日志平台 | 8 小时(ELK 三件套) | 磁盘与索引管理,每月约 2 小时 | 云日志服务 |
| 对象存储 | 2 小时(MinIO) | 容量与备份 | OSS / COS |
| 监控 | 4 小时(Prometheus 全家桶) | 很低,值得自建 | — |
关键差别在最后一列之外:托管贵的是钱,自建贵的是你的注意力。而兼管模式下,注意力是比预算更稀缺的资源。
一年下来,自建 MySQL + Redis + 日志,持续维护成本大约 50 个小时——相当于你六个完整的工作日,全部花在「维持现状」上,不产生任何业务价值。这笔账大多数人从来没算过。
判断标准只有一句话:这个东西出故障时,你有没有能力在 30 分钟内修好。修不好、或者不敢修,就买。
原则二:能自动就别手动
手动操作有两个成本:这一次的时间,和下一次的心智负担。
# 每次发布手敲这 8 条,一个月发 20 次 = 160 次输入机会
cd /opt/app && cp app.jar app.jar.bak && systemctl stop app \
&& cp /tmp/app-new.jar app.jar && systemctl start app \
&& sleep 20 && curl localhost:8080/actuator/health
写成脚本,第一次花 40 分钟,之后每次省 3 分钟,而且不会漏步骤。手动操作最难防的不是慢,是漏——漏掉那次备份,漏掉那个 sleep。
原则三:能不做的先不做
兼管最容易被拖垮的,是「看起来很专业」的那部分基础设施。
| 别做 | 为什么 |
|---|---|
| 五个服务就上 K8s | 集群本身的运维复杂度比五个服务加起来还高 |
| 自建 Service Mesh | 你连 Istio 的坑都排不完,业务需求还在排队 |
| 搞一套 CMDB / 自研发布平台 | 维护成本远超收益,不如 Git + 脚本 |
| 全链路追踪 + 日志 + 指标全家桶同时上 | 一次上一样,上一样用起来一样 |
判断标准:这个东西能否在未来一个月内,帮我减少一次故障或节省一小时。答不上来,就先放进待办,别放进日程。
但有三个东西不能省
减法不能减到裸奔。这三样是底线,宁可别的都不做:
- 备份,并且验证过能恢复
- 监控,至少能在用户之前发现问题
- 回滚,且提前演练过一次
一、把家底摸清楚
兼管的第一周不要装任何软件。先花两个晚上做两件事。
资产清单
一张表把所有机器、域名、证书、账号记下来。云控制台只告诉你「有什么机器」,不告诉你「这台机器上跑着什么」。
| 主机 | 公网 IP | 内网 IP | 规格 | 角色 | 上面跑什么 | 到期时间 |
| --- | --- | --- | --- | --- | --- | --- |
| prod-app-01 | 1.2.3.4 | 10.0.1.11 | 4C8G | 应用 | jar + nginx | 2027-03 |
| prod-db-01 | - | 10.0.1.21 | 8C16G | 数据库 | MySQL 8.0 | 2027-03 |
加上域名和证书的到期时间。证书过期导致全站不可用,是运维事故里最容易避免、也最常发生的一类,而它只需要一个到期提醒就能防住。
机器上到底跑着什么
# 谁在监听端口,对应哪个进程
ss -lntp
# 进程树,能发现被 nohup 起飞的野进程
ps -ef --forest | head -50
# 开机自启的服务清单
systemctl list-unit-files --state=enabled
# 定时任务,容易藏雷的地方
for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u $u 2>/dev/null | sed "s/^/[$u] /"; done
最后一条特别重要。接手系统里最常见的定时炸弹,是某个离职同事留下的 crontab——每天凌晨三点跑一个没人知道用途的脚本,某天它把日志目录清了,或者把一张表的数据同步错了。
拓扑图
不用画得多漂亮,能回答三个问题就行:请求从哪进来、经过哪些组件、数据落在哪。
用户 → CDN → Nginx(443) → 应用(:8080) → MySQL(:3306)
↓
Redis(:6379)
一张纸上的手绘图,比任何文档都管用。出事时你先看这张图,就知道该往哪一段查——省掉的是最贵的那几分钟。
二、Linux 命令只背四组
陌生命令背不完,也不用背完。你的目标不是「学会 Linux 运维」,是故障发生时 5 分钟内判断出方向。按「查负载、查磁盘、查网络、查日志」四组记,覆盖 90% 的现场。
第一组:机器累不累
uptime # 1/5/15 分钟负载,对比 CPU 核数
top -o %CPU # 看谁在吃 CPU
free -h # 内存,重点看 available 不是 free
vmstat 1 5 # r(排队)、si/so(换页,非 0 就是内存不够)
负载和核数对比:4 核机器负载 8 就是满负荷。但负载包含不可中断的 IO 等待,所以先看 vmstat 的 r 列,区分是 CPU 卡还是 IO 卡。
第二组:磁盘满没满
df -h # 空间
df -i # inode,小文件多时空间没满也写不进
du -sh /var/* | sort -h | tail
iostat -x 1 # %util 接近 100 说明磁盘打满
磁盘满是最常见的线上故障,而且原因往往不是业务数据,是日志没轮转。
第三组:网络通不通
ss -lntp # 监听端口
ss -s # 连接数汇总,TIME_WAIT 多不多
curl -v --max-time 5 http://127.0.0.1:8080/actuator/health
mtr -rw -c 20 example.com # 持续探测,看丢包在哪一跳
排查顺序永远是从内到外:本机进程 → 本机端口 → 到目标 IP → DNS → 外网。先证明「服务自己活着」,再证明「别人能连上」。
第四组:日志在哪
journalctl -u myapp -n 200 --no-pager
journalctl -u myapp --since "10 min ago"
tail -f app.log | grep -E "ERROR|Exception"
dmesg -T | tail -50 # 内核日志,OOM Killer 在这里
进程莫名消失,第一反应就是 dmesg -T | grep -i "killed process"——被 OOM Killer 干掉了,业务日志里只留一句进程退出,看不出原因。
三、中间件:一套模板套所有
如果你已经按原则一迁到云托管,这节大部分可以跳过。剩下的自建部分(通常是内部工具依赖的中间件),统一用下面这套模板。
目录规范
跨机器统一目录,比任何文档都有效:
/opt/{name}/ 程序本体(版本化,current 软链指向在跑的版本)
/data/{name}/ 数据目录(单独挂盘)
/etc/{name}/ 配置
/var/log/{name}/ 日志
好处是写脚本、做备份、换机器时路径不用想。/opt/redis/7.2.4/ + /opt/redis/current -> 7.2.4,升级就是换软链再重启,回滚就是换回去。
systemd 模板
别再 nohup java -jar &。进程挂了没人管、开机不自启、日志散在 nohup.out、PID 找不到——这四个问题 systemd 一次解决,而它只需要你写一次配置文件。
# /etc/systemd/system/myapp.service
[Unit]
Description=myapp
After=network-online.target mysql.service redis.service
Wants=network-online.target
[Service]
Type=simple
User=app
WorkingDirectory=/opt/myapp/current
ExecStart=/usr/bin/java -Xms2g -Xmx2g \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump \
-jar /opt/myapp/current/app.jar
SuccessExitStatus=143
Restart=always
RestartSec=5
LimitNOFILE=65535
StandardOutput=append:/var/log/myapp/stdout.log
StandardError=append:/var/log/myapp/stderr.log
[Install]
WantedBy=multi-user.target
几个容易漏的点:
SuccessExitStatus=143:SIGTERM 退出码是 143,不加这行systemctl stop会被判为失败。Restart=always配RestartSec=5:别设 1 秒,否则启动失败会疯狂重启,把 CPU 打满、日志刷爆,反而更难排查。LimitNOFILE:默认 1024,高并发下Too many open files就是这么来的。After=里写依赖的中间件,但不加Requires=——中间件挂了不该连带停掉应用。
改完统一三步:
systemctl daemon-reload && systemctl restart myapp && systemctl status myapp --no-pager
Compose 模板
自建的中间件用容器,理由是版本可回退、不污染宿主、起停一条命令。但两条铁律:数据目录必须挂到数据盘,端口必须只监听本机。
services:
mysql:
image: mysql:8.0.36
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
- --innodb-buffer-pool-size=4G
- --max-connections=500
- --slow-query-log=1
- --long-query-time=1
ports:
- "127.0.0.1:3306:3306" # 只监听本机,不暴露公网
volumes:
- /data/mysql:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 10s
retries: 5
注意 127.0.0.1:3306:3306 这个写法。直接写 3306:3306 会监听 0.0.0.0,安全组一开就等于把数据库挂到公网上——这是比弱密码更常见的事故来源,扫库机器人几分钟就能找上门。
自建中间件的最小必要参数
不求调优,只求不出事。以下是我认为必须显式设的项:
| 中间件 | 必设参数 | 不设的后果 |
|---|---|---|
| MySQL | innodb_buffer_pool_size(物理内存 50%~70%)、max_connections、slow_query_log=1、binlog_expire_logs_seconds=604800 | 缓冲池不设性能差一个数量级;binlog 不设过期会撑满磁盘 |
| Redis | maxmemory + maxmemory-policy、appendonly yes、requirepass、bind 127.0.0.1 | 不设上限会吃光内存被 OOM Killer 杀掉 |
| Nginx | worker_connections 10240、client_max_body_size、proxy_read_timeout、日志轮转 | 上传 413、后端慢响应 504,都是默认值造成的 |
| RabbitMQ | vm_memory_high_watermark、磁盘告警阈值 | 内存触顶后阻塞生产者,表现为「应用莫名卡死」 |
部署后 checklist
每装完一个中间件按表过一遍,别靠记忆:
- 配置文件在版本控制里(或至少有
.bak) - 数据目录在独立数据盘,且有备份任务
- 只监听内网/本机,公网端口已确认关闭
- 有强密码,且不在代码仓库里
- 开机自启已验证(重启一次试试)
- 日志有轮转,不会被写满
- 已接入监控(端口存活 + 关键指标)
- 有一条能跑通的备份恢复记录
最后一条最容易被跳过,也最要命。没验证过的备份等于没有备份。
四、变更管住:脚本审核
这是我最想强调的一节,因为它和你写代码的习惯直接冲突。
线上事故里,误执行 SQL 的比例远高于代码 bug。代码有 CI、有 review、有测试,但一条 UPDATE 往往是「群里说一声,然后在客户端里手敲回车」。兼管模式下你既是执行者又是审批者,更容易省掉这一步——所以流程必须轻到你不觉得麻烦,否则一定会绕过去。
按投入选方案
| 投入 | 方案 | 能力 |
|---|---|---|
| 半天 | Git 仓库 + MR 审批 + 脚本执行 | 有留痕、有 review |
| 1~2 天 | Yearning 或 Archery | SQL 自动审核、工单、执行审计 |
| 需要合规审计 | JumpServer 堡垒机 + 数据库审计 | 命令级审计、录屏、权限最小化 |
先上第一层,立刻能挡住八成的低级错误。第二层等到「团队变成三个人以上」或者「老板要审计」再考虑——一个人的团队上堡垒机,成本收益完全不成立。
第一层的目录结构:
db-changes/
2026/
20261030_add_order_index/
up.sql -- 变更
down.sql -- 回滚
README.md -- 目的、影响面、预计耗时、验证方法
脚本本身的四个属性
- 幂等:重复执行结果一致。
CREATE TABLE IF NOT EXISTS、DROP INDEX IF EXISTS。 - 可回滚:
down.sql和up.sql一起写,不能事后补。 - 有边界:
UPDATE必须带WHERE,先SELECT COUNT(*)验证影响行数;DELETE单批不超过 1000 行。 - 可预演:先在从库或预发环境跑一遍,记下耗时。超过 10 秒的
ALTER要评估锁表时间。
Shell 脚本第一行永远加:
#!/usr/bin/env bash
set -euo pipefail
少了 -u,变量没传时 rm -rf $DIR/ 会变成 rm -rf /。这类事故不需要第二次提醒。
数据库变更和代码发布的顺序
这是后端兼管运维最容易出事的地方——代码回滚容易,数据库回滚难。
原则是 expand-contract(先兼容,后清理),拆成三次发布:
| 阶段 | 数据库 | 代码 | 状态 |
|---|---|---|---|
| 1. 扩展 | 加新列(可空)/ 新表 | 新旧都写,读旧 | 任一版本代码都能跑 |
| 2. 迁移 | 回填数据 | 读新,写新 | 双写校验一致 |
| 3. 收缩 | 删旧列 | 不再引用旧列 | 确认无需回滚后再删 |
很多团队图省事,一次发布就「改列名 + 改代码」,结果发布失败要回滚——代码回滚了,列名已经改了,旧代码直接报错,只能硬着头皮往前修。一次可回滚的发布,就这么变成了不可回滚的故障。
五、发布:流程要轻到你自己愿意执行
发布流程的价值不在于仪式感,在于让任何一个人(包括三个月后的你)都能在凌晨三点照着做完。
制品管理
构建一次,全环境用同一个产物。不要在服务器上 git pull && mvn package——你永远不知道线上那个 jar 是哪个 commit 编的。
命名带提交哈希和构建时间:app-20261030-1a2b3c4.jar。配合 Actuator,上线后一眼确认版本:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
curl -s localhost:8080/actuator/info
三张清单
发布前
- 变更内容、影响面、回滚方案写在发布单里(写不出回滚方案就不许发)
- 数据库变更已提前执行(按 expand-contract,不与代码同批)
- 配置项 diff 过一遍(新增的环境变量、连接池、超时)
- 当前监控无异常告警,基线正常
- 发布窗口在你能连续盯 30 分钟的时间段
最后一条是兼管模式特有的。别把发布安排在「你马上要去接孩子」或者「五分钟后有个会」的时候,出了问题你没法处理,小故障会拖成大故障。
发布中
# 1. 从负载均衡摘除(有的话),等存量请求结束
# 2. 备份当前版本,方便秒回滚
cp /opt/myapp/current/app.jar /opt/myapp/backup/app-$(date +%Y%m%d%H%M).jar
# 3. 优雅停机
systemctl stop myapp
# 4. 换包
ln -sfn /opt/myapp/releases/20261030-1a2b3c4 /opt/myapp/current
# 5. 启动并观察
systemctl start myapp
journalctl -u myapp -f --since "1 min ago" | grep -E "Started|ERROR|Exception"
发布后(留观 15~30 分钟,别发完就走)
- 健康检查通过,核心接口 smoke test 通过
- 错误率、P99 延迟、GC 次数回到基线
- 日志无新增异常栈
- 数据库连接数正常(没有泄漏)
- 发布记录归档(谁、什么时候、发了什么、结果)
Java 服务的三个专属坑
1. 优雅停机
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
顺序必须是:摘流量 → 等在途请求结束 → 关连接池 → 停 JVM。只加配置但没摘流量,新请求还会打进来,等于白配。
2. 启动慢被误判为失败
Spring Boot 启动 30~60 秒很常见,健康检查的初始延迟必须大于启动耗时,否则会「启动中被判死 → 重启 → 又启动中」死循环。
startupProbe: # k8s 1.16+,启动期不判死
failureThreshold: 30
periodSeconds: 5
3. 堆内存和容器限制打架
-XX:MaxRAMPercentage=70.0 # 让 JVM 按容器限制算,比写死 -Xmx 安全
JDK 8 早期版本不认 cgroup 限制:容器限 2G,JVM 按宿主机 16G 算,堆设到 10G,直接被 OOM Killer 干掉。这类问题在容器化初期特别常见。
六、监控告警:为「没人值班」而设计
这是兼管模式和专职运维差别最大的一块。
专职团队有值班表,假告警多一点,无非是多几个人骂两句。你没有值班表——半夜响的那条告警,只能你自己爬起来处理。所以每一条假告警的边际成本极高,一次误报就足以让你开始无视告警,而「无视告警」是事故的前奏。
按顺序建三层,每层独立产生价值
第一层:黑盒拨测(半天)
从外部探测「服务能不能用」。用 Uptime Kuma,Docker 一行起:
docker run -d --restart=always -p 127.0.0.1:3001:3001 \
-v /data/uptime-kuma:/app/data \
--name uptime-kuma louislam/uptime-kuma:1
监控项:首页 200、核心接口返回预期内容(不只看状态码,要检查响应体里有关键字段)、证书剩余天数、域名解析、端口连通。
这一层的性价比最高:用户还没发现,你已经知道了。
第二层:白盒指标(1~2 天)
Prometheus + node_exporter + Grafana 一个 compose:
services:
prometheus:
image: prom/prometheus:v2.53.0
restart: always
volumes:
- /etc/prometheus:/etc/prometheus:ro
- /data/prometheus:/prometheus
command: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.retention.time=30d"]
ports: ["127.0.0.1:9090:9090"]
node-exporter:
image: prom/node-exporter:v1.8.1
restart: always
pid: host
volumes: ["/:/host:ro,rslave"]
command: ["--path.rootfs=/host"]
ports: ["127.0.0.1:9100:9100"]
grafana:
image: grafana/grafana:11.1.0
restart: always
volumes: ["/data/grafana:/var/lib/grafana"]
ports: ["127.0.0.1:3000:3000"]
监控自身也是单点。Grafana/Prometheus 挂了你连看板都打不开,所以至少让 Uptime Kuma 同时监控监控组件自己的 /metrics 和 /login。
第三层:应用指标(半天,收益最大)
Java 服务的指标不用自己造,Actuator + Micrometer 全都有:
management:
endpoints.web.exposure.include: health,info,prometheus
metrics.tags.application: ${spring.application.name}
metrics.distribution.percentiles-histogram.http.server.requests: true
/actuator/prometheus 一开,JVM 堆、GC、线程、连接池、接口 QPS 和 P99 全有了。这个端点绝不能暴露到公网,里面能看出框架版本和内部结构。
只看两个方法论就够
- 四个黄金信号(Google SRE):延迟、流量、错误、饱和度。做面板先放这四个。
- USE(Brendan Gregg):对每个资源看利用率、饱和度、错误。排查时按 CPU / 内存 / 磁盘 / 网络逐项对。
看板按「用户视角 → 服务视角 → 资源视角」三层组织:第一层回答「用户有没有受影响」,第二层「哪个服务出问题」,第三层「是 CPU、IO 还是连接池」。排查时从上往下钻,方向不会错。
最小告警集:六条,其余全进看板
没人值班的情况下,我建议只保留这几条真告警:
| # | 告警 | 条件 | 级别 |
|---|---|---|---|
| 1 | 站点/核心接口不可用 | 拨测连续 3 次失败 | P0 |
| 2 | 5xx 错误率 | > 5% 持续 5 分钟 | P0 |
| 3 | P99 延迟 | 超基线 2 倍持续 10 分钟 | P1 |
| 4 | 磁盘 / inode | > 85% | P1 |
| 5 | 进程消失或重启次数增加 | 5 分钟内 restart 计数上升 | P1 |
| 6 | 备份任务失败 | 单次失败即报 | P1 |
第 6 条最容易被忽略,但它是唯一能救命的告警——备份连续失败三个月,只有在需要恢复的那天才知道。
其余所有指标一律降级为看板,不推送。想看的时候自己看。
三个告警反模式
反模式一:阈值拍脑袋。 CPU 超 80% 就报,业务高峰天天响。正确做法是看趋势和持续时间:
- alert: HighCPU
expr: 100 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 > 85
for: 10m # 扛过瞬时尖峰
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} CPU 持续高于 85%"
runbook: "https://wiki/runbook/high-cpu"
for: 是被低估的字段。加一行 for: 10m,能过滤掉八成的误报。
反模式二:所有告警一个级别。 全标 P1 的结果是全都不看。至少三级:
| 级别 | 定义 | 响应 |
|---|---|---|
| P0 | 用户不可用 / 数据风险 | 立即(含夜间) |
| P1 | 部分功能受损 / 容量告急 | 30 分钟内 |
| P2 | 需关注但不影响用户 | 次日工作时间 |
反模式三:告警没有 runbook。 半夜收到「MySQL 连接数过高」,然后呢?
写 runbook 的过程本身就在验证这个告警是否值得存在——如果写不出处理步骤,这个告警大概不该报。
七、时间管理:把碎片救火变成批量建设
这一节是兼管模式的核心,也是绝大部分技术文章不提的部分。
分清救火和建设
SRE 里有个概念叫 toil(杂务):重复、可自动化、不产生长期价值的工作。专职团队建议控制在 50% 以下,兼管模式要压到 30% 以内,否则你会陷在「天天救火、没时间修根因、继续救火」的循环里。
判断一条工作是不是 toil,问三个问题:重复吗?能自动化吗?做完之后系统变得更好了吗?三个都是「是、能、没有」,就是 toil。
每周留一个固定时间块
哪怕只有两小时。这个时间只做建设性工作——写脚本、补监控、清技术债,不处理当天的告警和需求。
没有这个时间块,你永远只会做「今天必须做的事」,系统就永远停在「勉强能跑」的状态。它的作用不是完成多少工作,是保证建设这件事不被无限推迟。
维护一张技术债清单
按「出事概率 × 影响面」排序,每周挑一件处理。清单长这样:
| 事项 | 出事概率 | 影响 | 预估耗时 | 状态 |
| --- | --- | --- | --- | --- |
| 数据库没配从库,单点 | 中 | 全站不可用 | 4h | 待办 |
| 日志没轮转,磁盘会满 | 高 | 全站不可用 | 30m | 本周 |
| 发布脚本没有回滚分支 | 中 | 发布失败无法快速恢复 | 1h | 待办 |
| 证书 60 天后到期 | 高 | 全站不可用 | 10m | 已加提醒 |
这张表的价值在于:它把「隐隐觉得有问题」变成了「可排序的任务」。焦虑来源往往不是工作量,是不确定。
每次救火后补一条改进项
24 小时内,把这次故障对应的一条改进项写进清单。不写,同样的火三个月内一定再烧一次——而且你会发现自己第一次的处理过程完全没留下记录。
八、兼顾模式的 12 周节奏
按每周 4~6 小时排,也就是每周挤两个晚上。不要指望一口气做完,那样必然半途而废。
| 周次 | 投入 | 目标 | 验收标准 |
|---|---|---|---|
| 1~2 | 6h | 资产清单、拓扑图、crontab 盘点、备份现状确认 | 能说清每台机器上跑什么 |
| 3~4 | 6h | 中间件收敛:能托管的迁走,剩下的改 systemd/compose | 重启任意一台机器,服务自动起来 |
| 5~6 | 4h | 脚本仓库 + MR 审批 + 发布 SOP 成文 | 完整走一遍「提交 → 审批 → 发布 → 回滚」 |
| 7~8 | 6h | 拨测上线 + Prometheus + 应用指标接入 | 人为停掉服务,10 分钟内收到告警 |
| 9~10 | 4h | 六条最小告警 + runbook + 分级 | 连续一周假告警少于 3 条 |
| 11~12 | 4h | 备份恢复演练 + 技术债清单 + 复盘模板 | 真实恢复一次,数据完整 |
| 之后 | 每周 2h | 固定运维时间块,清技术债 | 救火占比降到 30% 以下 |
第 7~8 周的「人为制造故障」必须做。 监控搭完不验证等于没搭——你以为它会响,实际上规则写错了、webhook 配错了、告警被静默了。
我在这个问题上吃过亏:监控搭好三个月,第一次真出事,发现 Alertmanager 路由里 severity 写成了 Severity,一条告警都没发出去。三个月里我一直以为「有监控」。
九、几条用事故换来的经验
1. 变更三板斧:备份、灰度、回滚。 三样缺一样就别做。尤其「回滚」必须提前验证——很多人以为换个 jar 就能回滚,结果数据库结构已经变了,回滚包起来直接报错。
2. 不要在周五下午发布。 不是迷信。周五下午发布意味着出问题要带着整个周末的压力排查,而且找不到人商量。同理适用于节假日前、大促前、你自己很困的时候。
3. 一次只改一个变量。 同时改配置、重启、换网络,问题消失了也不知道为什么消失,下次还会再来。
4. 所有操作留痕。 命令写进脚本,脚本进仓库,执行有日志。手敲的东西三个月后自己都想不起来。
5. 敬畏 rm、iptables、chmod -R、kill -9。 这四个命令我现在的习惯是:先 echo 出完整路径看一眼,再执行;rm -rf 一律用绝对路径且变量带 :? 保护。
6. 出事后先恢复,再复盘。 别在现场分析根因,用户还等着。恢复手段可能是粗暴的——重启、回滚、摘流量。根因留给事后。
7. 复盘不追责,但要有结论。 每份复盘至少产出三样:直接原因、根本原因、可落地的改进项(带负责人和截止时间)。没有改进项的复盘等于写作文。
小结
兼管运维和转岗运维,路径完全不同。
转岗要补知识面:网络、内核、存储、编排,越深越好。兼管要补的是判断力:哪些必须自己做,哪些花钱买掉,哪些干脆不做,哪些做了能省下未来十次救火。
所以顺序是:
- 先做减法——把维护面积压到最小
- 摸清家底——知道有什么、在哪、怎么恢复
- 管住变更——让不可逆操作有审批和回滚
- 建好观测——在用户之前发现问题
- 守住时间——每周留出建设的时间,否则永远在救火
最后一句:你不需要成为运维专家,你只需要成为那个**「线上不需要你天天在」**的后端。做到这一步,养家糊口的工作和守夜的焦虑才算分开。