网站高可用架构设计——运维

从公众号转载,关注微信公众号掌握更多技术动态

---------------------------------------------------------------

一、灰度方案

1.灰度的基本概念

(1)灰度方案引入

一个较大的业务或系统改动,往往会影响整个产品的用户体验或操作流程。为了控制影响面,可以选取一批特定用户、流程、单据等,只允许这一部分用户或数据按照变更后的新逻辑在系统中流转,而另一部分用户仍然执行变更前的老逻辑。这一步是线上系统灰度方案的起点。

将用户按照特定规则分隔为两类之后,主要需要关注命中灰度的这部分用户,是否按照预期执行了新逻辑、产生了符合预期的数据,以及系统整体的变化等。此阶段即灰度观察阶段,线上验证工作也是其中的关键步骤。随着系统中使用新逻辑的用户、订单等数据的逐步累计,即可证明新系统的正确性、有效性,那么更多的用户就应当被迁移进新逻辑中,这一阶段一般称作灰度推进。灰度推进有时是小流量验证后立即切全量的,也有需要逐步放量的,这需要结合实际业务&系统能力做出决定。

最终,全部用户被纳入到新逻辑的范围内,此时需要决定是否将灰度逻辑本身和系统中的老业务逻辑同步下线,全部用户仅可以使用新逻辑,此时即灰度完成。也有由于历史数据原因,长期无法完成全量灰度切换的,此时业务系统中将会长期驻留两套逻辑。

(2)灰度在解决什么问题

一个变更如果在发布后立即全量上线,那么如果出现系统、逻辑、数据等问题,将会是灾难性的,比如全部用户无法创建新订单、全部新订单出现脏数据等,甚至有可能会影响到变更前的数据。灰度过程就是在规避变更过程中这个最大的风险:全局影响。通过减小影响范围,再配合灰度线上验证、监控报警等手段,将出现问题时影响面,控制在有限的范围内,如减少订正的数据量或降低资损金额等。

(3)灰度会带来什么风险

①首先是如何发现灰度过程中的问题

这与上线过程中的监控报警有一定的相似性,二者主要都是依赖日志&监控&报警规则的建设和配置;但二者又存在一定的差异,如报警阈值如何配置才能有效发现小流量异常?灰度名单外的老逻辑会不会触发新逻辑的监控报警?灰度系统影响的上下游是否也有对应的灰度监控?这些问题都可能影响灰度问题能否被发现与发现问题的时效性。

此外,对灰度系统要重点关注资损风险。资损字段在上线前一定要做好核对的保障,或者至少应当在灰度开始阶段之前完成,尤其是对新变更引入或影响的资损字段,要做到全覆盖,“无核对不上线”。灰度过程中还可以协同客户、运营、产品等多条线的同学做好布防,及时感知处理相关舆情,使用非技术手段作为问题发现的兜底与补充。

②其次,如何控制灰度中问题的影响面

灰度过程中产生的灰度数据,不能侵入非灰度数据,反之亦然,要确保二者的充分隔离。但是灰度系统需要与上下游联动,灰度本身也需要推进,一旦遇到问题,还需要进行灰度停止、灰度回退等更复杂的操作,因此灰度整体是一个动态的过程,而在整个动态过程中,需要严格保持灰度数据&非灰度数据的隔离,否则将会导致问题影响面扩大化,危及整个系统,甚至发生严重故障。

这里尤其需要注意的是灰度停止与灰度回退的复杂性:如果灰度停止手段不能生效,那么问题影响就无法得到有效控制;灰度回退则需要涉及阻断灰度流程、修改已有灰度数据、修复错误数据等,一般来说是整套灰度方案中最复杂的部分。

③最后,发生问题时的处理也会比较复杂

生产系统往往没有太多的资源或条件进行AB-test,灰度与非灰度数据都是真实的业务数据,一旦出现问题,并不能通过删除灰度数据或脏数据的方式解决问题,一般需要进行数据订正,或发布新的变更进行修复。数据订正的数量、订正数据的正确性、如何甄别灰度用户、如何保证新变更的正确性、如何保证新变更可以有效修复问题数据等,都是恢复过程中的难点工作与潜在风险。

2.灰度设计要解决的基本问题

(1)灰度维度的选取

生产系统中常见的灰度的规则,有用户id尾号、业务单据id尾号、白名单、黑名单、时间戳等。

  • 白名单常用于线上测试,如使用测试账号等进行单独的验证。这种方式不适合单独使用,因为无法快速扩大灰度范围,但是推荐与其他方式联合使用,增加灰度过程的灵活性。

  • 黑名单则是一种兜底手段,可以对特殊用户(如数据量特别大用户、重点客户等)进行屏蔽,减少或避免其受到灰度的影响,尤其是在灰度过程出现问题时,直接阻断其进入系统中的问题逻辑。

  • 采用用户id尾号或业务单据id尾号作为灰度key,是更常见的灰度区分方式。但如何选取这类灰度key,需要注意几个要点。

  • 第一,选取的key应当是均匀分布或近似均匀分布的,如集团的havanaId等,否则全量用户无法分批分散的命中新逻辑,灰度的逐步放量的能力就失去了作用,极端地,整个灰度能力会退化为布尔化的全局开关。这里容易犯的错误并不是使用了全部相同的灰度key,而是误认为某个id是均匀分布的。举例来说,某单元化应用中如果使用用户id的后四位作为灰度key,那么很可能会出问题,因为用户id已经是用于区分单元化的标记了。常见的id的生成本身是随机的,但触达业务系统时,可能已经带有某种特定的规律了,因此需要对此类情况做好识别与防范。

  • 第二,计算key的逻辑需要尽量简化。系统中使用灰度key来判别走新逻辑还是旧逻辑,这个条件判断一般会在系统中反复出现、多次执行,此时如果设计特别复杂计算方式,则会给系统带来额外的开销。除此之外,简化key的计算逻辑也会带来业务语义上的简化,便于整个业务链上的技术同学与非技术同学快速理解,也便于遇到问题时快速定位与排查,更有利于系统的长期维护。

  • 第三,要结合业务实际选取。如果选取一个当次变更新增的业务字段作为灰度key,那么上下游系统是否需要做同步改造?离线数据&报表是否需要配合改造?如果选取一个对下游业务未记录的或无意义的字段呢?这些都是通过合理设计可以节省的改造成本。

因此在选取灰度key时,需要选取上下游业务已有的、通用的、具有业务意义的字段。

(2)简化灰度逻辑

灰度逻辑仅仅是将一个用户或单据非此即彼的区分开,因此灰度逻辑不仅没有必要做的太过复杂,而且还应当尽量简化,如果业务上有条件,最好能用一个字段或一个变量搞定。

首先,有利于完成灰度进度的调整,如灰度推进,灰度暂停等,可以通过单变量的调整快速完成,否则一次性调整几个灰度变量,会出现灰度推进情况不符合预期、灰度覆盖不全,灰度数据不一致等复杂问题。比如同时调整用户id覆盖范围与订单创建时间,则可能导致一部分用户被跳过,也可能导致调整后的灰度范围远超预期等问题。其实这类问题在实际生产中是最常见的,回想一下,每次在灰度推进或灰度暂停等进度调整时,是不是都需要多人共同监督灰度脚本,反复确认发布内容?甚至在加入了如此重的流程之后,仍然不能达到百分百的无问题。

其次,开始灰度后,灰度数据往往错综复杂,如果需要多个条件协同判断,对问题定位则是不利因素,甚至可能会导致误判。还用上面的用户id+时间戳的例子来说,原本是灰度逻辑出错时产生的数据,可能被误判成由于时间未到而走旧逻辑产生的数据,这种复杂性导致的误判将会严重影响线上问题的止血与处理效率。

最后,对可灰度的用户或单据,应当宽进严出,适当提升灰度准入的门槛,这样做有利于将大部分数据快速的排除到灰度范围之外。因为总体而言,当我们决定采用灰度方案去推动变更时,我们总是抱着对系统悲观的态度,防止潜在的问题快速扩大化。因此在初始阶段让尽可能少的数据走到新逻辑,可以给我们留出时间做人工数据校验、监控报警有效性校验、核对有效性校验等等工作,防止第一波灰度用户出问题时,直接演变成大问题。那样的话,就完全失去了做灰度的意义。

减少灰度变量和灰度命中宽进严出二者并不矛盾,前者一般是动态的、配置在开关内供应用读取的,后者一般是静态写在代码中的固定条件。举例来说,某一个变更使用用户id作为灰度变量,但初期应当设置仅对某等级以上的用户开放的门槛。

(3)灰度数据如何初始化

灰度最好是可以从0启动的,就是说无需事先通过数据订正或批量触发的方式修改初始数据,而是通过某个真实的业务请求来触发,比如用户下单等。这里常见的做法是,当业务请求中的数据命中灰度之后,在创建对应的DB记录时,打上特殊的标记,用以标识灰度命中。如果有必要的话,还可以单独建立新的表,在DB中写入一条新记录就代表相关的用户或单据命中灰度。

这种方式的优点就是0启动,无需前置数据准备流程,但问题是整体的灰度进展可能会变慢。因为在上线前产生的部分部分线上数据已经被确定为仅能走旧逻辑,想要进行全量灰度后的灰度逻辑下线,一般来说,只能等待业务数据自然关闭。

举一个简化的例子,灰度启动前已经付款的订单走旧逻辑,如果不对这部分订单数据做处理,那么只能等待这部分订单全部确认收货,才能对灰度逻辑和旧逻辑进行整体下线。而在实际的生产系统,还要考虑退款、计费等等相关流程,所以等待的周期只会变得更长。

但有些灰度方案并不能简单的通过请求中携带的数据进行灰度初始化,还需要对全量的用户数据做一次初始化。比如将线上A系统中的数据,按一定规则导入本次变更涉及的B系统中,作为灰度过程的数据准备。这样做的好处有两个。

  • 第一是可以在一些场景中简化灰度门槛的判断,即可认定所有的数据全部符合某一个前提条件,节约一次判断。而且这次查询一般会是一个查库操作,而使用全量业务数据去查库,常常会出现DB性能问题,甚至会出现由于灰度数据的分布问题导致分布式DB出现单库单表的热点,这里的DB问题不做深入。总之这个方案可以有效减轻甚至规避此类问题。

  • 第二就是在业务上可以加速整体的灰度进度,缩短从灰度开始到全量的周期,有时出于业务的考量,我们可能不得不选择这个方案。

但这样做的缺点也是明显的。举例来说,比如数据初始化的方案是从A表导入B表,那么首先需要对数据迁移的逻辑进行经过额外的验证工作;之后进行迁移数据时也需要占用一定的项目周期;还要在设计中考虑迁移数据过程AB系统的数据一致性如何保障,比如迁移数据的过程中,A系统有产生了新的业务数据,要迁移吗?还是迁移时要对A表的部分记录加锁?或者甚至停掉A表对应的服务?真的需要停服务的话,那这也太不互联网了。

(5)灰度过程中保持数据一致性

灰度过程往往会从前一个业务步骤开始,随后才会影响下一个业务步骤。举例来说,同一个用户在t时刻命中了灰度规则,并在写表时打标命中灰度;而在之后的t+1时刻,发生了一个需要更新表记录的操作,但由于灰度回退或其他原因,导致没有命中灰度规则,这时要怎么判定?这类问题其实就是灰度数据一致性的问题,也是灰度设计中最核心的问题。

  • 原则1:以已有的灰度命中数据为准。在很多业务场景下,前一步写表后一步更新的操作是非常常见的,创建时打标无需多言,更新时的基本的判断原则应当是将已有的灰度数据作为判断标准,而不是以灰度key是否命中为判断标准。即后一步更新操作时总是以查DB的结果为准:DB中记录为灰度命中,那么就要执行新逻辑,否则按照灰度未命中的旧逻辑执行。

  • 原则2:优先考虑灰度推进过程中数据的一致性。当灰度推进时,更多的用户或单据会被纳入到灰度命中的范围内,因此要考虑此部分数据能否进入新逻辑。举例来说,以用户当月账单id尾号为灰度规则,那么用户的当月账单一旦被打标为灰度命中,后续账单再次更新时,也一定要遵循新逻辑;而在账单创建时如果为灰度未命中,那么这笔账单将会一直保持旧逻辑直到结清。这个原则与前一条有一定的相似性,但核心关注的是灰度进展导致的灰度key命中情况在创建和更新两个阶段发生了变化,这时一般仍要遵循以DB记录打标为准的原则。另一方面,灰度推进前已命中灰度的数据,要确保在灰度推进后仍能命中灰度。这是一条不言自明的规则,在确保数据一致性的基础上,只有这样才能被称为灰度推进。但在实际操作推进的过程中,有时会因灰度开关配置错误等原因违背了这一规则,因此可以考虑对配置项进行一定的防错设计。此外,灰度推进过程中,还需要关注集群内各机器开关数据数据的一致性。首先要确保变更后的灰度开关值被推送到集群内的全部机器中,其次为了灰度推进时间的一致性,一般会在灰度开关内加入一个生效时间戳,避免开关推送延迟可能带来的问题。

  • 原则3:如果需要快速推进灰度,可以尝试在第一个灰度维度全量后,再开始另一个灰度维度。上述原则中提到,更新数据时发现记录创建未打标的,即使灰度key已被命中,仍应使用旧的业务逻辑。但是这样做,整体的灰度进展将会被拉到非常长,比如确认收货后90天内都可以发起退款,那么是否要等到4个月之后才能全量切到新逻辑?业务上允许这样做吗?对上述这个例子可以这样做,就是当订单创建已经全量灰度后,那么就可以理解为创建已经全部切入新逻辑,此时继续在付款或确认收货操作时进行灰度打标,这样仍然可以保持一次仅对一个变量进行灰度的原则。

这里给出几个不是很理想的快速推进灰度的做法。

  • 同时对多个灰度维度做推进。这是上文在讨论简化灰度逻辑时,就力图避免的一种设计。与其这样做,还不如在验证充分之后,对第一个灰度维度直接全量,之后再推进第二个灰度维度。

  • 在多个入口同时进行灰度打标。这个方式看上去可以加速消除数据创建时未打标的记录,但多个入口打同一个标,出现问题的时候怎么排查原因?更新时要不要覆盖创建时的标记?灰度暂停时如何同步停止打标?总之这是个复杂度高且验证工作量大的方案。

  • 手工数据订正。既然要做数据订正,还不如在灰度启动前就做一轮,这样在整个灰度方案开始时就可以获得收益。灰度进展中做订正,成本更高,收益却更低,从ROI角度看很不划算。灰度设计的过程中,不要轻易尝试去推翻上述这些简单的原则,因为越是简单基础的原则,其影响就越大,对这些原则的改动,往往会造成前述的设计被全盘推翻。

当然,也存在一些只创建记录,而不再更新的业务,这种业务考虑的重点往往不是灰度推进,而是下面的灰度暂停&回滚策略。

(5)灰度暂停与灰度回滚

①灰度过程务必具备整体暂停能力,也即灰度熔断。

灰度熔断不要求对已经进入灰度的数据进行纠正,而是只需要不继续产生更多的灰度数据即可。为什么灰度不继续推进了还不行,还需要加入一个这样的开关?下面举个例子。使用用户id尾号作为灰度key,已有n个用户进入新逻辑时,发现DB侧出现了瓶颈需要修复。这时业务层的应用有三种应对方式可选。

  • 第一,立即缩小灰度范围或对代码进行回滚。这是不可取的,已经命中灰度进入新逻辑的用户,往往不能再轻易的回退到旧逻辑中。

  • 第二,不继续推进灰度,也不操作灰度开关,放任系统继续运行。这也是有风险的,因为现阶段只有n个用户进入新逻辑,但按照用户群体总数*灰度比例测算,可能还有m个用户即将命中灰度进入新逻辑,甚至m>>n,如果DB问题不能在用户大量进入之前修复,整个系统将面临灾难性的后果。

  • 第三,灰度熔断。不操作灰度开关,但停止新用户命中灰度规则,即目前有n个用户进入新逻辑,那么稍后即使有m个用户命中灰度规则,也仍然不能进入新逻辑,这样就可以确保在DB问题得到修复之前,系统保持现状继续运行。

②可操作的灰度回滚方案,才是有意义的灰度回滚方案。

一般来讲,希望可以做到灰度的可观测、可回滚。但前文反复讲述的这类灰度方案中,避免出现数据一致性问题,对业务来讲才是更重要和更安全的。出现问题时机械的执行回滚,反而会造成更大的影响,而使用灰度暂停能力进行快速止血,并积极修复问题,反而是更合适的。那么回到灰度回滚方案中来,在保证数据一致性等原则的前提下,可以设计一个合理的回滚方案吗?

工程中的资源往往都是有限的,不可能把大量的时间和精力,投入到高度复杂的回滚方案中去。因此对于灰度回滚方案,有一些比较负面的结论:

  • 灰度回滚方案的复杂性如果难以控制,那么正确性也将难以验证;

  • 复杂的设计将带来开发周期和测试周期的延长,对业务的伤害可能更大;

  • 就像应急预案需要提前演练一样,没有人敢在线上直接使用未经验证的回滚方案,最终做了也是白做。

所以建议仅在模型上更为简化的业务中,才去考虑设计完整的灰度回滚策略。

3. 更完善的灰度方案

(1)具备良好的可测性

在项目本身的业务复杂度之上,再叠加灰度引入的技术复杂度,此时如何进行完备的测试就成了一个不小的挑战。我们需要明确,可测性问题是需要在设计中认真考虑的问题。让系统内的数据流动&状态迁移都是可观测的,把请求、处理数据的过程值、开关值、分支判断结果等信息明确的、无遗漏的持久化到日志或DB中,尤其是灰度是否命中、灰度判定规则等关键信息;而不要让复杂的系统变成一个黑盒,只有起始的输入和最终的输出。否则的话,在调试和测试的阶段,都要花费大量的沟通成本,甚至可能埋下无法被发现的缺陷。

对于日志的处理,应当尽量保持上下游的一致性。最好的代码是自解释的,最好的日志也应当是自解释的。上游的系统如果使用了一个灰度标记,则下游的系统应当使用相同的标记;如果有下游有业务语义的变化,可以新增一个字段,而不是将上游的同名字段覆盖或清除。这样在跨多个系统或团队进行联调或处理问题时,大家对同一个标记或概念都持有同一个理解,这是对提效非常有帮助的。举个具体的例子,比如上游命中AA规则后记录了AA=true的日志,下游根据AA规则衍生了BB规则,那么记录日志时可以保留AA字段的信息,再额外记录BB=true。

对于落库的数据的处理,则要考虑可核对性方面的问题。数据在跨系统传递时,应明确各个系统中的业务主键是什么,下游系统要将上游系统的主键或唯一键落库,如果条件允许,还要将上游传入的键值作为平铺字段,甚至为其在DB中建立索引。这样做的好处首先是为了后续建设核对方便,方便使用相同的唯一键查找上下游系统的关联记录。这也是为将来的系统扩展性做考虑,如果将来下游系统的下游还需要再接其他系统,此时通过上游的这个统一键值即可有效串联多个系统。典型的例子是将交易系统唯一id透传到下游的所有额度明细、账单明细、退款等系统中。

(2)关注全链路的压力

系统改造中需要关注对下游依赖的压力变化,灰度设计中也需要考虑这一点,尤其是系统压力随着灰度推进而改变的情况。一种典型的场景是随着灰度推进,传递到下游的请求越来越多。这种情况下,主要需要梳理下游请求的增长率,是随着灰度推进线性增长、对数级增长、还是指数级增长(指数的情况就很可怕了,极易引发故障)?此外,灰度推进结束之后,流量模型是相对稳定的、还是继续变化的?

实际业务中的情况并不都是这么简单,有的场景中,在灰度开始启动的时,才是下游流量最大的时候。随着灰度的推进,下游的流量反而会越来越小。举例来说,开始灰度后,全量用户都需要查询某服务,而命中灰度的用户可以通过其他短路方式规避这个查询。如果评估发现这种特殊情况,除了按照常规做出压力评估,也可以考虑对依赖方案做调整以规避这种反直觉的情况。

其他的可能性还会有很多,再举一个极端的例子:本月的流量是随着灰度推进逐渐上升的,经过N天后灰度推进至全量,流量保持稳定;但到了下月1日,业务数据需要重新生成,由于已经灰度全量,导致突然爆发出了非常大的流量,给下游系统带来很大的影响。这种极端的场景如果不能提前发现识别,并做出合理的应对,则可能引起意想不到的严重故障。

除了下游的业务系统,还要关注DB侧可能存在的瓶颈,业务系统一般可以快速地进行集群平行扩容以应对大流量,但DB的扩容就比较复杂了,可能会涉及到数据迁移、锁库、索引重建等操作,有些操作属于高危操作,如有不当,甚至会影响使用同库或同表的其他业务。当识别到这类问题时,需要提前与DBA联系,讨论合理的扩容方案,在灰度启动之前,预留充足的时间完成扩容。

当然,所有的压力评估,都可以用压测来进行检验。但是要把压测当做对设计成果的验收,而不是作为发现问题的兜底手段。即将上线之前才发现系统性的问题,可能为时已晚,强行上线或延期,成本都将是巨大的。

(3)灰度的进展与监控

首先,监控是灰度前期最重要的观察手段,建立完整全面的监控,对于上线初期、灰度开放初期、灰度放量初期的数据观察,都至关重要:上线初期重点要关注新代码下的旧业务逻辑是否能正常运行;灰度开放初期则要观察何时出现命中新逻辑的数据,以及进入了哪些业务分支;灰度放量期间则要观察流量的变化是否能与开关调整相匹配,报错量是继续处于低位、还是随之线性甚至更快速的增加。此外,灰度放量过程中所关注的监控,在后续灰度全量后也仍然需要持续观察,有些还要建立相应的报警规则。

其次,针对灰度方案的核对也有一些不同。常见的核对一般都是以上游系统对应的A表作为左表(即核对数据源),下游系统的B表作为右表。但是下游系统在灰度阶段会将上游数据做二分类,命中灰度的写B表,未命中的不写,此时建立核对就要反转过来,将下游灰度命中后写入的B表作为左表,反过来与上游A表建立核对,确保所有命中灰度的数据仍与上游保持正确的关联关系。

但这里又有一个问题,一个用户本应命中灰度,但却没有写入B表,如何发现这类问题?这里我提出一个解法,即仍然建立从A到B的核对,但在核对规则中加入灰度规则的等效条件语句,并随着灰度推进修改核对规则。但这样做,核对规则将会非常复杂,而且也对如何设计落库字段提出了更高的要求。

最终,还可以为整个灰度方案建立一个小型的报表用于速查,从落库结果的角度判断某个特定的用户或数据是否已经命中灰度。更进一步的,还可以在报表中展示灰度相关的聚合与统计数据,判断数据分布是否符合灰度推进节奏,下一步需要加快推进还是暂缓。借助这些数据,一来为技术侧同学在做答疑或问题排查时提效,二来可以向业务侧的同学提供灰度的整体数据或局部情况,以便做出更多业务决策。

(4)应急策略与修复手段

灰度推进过程中,可以通过各种方式和渠道获取来自系统和用户的反馈,包括但不限于监控、核对、用户咨询等。当发现不符合预期的、甚至存在严重问题的数据与场景时,标准的操作是先止血再修复:通用的止血方案可以是先将业务逻辑开关关闭、下线或回滚掉新逻辑的代码;随后在修复阶段对错误的数据进行订正。

安全生产应当摆在重要的位置,但工程的目标从来不是单一的。灰度系统的设计在这个部分上一定要有所取舍,并不能一味地从系统稳定性的角度出发贪大求全,而应当结合实际业务情况,平衡由于复杂度提升而引入的设计、开发、测试、运维成本,和对产品、用户体验的影响。

4.灰度方案的质量保障

(1)灰度基本逻辑

灰度命中的结果,不仅要是可预期的,还应当是稳定的。使用同样的数据与配置,不能在某次请求时命中灰度,另一次请求时却未命中灰度,否则将产生严重的问题。

(2)灰度命中后的持久化

命中灰度的数据有时还需要持久化到数据库中,在测试中除了检查灰度标记,还要检查新增的字段。如果灰度命中后写入全新的表,也要对全部字段进行完整的校验。落库的数据与上游有关联关系的,要检查记录是否一致,如果可行,最好推动开发将其设为平铺字段,方便在上下游间建立核对。单据号等字段具备唯一性的,要额外做幂等性测试,防止同一条灰度命中数据多次写入。

除了结果数据,过程数据也至关重要。判断灰度过程中可能经历了多个条件,那么需要将每个条件的输入值、判断结果值都打印在日志中,方便联调与后续问题排查。此外还要检查日志中变量名的唯一性与变量值的正确性,防止打印语义混淆的废日志。

(3)灰度兼容性

由于灰度过程中的请求会分为两部分,因此系统内应当对两类请求都有相应的处理能力,即在灰度全量之前,旧逻辑仍要保持可用。

新版本代码中的旧逻辑,在本质上已经和上一个版本的逻辑有所差异,因为上一个版本中是未经灰度判断,直接执行旧逻辑;而新版本代码中是多一层判断逻辑的。这层判断逻辑有时可能还会在入参上添加各类标记后,再进入系统的下游模块流转,并会引发更多复杂的情况。比如系统对未命中灰度的数据加入了一个属性,但下游流程判断有任意标记的流量都不再处理,那么这种情况下的旧逻辑就会受到影响。

如果灰度系统涉及多个应用,还要考虑应用间的兼容性。常见的测试要点包括:

  • 灰度系统是否影响了上下游的流程交互,如命中未灰度走A应用,灰度命中则不走A应用,这样对A应用的监控和核对是否会造成影响;

  • 灰度是否新引入了下游依赖,原有的依赖关系是否被解除或需要削弱,强弱依赖的设计是否合理,具体的依赖关系如何,是否引入了循环依赖,或者数据流是否构成回环;

  • 上下游应用均有改动时,在下游应用先行发布后,是否会影响尚未发布的上游应用。

(5)灰度推进

灰度从0开始,到部分覆盖,再到全量覆盖,这个灰度推进的过程也需要测试重点关注。首先是一头一尾的情况,灰度开关配置为全量老逻辑和全量新逻辑的情况下,请求的结果是否符合预期;其次是灰度推进的过程中,如果用户A在上一次请求时未命中灰度,但下次请求时由于灰度范围扩大而命中了灰度,那么用户A的请求能否正常处理?用户A能否按照预期被纳入或排除出灰度新逻辑的范围内?

最后还要评估灰度推进可能引起的兼容性问题,这里要关注的点是在灰度开关变化的情况下,动态的评估内部逻辑的兼容性,而这可能是上述静态的兼容性测试不能覆盖的点。这里需要结合实际业务与设计方案仔细分析,排除可能的、隐藏较深的、重现条件较为复杂的缺陷。举例来说,当月A用户第一次请求时未命中灰度,故写入一条不带灰度标记的记录,意味着本月A用户将不再命中灰度;当A用户第二次请求时,查询是否存在灰度不命中的记录时服务超时,且由于灰度推进导致A用户变为灰度命中,故又写入了一条带灰度标记的记录,导致库中同时存在两条业务语义存在相矛盾的记录。

(6)为灰度建立核对规则

为了保证新项目上线后及时发现线上数据可能存在的问题,最晚在灰度启动之前就要将相关的核对规则全部上线。在灰度项目中,常会出现灰度命中与未命中时落表不一致的情况。此时建立核对就要考虑如何选取左表。我们把系统的全量请求作为全集,把命中灰度的部分作为子集,那么灰度命中子集中的数据,必然要与全集的数据保持一定的关系。反之则不然,因为全集中还有部分灰度未命中的数据,无法与灰度命中子集中的数据保持一致。

举例来说,灰度系统位于下游,需要与上游系统进行核对,确保上游发来的请求全部被正确的处理了。这时就要用命中灰度之后的表作为左表,上游请求的表作为右表来建立核对。

此外,对于灰度未命中的部分也需要建立核对来保障一致性。这里的处理方式有两类:如果灰度命中只是新增落表,而不影响原有落表逻辑,那么可以先为旧逻辑做全量核对,即在灰度启动后,无论是否命中,都仍然应当遵循旧逻辑下的一致性约束;如果灰度命中后数据将会从旧表中迁走,只写入新表,就需要对灰度未命中的部分也进行上下游关系、子集全集关系的分析,然后选取子集作为左表建立核对,这与灰度命中的处理方式是相似的。

上述的原则可以帮助我们检查在灰度命中与未命中两种情况下,数据总是一致的,但是无法确保灰度命中与否这一结果的正确性。要想确保这一点,主要还是要依赖基本的功能测试,其次可以考虑在核对规则中引入与灰度规则等价的条件语句,并在每次灰度推进之后,同步修改这个条件语句。不过这种解法一般只能用在实时或准实时核对中,对离线数据的核对可能并不适用,因为离线表中历史数据所遵循的灰度规则,与当下的灰度规则可能是不一致的。如有需要,可以通过手工单次查询离线表,并结合灰度开关操作记录对结果进行判断。

最后,对于灰度系统的核对规则,我们还要适当提升时效性,因为从发现问题效率的角度讲,实时类型的核对是远优于离线与隔日核对的。灰度初期发现问题的概率更大,修复的成本也更小,但前提是能够及时的发现。

二、故障应急响应

故障应急响应是维持系统高可用的最后一个机会,这个环节的不专业表现,对于稳定来说是最后彻底的失守

1.解决流程

生产环境永远不允许调试问题,出现问题立刻回退,查问题要去测试环境。

(1)快速止血

问题排查的第一步,一定是先把血止住,及时止损。如何快速止血?常见方式包括:

  • 发布期间开始报错,且发布前一切正常(或者发布了不长时间后出错)?啥也别管,先回滚再说,恢复正常后再慢慢排查。

    • 回滚内容需要包含上一个版本的所有修改,不管该修改是否可能导致问题的产生。

    • 运维不应以开发意见为主导,应按照手册走规范的完整的回滚流程。

  • 应用已经稳定运行很长一段时间,突然开始出现进程退出现象?很可能是内存泄露,默默上重启大法吧。

  • 只有少数固定机器报错?试试隔离这部分机器(关闭流量入口)。

  • 单用户流量突增导致服务不稳定?如果不是惹不起的金主爸爸,请勇敢推送限流规则。

  • 下游依赖挂了导致服务雪崩?还想什么呢,降级预案走起。

(2)保留现场

下一步,就是要根据线索找出问题元凶了。作为一名排查老手,需要有尽量保留现场的意识,例如:

  • 隔离一两台机器:将这部分机器入口流量关闭,让它们静静等待你的检阅。

  • Dump 应用快照:常用的快照类型一般就是线程堆栈和堆内存映射。

  • 所有机器都回滚了,咋办?别慌,如果你的应用监控运维体系足够健全,那么你还有多维度的历史数据可以回溯:应用日志、中间件日志、GC 日志、内核日志、Metrics 指标等。

(3)定位原因

OK,排查线索也有了,接下来该怎么定位具体原因?这个环节会综合考验你的技术深度、业务熟悉度和实操经验,因为原因往往都千奇百怪,需要 case by case 的追踪与分析。这里给出几个排查方向上的建议:

  • 关联近期变更:90% 以上的线上问题都是由变更引发,这也是为什么集团安全生产的重点一直是在管控“变更”。所以,先不要急着否认(“肯定不是我刚加的那行代码问题!”),相信统计学概率,好好 review 下近期的变更历史(从近至远)。

  • 全链路追踪分析:微服务和中台化盛行的当下,一次业务请求不经过十个八个应用处理一遍,都不好意思说自己是写 Java 的。所以,不要只盯着自己的应用不放,你需要把排查 scope 放大到全链路。

  • 还原事件时间线:请把自己想象成福尔摩斯(柯南也行),摆在你面前的就是一个案发现场,你需要做的是把不同时间点的所有事件线索都串起来,重建和还原整个案发过程。要相信,时间戳是不会骗人的。

  • 找到 Root Cause:排查问题多了你会发现,很多疑似原因往往只是另一个更深层次原因的表象结果之一。作为福尔摩斯,你最需要找到的是幕后凶手,而不是雇佣的杀人犯 —— 否则 TA 还会雇人再来一次。

  • 尝试复现问题:千辛万苦推导出了根因,也不要就急着开始修 bug 了。如果可以,最好能把问题稳定复现出来,这样才更有说服力。这里提醒一点:可千万别在生产环境干这事(除非你真的 know what you're doing),否则搞不好就是二次伤害(你:哈哈哈,你看,这把刀当时就是从这个角度捅进去的,轨迹完全一样。用户:...)。

(4)解决问题

最后,问题根因已经找到,如何完美解决收尾?几个基本原则:

  • 修复也是一种变更,需要经过完整的回归测试、灰度发布;切忌火急火燎上线了 bugfix,结果引发更多的 bugs to fix。

  • 修复发布后,一定要做线上验证,并且保持观察一段时间,确保是真的真的修复了。

  • 最后,如果问题已经上升到了故障这个程度,那就拉上大伙好好做个故障复盘吧。整个处理过程一定还有提升空间,你的经验教训对其他同学来说也是一次很好的输入和自查机会:幸福总是相似的,故障也是。

2.结构化问题解决

结合软件系统的生产环境故障来描述的话,一个典型的结构化问题解决步骤如下:

  • 问题定义:清晰的描述问题现象、影响,其中影响要尽量量化。例如xx时xx分开始,xx服务异常,成功率从99%下跌到90%。

  • 临时解决:基于预案的临时解决方案和实施结果,包括符合条件的预案执行,或者应用发布过程中出现的异常后立即回滚。

  • 分析问题原因:结合已知因素,找到问题的根本原因。

  • 制定解决方案。

  • 实施解决方案。

  • 标准化解决方案:将解决方案标准化,举一反三,避免同类问题继续发生。

3.故障排查流程

从理论上讲,将故障排查过程定义为反复采 用假设 - 验证排除手段的过程:针对某系统的一些观察结果和对该系统运行机制的理论认 知,不断提出一个造成系统问题的假设,进而针对这些假设进行测试和排除。

从收到系统问题报告开始处理问题。通过观察监控系统的监测指标和日志信息了解系统 目前的状态。再结合对系统构建原理、运行机制,以及失败模型的了解,提出一些可能 的失败原因。 接下来,可以用以下两种方式测试假设是否成立。

第一种方式,可以将我们的假设与观 察到的系统状态进行对比,从中找出支持假设或者不支持假设的证据。

另一种方式是, 可以主动尝试 “治疗” 该系统,也就是对系统进行可控的调整,然后再观察操作的结果。 第二种方式务必要谨慎,以避免带来更大的故障。但它的确可以让我们更好地理解系统目前 的状态,排查造成系统问题的可能原因。

真正意义上的 “线上调试” 是很少发生的,毕竟我们遇到故障的时候,首先不是排查故障 而是去恢复它,这有可能会破坏掉部分的现场。所以,服务端软件的 “线上调试” 往往在 事后发生,我们主要依赖的就是日志。这里的日志是广义的,它包括监控系统背后的各类观 测指标的时序数据,以及应用程序的程序日志。

为了排查故障,平常就需要准备。如果缺乏足够的日志信息,我们很有可能就无法定位 到问题的原因。 首先,必须能够检查系统中每个组件的工作状态,以便了解整个系统是不是在正常工 作。 在理想情况下,监控系统完整记录了整个系统的监控指标。这些监控指标是我们找到问题所 在的开始。查看基于时间序列的监控项的报表,是理解某个系统组件工作情况的好办法,可 以通过几个图表的相关性来初步进行问题根源的判定。

但是要记住,相关性(Correlation)不等于因果关系(Causation)。一些同时出现的现 象,例如集群中的网络丢包现象和硬盘不可访问的现象可能是由同一个原因造成的,比如说 断电。但是网络丢包现象并不是造成硬盘损坏现象的原因,反之亦然。况且,随着系统部署 规模的不断增加,复杂性也在不断增加,监控指标越来越多。不可避免的,一些现象会恰巧

和另外一些现象几乎同时发生。所以相关性(Correlation)只能找到问题的怀疑对象,但 是,它是否是问题根源需要进一步的分析。 问题分解(Divide & Conquer)也是一个非常有用的通用解决方案。在一个多层系统中, 整套系统需要多层组件共同协作完成。最好的办法通常是从系统的一端开始,逐个检查每一 个组件,直到系统最底层。

第三种重要的排查问题手段,是提供服务状态查询 API 和监控页面来暴露当前业务系统的 状态。通过监控页面可以显示最近的 RPC 请求采样信息。这样我们可以直接了解该软件服 务器正在运行的状态,包括与哪些机器在通信等等,而不必去查阅具体的架构文档。另外, 这些监控页面可能同时也显示了每种类型的 RPC 错误率和延迟的直方图,这样可以快速查 看哪些 RPC 存在问题。 这种方法很像 X-RequestTrace 机制,它非常轻便。如果故障的现场还在,或者故障过去 的时间还不长,那么它将非常有助于问题的排查。但是如果问题的现场已经被破坏,那我们就只能另寻他途。

4.关键角色

突发异常的情况都各有不同,很难有一个完全统一而且颗粒度很细的标准流程,但是可以提前约定好几个关键角色,定义角色的作用和关键动作,来提升协作效率。主要包括这些角色:

  • 指挥员:负责组织和协调故障快速恢复、故障群里通报相关进展。

  • 通讯员:负责收集、记录关键信息,并在故障群等渠道跟相关团队沟通。

  • 快恢负责人:根据故障现象、监控大盘,决策并执行预案。

  • 问题诊断负责人:定位故障根本原因,当快恢不起作用的话,该角色至关重要。

(1)指挥员

①指挥员的选择

  • 第一接警人:默认第一个收到告警、投诉反馈的技术人员作为指挥员。第一接警人判断是否能够指挥,或者是否有自己熟悉且充分演练的预案可用,如果可以则立即恢复服务,否则联系专职指挥员接手。在专职指挥员接手之前,第一接警人就是默认的指挥员。

  • 专职指挥员:团队 Leader 和稳定性负责人是大多数风险的最佳指挥员,当应急团队建立联系后,指挥员可以交由 TL 或团队内的稳定性负责人。

  • 各级TL:当故障时长和等级持续上升后,根据实际情况会上升,由更高层级 TL 接掌指挥员角色,以协调更多资源加入。

②指挥员关键动作

  • 确认问题:确定该次突发事件的现象、影响。

  • 确定角色:确定参与该次事件处理的关键角色,包括通讯员、快恢负责人、问题诊断负责人。

  • 向上沟通:让组织中关键角色知晓该问题,这样在需要时候,可以更快的调动更多人员和资源参与进来。

  • 协调:协助快恢负责人和问题诊断负责人解决问题,在信息、领域专家等资源上给予支援。

③对指挥员的要求

  • 启动:确定人员,并通过视频会议、故障群等方式建立起应急小组。

  • 前期:紧盯快恢负责人进展,优先落地快恢,而不是分析根本原因。当快恢不生效后,也要继续探索可能的快恢手段,例如回滚近期的变更等操作。过往的故障时长没有满足1-5-10的案例中,大多数情况下都是指挥员在分析问题根本原因,错失了快恢的最佳时机。

  • 中期:尝试大量手段都无法恢复服务的话,重心逐渐转移到问题诊断负责人这里,找到根本原因。通常进入到这个阶段故障还没恢复的话,就是大故障了,1-5-10基本上是无法达标的。

  • 后期:组织团队继续观察,确认不会问题再复现。组织善后和复盘等工作。

(2)通讯员

如果故障不能在第一时间通过预案恢复的话,通讯员将会是仅次于指挥员的角色。高效组织信息收集、整理,会让整个应急小组更快速度找到解方案。

①通讯员选择

  • 专职通讯员:在团队内有一定稳定性认知,然后通常又不是快恢负责人和问题诊断负责人第一人选的那个同学。

  • 其他不参与问题诊断和快恢的团队成员。

②通讯员关键动作

  • 持续确认问题和通报:随着时间推移,问题的现象、影响面也在动态变化,需要定期通报(故障群、电话会议等渠道),前期要做到5分钟换一次通报,随着时间推移,后期可以改成15分钟、30分钟等间隔。

  • 信息收集:按照标准模版,为该问题建立一个统一的文档,把文档链接放到群公告、故障群中。并持续将收集的关键信息更新进去。方便后续加入到应急小组的同学快速了解上下文。

  • 收集舆情:这一点跟信息收集有重叠,之所以特别强调出来,是因为该环节通常容易被忽略,技术同学容易陷入在技术指标中,对于舆情缺乏关注。

  • 对外发声:联系客服负责人,与客服团队合作,安抚客户。

③对通讯员关键要求

  • 前期要快:快速收集关键信息,黄金10分钟内要做到每分钟有信息更新,并持续通报。

  • 通报及时:好的信息通报是告知下次通报时间,例如xx问题yy正在处理中,目前情况是zzz,xx分钟后将进行下一次通报。如果有可靠和及时的通报,关注该问题的人只需持续留意信息通报即可,避免非专业的插手影响应急小组快速反应。

  • 联系外部支援:涉及到外部依赖方的时候,例如OSS、MySQL等,通过指挥员、应用Owner等渠道知晓外部接口人的时候,及时组织外部接口人加入到应急小组中来,并向对方通报问题上下文。

(3)快恢负责人

期望是所有的风险都能够通过快恢来解决,如果不能的话,也是第一时间探讨其他可行的快恢方案(比如回滚等操作)。

①快恢负责人选择

  • 应用Owner/核心骨干。

  • 执行过该应用预案的团队成员:我们鼓励团队之间交叉执行预案,当应用Owner联系不上的时候,其他同学也可以通过预案来协助问题恢复。

②快恢负责人关键动作

  • 执行快恢预案:根据问题现象,找到预案大盘,根据大盘上监控指标指引去执行相应的预案。

  • 制定其他候选恢复方案:当已知快恢预案不生效时候,分析可能的变更等因素,通过回滚等方法尝试恢复。必要时候,让指挥员协调更多人进来支持。

③快恢负责人关键要求

  • 以恢复服务为第一优先级,问题根因分析请交给问题诊断负责人。

  • 既定预案不能快恢,也要继续探索其他可能的恢复手段。

(4)问题诊断负责人

通常不希望这个人出现在故障1-5-10的恢复环节,但是当快恢失效并且短时间内缺乏有效手段恢复服务的话,最后只能靠问题诊断负责人来找到根本原因,并制定解决方案。

①问题诊断负责人选择

  • 应用Owner/骨干:了解相关代码的人最适合去做问题诊断。

  • 领域专家:比如网络问题,可以从集团找到该领域专家协助参与进来。

②问题诊断人关键要求

  • 根据收集的信息,找到问题根本原因。

  • 向指挥员、通讯员提出要求,把外部支援邀请加入到应急小组中。

5.预演

故障应急也需要重点锻炼。一些可以锻炼的机会包括:

  • 真实的故障场景。

  • 红蓝对抗演习:与SRE联动,通过突袭方式,模拟一次故障。

  • ​常规报警升级:TL或者稳定性负责人随机抽取一个短信告警,人为将其升级为故障,进入故障应急响应流程。

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值