最难不过二叉树

大型项目管理经验分享

2024-11-27

最近在看《SRE Google运维解密》这本书,里面谈到了很多谷歌项目中的软件项目管理的经验和方法论,这些理论在我经历过的项目基本都有过具体实践,因此这本书阅读起来有种相见恨晚的感觉。本文会结合我在QQ音乐、全民K歌、梦幻端游这些大型成熟项目工作中积累的一些实际案例和经验,对比着《SRE Google运维解密》这本书给出的项目管理的建议,总结出我对大型项目管理的看法和实践。

团队必须将50%的精力花在真实的开发工作上

Google为整个SRE团队所做的所有传统运维工作(工单处理、手工操作等)设立了一个50%的上限值,另外50%的时间需要花在真实开发工作上。

这个想法应该在开发团队里落实。很多项目中,即使作为研发团队,也可能做不到50%的精力放在真实开发工作上。举个例子,以一周5天40小时工作为例,看看我们的时间都花在哪里去了:

  • 日常运营问题的反馈查证、日志报警的查证等 -- 10%;
  • 产品或者领导提出的数据导出需求,或者查看代码逻辑的需求 -- 5%;
  • QA相关需求跟进,其中包括BUG修复,QA环境问题查证和处理 -- 10%;
  • 日常线上问题定位、查证和修复,以及总结(线上出BUG不是每周必现的,看情况) -- 5%;
  • 产品需求开发 -- 40%;
  • 内部技术需求,比如一些内部平台建设、自动化脚本编写等 -- 5%;
  • 打字训练(团队特殊要求)-- 2%;
  • 产品体验(团队特殊要求)-- 10%;
  • 需求讨论会 -- 2%;
  • 代码评审会 -- 2%;
  • 周报月报编写 -- 1%;
  • 版本发布相关准备(一周一版)-- 5%;
  • 技术分享或者学习 -- 3%

这里真正的开发任务是:产品需求开发、内部技术需求以及QA相关需求,这些都是真正的开发任务,这里占比约为60%,这是我们研发团队实况,也就是说,一周时间,研发只有3天能进行需求开发,其余时间都在处理非研发相关的工作。如果这一周发生了线上bug要处理,可能要花额外的半天或者一天来处理,这样就会进一步压缩我们的研发时间。

做了一圈调研,大家最反感的工作是打字训练、产品体验、帮导数据查逻辑以及运营问题报警查证这几类,因为这些都是对个人无成长对职业生涯无帮助的任务。打字训练、产品体验是上面领导拍脑袋添加的额外任务,我认为每个团队多多少少都有类似的任务但又无法拒绝。导数据查逻辑算是突发的任务,虽然不是我们明面上要做的事,但是实际上根本不好推辞。运营问题报警查证就纯粹业务问题,对技术上帮助不大,对业务的理解倒是会有帮助。

这样的模式下,研发团队加班、开发进度延期、研发出来的产品质量不高是常有的事情,如果要解决这些事情,就是尽可能把一些无关的任务移除,腾出专门的开发时间给研发人员。这个可能要研发经理来调整,比如:

  • 打字训练和产品体验移除
  • 导数据,查代码逻辑的非正式需求请走工单,工单会按需调度,原则上不接受私活
  • 日常运营问题的反馈查证、日志报警的查证以工单或者值班的形式分配
  • 技术分享和学习不要强制参加,技术分享并不是每个人都对此主题感兴趣,按需报名即可
  • 周报月报不要采取邮件编写的形式提交,而是有一个专门的周报月报系统来提交,一些格式、套话直接模板已经写好,周报月报只需要按照模板一条条填进去就好

研发团队和运维团队的分歧

传统的研发团队和运维团队分歧的焦点主要在软件新版本、新配置的变更的发布速度上。研发部门最关注的是如何能够更快速地构建和发布新功能;运维部门更关注的是如何能在他们值班期间避免发生故障。

研发团队会更希望能高效率地迭代需求,可以更高效地发布新功能,希望在发布新功能时,流程上尽可能少或者没有任何拦阻。但是运维团队则希望一旦一个东西在生产环境中正常工作了,就不要再进行任何改动,稳定压倒一切,所以发布流程上会越来越多二次确认。

所以一定程度上两个团队的目标本质上是矛盾的,因为他们的职责不一样。所以开发团队某些人员为了提高效率,会铤而走险地绕过一些流程,并宣称不再进行大规模的程序更新,而是逐渐转为功能开关调整、增量更新,以及补丁化。采用这些名词的唯一目的,就是为了绕过运维部门设立的各种流程,从而能更快地上线新功能。

所以在团队管理里明确指出,绕开既定好的流程来发布代码是严格禁止的,这是一项严重影响绩效的事情,不会有任何妥协。

在每8~12小时的on-call 轮值期间最多只处理两个紧急事件

这个准则保证了on-call工程师有足够的时间跟进紧急事件,这样工程师可以正确地处理故障、恢复服务,并且要撰写一份事后报告。如果一次轮值过程中处理的问题过多,那么每个问题就不可能被详细调查清楚,运维工程师甚至没有时间从中学习。

这个在实际上很难操作,这个理论倒是可以作为理想目标来指引。实际项目中,每天值班出现的紧急事件是随机的,但是发版后的6小时内,是紧急事件出现的高峰期。如果有新活动在某个时间点上线,比如中午12点,那么12点也是线上问题出现的高峰期。我们的发版时间是每周二8点,所以大家都很怕周二轮到自己on-call,因为周二早上大概率会遇上多个紧急事件,甚至可能持续一天。

针对这些问题,我们会设置主on-call工程师和副on-call工程师两个角色,主on-call工程师是所有报警、线上问题的第一经手人,对本次on-call负责,该角色一般由初中级工程师担任。假设主on-call工程师没有响应报警,或者线上问题过多处理不过来,或者修复方案需要更多讨论,那么就会向副on-call工程师请求支援,副on-call工程师一般由高级资深工程师担任。

主on-call工程师一般都是按天排期,副on-call工程师一般按周排期,我们这样的实践也尽可能降低每个on-call轮值期间压力过大的问题。

100%不是一个正确的可靠性目标

对最终用户来说,99.999% 和 100% 的可用性是没有实质区别的,就算我们花费巨大精力将系统变为100% 可靠也并不能给用户带来任何实质意义上的好处。

这里的可用性一方面用于系统的可用性,也可以用于需求实现的可用性。如果单纯追求100%的可用性,如果为此技术团队的付出远远大于收益,那么这个是需要探讨的。很多情况下,99%完成需求,花费1天的工时,但是100%完成需求,需要3天的工时。那么这里就要权衡和妥协。

如果100% 不是一个正确的可靠性目标,那么多少才是呢?这其实并不是一个技术问题,而是一个产品问题。要回答这个问题,必须考虑以下几个方面:

  • 基于用户的使用习惯,服务可靠性要达到什么程度用户才会满意?
  • 如果这项服务的可靠程度不够,用户是否有其他的替代选择?
  • 服务的可靠程度是否会影响用户对这项服务的使用模式?

举个实际例子来回答上面的3个问题:假设产品要设计一个玩法排行榜,原本的设计这个是全服实时排行榜,数据实时刷新,玩家可以实时看到自己的最新数据和最新排名。因为之前有单服实时排行榜的实现,所以认为这个需求比较简单可以下周上线。

经过和技术团队讨论,发现要实现这个方案并非易事,需要额外的机器以及更复杂的系统来支持,这就不可避免地需要更多工时来实现。当产品策划发现自己的需求额外需要那么多资源,而且有可能导致上线时间推迟,经过权衡,自己会更关注能按时上线,用户体验能最大限度保证就好,最后跟技术团队妥协,需求设计调整为:排行榜数据一小时一刷新。

完备的预案手册能有效降低人工误操作的概率

当不可避免地需要人工介入时,我们也发现与“船到桥头自然直”的态度相比,通过事先预案并且将最佳方法记录在“手册(playbook)”上通常可以使平均失败时间降低3倍以上。

在出现线上问题后,此时人工介入处理问题,在巨大的时间压力和产品压力下,如果没有一份明确的处理方案,只靠直觉和经验,很容易出问题,这是很多事故总结后得到的经验。

我们实践下来发现,在处理紧急问题时,人都会难免感到很慌,如果没有明确实施方案的指导,很容易做出错误的判断或者根本没有排查思路。所以我们团队里建立了线上问题处理checklist(我们搭建了checklist平台),跟上面提到的预案手册本质上是一样的,checklist中记录的处理问题每一个步骤,完成之后手动打√表示已完成该步骤。实际操作起来,这样确实可以有效降低人工误操作的概率,老司机也减少了经验固化导致的误判,新人也不至于太慌而没有不知道如何处理。

当工作经常被打断,效率将很低

工程师能够被分心的方法太多了,即使工程师当天不是on-call工程师,也可能会被各种事务打断自己的工作节奏。很多时候项目进度延期时,我们会回顾我们的工作安排是否不合理,同时也会梳理之前的工作排期,以及统计员工真实花费在项目的时间有多少。但是,很多情况下,如果工程师没有刻意记录自己每一分钟的工作内容,那么很可能出现自己这段时间很忙很忙,但是又说不出在忙什么的尴尬情况。这里举一个我们项目中的真实案例说一下这个问题。

小李的星期一正常上班,他今天不是on-call,也不负责处理中断性事务。他计划今天能专心高效地进行他自己的项目工作,按照自己的进度,今天就可以将本项目开发完成并且提测。

但是,今天发生了这些事情:

  • 小李的团队使用随机工单分配系统,某个需要今天处理的工单被分配给了他。
  • 小李的同事目前负责on-call,收到了一个紧急警报。发出警报的组件小李最熟悉,所以这个同事过来咨询意见,因为这个事情不是三言两语可以解析清楚,因此小李和这位同事一起线上排查问题。
  • 某个工单的优先级提高了,需要加急处理,本来这个工单排期是3天内处理好就行,小李本来安排明天开始处理,然而现在就要处理了。
  • 小李负责的功能出现了线上问题,因此他停下手上的工作马上去处理,因为问题比较轻微,因此很快就处理好了。
  • 产品过来咨询一个问题,同时希望小李帮忙导一下产品数据,因为小李为人和善,所以帮助了这位产品同事。

今天结束时,小李发现自己今天在处理非研发工作的事情上花费的时间很多,因此本来定的项目开发计划并没有完成。第二天研发经理问项目进度为什么延期了,小李因为没有梳理昨天每一件事情的时间花销,只能说是有很多额外事情在打扰他开发进度,但具体是哪些又说不清楚。

这是我们开发团队普遍遇到的问题。

为了限制干扰数量,我们应该减少上下文切换(指工作类型、环境等的改变)。某些中断性任务是无法避免的。然而,将工程师当成是可以随时中断、上下文切换没有成本是不正确的。给每次上下文切换加上成本的考虑。在项目工作中,一次20分钟的中断性任务需要进行两次上下文切换,而这种切换会造成数个小时的生产力的丧失。

为了避免这种经常性的生产力丧失,我们应该延长每种工作模式的时间。以上面小李的例子为例,我们分析了为什么他的效率会降低:

  • 在研发过程中,需要处理突如其来的工单。
  • 组员上的技术咨询(团队支援需求)
  • 产品同事的咨询需求
  • 自己的线上问题处理

我们可以从这几个方面来优化下:

  • 停止随机分配工单的策略。工单可以采取预分配的策略,比如工单可以提前分配而不是临时分配,这样可以让组员提前规划工作内容;工单处理应该由全职人员负责(比如组员轮值在某天专门处理工单),保障任何时间都有两个全职员工处理工单,不要将复杂分散到整个团队中去。
  • 如果某个项目非常重要,不能等待一周,那么这个人就不应该参与on-call。管理层应该介入,安排其他人替代on-call。管理层不应该期望员工在on-call的同时还能在项目上有所进展(或者其他高上下文切换成本的活动)。
  • 如果副on-call的工作仅仅是给主on-call工程师做后备,那么可以认为副on-call工程师能够在项目上取得一些进展。
  • 让组员合理拒绝产品/运营的个人需求。如其他同事确实有类似的需求,需要走工单系统,统一走工单系统调配。

当书写事后总结时,一定要记住这个文档的最佳受众可能是一个还没有入职的工程师

事后总结是持续改进的一个重要部分。我们团队都要求发生线上事故都需要在事后写总结文档,记录问题发生的原因、处理经过和造成的影响。一开始这些事后总结文档都写得比较好的,但是随着时间的推移,这些文档记录得越来越省略,只记录处理过程中最重要的步骤,其他的背景描述得越来越少了。因此这样的一份事后总结,如果给到其他的工程师,其实是没有太多参考价值的,唯一的价值是给当事人看。尤其是新工程师,在看事后总结文档时,如果文档没有描述好事故发生的背景以及一些表现信息,以及分析过程,这份文档对于他来说只能是个负担。所以后面团队开始对加强这些文档的规范,提供了事故文档的模板,指定了文档需要填充哪几点内容。

另外,这些事后总结如果被束之高阁是没有什么用的。每个团队必须收集,并且整理有价值的事后总结文档,将它们用于未来新手的教育材料。某些事后总结文档就是需要死记硬背的,某些事后总结文档可以作为“课程”进行教授,可以让学员了解大型系统的结构性弱点,或者新奇的故障方式,这些都是非常有价值的。

当学员掌握了所有的系统基础知识之后,我们会考虑将报警系统的警报复制一份给学员

当有新员工加入团队时,第一件事情是安排他们去学习团队所需要的基础知识,我们团队有一系列的课程和文档供新员工学习。另外,我们也会指定一个学习checklist,上面列举了新员工需要学习的知识点,新员工按照顺序一直往下学习就好了,事实论证了这样会很有节奏感。

当以上事情完成后,我们开始让新员工学习如何处理工单,我们认为处理工单是让新员工最快熟悉业务需求的途径。我们会考虑将报警系统的警报复制一份给学员而不是直接让新员工直接上手接警报工单。其实有了解到其他团队是直接给新员工实际工单来锻炼的,但我们认为最佳方式还是先跟着老员工一起处理问题比较好:老员工处理工单时,copy一份给新员工,老员工在处理工单的同时,新员工也在学着如何处理该工单。这样做的好处是,当新员工处理工单遇到问题时,会咨询老员工,此时老员工一般都已经处理完工单了,此时的提问不会额外占用他们太多时间,这些问题他们大都有思考过,而且能给出快速回应。这个模式下,新员工的心理负担也不会太大,因为他的提问不会对老员工造成太大的额外工作量,老员工的回答只是举手之劳而已。

如果老员工并不是处理该工单的负责人,那他还需要重新分析和定位该工单描述的问题,并指导该新员工,直到工单处理完成。这种方式下,等于一个工单处理2次,重复消耗了人力资源。但更为重要的是,这个模式下,新员工去找老员工请教问题,此时对于老员工来说,这种提问是个中断他手头工作的负担,此时他需要切换状态来回答新员工的问题,导致自己的工作进度落下,心态层面是比较消极的。

容量规划

容量规划简单来说就是保障一个业务有足够的容量和冗余度去服务预测中的未来需求。但是我们发现行业内有许多团队根本没有这个意识和计划去满足这个要求,这会导致很多线上服务不可用的问题。容量规划有几个步骤是必需的:

  • 必须有一个准确的自然增长需求预测模型,需求预测的时间应该超过资源获取的时间。这里可以理解为我们预计这个功能会有多少人使用,比如1000人使用的功能和100万人使用的功能,从系统设计上就不一样的。比如我们从数据统计得出我们这个功能每天有10万人次的使用增长,那么我们就会根据该数据做新的容量规划。
  • 规划中必须有准确的非自然增长的需求来源的统计。这个可以理解为一些商业推广或者活动发布带来的流量增长,比如我们营销团队会去开启一些拉新活动,或者代言人广告活动,此时进入我们产品的人数就会增多,这个是可以预测的,但是具体有多少增长,那就没法具体说出来数量了,只能说个大概,此时我们也会对容量进行一定程度的扩容。
  • 必须有周期性压力测试,以便准确地将系统原始资源信息与业务容量对应起来。

大概 70% 的生产事故由某种部署的变更而触发

基于统计,大概 70% 的生产事故由某种部署的变更而触发。我们在这里踩过的坑也不少,项目里很多流程规定都是事故后建立的,我们在此积累了不少经验。

之前遇到过一个P0生产事故,这里分享一下。事情发生在某个冬至的傍晚,公司给我们提前2个小时下班,我们兴高采烈地去打火锅去了。吃着吃着,大家的手机响个不停,报警信息一直在响。一看具体信息,25台服务器同时宕机了,这是我们项目20年来第一次宕机这么多台服务器。我们十分困惑,如果是软件bug,不可能会同一时间宕机的。后面确定了原因,原因十分好笑:运维在删除一些废弃服务器上的文件,但是没有把正式服务器列表排除,因此把正式服务器上的关键文件也删除了,所以就发生了多台服务器同时宕机的现象。

变更管理的最佳实践是使用自动化来完成以下几个项目:

  • 采用渐进式发布机制 -- 灰度发布机制
  • 迅速而准确地检测到问题的发生 -- 监控系统机制,包括机器接口等粒度指标的监控,也包括日志报警监控
  • 当出现问题时,安全迅速地回退改动 -- 回滚机制,出现问题后,可以第一时间回滚为前一个版本

以一个案例实践介绍变更的风险是怎么关在笼子里的:

  • A项目某员工直接发布了错误的配置文件到生产环境,根据项目规定,他需要做灰度发布,所以他的配置文件先发布到1台外网机器上。
  • 按照流程,灰度发布后需要等待15分钟,观察监控以及外网反馈,以确定本次发布是否正常。
  • 5分钟后,报警群开始推送报警信息,某个服务的调用成功率已经降到了10%,另外有用户反馈说某个功能点击没有反应。
  • 该员工意识到可能是自己配置的问题,因此重新查看了自己的修改,确定本次改动有bug。
  • 该员工立刻选择回滚,将配置文件回滚到上一个版本并发布,然后继续观察各种指标。
  • 回滚后,服务指标恢复正常。

硬件故障是常态

因为一个集群中包括很多硬件设备,每天硬件设备的损坏量很高。谷歌那边一年内,一个单独集群中平均会发生几千起物理服务器损坏事件,会损失几千块硬盘。我们的项目没那么多机器,不过基本每个月都会有1到2起硬件故障发生,经常都能看到SRE半夜爬起来,处理类似的报警,一般都是硬盘故障,主板过热这些。所以,我们一定要接受硬件故障是常态,我们不能抱着侥幸心理不去做这方面的预案。

硬件过保和淘汰是常见的事情,我们在这块有自己的服务器管理方法:

  • 我们的机器分为新机器、正常运行机器、过保机器;
  • 新购买的机器不会直接上线到生产环境,而是先部署服务上去空跑一段时间,排除可能潜在的硬件故障;
  • 新机器运行正常后,会被加入到正常运行机器中服务正式需求;
  • 机器是会过保质期的,我们称为过保机器,过保机器预示着硬件故障几率会更高,这类机器我们一般用于非核心功能服务,比如游戏中的鬼服(没什么人玩的服务器),即使硬件故障发生宕机,也不会造成广泛的用户影响。
  • 过保机器也可以转为内网研发机器,比如开发机或者内测服务器,即该过保机器只能作为承担用户量不多且承载非核心功能的服务器。

长尾效应

构建监控系统时,很多人都倾向于采用某种量化指标的平均值:延迟平均值,节点的平均CPU使用率,数据库容量的平均值等。但是使用这种指标来监控系统是不准确的,因为忽略了长尾效应。

image (10).png

考虑下图这个接口调用时间表,该接口的平均响应时间是13.9ms,99.9%的请求响应时间是195ms,最大响应时间是488ms,那么请问你会采取哪个时间作为监控系统中判定接口异常的阈值?

image (11).png

  • 如果采取平均响应时间,那么你会发现你的报警系统一定报警不断;
  • 如果你采取最大响应时间,那么系统低速运行了一段时间也还没触发报警;
  • 如果采取99.9%的请求响应时间,我认为是最合适的,因为基于统计,99.9%的请求都是在这个时间内返回的,如果发生大量响应时间超出这个时间报警,那么系统多多少少是有些问题了。

区分平均值的“慢”和长尾值的“慢”的一个最简单办法是将请求按延迟分组计数(可以用来制作直方图):延迟为0~10ms之间的请求数量有多少,30~100ms之间,100~300ms之间等。比如下面的接口响应时间分布:

Percentage of the requests served within a certain time (ms)
  50%     47
  66%     51
  75%     54
  80%     55
  90%     61
  95%     71
  98%     71
  99%     71
 100%     71 (longest request)

降低琐事开销

琐事不仅仅代表“我不喜欢做的工作”。它也不能简单地等同于行政杂务加上其他脏活累活。每个人满意和喜欢的工作类型是不同的,有的人很喜欢手工的、重复性的工作。同时,一些管理类杂务是必须做的,不应该被归类为琐事:这些是流程开销(overhead)。流程开销通常是指那些和运维产品服务不直接相关的工作,包括团队会议、目标的建立和评估、每周总结以及人力资源的书面工作等。而脏活累活通常具有长期价值,这些也不能算作琐事。例如,为服务清理警报规则或降低噪声率可能是一件繁重的工作,但这些不是琐事。

琐事更准确的定义是,手动性的,重复性的,可以被自动化的,战术性,没有持久价值的工作。

  • 手动性:比如手动执行脚本来启动一些任务,比如执行A任务需要手动执行5个脚本,操作过程中可能还需要二次确认一些东西。
  • 重复性:如果某件事是第一次做,甚至第二次做,都不应该算作琐事。琐事就是不停反复做的工作。如果你正在解决一个新出现的问题或者寻求一种新的解决办法,不算作琐事。
  • 可以被自动化的:如果计算机可以和人类一样能够很好地完成某个任务,或者通过某种设计变更来彻底消除对某项任务的需求,这项任务就是琐事。如果主观判断是必需的,那么很大程度上这项任务不属于琐事。
  • 战术性的:琐事是突然出现的、应对式的工作,而非策略驱动和主动安排的。处理紧急警报是琐事。我们可能永远无法完全消除这种类型的工作,但我们必须继续努力减少它。
  • 没有持久价值:如果在你完成某项任务之后,服务状态没有改变,这项任务就很可能是琐事。如果这项任务会给服务带来永久性的改进,它就不是琐事。一些繁重的工作—比如挖掘遗留代码和配置并且将它们清理出去也不是琐事。

如果组员反馈琐事太多,这个问题一定要重视。如果你在花在工程项目上的时间太少,你的职业发展会变慢,甚至停滞,没有人可以通过不停地做脏活累活满足自己的职业发展。换句话说,如果现在的工作没有成长性和挑战性,那么就会产生离开团队的想法。

所以团队一定要平衡好每个组员琐事的工作量,比如上文提到的一定要保证组员有一定比例的时间放在工程项目上,这样才能提升士气,团结队伍。

报警太多等于没有报警

当系统无法自动修复某个问题时,需要一个人来调查这项警报,以决定目前是否存在真实故障,采取一定方法缓解故障,最终找出导致故障的根源问题。我们团队认为,报上来的每一个报警都是有意义的,都需要有人来跟进,如果是无意义的报警,那就必须调整这个报警的逻辑,比如达到某个阈值才报或者累计到某个数量才报。

紧急警报的处理会占用员工的宝贵时间。如果该员工正在工作时间段,该警报的处理会打断他原本的工作流程。如果该员工正在家,紧急警报的处理则会影响他的个人生活,甚至是把他从睡眠中叫醒。我们对此太有感悟了,一开始我们兴致勃勃地上了一个报警系统,但是告警过滤规则没有定义好,值班工程师查看了很多报警都是无意义的报警,这样十分浪费工程师的时间以及打击他们的士气。更为重要的是,紧急警报出现得太频繁时,员工会进入“狼来了”效应,怀疑警报的有效性甚至忽略该警报,有的时候在警告过多的时候甚至会忽略掉真实发生的故障。好长一段时间里,群里的报警根本无人响应,即使是值班工程师,也会选择忽视,理由是之前出现过类似的报警,查证过没问题,这个报警跟上次的很类似,所以不用看了。

由于无效信息太多,分析和修复可能会变慢,故障时间也会相应延长。高效的警报系统应该提供足够的信息,并且误报率非常低,因此我们花了很多时间来整顿这个报警系统,后面确实可以做到报上来的都是有意义的报警,且每个报警都会有人响应跟进。

监控系统的4个黄金指标

监控系统的4个黄金指标分别是延迟、流量、错误和饱和度。

  • 延迟:服务处理某个请求所需要的时间。这里区分成功请求和失败请求很重要。例如,某个由于数据库连接丢失或者其他后端问题造成的HTTP 500错误可能延迟很低。计算总体延迟时,如果将500回复的延迟也计算在内,可能会产生误导性的结果。但是,“慢”错误要比“快”错误更糟。因此,监控错误回复的延迟是很重要的。
  • 请求数:对Web服务器来说,该指标通常是每秒HTTP请求数量,同时可能按请求类型分类(静态请求与动态请求)。
  • 错误:请求失败的速率,要么是显式失败(例如HTTP 500),要么是隐式失败(例如HTTP 200 回复中包含了错误内容),或者是策略原因导致的失败(例如,如果要求回复在1s内发出,任何超过1s的请求就都是失败请求)。
  • 饱和度:服务容量有多“满”。通常是系统中目前最为受限的某种资源的某个具体指标的度量。(在内存受限的系统中,即为内存;在I/O受限的系统中,即为I/O)。这里要注意,很多系统在达到100% 利用率之前性能会严重下降,增加一个利用率目标也是很重要的。

系统故障后尽最大可能让系统恢复服务

当你收到一个错误报告时,接下来的步骤是弄明白如何处理它。问题的严重程度大不相同,有的问题只会影响特定用户在特定条件下的情况(可能还有临时解决方案),而有的问题代表了全球范围内某项服务的不可用。你的反应应该正确反映问题的危害程度:对大型问题,立即声明一个全员参与的紧急情况可能是合理的,但是对小型问题就不合适了。合理判定一个问题的严重程度需要良好的工程师判断力,同时也需要一定程度的冷静。

在大型问题中,你的第一反应可能是立即开始故障排查过程,试图尽快找到问题根源。虽然这个是很多人的处理做法,但是我们认为这是错误的。正确的做法应该是:尽最大可能让系统恢复服务,也就是让系统重新跑起来比停机修复更为重要,停机越久影响面越大。

这个我们产品项目是这么实践的:某个功能发生异常时导致了进程crash,我们首先分析crash现场,结合日志和最近的提交记录来定位问题,并尝试修复。如果是一些很浅显的bug,我们会选择快速修复后重新发布,然后重启进程。但是对于一些难以定位的bug,我们一下子没有修复思路,那么就得先停用这个功能,产品策划会给出一些提示语告知用户该功能正在维护。通过临时关闭部分功能以保证整体功能可用可以防止问题进一步扩大,然后研发团队再集中精力定位问题的根源。

如果一个Bug有可能导致不可恢复的数据损坏,停止整个系统要比让系统继续运行更好

这个想法可能有点反直觉,但是我们确实是这么实践的。数据就是产品的底牌,所以保证用户数据的完整可用非常重要。假设我们的代码出现了bug,导致用户数据在某一刻开始就开始写错,那么此时最好的办法就是立即shutdown。

我们工程团队都清楚,修数据的工作量远大于修bug的工作量,我们试过3天研发团队什么研发工作都不做,只修数据。对于产品策划而言,功能暂时不可用可能只会被用户埋怨,停用时间不长可能只是P1事故;但是写坏了用户数据需要做数据回档,那么用户就可能会爆炸了,这个是妥妥的P0事故。

紧急事故的处理流程

当发生紧急事故时且暂时无法定位到原因时,我们有一套完整的流程来应对:

  • 职责分离,成立事故处理小组,明确每个人的角色职责。在事故处理中,让每个人清楚自己的职责是非常重要的。明晰职责反而能够使每个人可以更独立自主地解决问题,因为他们不用怀疑和担心他们的同事都在干什么。
  • 设立一个“作战室”(war room),将处理问题的全部成员挪到该会议室办公,我们认为集中当面讨论和当面共享进度是最高效地沟通方式,有任何想法或者信息都可以直接说出来,这样我们可以快速地达到思路一致。
  • 我们需要一个对外的发言人,因为事故发生后,其他团队或者产品、上级领导都会过来问故障的影响面和修复进度,这里需要一个人做发言人来处理类似的回复工作,保证处理问题的人可以专心工作。
  • 明确公开的职责交接,很多时候问题并不能在短时间内解决,负责人可能需要暂时的休息,此时负责人的职责能够明确、公开地进行交接是很重要的。当前事故总控负责人必须明确地声明:“从现在开始由你负责事故总控,请确认。”当前事故负责人在得到明确回复之前不得离开岗位。交接结果应该宣布给其他正在处理事故的人,明确目前的事故总控负责人。

谨慎使用重试

当请求失败后,如果鲁莽进行重试,那可能造成重试风暴,进一步把自己的系统打垮,连锁反应导致各个系统都瘫痪。

当发送自动重试时,需要将如下部分考虑在内:

  • 一定要使用随机化的、指数型递增的重试周期。
  • 限制每个请求的重试次数。不要将请求无限重试。
  • 考虑使用一个全局重试预算。例如,每个进程每分钟只允许重试60次,如果重试预算耗尽,那么直接将这个请求标记为失败,而不真正发送它。这个策略可以在全局范围内限制住重试造成的影响,容量规划失败可能只是会造成某些请求被丢弃,而不会造成全球性的连锁故障。
  • 从多个视角重新审视该服务,决定是否需要在某个级别上进行重试。这里尤其要避免同时在多个级别上重试导致的放大效应:高层的一个请求可能会造成各层重试次数的乘积数量的请求。
  • 使用明确的返回代码,同时详细考虑每个错误模式应该如何处理。例如,将可重试错误和不可重试错误分开。当服务过载时,返回一个详细的信息,这样客户端和其他层可以加大延时,甚至不再重试。

压测的思路

从理论上预测服务会以什么方式进入故障状态是很困难的。你应该针对服务进行压力测试,通过对过载下的服务行为的观察可以确定该服务在负载很重的情况下是否会进入连锁故障模式。

有些理论指导是可以借鉴的:

  • 测试直到出现故障,还要继续测试。知道系统过载时如何表现可以帮助确定为了修复问题所需要完成的最重要的工程性任务。最不济这种知识也能够在紧急情况下帮助on-call工程师处理故障。
  • 在这个压测临界点上,组件理想情况下针对多余的负载返回错误或者降级的回复。但是不应该显著降低它成功处理请求的速率。设计良好的组件应该可以拒绝一小部分请求而继续存活。
  • 测试非关键性后端,以确保它们的不可用不会影响到系统中的其他关键性组件。
  • 我们还应该测试一个组件在过载之后再恢复到正常水平的行为状态(多久能恢复正常,期间系统的表现)。