我们每天要进行大量的线上变更操作。怎么保证这些操作安全,不会导致故障,是我每天都在思考的问题。
这篇文章从工作经历总结一些原则和想法,希望能有帮助。
线上操作有几点基本的要求:
- 操作需要是可以灰度的 (Canary):即能够在一小部分范围内生效,如果没有问题,可以继续操作更多的部分;
- 操作必须是可以验证和监控的:要知道自己操作的结果,是否符合预期;
- 操作必须是可以回滚的:如果发现自己的操作不符合预期,那么有办法能够回到之前的状态;
逻辑很简单:假设我一开始做操作范围很小,可以灰度,做完之后我可以监控是否符合预期,如果不符合预期就回滚,那么,操作就是安全的。
这三步中的每一步看似很简单,但是实际做起来很难。
灰度
发布过程是最简单的一种灰度场景,现在有蓝绿发布模式:
- 分成两组,将所有的流量切换到绿组;
- 先发布蓝组,此时是没有流量的,发布完成之后,将流量逐渐切换到蓝组;清空绿组;
- 然后发布绿组,发布完成之后将流量切换到平均到两组
还有滚动发布,对于每一个实例:让 Load Balancer 不再发给它新的流量,然后升级,然后开始接收流量,如果没有问题,继续以此处理其他的实例。
几乎每个人都可以理解灰度的必要性,但是不是每一种操作都是可以灰度的。
比如说数据库 DDL 的变更,很难灰度,提交到数据库,数据库就开始应用了;还有一些动态配置系统,一些全局配置,如果修改,就对所有的应用同时生效的;一般都是这样一些数据源类型的变更,很容易出现不支持灰度的情况。不可以灰度的情况也是最容易导致问题的。
一个替代方案是,搭建一套一模一样的环境,在这个环境先应用变更,测试一下是否符合预期。但是在今天分布式环境下,很难模拟出来一模一样的环境,可能规模小了,可能测试环境没有一些用户的使用场景,等等。总之,模拟的环境没有问题,不能代表生产的环境就没有问题。
最好的解决办法,还是在软件和架构,从设计上就能支持灰度。
验证与监控
所有的操作,一定要知道自己在做什么,效果是什么。做完之后进行验证。听起来很简单,但是实际上,很多人做事像是闭着眼睛,不知道自己在做什么,做完之后有什么效果也不管。
验证操作的结果
举一个例子,比如目前网关遇到了什么问题,经过查询,发现和 Nginx 的一个参数有关,然后根据网上的内容修改了这个参数,回头去看问题解决了没有。如果没有,继续在网上查资料,看和什么参数有关。
上述操作,一个潜在的问题是,当问题真正修复了之后,我们不知道自己做了啥才修复问题的。也有一些时候,相同的配置变了名字,实际上这个修改这个参数是可以解决问题的,只不过我们用了从网上得到的过时的参数名字,所以不生效。
所以,对于每一个操作,推荐直接去验证目前的操作结果。比如改了一个 log 参数,那么直接去看这个参数是否生效,是否符合预期,然后再去看其他的问题是否得到解决。
做操作要一步一步来,做一步验证一步。
另外,最好去验证操作的副作用,而不是验证操作本身。比如,修改了一个配置,不是去 cat
一下配置文件确认就可以了,而是要去看自己修改的配置是否真的生效了。比如路由器设备,我们执行了一些命令 ip route ...
,验证的方法并不是 show running-config
去看配置是否有这一条,而是要去看 show ip route
确定配置是否生效。比如修改了 postgres 数据库的一个配置,重启数据库,并不一定意味着配置生效了,你可能修改了一个错误位置的配置文件,验证的方法应该是进入到 postgresl 数据库中,然后执行 SHOW ALL
命令,校验配置是否是预期的。
验证核心的业务指标
除了验证操作结果之外,也要关注业务指标是否还正常。
如果业务指标不正常了,而恰好和自己的操作时间吻合,那么就应该立即回滚。
听起来很合理?但是实际上,很多人(我也是)第一反应都会是,我的操作不可能引起这个问题,让我先看看日志,到底发生什么了。
当发生问题的时候,时间很宝贵,正确的做法是第一时间在群组里面宣布自己的操作(事实上,操作之前就宣布了,但是消息太多,没有问题的时候没有人会认真看操作历史),然后开始进行回滚。可惜的是,我发现这么做的人很少,大部分都是想去排查,直到确定是自己的操作导致的,才开始回滚。
回滚
回滚方案至关重要。有了能够工作的回滚方案,在出现未预期的问题的时候,我们可以不需要调查根因就直接触发回滚操作,最大限度减少损失。
但是同上,不是所有的操作都可以回滚的。一些可以补偿的方案有,操作上尽量设计成可以回滚的(有些废话)。比如,DDIA 这本书就介绍了数据上如何做向前兼容和向后兼容的方法。
举个例子,比如软件新版本的一个配置要从名字 A 改成 B,不要直接改,而是添加一个配置 B,代码里面可以读 B,如果没有的话,尝试读 A。等升级完成之后,在下一个新版本中,去掉 A 的逻辑。这样,每两个版本之间都是兼容的。
一次只做一个操作,不要将多个操作合在一起
如果将多个操作合在一起,上面的灰度、监控和回滚都不好做了。不知道问题是哪一个变更造成的。
原则上一次操作只做一个修改。
操作计划和操作记录
一些复杂的操作,比如修改 DNS,配置网关,配置其他东西,可能是联动的。而且现实中也不是所有的东西都适合自动化的。这些复杂的操作,推荐在操作之前就写好操作计划,然后对着一步一步操作,贴上必要的验证结果和操作时间。万一出现什么异常,就可以将异常出现的时间和自己的操作记录对照,很有用的。操作计划也可以相互 review,如果是 gitops 的话,就更好了。
将参数尽量写在操作流程,而不是操作的时候
比如在操作某一个节点的时候,一种方案是,直接在操作的 pipeline 中输入 IP,然后执行操作。但是 IP 很容易输入错误,看到这个 IP,也不会反应过来这到底是哪一台机器。所以,更好地一种方法是,给这个 IP 一个名字,我们使用名字操作,而不是指定 IP。
比如在 Ansible 的 inventory 中,我们可以这样写:
1 |
webserver_us_1 ansible_host=10.0.0.1 |
这样,我们在操作的时候,输入的参数就是 webserver_us_1
了,输入一个 IP。(我已经见过很多个因为搞错测试和生产的 IP 而导致的线上事故了)。
另外如果要操作多个机器,可以将 Host 按照区域,灰度步骤等,进行编组。这样在操作的时候,直接指定编组,而不是临时将机器进行分组。
使用像 Jenkins 这种平台的时候,执行 Job 的时候,最好将参数变成选择项,而不是 text 输入,减少错误的可能性。
总结来说,就是尽量将操作的时候需要填写的选项降低到最小。将操作固化下来,review 这些操作,进行测试,测试没问题之后,merge 到主干。这样,对操作步骤就可以更有信心。
操作的每一步都设计成可以失败的
为了进一步防止在操作的时候选错参数的情况,需要在设计操作流程的时候多花点心思才可以。
操作的每一个都应该设计成可以失败的,在大多数的实例是可用的情况下,少数实例的重启甚至宕机应该不引发问题才对。这样,即使在操作的入口选择错了参数,本想执行测试环境却执行了线上环境,那么也不会有什么大问题。
举一个例子,服务升级的步骤,不好的写法是,直接将服务的 binary 下载到执行目录,然后 reload;比较好的写法是,将服务的 binary 下载到一个临时目录,下载完成,校验 checksum,然后设定权限,最后通过 symbolic link 链接到可执行的位置。后者的好处是,无论在哪一步失败,都不会造成问题。
理想的情况下,所有的步骤无论执行或者失败都不应该造成影响。比如,保证所有 merge 到主干的代码都是可以部署的,即使不小心触发部署,也不应该造成很严重的事故。这样,即使「手抖」也不应该造成问题,SRE 的生活将会充满阳光,而不是每天提心吊胆。
Ad-hoc 命令
我们经常需要 ssh 登录机器来运行一个命令做检查。对于敲下的命令,一定要知道这些命令的原理。
举几个不当操作的例子:
- 有同事使用过 Vim 查看日志。这是不正确的,用 Vim 打开日志文件(通常很大)会将文件读入内存,导致机器的内存不足,OOM-killer 开始杀掉业务进程。在机器上处理文本,需要使用 grep, awk, sed 这种「流式风格」的软件,或者使用
less
这种使用文件 offset 风格的软件; - tcpdump 运行的繁忙的机器上,对性能是有影响的;
- 有些命令行看起来是读操作,但是也是会对性能造成影响的,比如 RAID 卡控制器;
mount
主机的/etc/hosts
文件进 docker container,如果使用 Vim 修改 Host 的文件,docker 中的文件不会改变(要验证操作结果而不是验证操作本身,不然,就不会发现问题)。因为 Vim 编辑文件默认会修改 inode(:h backupcopy
), 应该用echo
来修改;
一个小技巧之避免整点操作
如果公司比较大的话,那么不可避免的,很多操作都是在同时进行的。可能有人在进行发布操作,有人在修改配置。如果造成了故障,就很难知道故障是谁导致的。
所以我倾向于避开和其他人同时进行操作——选择一些看起来不是整齐的时间。比如 15:13, 16:27 这样的时间。当发生故障的时候,可以快速判断和自己的操作的相关性。
另外一个原因是,公司的业务可能在整点触发一些发劵,通知,促销等,所以这样做也可以避开业务高峰。
不过,还是最好要选择在工作时间进行线上操作。如果是非工作时间,出了问题,就会加大发现问题的时间(可能造成的问题你发现不了,但是其他的业务方会发现),也会加大找到相关负责人的时间。
效率即是安全
这是 Last but not least! 操作的效率至关重要。
我认为运维平台要设计成简洁,没有歧义,流程清晰的,非必要不审批。这可能跟直觉相反,尤其是领导的直觉。
领导(不知为何)觉得审批流程越多越好,出了事故就开始思考在哪一个阶段可以加上一个审批流程,来避免类似的问题发生。但其实,我觉得流程越多,出问题的概率不减反增。
程序员天生就不喜欢繁重的流程,如果流程太重,就会出现其他的问题,比如,人们会想办法绕过不必要的流程;会想办法“搭车发布”(意思就是将多个操作合并成一个,这也是违反原则的,一次应该只做一个操作);对于明显出现异常苗头的时候,因为不想重新走审批而铤而走险。
但是出现这种情况,领导不会觉得流程有问题,领导会觉得你小子不按照流程办事,开除。
最后导致 SRE 的幸福感很低,事情还是要那么多,完成工作不得不铤而走险,还得责任自负。
事实上,真正能保证安全的是架构设计简单,做事的人知道自己在做什么,操作按照如上灰度、验证,出问题回滚,而不是靠流程。SRE 之间 Review 是有价值的,审批是没有价值的,大部分的审批仅仅是请示一下领导而已,领导可能看不懂操作的后果是什么。
所以,流程是有代价的。
赞
操作计划也可以相互 revew,少了i
感谢, 已经改正。
吃我一记三板斧
抓个虫,逻辑很简答,应该是很简单
感谢,已改正。
在第 4 个小节的「操作计划和操作记录」中的第一段,有个 typo:
一些复杂的操作,比如修改 DNS,配置网关,配置其他东西,可能是联动的。而且显示中也不是所有的东西都适合自动化的。显示中 => 现实中。
(无恶意,只是为了便于后面的读者看文章)
感谢!已改正。真心感谢指出错误的读者!
> 举一个例子,比如目前网关遇到了什么问题,经过查询,发现和 Nginx 的一个参数有关,然后根据网上的内容修改了这个参数,回头去看问题解决了没有。如果没有,继续在网上查资料,看和什么参数有关。
> 上述操作,一个潜在的问题是,当问题真正修复了之后,我们不知道自己做了啥才修复问题的。也有一些时候,相同的配置变了名字,实际上这个修改这个参数是可以解决问题的,只不过我们用了从网上得到的过时的参数名字,所以不生效。
这段说得深有体会,最好是从官方源头,如文档,Wiki,邮件列表,Issue 区去获取资料,尤其关注信息的时效性,如果这是一个创建于 359 天前的文档或文章,其中的信息可能已经有所发展或是发生改变。
还有一个踩过的陷阱是,ChatGPT 这类 LLM 特有的幻觉,像模像样地编造一些似是而非的参数,对于 AI 生成的答案还是要抱着怀疑的态度去验证,不能盲目相信。
是的,一步一步验证操作的结果,很重要。
不是在变更就是在写变更计划流程
写的太棒了!我也是一名 SRE。我想把您这篇文章转载到我的公众号《咸鱼运维杂谈》可以吗?之前有跟您联系过,那会转载了您之前的文章《SRE 的工作介绍》
你好,可以的。
非常感谢 ! :)
「SRE 之间 Review 是有价值的,审批是没有价值的,大部分的审批仅仅是请示一下领导而已,领导可能看不懂操作的后果是什么。」这个深有同感,赞,很多时候审批是在形式,具体审批什么很少有人关注