Java 后端兼管运维:用每周几小时把线上守住

白天写需求,晚上看告警。

发布只能挤在下班后,因为白天要开会、要改 bug、要联调。出了问题没有专职运维兜底,群里第一个被 @ 的是你。想系统学一遍运维,翻了两页《鸟哥的 Linux 私房菜》,第二周就放弃了。

这就是「兼顾」的状态。它和转岗是两回事——重点不是你要懂多少运维知识,是你能投入多少连续时间。

专职运维可以花两周搭一套 ELK,可以研究一周末的 K8s 网络模型。你不行。你的时间被切成一个个两小时的碎片,中间还要随时被打断。所以照搬运维教程的路径一定会失败:不是学不会,是排不进日程。

兼管模式下唯一的正确姿势是做减法:能买的不自建,能自动的不手动,能不做的先不做。用最少的维护面积,换最大的可用性。

下面按「先做减法 → 摸家底 → 管变更 → 建观测」的顺序写,最后给一份按小时排的 12 周节奏。

零、做减法:三条原则和三个不能省

原则一:能托管就别自建

这不是偷懒,是算账。

组件自建一次性投入自建持续成本托管方案
MySQL4 小时(安装 + 主从 + 参数)备份、升级、慢查询、磁盘告警,每月约 2 小时云 RDS / PolarDB
Redis1 小时内存、淘汰策略、持久化,每月约 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 + 脚本
全链路追踪 + 日志 + 指标全家桶同时上一次上一样,上一样用起来一样

判断标准:这个东西能否在未来一个月内,帮我减少一次故障或节省一小时。答不上来,就先放进待办,别放进日程。

但有三个东西不能省

减法不能减到裸奔。这三样是底线,宁可别的都不做:

  1. 备份,并且验证过能恢复
  2. 监控,至少能在用户之前发现问题
  3. 回滚,且提前演练过一次

一、把家底摸清楚

兼管的第一周不要装任何软件。先花两个晚上做两件事。

资产清单

一张表把所有机器、域名、证书、账号记下来。云控制台只告诉你「有什么机器」,不告诉你「这台机器上跑着什么」。

| 主机 | 公网 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,安全组一开就等于把数据库挂到公网上——这是比弱密码更常见的事故来源,扫库机器人几分钟就能找上门。

自建中间件的最小必要参数

不求调优,只求不出事。以下是我认为必须显式设的项:

中间件必设参数不设的后果
MySQLinnodb_buffer_pool_size(物理内存 50%~70%)、max_connections、slow_query_log=1、binlog_expire_logs_seconds=604800缓冲池不设性能差一个数量级;binlog 不设过期会撑满磁盘
Redismaxmemory + maxmemory-policy、appendonly yes、requirepass、bind 127.0.0.1不设上限会吃光内存被 OOM Killer 杀掉
Nginxworker_connections 10240、client_max_body_size、proxy_read_timeout、日志轮转上传 413、后端慢响应 504,都是默认值造成的
RabbitMQvm_memory_high_watermark、磁盘告警阈值内存触顶后阻塞生产者,表现为「应用莫名卡死」

部署后 checklist

每装完一个中间件按表过一遍,别靠记忆:

  • 配置文件在版本控制里(或至少有 .bak)
  • 数据目录在独立数据盘,且有备份任务
  • 只监听内网/本机,公网端口已确认关闭
  • 有强密码,且不在代码仓库里
  • 开机自启已验证(重启一次试试)
  • 日志有轮转,不会被写满
  • 已接入监控(端口存活 + 关键指标)
  • 有一条能跑通的备份恢复记录

最后一条最容易被跳过,也最要命。没验证过的备份等于没有备份。

四、变更管住:脚本审核

这是我最想强调的一节,因为它和你写代码的习惯直接冲突。

线上事故里,误执行 SQL 的比例远高于代码 bug。代码有 CI、有 review、有测试,但一条 UPDATE 往往是「群里说一声,然后在客户端里手敲回车」。兼管模式下你既是执行者又是审批者,更容易省掉这一步——所以流程必须轻到你不觉得麻烦,否则一定会绕过去。

按投入选方案

投入方案能力
半天Git 仓库 + MR 审批 + 脚本执行有留痕、有 review
1~2 天Yearning 或 ArcherySQL 自动审核、工单、执行审计
需要合规审计JumpServer 堡垒机 + 数据库审计命令级审计、录屏、权限最小化

先上第一层,立刻能挡住八成的低级错误。第二层等到「团队变成三个人以上」或者「老板要审计」再考虑——一个人的团队上堡垒机,成本收益完全不成立。

第一层的目录结构:

db-changes/
  2026/
    20261030_add_order_index/
      up.sql          -- 变更
      down.sql        -- 回滚
      README.md       -- 目的、影响面、预计耗时、验证方法

脚本本身的四个属性

  1. 幂等:重复执行结果一致。CREATE TABLE IF NOT EXISTS、DROP INDEX IF EXISTS。
  2. 可回滚:down.sql 和 up.sql 一起写,不能事后补。
  3. 有边界:UPDATE 必须带 WHERE,先 SELECT COUNT(*) 验证影响行数;DELETE 单批不超过 1000 行。
  4. 可预演:先在从库或预发环境跑一遍,记下耗时。超过 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
25xx 错误率> 5% 持续 5 分钟P0
3P99 延迟超基线 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~26h资产清单、拓扑图、crontab 盘点、备份现状确认能说清每台机器上跑什么
3~46h中间件收敛:能托管的迁走,剩下的改 systemd/compose重启任意一台机器,服务自动起来
5~64h脚本仓库 + MR 审批 + 发布 SOP 成文完整走一遍「提交 → 审批 → 发布 → 回滚」
7~86h拨测上线 + Prometheus + 应用指标接入人为停掉服务,10 分钟内收到告警
9~104h六条最小告警 + runbook + 分级连续一周假告警少于 3 条
11~124h备份恢复演练 + 技术债清单 + 复盘模板真实恢复一次,数据完整
之后每周 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. 复盘不追责,但要有结论。 每份复盘至少产出三样:直接原因、根本原因、可落地的改进项(带负责人和截止时间)。没有改进项的复盘等于写作文。

小结

兼管运维和转岗运维,路径完全不同。

转岗要补知识面:网络、内核、存储、编排,越深越好。兼管要补的是判断力:哪些必须自己做,哪些花钱买掉,哪些干脆不做,哪些做了能省下未来十次救火。

所以顺序是:

  1. 先做减法——把维护面积压到最小
  2. 摸清家底——知道有什么、在哪、怎么恢复
  3. 管住变更——让不可逆操作有审批和回滚
  4. 建好观测——在用户之前发现问题
  5. 守住时间——每周留出建设的时间,否则永远在救火

最后一句:你不需要成为运维专家,你只需要成为那个**「线上不需要你天天在」**的后端。做到这一步,养家糊口的工作和守夜的焦虑才算分开。