今年三月的一个凌晨,一点二十,值班同事在配置中心把一个限流阈值从 0.5 改成了 50,本意是给压测放开一点余量。改完没重启,配置热加载生效,五分钟后网关开始大量报 502。从告警响起到定位到那一行配置,用了三十七分钟;从定位到回滚完成,用了九分钟。
那三十七分钟里,大部分时间花在确认到底改了什么。配置中心有变更流水,但只记录键和值,没有 diff,没有生效时间,也没有和发布单关联。值班的人只能把这段时间改过的十几个键挨个看一遍,靠猜。
这件事之后,我把配置从配置中心的一部分挪出来,当成代码管:有版本、有快照、有回滚脚本、有演练。结论先说清楚:回滚快不快,不取决于手速,取决于改之前有没有留下能一键还原的版本,以及回滚之后有没有办法确认真的恢复了。
代码改动要过评审、过流水线、过测试环境,配置改动往往一个人点几下就生效了。没有评审,没有灰度,甚至没有通知,出了问题才发现没人知道这个值是谁改的、为什么改。
风险被低估,是因为大家默认配置属于小改动。实际上限流阈值、超时时间、开关状态这几类配置,改错一个就能让整个入口不可用。它们的共同点是影响面大、回滚窗口窄,和一行核心代码的破坏力没有区别。
很多配置中心保留变更记录,但记录的是流水,不是快照。出问题时想回到昨天下午三点那一版,得自己把流水倒着推一遍,还得判断哪些键是同一批改的。
更麻烦的是配置之间存在依赖。超时时间和重试次数要配套改,连接池大小和数据库上限要配套改。单独回滚其中一个键,可能让系统进入比故障时更奇怪的状态。快照是按批次的,一次还原一整批,才能保证配置之间的相对关系不变。
凌晨被叫起来的人,未必是改配置的人。他需要知道三件事:上一版是什么、怎么让它生效、生效后看哪个指标确认。这三件事如果都要靠问人,十分钟的窗口根本不够。
回滚脚本的价值就在于把这三件事固化成一条命令。命令里包含取哪一版、写到哪里、调什么检查。人被叫醒的时候脑子不清醒,能复制粘贴一条命令,比让他回忆流程可靠得多。
配置放在一个独立仓库,目录结构是环境、服务、文件三层。每个文件头部带版本说明,变更走合并请求,和代码一样的流程。合并之后由流水线生成快照,快照目录名带时间戳和提交号。
快照存两份:一份在对象存储,一份在应用机器的本地备份目录。网络故障时本地那份能救命,对象存储那份用来做异地留存。两份都要能按时间列出,不能只留一份。
快照不是简单复制文件,要带三样东西:每个文件的内容校验和、变更人、变更说明。校验和用来确认回滚时取到的文件没有损坏或被截断;变更说明用来在事后复盘时知道当时想解决什么问题。
这三样写进一个清单文件,和配置放在同一个快照目录里。回滚脚本先读清单,再动配置,清单缺失或校验不通过就直接退出,不做任何写入。这个顺序不能反,否则出问题时会把坏文件写进现网。
下面这段是实际在用的脚本,做四件事:定位快照、校验完整性、备份当前配置、写回并触发健康检查。
import hashlib, json, shutil, subprocess, sys
from datetime import datetime
from pathlib import Path
ROOT = Path("/opt/conf")
SNAP = ROOT / "snapshots"
LIVE = ROOT / "live"
def digest(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def list_snapshots(limit: int = 10) -> list:
dirs = sorted([d for d in SNAP.iterdir() if d.is_dir()], reverse=True)
return [d.name for d in dirs if not d.name.startswith("before-")][:limit]
def rollback(service: str, snap_name: str) -> int:
src, manifest = SNAP / snap_name / service, SNAP / snap_name / "manifest.json"
if not src.exists() or not manifest.exists():
print("snapshot not found:", snap_name, service)
return 2
meta = json.loads(manifest.read_text(encoding="utf-8"))
for f in sorted(src.glob("*.yaml")):
if digest(f) != meta["files"].get(f"{service}/{f.name}"):
print("checksum mismatch:", f.name)
return 3
dst = LIVE / service
stamp = datetime.now().strftime("%Y%m%d%H%M%S")
shutil.copytree(dst, SNAP / f"before-rollback-{stamp}" / service)
for f in sorted(src.glob("*.yaml")):
shutil.copy2(f, dst / f.name)
print("rolled back", service, "to", snap_name)
return 0
def health(service: str) -> bool:
try:
return subprocess.run(["/opt/bin/healthcheck", service], timeout=60).returncode == 0
except subprocess.TimeoutExpired:
return False
if __name__ == "__main__":
svc, snap = sys.argv[1], sys.argv[2]
if rollback(svc, snap) != 0:
sys.exit(1)
ok = health(svc)
print("health ok" if ok else "health failed, keep observing")
sys.exit(0 if ok else 4)脚本里有一行容易被忽略:回滚之前先把当前配置复制到 before-rollback 目录。这一步的意义是,如果回滚的目标版本本身也有问题,还能再退回来。没有这一步,回滚就成了单向操作,第二次想改只能手写。
回滚能不能成功,还取决于代码怎么读配置。所有配置项都要有代码里的默认值,配置缺失时用默认值继续跑,不要让服务因为少一个键起不来。这一条在回滚时特别重要,因为旧版本的配置可能缺新版本才有的键。
反过来也要注意:新版本代码读不到旧配置里的新键,应该能正常降级,而不是报错退出。配置的向前兼容和向后兼容都要测,测试方法是拿三个版本的配置文件各跑一遍启动。
定位靠两样东西:告警里的错误码分布,和变更记录的时间对齐。那次告警在一点二十五分开始,把时间往前推十五分钟,这段时间内的变更只有一条,直接锁定。这一条能成立的前提是变更必须打时间戳,并且集中在一个地方记录,散落在各平台就对齐不了。
定位阶段很怕并行排查。两个人同时猜两个方向,各自改一处,到头来不知道是哪一处生效了。规矩是同一时间只允许一个人动配置,其他人在旁边看日志,不许动手。
回滚包含三步:确认目标快照、执行脚本、等健康检查。健康检查要设超时,脚本里给了六十秒,超时判失败。失败不自动重试,配置类故障重试可能放大影响,交给人判断更稳。
五分钟里真正执行写入的时间不到三十秒,其余都花在确认和观察上。这个比例是对的,回滚应该慢在判断上、快在动作上。反过来,判断很快、动作拖很久,说明脚本写得太复杂。
回滚生效不等于故障结束,要看三个指标回落:错误率、接口耗时、队列积压。错误率通常在一分钟内回落,队列积压要看处理速度,可能需要更久。观察期不做别的变更,避免两个变量混在一起。
确认稳定后再写复盘记录,记录三件事:故障时间线、根因、以及这次为什么没被流程挡住。复盘不追责,但要追流程。那次之后我们加了一条规则:限流、超时、开关三类配置的改动必须两个人确认。
演练的做法是:随机挑一个非核心服务,回滚到三天前的快照,看脚本能不能跑通、健康检查能不能过、日志有没有断。演练安排在业务低峰,提前通知相关人。
我们做过四次演练,两次发现问题:一次是快照目录权限不对,脚本读不到;一次是健康检查依赖的路径变了,检查一直失败。这两类问题在真故障时才会暴露,演练提前把它们挖出来了。
配置变更集中在晚上八点到十点,超过十点只允许回滚,不允许新改。值班表上写明谁有回滚权限,权限不按职级给,按熟悉程度给。凌晨被叫起来的人如果不会用脚本,给他权限也没用。
值班交接要交接未完成的变更。上一班改了一半的配置,下一班不知道,很容易在回滚时把两批改动混在一起。交接单上写清楚改了哪些键、验证到哪一步、还要做什么。
现在这套快照和健康检查,一部分放在搭贝的定时任务里跑,每小时比对现网配置和仓库里的版本,不一致就记一条告警,这样即使有人绕过流程直接改配置,第二天也能被发现。
工具只是兜底,关键还是变更要留痕。留痕的目的不是审查,是让下一个接手的人能在两分钟内搞清楚现状。这一点做到了,十分钟恢复才不是运气。
问:回滚脚本能不能做成自动触发?
答:可以做半自动。告警触发后自动生成一条待确认的回滚建议,把变更时间、候选快照、影响范围列出来,人点确认才执行。全自动的风险在误判:错误率上升未必是配置引起的,也可能是下游故障,自动回滚会把问题搞得更乱。
问:配置和代码一起发布,回滚顺序怎么定?
答:先回代码再回配置,或者把两者打进同一个发布单元一起回。怕的是代码回了、配置没回,新代码读到不认识的字段。我们的做法是配置里保留兼容默认值,代码先同时支持两版格式,稳定后再删旧字段。
问:快照保留多久合适?
答:按变更频率定。我们保留近三十天的每小时快照和近九十天的每日快照,再往前的只留每周一份。存储成本不高,检索成本才是问题,所以快照目录要能按时间和提交号两种方式查,否则找起来很费劲。
问:热加载和重启,回滚时选哪个?
答:看配置项是否支持热加载。支持的就热加载,恢复快;不支持的只能重启,要评估重启期间的容量。判断方法是查配置项的注册方式,启动时读取的只能重启,监听变更的可以热加载。拿不准时按重启准备,容量留够。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。