万字总结:腾讯会议后台告警治理实践——如何才能避免“事后诸葛亮”

图片

图片

👉目录

1 让人头疼难题——为什么我们总是事后诸葛亮?

2 告警的“向左”与“向右”

3 为什么要做到精确告警是很难的

4 推动告警治理,落地过程也是困难的

5 如何有效识别出无效错误?

6 腾讯会议部分新业务错误码设计

7 告警策略的设计

8 团队内如何切实推动告警消音

9 消音战果

10 还在路上

11 小结

告警治理永远是后台架构中绕不开的话题,几乎可以认为告警是否治理得好,决定能否做好后台的服务质量。现网运作过程中,时而都会面临现网质量的问题,可能是大范围的故障,也可能是一个发布有 Bug 导致某一小群用户某个功能功能不可用。让人尴尬的是,事后复盘之时,总会发现优化措施里总是有告警优化措施的身影。而告警措施无论怎么补全,似乎永远补不完。

本文将结合笔者最近一年团队的成功实战经验,从如何结合错误码设计开始,再到统一告警策略、工具建设乃至团队值班制度管理等,介绍腾讯会议部分模块告警治理经验。

本文全文1.4w字,阅读本文后,后台团队质量负责人将能回答出以下三个问题:

  1. 怎么样让告警是覆盖有效的,且能真实告警是故障(这个通常不难,一旦有大范围问题,告警通常是泛滥的),还能包含一些功能性的质量/bug问题,在大面积用户反馈之前介入。

  2. 在告警有效的前提下,如何做到后台服务的告警不会一直处于轰炸状态,导致团队麻木。

  3. 如何推动团队真正长期有效地对告警所反映的问题做到闭环解决。

关注腾讯云开发者,一手技术干货提前解锁👇

🏃揭秘腾讯云爆款产品!抢跑618,开发者直播间限定福利,爆款低至1元起,手慢无!

CodeBuddy一号位深度对谈,技术专家如何看待AI编程终局?周三晚19:30,TVP技术夜未眠带你展望AI未来,预约直播,更有多款好礼等你领取🎁!

01

让人头疼难题——为什么我们总是事后诸葛亮?

像腾讯会议这种超级大用户使用量的 APP,不太可能一发布就遇到什么大的功能问题,但是还是很难避免一些小的或者隐蔽质量问题。例如某个条件组合下的功能问题,或者某类用户群体体验问题。这些问题很难通过自动化测试、功能测试覆盖能在测试阶段发现,因为我们很难把所有条件都覆盖得很全,再者现网的数据状态永远比测试环境复杂,有些问题只能在特定的数据状态下才会暴露。

这时候,告警就显得尤为重要。告警就像前线打战的哨兵,一旦敌情有所动静,我们就要能快速做出响应,而不是城门都被攻打了才有所反应。然而,似乎所有的后台团队在告警问题折腾了很久之后,依旧会出现这样的问题。

  1. 今天发布了一个功能,测试回归都是 OK 的。跑了一个星期,收到用户反馈,某一小部分类用户(例如付费用户、部分企业用户)在某个场景下这个功能不可用了,研发毫不知情,只能快速救火。

  2. 早上发布了一个功能。看日志、监控似乎没啥问题,刚要发布完,下午就收到用户反馈,某个操作路径几乎100%必现不可用,然后急急忙忙的回滚。一复盘,发现其实有一个告警,但是这个告警平时正常的时候就有,所以没去关注。

  3. 线上运营期间,调用下游某个第三方服务失败导致功能受损。失败过去几十分钟了,后面收到这个三方服务的报告。才发现这几十分钟团队居然没人关注到这个问题。一复盘发现告警群信息太多了,中间藏着一个告警被人忽略了,

第一个问题在某些大领导眼里可能都是很不可思议的,“为什么这么大的一个后台团队,做告警建设做了这么久,有问题都得等用户反馈”?

第二及第三个问题在某些 leader 眼里则是很尴尬,因为这代表着自己的团队的成员在漠视告警,或者说告警是否有效纯属看运气(是否有人关注到,是否刚好关注到有效的那条)。

02

告警的“向左”与“向右”

假设我们用逻辑数学去描述告警与出质量问题,理想情况下,质量问题应该是告警的充分必要条件。即:

有告警就一定是哪里质量出了一些问题,如果质量出了问题,一定会出告警。

分毫不差,精确有效。然而,事实上要做到这点是很难的。

   2.1 向左的告警

前面第一个问题,研发毫不知情原因就是:质量问题不是告警的充分条件,即有质量问题并不能保证伴随着告警。即告警缺失了。太麻木了。

   2.2 向右的告警

第二个及第三个问题,其实问题发生的时候是有告警的。但是很可惜,平时没问题的时候,告警还是一直叫个不停,导致团队的成员麻木了或者关注不到真正有效的那条。这意味着质量问题不是告警的必要条件。即误告了,太敏感了。

   2.3 真实场景下的告警

如果告警稳定向左或者稳定向右,总还是有可执行的手段去优化的,但是事实上大部分情况下的后台系统是向左向右并存的。

也就是说告警是质量问题的不充分且不必要条件。而想要做到充分必要条件,即又不麻木又不敏感,分毫不差,不左不右,在现实中系中却是极具挑战的。

03

为什么要做到精确告警是很难的

一开始的系统,总是全力把精力在上线功能上,很难有精力照顾周边如告警、监控、性能、容灾等话题上。这时候上线功能,获取用户才是最重要的。等到用户变多了,一点点的质量问题都会摆上台面,这时候告警就会被高度重视起来。

现在微服务的架构下,所有的服务间调用都是通过网络通信的,哪怕后台是个单体服务,上面总还是挂着一个网关。所以如果某个功能不可用了,总会在某个环节以接口成功率的形式体现出来。所以在某些人眼里:不就是把所有接口都覆盖一个告警的事吗?似乎如此。假设我们对于后台服务的质量要求是99.99%的可用性。那么很简单,把所有服务的所有接口的被调告警以99.99%的成功率去配置即可。这样基本上就可以做到版本有功能不可用的一些问题一定会伴随着告警。

然而,想法很美好,现实是很残酷的。因为很多时候所谓的接口失败并不一定就是质量有问题了,反而可能是符合预期的。例如用户本身没有权限观看这个录制,所以返回了错误给用户。例如用户是免费用户,无法使用这个付费功能;再例如用户本身输入的文本格式有误,所以没办法修改它的昵称等等。如果一刀切99.99%的成功率,那么告警一定会泛滥、导致“向右”。

要解决这个问题,普通的做法是以下两个:

手段1:针对某些接口,专门把成功率调低阈值。例如平时没啥问题的时候,成功率是97~99%,波动,那么告警可能就会调到96.99%

手段2:专门梳理筛选出那些没有问题的错误码,加白剔除。

然而这两个手段都存在很大的缺陷,我们一个一个分析。

   3.1 降低告警成功率阈值

一旦开了一个降低阈值的头,这个告警几乎就约等于无效告警了。为什么呢?

第一,这种用户行为类的成功率不可能一直稳定在一个数值,例如97%,他肯定会随时间波动,这一分钟97%,下一分钟可能又回到99%,再下一分钟98%这样子。所以你只能按照参考97%的阈值去设置告警的策略。这也意味着如果有一个功能的问题导致了0.5%左右的成功率下降,几乎是告警不出来的。

下图所示的监控中,大部分情况下低谷时在98%左右,但是一天总会有比较大的不动这20分钟的最低谷其实是91%。如果按照阈值91%去配置告警。可能平峰的时候出现一些bug也会被吞掉告警不出来。

第二,97%这个阈值可能也是很不准确的,这个星期最低阈值是97%,下周可能就突然有个低谷时96.5%,这意味着这个告警可能也是误告。误告多了,就是“狼来了”,大家也就不管这个告警了,告警又变成向右了。哪怕真的有同学去管这个告警,最后他的策略大概率把97%的阈值变成96%。告警又从向右变成向左了。

以下是一个非常极端的场景,成功率波动甚至有20%的点。

最后降低告警成功率阈值的头一开,告警几乎很难避免在向左向右中反复横跳,要不就是

  1. 有问题被低阈值掩盖了

  2. 要不就是告警确实出来了,但是看了几次都是用户自己的问题,狼来了多了,后续干脆就不关注这个告警了。

所以说这样的告警策略几乎就是无效的(但是也不是说这个告警完全没有用,出现一些大规模故障,成功率调到了80%,肯定还是有效的)。

但是问题是,96%,97%的告警麻木看多了,突然来个80%的告警,团队是否能警惕起来马上处理呢?这也是一个问号。

   3.2 加白错误码

另外一个常见手段则是在告警策略中针对某些用户行为类或者已知的一些不影响用户功能的报错的错误码进行剔除。

看起来是较为合理的措施,但是实施起来效果通常不好。因为这是一个无底洞的体力活工作。有以下3点:

  1. 工作持续性维护,重复工作繁重。从某个 A 接口的视角来看,他总共会返回多少错误码你是不一定能梳理完的,因为 A 接口的错误码除了自己定义的一些之外,有些时候还会透传下游错误码的。最后导致这个加白的工作通常是持续性、无法预测的,这就导致这个工作很繁琐。最后的结果就是,告警策略一直在改,一线同学在体力工作上疲于奔命,最后实行不下去,年久失修。

  2. 容易造成过度加白。刚刚说了一线同学会在告警接口上疲于奔命,所以最后实操上告警策略大概率会下放到一线同学身上的,也就是随便一个有模块权限的人都可以随意可以增加。这就容易导致过度加白。例如现在发现一个告警,是错误码是“XXX 校验失败”导致的,这个同学发现用户的操作路径是 A 路径下,这个 A 路径遇到这个错误是符合功能预期的,于是它顺手就把这个错误码加白了。但是这个错误码如果出现在 B 操作路径或者 B 接口,可能就不是正常的了,但是很可惜,在疲于奔命的情况下,这个错误码被加白几乎是必然的结果。

  3. 每个接口的错误码都不一样,这意味着每个接口的策略都需要个性化加白。这就变成了一个无底洞了。因为一个大后台系统,CGI 接口、内部 RPC 接口、Kafka 异步调用、定时任务调度等等加起来没有10万个接口也肯定上万。每个接口都自定义加白是不可能的。最后只会有两个结果:1.粗放到一定的维度去大杂烩加白,这样可能会造成告警吞没  2. 只针对重要的一些接口做个性化策略,其他接口走成功率阈值调低的策略,这样就走去了阈值下降的缺陷中。

两个手段都最后都不尽如人意,工作做得粗放一些,可能会无时无刻都在告警,导致团队麻木;工作做得精细一些但不是满分,又会造成一些告警吞没掉,平时可能没问题,一旦遇到问题复盘了就是告警做得不够好,又一点一点补,但是永远在“补漏”的路上,没法一下子做到很好。

最终一个系统这么多功能、环节,总会存在有些 case 误告,有些 case 却告警缺失的情况,这就是为什么好像告警是一个策略就能覆盖全的但是总是有盲点的原因。

04

推动告警治理,落地过程也是困难的

理想情况下,哪怕告警已经是有效的。也必须治理。使之到一个相对安静的情况,否则告警太多就等于告警无效(狼来了)。

但是哪怕管理上下达了很多指示,真正要把告警治理好也是挺困难的,原因在于前面所说的挑战,群里充斥了的无效告警、重复告警,导致很多有效告警都麻木了,根本不会有人关注到,更别说落地处理了。

另外,哪怕每个告警都被关注到、分析到了,告警背后的这个问题能被修复其实也是不容易的。

如果这个告警背后的问题很明显,这反而是好事,因为大家一定会重视起来去修复问题,随后告警以后就不报了。

但是很多有效告警其实背后的问题都是相当小的小问题,可能是触发的概率很低,正常条件根本触发不了(连续操作同一个按钮、多少个人并发的操作某个数据),又或者是只有非常特殊的数据特征的群里才会遇到,而且遇到了之后也是很小的体验性问题不影响功能。

然而修复一个问题是需要经过代码修改、自测、测试、发布等多个工作的。看起来好像收益很少,所以这些问题最后命运大多就像我们代码的“TODO”一样——“后面有空的时候处理”,其实最后一直没处理。

然而,代码是一直在快速新增的、功能也是也是在敏捷变化,理论上说,每多发布一个功能,就会有概率性的带入 X 个 bug。这些 bug 或大或小,如果告警是全面的,但是最后总会以某个告警的方式告警出来。所以一方面我们修复告警的速度可能是较为缓慢的,另一方面系统由以更快的速度在增加 bug 从而导致引入更多的告警。生产速度超过消费的速度,最后告警的数量一直是处于单调递增的状况中。

可能突击一轮告警数量确实下降了,但是很快告警又会反弹,就像吃一些减肥药一样。这也是很多团队的痛点。

05

如何有效识别出无效错误?

刚刚提到,加白错误码是一个看似有效但是实际非常起到好效果的手段,本质原因就是错误码在各个接口,各个场景上具备个性化、不可枚举两个特点。

但是假设我们通过一些通过规范错误码的使用和设计,是否可以做到“一键识别用户错误呢?”。我们想象一个最简单的例子,假设我约定预期之类符合产品功能的错误码号段规定为0<code<10000。那么告警策略上就可以无脑剔除1-9999的错误码了。从告警的角度上看,确实就可以做到简化告警策略。采取一刀切的方式剔除错误码,团队就不用疲于奔命在分析、加白、告警麻木的路上奔流不息了,这些都是战术上的勤奋,并不能很有效的解决团队问题。

当然简单无脑地一刀把符合功能的错误码定义为0-9999这是一个很粗糙的思路,实际上操作不能这样简单,因为“符合产品功能”就是一个非常模糊且不同一线人员可能有不同理解的表现。再者,有些错误是通过外部服务的下游透传的,外部服务的错误码规则又可能和已有规则冲突、重叠,从而导致告警误判。

   5.1 错误码的目标

腾讯会议后台团队在思考告警问题的时候,确实就是在错误码上下了大功夫,企图希望提前全局规范错误码这样的“战略勤奋”,去减少后续频繁的告警策略配置、告警加白、自定义指标上报&告警等战术工作。即希望用战略的勤奋去让战术工作轻松点,就像王者荣耀一样,高手只会运营经济从而打稳赢的团战,没必要和对手拼操作、拼龙团。

然而错误码的第一目标并不是服务告警的,所以不能本末倒置。那么应该如何设计好错误码呢,首先我认为错误码应该要简单的回答好以下3个问题:

1.  错的原因是什么?

例如是第三方哪个服务报错了,还是用户的什么行为导致的错误,还是基础组件出现故障了。并且最好是能直接从错误码本身就能直接通过数学运算、正则计算出来。

如果满足这一点的话,前文提到的有效剔除业务逻辑错误这个目标就能达成了。例如前文举例中最简单的:错误码是0-10000就是用户错误就是一个可以通过数学简单运算算出来的有效规则。

2.  错在哪里?

微服务场景下,链条非常的深,一条链路下来可能经常几十个不同的微服务,但是报错时候上游看到的只有错误码,并不能携带足够的上下文信息。

理想的情况下,这个错误码应该能回答这个报错是哪个服务出了问题,这样能最快的找到对应服务负责人看对应的错误,该负责人能直接定位错误而不是从入口层开始一层一层的找人,这样事故可能早就蔓延了。

如下图,A 接口报错,理想情况 A 负责人看到错误码的时候就应该知道F报错了,直接找到F负责人看这个问题。

而不是 A 去找 B,B 去找 D,D 去找 E,E 再去找 F。


3.  错误码最好是有限、明确且容易记忆的

理想的错误码就像 HTTP 400, 502一样的,读到错误码就能立刻大脑第一反应知道大概是什么错,排查的方向是什么。

这样就需要错误码是固定下的有限几个(就像HTTP错误码那样,错误码是能全局枚举的),这样一来,即便没有文档、代码,不同的值班人员排查问题时候也是可以线下沟通起来的。

因为随着排查问题的积累,遇到的错误码最好是能逐步固定在有限范围,变成大脑的字典,而不是每次错误码都非常零散,随意被定义添加的。

06

腾讯会议部分新业务错误码设计

   6.1 概述

基于以上错误码目标,腾讯会议的部分新业务模块采取的是以下三段式的设计,可以满足以上三个目标。如一下图所示:

每个错误码是由第一段模块号+错误分类+具体错号组成。例如1010502这个错误码是由三部分组成的:表示这个错误的根源由模块=10这个模块抛出的。

10502则需要表示这是一个1类错误码,1类(A类)错误码的意思是用户的行为导致的错误类型。而0502则是预设好的号段:参数缺失。

联合起来读这个错误码,则可以立刻知道:

  1. 错误是在10这个模块报的,可以通知10模块的负责人进行排查。

  2. 而因为错误分类是A类(第三位是1)错误,所以是用户行为导致的,大概率不影响业务功能。

  3. 至于具体的0502,如果记忆力好,应该能记得他是参数缺失的错误,如果不记得,由于值班过程多次看到,很快也会成为值班手册中的一个字典项。

以下介绍其具体设计思想

   6.2 设计思想

模块号:

每个微服务需要申请一个唯一的模块号。增加模块号的原因是希望能通过模块号快速知道错误码的根源是出现在什么模块。

前面说到,错误码的目标的第二位是:错在哪里。所以我们设计了模块号在其中(只设计两位的原因是当时我们团队负责的业务应该总体是100个以内的)。

同时,我们希望错误码能够做到全局唯一,例如同样的0502(参数缺失)错误如果是A模块抛出来的和B模块抛出来的,我们希望最后结果是不一样的错误码。这样在某些场景需要"equals"比较精准逻辑分支时候不会误命中。

错误分类:

错误分类其实是具体错号的前缀,每个错误码会被预先做好分类,以便于只读这个分类就能大致对于后台的异常做出初步判断。

提前分类的原因最大的好处是一眼看出错误的类型。以下是我们的A、B、C、D四大分类,分别错号端对应着1、2、3、4。

错误分类分类含义默认错误号含义
A(1)用户行为错误10000用户端错误
B(2)系统错误20000系统执行出错
C(3)下游服务错误30000调用第三方服务出错
D(4)依赖组件错误(如Redis、MySQL、ES等)40000调用基础组件出错

虽然错误码其实是1xxxx,2xxxxx,但是平时我们会称这个错误码是A类或者B类。原因这样能更好地做线下的沟通,从而强化大家对错误号段的理解。

好的错误码要涉及得符合人性。那么符合感性认知+有利于口口相传的错误码会更佳。所以其实使用纯数字来进行错误码编排并不利于感性记忆和分类。可惜trpc框架错误码要求是整形,所以我们才把错误分类映射回了1、2、3、4。初衷上我们更倾向10A0502这样的错误码,因为对比1010502,其前后的分割作用更醒目。更有利于错误码的瞬间分析。但是为了强调第三位“1”的分割作用,我们还是保留了A、B、C、D这样的沟通习惯。

如果读者业务的错误码是支持字符串的,可以考虑用A、B、C、D这样的分类直接分割。

具体错号:

可以认为就是普通意义上具体的一些错误码,只是这个错误码最后被压缩成固定的长度。具体错号的前缀是错误分类。其中每个错号还会继续分为1级、2级、3级

例如A类错误的起始号段是10000。我们称之为1级错误,即非常笼统的用户行为错误的错误码。其后每100个数字会切出2级号段,例如10500是代表参数非法的错误码。是相对1级较为具体的错误码。但是参数非法实际上有很多可能,例如可能是用户输入项为空(10501)、也可能是校验失败(10502),这里又定义了具体的错误码,这称为三级错误码。

一般情况下,我们建议使用三级错误码,如果不想追加三级错误码,使用找一个对应二级错误码也是可以的。以下是我们现在在使用的其中一些错误码列表。

   6.3 错误码定义

错误分类分类含义1级错误号起始号含义示意
A(1)用户行为错误10000用户端错误10500 参数非法(为空,或者基础校验不通过)
○10501 参数校验失败(page传了负数,手机号位数不对/有字母)
○10502参数缺失(没传必传参数,例如eventId没传)
10600审核不通过
○10601自动审核不通过(信安审核不通过)
○10602人工审核不通过
10700业务校验失败
○10701业务状态错误(业务状态校验不通过,例如 活动已经发布了,不能修改xxx)
○10702业务重复(业务逻辑不允许重复执行,例如 已经报过名了,重复报名)
10800没有业务权限(越权行为,这里告警可能会特殊处理)
○10801活动商业化权限不足(例如会员版本不足导致的权限不足)
○10802业务操作权限不足(例如不是主办方组织的owner,导致的权限不足)
○10803横向越权拦截(交叉校验的拦截 例如 A主办方查询B主办方活动信息)
10900业务操作限频(例如 短时间内重复点击xxx功能)
11000获取用户登录态失败(通过ctx 获取登陆信息失败)
11100数据不存在异常(应该存在的关联数据不存在,例如 有日程信息,但是对应的活动信息查不到)
B(2)系统错误20000系统执行出错20100资源耗尽(系统资源,一般很少用)
20200容灾功能被触发(系统限流、有损降级、过载保护)
20300应用程序错误(业务bug之类)
○20301业务序列化失败(例如 json序列化失败)
○20302业务未知异常(例如 panic了或者一些稀奇古怪的异常)
○20303业务异常场景(例如 进入异常分支无法处理)
20500分区表错误
20600数据一致性异常(例如 多张表对同一状态存储不一致)
○20602系统对账异常(例如 对账不平)
C(3)下游服务30000调用第三方服务出错30100调用内部下游服务超时
○三级用于区分不同的服务
30200调用内部下游服务失败
○三级用于区分不同的服务
30300调用外部下游服务超时
○三级用于区分不同的服务
30400调用外部下游服务失败
       三级用于区分不同的服务
D(4)依赖组件错误40000调用基础组件出错40100调用MySQL错误
      三级用于区分不同的db
40200调用Kafka错误

   6.4 错误码使用原则

同时,我们对于错误码的定义有以下几个原则:

  1. 错误码全局定义。不同服务也复用相同的错误码。原因是错误的类型总能够找到一些公共的特征,是具备复用的价值的。特别需要避免的是,有些时候为了提示用户不同的信息而独立一个新的错误码。这种情况应该把具体的错误信息或者提示用户放到独立的字段中。例如我们规定详情放在的 error_detail 字段,给用户看的提示信息则放到 msg 字段。

  2. 错误码不可随意追加。既然全局可以服用,那么就应该尽可能在已有定义的错误码中找到具备兼容含义的错误码。

  3. 如果一个错误是下游服务报错,没有特殊原因,允许直接透传此错误码。如果需要包装成本服务错误码,则需要包装为本服务的 B 类错误码,特殊情况可以包位 A 类。例如下游查询一个数据不存在如果是以类似404错误码的方式外抛的,我们需要转成 A 类错误往外抛,因为这属于很正常的业务场景。

   6.5 错误码使用场景

场景1:使用错误码透传,方便快速定位出问题的模块


前端通过 code 的报错知道报错的模块是12(domain-center),通过错误分类4,快速知道是组件出现问题,通过 err_detail 知道对应的 code 背后的报错含义。用户通过"msg"字段得到友好的弹窗信息。

这种直接透传错误码的场景使用是更为多的,一来这符合程序员的写代码的人性:这样写最省事,对于 go 语言来说,写法就是常见的。

if err !=nil {return err}

二来,这样确实因为下游服务是符合规范的,那么就能一直透传出对应的模块号,非常方便定位问题。 

场景2:转换下游错误码,以标准化错误码的对外表现

少部分情况,直接 return err 可能是不妥的:

  1. 调用外部系统报错。例如一个陈旧的老系统其错误码并不是符合这套新规则的,这时候直接透传可能还会和本系统已有的错误码冲突。

  2. 部分系统或者场景需要稳定自身系统的错误码返回。例如作为一个平台性的产品,其错误码可能是提前枚举好的。如果直接透传一些下游错误码出现不可控的扩散情况。

这两种情况下,会把错误码转为自身的 C 类错误。如下图所示: 

其中,因为下游报错还有本身的错误码,为了保证这个链条没有丢失,所以原始错误码要求追加在 err_detail 字段中。 

如会控某服务调用失败的 C 类错误是300001,其含义是“访问会控系统失败”。假设这次请求错误码返回了123456789。呢么最后加工后的错误响应体中的 error_detail 会变成"访问会控系统失败_123456789"。

这样的收益收益很明显:

  1. 错误码依旧是规范的,虽然无法追溯到报错根源是会控系统,但是通过 code 的模块号段=10知道抛错系统就是10这个模块。而且看到是 C 类报错,那么也能精确知道是外部系统报错了。

  2. 通过 err_detail 可以知道实际上是外部的系统报错,且原始的错误码是123456789在错误响应体中是带有的。那么就可以带此信息去找外部系统的同学询问原因。

   6.6 规范的落地要符合人性

很多时候规范是规范,但是一线研发同学落地却很难推进,原因就是规范很多时候是增加了一线同学开发的工作量的。一旦规范无法统一落地,哪怕落地了90%,只有有10%不规范,那么维护起来就是两套,但是收益可能还达不到10%。

所以保证规范的落地是很重要的。我们的做法是尽可能让规范的落地是符合一线同学人性的,所谓人性就是“偷懒”。

人性1:能直接 return error 为什么要我包一层

写 golang 的同学应该都习惯于写

err := callRpc()if err !=nil {return err}

写 java 则习惯:

try{    callRpc();}catch (Exception ex){thrownew RuntimeException(ex);}

甚至 catch 都没有直接把异常定义在签名中 。

虽然很多最佳实践是说所有的异常都应该被处理,但是从来没见过这样的规范能落地的。因为这种规范反人性且收益甚微。

违反人性的规划没有收益且有害

既然异常事实上都是不加处理就往外抛的,那么错误码就应该是定义在 rpc 框架的错误码中。

但是有些系统的定义是 rpc 错误码全部被吞成0。真实的错误码定义在报文中的 body.err_code 中。如:

{code: 0:data:{    code: 10001    err: 参数错误}

设想中理想的代码:

if (err!=nil) {//处理好异常} elseif (data.code!=0) {//处理好每个错误码的异常}

这样的规范有几处缺点:

1. 使用方的使用反人性。因为 callRpc(); 返回的错误码都是0,也就是说这个接口都是成功的。那么我就没办法直接透传 error,只能 if (data.cade!=0) 之后,重新包一个错误码出去。代码量凭空多了,但是实际上错误却依旧止不住简单往外抛。没有得到收益,工作量变多了。

2. 有些情况框架本身是可能报错的,那么这时候 code 可能就是一些框架标准的错误码。如场景的21=服务端超时,这些错误还是需要业务去处理。所以代码就变成了:

实际上的代码:

if (err!=nil) {return 一个新error} elseif (data.code!=0) {return 一个新error}

对代码的健壮性要求更高了,写少了还可能有 bug。

3. 模调监控缺失。因为 code 是否失败都是0,那么业务的所有报错全部都无法通过自带的模调监控。最后本来模调监控可以做的,缺需要人肉写一堆的指标上报。

可以看到,这种错误响应的规范初衷可能是要求大家处理好每个错误(就像 golang 语言的 error 设计一样),实际上会非常多的缺点。本意所希望带来的优点却因为反人性而无法落地。最后导致系统增加了一堆的意大利面条代码,维护还变得更难了。

人性2:落地规范不能增加我的使用复杂度

前文提到的我们分段设计初衷很好,也有显著收益收益。但是原本我透传 RPC 错误的时候 return 新的错误是使用:

errs.New(errs.RetClientNetErr, "net error")

一个函数,两个参数即可。

但是现在的新规范对研发人员有新的要求:

  1. 错误码是分段的 

  2. 错误码返回之前一定要加模块号。

如果都要依靠具体写需求的代码自觉执行,是很难的。一定有人偷懒,或者写错。

所以要保证规范能落地,至少要保证他的使用复杂度要和原来的使用差不多。为了实现这个目标,需要提前提供足够好的封装库,最后我们可以把以上的代码用下面的代码平替:

DCodeRedisError.NewWithDetail("net error")

甚至,如果不需要填充特殊的 detail 信息,可以简写为:

DCodeRedisError.NewWithDetail()

这样一来,代码甚至比原来裸写的代码还短。规范的代码是简单的,而且有人开了头,就会起到带头效应,全部人都复制粘贴规范的代码写法。这也给我们一些思考,好的代码一定要以简洁的方式要提前出现在系统的关键位置,这样大家会模仿,自然代码都是好的。否则大家都去模仿屎山代码,很快系统就会腐化了。

于团队而言,规范得以推广,系统更好维护,于一线同学而言,代码都封装好了,写得更简单。

由于错误码的具体实现代码不是本文重点,以下仅对我们类库的部分代码简单片段,大家可以参考:

funcregError(code int32, defaultErrDetail string)Error {  _, exists := codeDetails[code]if exists { // 防止定义错误码重复panic(fmt.Sprintf("error code %d already used, please redefine a new one!!", code))  }
  codeDetails[code] = defaultErrDetailreturn Error{Code: code, ErrDetail: defaultErrDetail}}
// D类错号(依赖公共组件错误),40000~49999开头var (  DCodeDefault = regError(40000, "调用基础组件错误") // 调用基础组件错误(一级)
/*40100*/  DCodeMySQLError                 = regError(40100, "调用MySQL错误")    // 调用MySQL错误(二级)  DCodeMySQLRecordNotFoundError   = regError(40101, "没有查询到MySQL记录") // (三级)没有查询到MySQL记录  DCodeMySQLDuplicatedRecordError = regError(40102, "重复的MySQL记录")   // (三级)重复的MySQL记录
  DCodeMySQLDBTicketError           = regError(40110, "票务库sql异常")    // (三级)调用票务库mysql错误  DCodeMySQLDBEventError            = regError(40120, "活动库sql异常")    // (三级)调用票务库mysql错误  DCodeMySQLDBActivityError         = regError(40130, "活动通用库sql异常")  // (三级)活动通用库mysql错误  DCodeMySQLDBShoppingError         = regError(40140, "货架库sql异常")    // (三级)货架库mysql错误  DCodeMySQLDBRecordError           = regError(40150, "快录制库sql异常")   // (三级)调用录制库mysql错误  DCodeMeetlogCloudRecordSlaveDbErr = regError(40151, "查询云录制从库异常")   // (三级)调用云录制从库  DCodeMySQLDBMarketingError        = regError(40160, "市场营销sql异常")   // (三级)调用市场营销mysql错误  DCodeMySQLDBCouponError           = regError(40170, "优惠券sql异常")    // (三级)调用优惠券mysql错误  DCodeMeetlogPermissionDbErr       = regError(40180, "查询会记权限数据库异常") // (三级)调用权限库
/*40200*/  DCodeKafkaError = regError(40200, "调用Kafka错误") // 调用Kafka错误(二级)
/*40300*/  DCodeRedisError              = regError(40300, "调用Redis错误") // 调用Redis错误(二级)  DCodeRedisCacheNotFoundError = regError(40301, "没有查询到缓存数据") // 没有查询到缓存数据(三级)  DCodeRedisDirtyDataError     = regError(40302, "读取到缓存脏数据")  // 读取到缓存脏数据(三级)
/*40400*/  DCodeCosError                 = regError(40400, "调用Cos错误")    // 调用Cos错误(二级)  DCodeCosResourceNotFoundError = regError(40401, "没有查询到Cos资源") // 没有查询到Cos资源(三级)

07

告警策略的设计

有了这个前置的错误码设计,可以解决很多告警的挑战点。例如,如何剔除用户行为错误码就变成可能了。

因为只需要看一个整数的第三位数字,是1的。就可以剔除,例如无论是1110501,还是2010501,其实都是一个错误(来源于不同模块,一个11模块,一个20模块),但是第三位都是1。同时1110501和1110666虽然是同一个模块完全不同的错误,但是因为第三位也是1,所以也是用户行为类错误。

属于用户行为类错误即可一刀剔除。

   7.1 告警策略以一刀切的智能被调策略为主

这样,无论多么复杂的场景模调中的被调告警应该都可以覆盖了。我们可以配置这样的告警策略:

  1. 成功率剔除 A 类的请求。

  2. 成功率阈值一刀切:例如99%(调用量越大,理论上干扰越少,这个阈值应该更大)。

这样就是一个能覆盖99%异常的告警策略了,无论哪一个模块都可以简单 copy 使用。

之所以能覆盖的原因是,无论是自己报错,还是下游报错,还是组件报错还是超时,99%都会以 error 的形式在本接口、本消费者抛出,最后会以被被调成功率体现出来。而由于错误码都被规范化了,我们只需要关注真的出问题的那些错误(非A类),那么阈值就可以设置得足够高(当然一开始质量问题较多的时候可以适度调低),由于提前就剔除了错误,干扰项非常少,监控视图会很稳定。

以下是腾讯会议录制业务所有 cgi 接口通过错误码统一治理后的大盘视图。

可以看到左边提前智能剔除后,成功率哪怕最低也有99.99%(符合我们只管认知的,因为系统常规时间肯定处于一个很健康的状态)。而右边则是没有特殊干预的成功率,其波动范围是非常大的,具有上下1.5%的波动率。

有了这样的提前干预,告警就变得很清晰了。而且大部分情况可以配置一刀切的策略。

笔者所在团队今年维护了60+个微服务模块,链路上的总接口数上万个,但是生效告警策略是非常少,原因就是提前治理后可以用一个统一的指导原则去规范告警。区别无非就是部分业务调用量较低为了的防抖动上需要做额外的设置。

   7.2 辅助的告警策略

有一些时候,我们可能写出一些 bug 导致某些 A 类异常码有所提升。例如,因为某些加密算法的密钥出了 bug,导致解析密码不成功,这时候可能会出现“密码错误”这样的 A 类错误增加,再例如前端/客户端可能出现了一些 bug,导致用户输入的内容有问题,出现一些校验类的失败,正常情况下也是可以认为是正常的。一刀切的被调策略就无法体现这个问题了,这时候还是需要一些辅助策略:

  1. 全局成功率。一开始提到的有一定波动成功率的告警。但是这个告警阈值可以稍微降低一点了,因为有了一个比较敏感的智能告警策略,现在这个策略更多是为了发现一些 A 类错误的 bug。

  2. 错误码环比上升策略。针对某个错误码如果环比、同步都有一些异常,哪怕是 A 类错误码,可能也需要关注起来。

  3. 自定义埋点。例如有些时候调用某个接口虽然报错了,可能会降级,降级后被调就不会体现出错误了。这时候自定义埋点的上报&告警就能做到这里的补充。

08

团队内如何切实推动告警消音

告警策略简化后,告警能大体准确的反映系统的一些健康情况了。但是这只是开了一个头,如果系统还是有不少 Bug(哪怕是小 Bug),告警还是会叫个不停。

年初的时候,哪怕提前排除了用户行为类的错误,60多个服务加起来告警数量能高达2000/月。平均下来就是60条告警/天。告警策略已经剔除了用户行为类错误,那么意味着这些告警要么是

  1. 真实告警(基本是无关紧要的小 Bug),

  2. 开发的错误码没处理好(例如某些下游服务(外部系统)的错误应该归为一个 A 类(用户行为类)错误,但是本系统没处理细致直接用 C 类(外部服务类)外露了),

  3. 下游部分服务质量问题/抖动。

前文提到,告警太多甚至比没有告警危害更大,原因在于极大地消耗团队人员的精力,同时频繁定位一些重复的小问题会让人麻木,这时候突然来一个大问题也响应不及时。

所以“消音”是告警有效且保持团队敬畏告警的必要一环。

但是告警不是说看一例就能马上修复一例的。中间隔着分析、开发、调试、测试、发布等多个环节。流程较长,很容易就跟丢,经常就会出现类似的声音:

“这个之前/上周定位过,相同的原因,错误码没处理好不影响业务”

“上次同步过了,后面有空处理”

然后一个月过去了,告警还是那么泛滥。这确实很让团队管理者/业务 owner 头疼。更可怕的是,理论上随着新代码的变更、新需求的发布,累积质量问题是会单调递增的,所以告警也会继续上升。

告警泛滥-->团队看不到头-->一线人员躺平不看告警-->告警持续泛滥。就变成了恶性循环。

如果无法让告警维持在一个较安静的水平,后续的治理其实非常的难。

以下会介绍过去笔者所在团队做过的一些努力:

   8.1 一阶段:突击抓大放小

面对泛滥的告警,无论你如何强烈要求团队每个告警都要分析,都是徒然,因为这是无理、过分的要求。试想一下,哪怕一天60条告警(在很多团队这已经算很少)。每条告警看一下日志、看一下代码哪怕只需要5分钟。一天就需要5个小时。那么日常开发根本就不用进行了,更不用说后面处理告警实际上还需要改代码和发布。

第一要务是要止住泛滥的告警。这时候可以考虑

  1. 先把部分不重要的服务但是已知告警很多的服务阈值调到很低的水平(例如95%),使得小问题不会再冒出来,大问题又不至于完全没有。

  2. 重要服务的阈值也适当调低到团队可处理的范围。例如99%,98%。

  3. 分析治理重复量最大的告警问题,快速突击消灭。通常情况下,一个时间段的一批告警很多都是同一类问题导致的,可能占50%甚至以上,抓住这些问题要求团队突击一波,是可以有效收敛告警数量的。

   8.2 二阶段:追踪每一条告警问题收敛进度

这时候一阶段完成后,告警应该处于一个吵闹但是不至于轰炸的阶段。有可能逐步具备条件要求团队每条告警都必须处理了。

但是前文说了,一个告警从发现到分析再到修复再到上线,实际上跟踪的链条可能很多。怎么保证其最后能修复呢。

有一句话是:“团队管理者关注什么,什么东西就能提升”。

所以必须要有一个方法让团队知道有人是真的在关注这个事,而不是一个口号。我们想到的方法是追踪值班效果。

故而我们在周会上会每条告警内容一条条的过,对其中原因和改进措施、处理进度。

每周的值班人是会轮换的,所以过了一周可能问题就跟丢了。维持我们喋喋不休地在周会会持续往前追溯,只要一个问题没有收敛,就会持续性的追溯。如果当周所有告警问题最后都处理完上线了,这周的对应的值班表才会被隐藏。

这个过程一开始是痛苦的,因为数量确实很多,但是实践下来是有效的:

  1. 因为能持续输出给团队这样的一个信息:告警非常重要,和项目进度一样,都是团队高优的事项,所以每周都过。

  2. 如果你的问题一直没有处理,相同的问题会持续性的在周会上被“鞭尸”,一定程度上会促进事情的落地。你的事不闭环,别人闭环了,一定程度上促进大家良性的“卷”起来。

  3. “消消乐”很让人解压。每周在周会追溯之前的问题时候,发现当周问题都被处理完了,然后隐藏该 sheet 的时候,就像玩“消消乐”看到积木消失一样,很让人解压,以及给到团队一定的成就感。

  4. 告警数量变少了,每天安静的工作,少了无效的定位工作,少了持续救火。工作体验大大上升。

   8.3 三阶段:没有度量就没有优化

一二阶段都治理过后,相信告警能下降到10位数乃至个位数了,这时候如果还想继续优化,乃至希望能清0。

就非常考验团队对于细微问题的忍耐力(反正就小问题,也没人反馈是不是就可以不处理?)以及对告警的重视程度(有告警是不是立刻有人去看)。如果有很负责的同学他当周处理得很好,可能当周告警能从100例下降到50例,但是感受上大家其实是没有什么感觉的,这变相抑制了有责任心的同学的积极性,也是对他们的不公平。

所以度量每周的告警指标,显得尤为重要。那么应该关注什么东西,又如何度量呢?

以下是笔者的思考:

1.从结果上看,告警应该是要呈现收敛的趋势。

那么这个可以通过度量每周的告警次数来判断。所以最核心的指标就是周告警数。

以下是半年来每周的告警追踪情况,可以看到持续的治理长期来看有效的,而有些时候有反弹的通常是当周外部团队或者发布需求确实发出了一些bug导致的突增,但是很快解决后又会下降。

2.告警应该是有效的,即有问题的话,应该一定要有告警。有告警的话,希望都是真实反馈问题的。

我们定义了一个指标:“告警主动拦截率”,就是说当周值班收到的所有问题如果告警先发现的,就算主动拦截了。如果一个问题最后暴露给了用户反馈,就算被动反馈。这样能一定程度反应告警的有效性及问题闭环性。

最后要求值班同学的周报中需要体现这个指标:

3.告警的处理指标。

如果以上是告警的关键结果指标(KPI),要达到这个 KPI,我们还需要一个关键的过程指标,去持续关注是否 KPI 能达成。我们还需要定义这样的 leading indicator(引领指标)去反映这样一个问题:告警是否有人处理,隔了多久才开始处理,处理了多久。

相信很多团队过往都遇到过这样的问题,有一个告警来了,但是事后回头看的时候不知道这个告警有没有人看过、处理过。大家可以搜下“谁在看”这个关键字,看看工作群里出现的频次。

笔者认为,这个问题的关键在于告警信息本身无法传达出处理人和处理结果。可能有读者用过智研的告警会说,告警有个“认领”的功能,可以解决这个问题。

这个功能实现上上有点鸡肋,有以下几个问题是解决不了的:

  1. 认领的时候会跳出来一条新的信息。如果告警只有一条的话,信息传达还算清晰。但是如果一个时段(例如1个小时)来了5条告警,事后有人再去点击某个告警的认领,其实是非常难对应起来哪条被认领过哪条没有认领。

  2. 认领是认领了。但是处理的结果呢?什么原因,处理了没有是没有传达的手段。所以最后还是得靠口口相传。

因此针对告警的样式,我们自研开发了一个小工具——火力哨兵,他是传达告警信息的工具,会吐出来一张告警卡片,具备一点交互的能力:

如果一个告警没有被认领,那么认领人会一直处于空的状态。直到有人点击“我来分析”,这个卡片的样式会自动变化(而不是吐出一条新消息),把认领人的信息填写上去。

再之后,这个过程的时长会被统计下来,这个时长会反应团队对于告警的响应是否及时。

最后,有了处理结果后,只需要点击“告警原因同步”。

这个结果就会继续在原告警卡片上迭代更新:

而且度量统计出这个处理时长。

这样以后,告警群再也不会有人问:“这个问题谁在看吗”这样的问题了,是否有人看,处理结果是什么都一目了然。

4.持续关注度量优化

最后,告警被处理了多少会以“处理率”、“认领率”等指标被工具自动统计而推送出来。有利于持续关注团队对于告警的重视程度。而且处理结果可以提前预设分类,从而分析每周的告警原因发布都在什么地方,反向驱动业务代码的优化。

09

消音战果

整个告警治理从年初到现在,持续滚动关注了大半年,总算取得了阶段性的胜利。笔者所在团队的两个子业务,会议录制、会议活动业务均有不错的成果。

数据汇总如下:

1月份:

业务 A 总量:1531, 

业务 B 总量:1144,

合计:2716。

7月份:

业务 A 月告警总量:54,

业务 B 月告警总量:17,

合计:71。

告警总共下降了97.39%,从告警总量上看,经过半年的告警治理,告警数量收敛效果明显,甚至超过了年初制定的目标:85%。

10

还在路上

即便取得了一定的成果,但是我们并不能掉以轻心,觉得自己已经做得足够好了。因为告警治理的道路是无止无尽的。原因如下:

  1. 业务代码是持续迭代的,也就是说质量问题本身是倾向于以单调递增的增加。如果不持续努力,总有一天告警数量又会泛滥。

  2. 告警总是有覆盖不到的情况。虽然我们提出了模调为主的告警策略,但是此策略只能告警出90%左右的问题,剩下的10%总还是需要辅以其他手段去治理。也就是说我们可以用战略的努力拿到一份90分的答卷,但是最后的10分还是需要付出非常大的勤奋、汗水。

  3. 新的模块可以尝试错误码提前规范去治理业务。老的模块则有很多历史包袱,这需要如何有效治理是我们还需要思考的。

11

小结

最后总结下这次告警治理的一些经验之谈:

  1. 战略上需要提前规划错误码,以便后续的治理战场上能百战不殆。

  2. 告警策略需要集中专注,以模调作为重点牵引指标去治理,其他指标为辅。

  3. 建立有效的值班机制和数据 review 流程,传达告警处理的重要性,跟进问题的闭环性。

  4. 治理结果度量化,过程工具化。

-End-

原创作者|林俊杰

感谢你读到这里,不如关注一下?👇

图片

📢📢来领开发者专属福利!点击下方图片直达👇

图片

图片

你在实际工作中遇到过哪些告警治理的难题?欢迎评论留言补充。我们将选取1则优质的评论,送出腾讯云定制文件袋套装1个(见下图)。6月4日中午12点开奖。

图片

图片

图片

图片

图片

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值