怎样预防"天外飞锅"

1,485 阅读9分钟

聊聊"兜底"和"容灾"

老赵,又名赵青铜,是一名前端程序员。

兜底

一个明媚的下午,赵青铜悠闲的在海边度假。带着墨镜,吹着海风,和旁边的小姐姐有说有聊。突然这时手机响了,他看了一眼来电号码,眉头一皱。

电话那边:"老赵啊,线上点击xx,页面跳转不了了,你快看看!"

老赵一惊,心想:"我x,前两天刚上线了这个功能,测试都通过了,没理由有问题啊"

这时小姐姐向老赵发出邀请,询问接下来的时间安排。老赵尴尬而不失礼貌的说道:"不好意思,世界需要我,你等我一会。"然后一溜烟的跑回酒店,从背包熟练的翻出他那15寸的macbook pro。

连上手机抓了一个包,一查,发现是后台数据有异常。

怎么办?前端发版容易,前端先简单兼容下。

老赵在酒店度过了一个难忘的下午,此时他心里一万头神兽飞过。

神兽.jpg

回公司以后,老赵复盘了一下,问题的原因就是因为没有对接口异常数据的提前预判和处理。

他吸取了上次的经验,针对数据写了很多增强健壮性的代码:空数据兼容、4xx5xx处理、错误码友好提示等等。

自此以后,接口的问题再也波及不到老赵了,开心下班,无人打扰,幸福指数up。

容灾

过了一年,老赵负责的业务规模大了起来,用户越来越多,他也从青铜升级到了白银。

又一个明媚的下午,赵白银又来到了熟悉的海边,熟悉的酒吧。不一样的是,旁边的小姐姐换了一个。正当此时,他的手机又响了。老赵看了一眼:又是你。

电话那边:"老赵老赵不好了,用户访问人数过多,服务器崩啦!现在进入首页得等3秒钟,你快想想办法啊!!"

老赵微微一笑:我早就不是当年那个青铜了。他镇定自若的对着电话回答道:"快启动我的预留方案。"

原来老赵早就考虑到了这样的可能性,提前在代码里面做了非主流程业务的开关。将这个开关打开,跟主流程不相关的花边功能都会停掉,减少了请求接口的数量,减轻了服务器的压力。

果然如他所预测,开关打开之后,服务器的压力减轻了很多,访问速度也正常了。

旁边的小姐姐问他:"谁打来的电话呀,你是不是有啥急事?"

老赵微笑道:"小事,不重要。"

小场面

前端的兜底和容灾

前端作为业务的门面,是用户和产品打交道的唯一途径。

这就意味着,不管是服务出的问题还是界面出的问题,用户都是通过前端感受到的。

不同规模业务的老板,对前端要求也不一样,大致区别就是:"功能可用" vs "功能一直可用"。

身为前端有时真的很南。但是如果提前做好了准备,不论在公司还是马尔代夫都能完美甩锅。

前端兜底

有的兜底方案,你的老板不一定要求你去做,属于附加题。但是对于有一定规模的业务来说,这是基础题。

异常数据兼容:

最常见的兜底就是接口异常数据兼容。

// ...
var data = null
try {
    // 接口的 json 格式不标准时会出现异常
    data = JSON.parse(json)
} catch (e) {}

if (data && data.foo) {
    // 遇到访问对象时,先判断对象是否存在,再判断属性是否存在
} else {
    // 异常情况,看情况处理
    close()
}

有时候代码写 high 了经常会忘了做个非空判断。最近 es2020 新增了个链式操作,备胎终于转正,非空检测也可以写的很优雅了。

obj?.prop // 访问可选的静态属性
// 等价于
(obj !== undefined && obj !== null) ? obj.prop : undefined

接口错误兼容:

如果接口挂了,确保业务不会受到它的影响。

request('//exception.api.com/get')
    .then(json => {
        // 正常处理
    }).catch(e => {
        // 接口请求异常,看情况处理
        closeFeature()
    })

要求比较高的会故意做些捣乱测试,将接口强行返回空数据或者 404,检查业务表现是否正常。

资源重加载:

当资源服务器返回静态资源挂了的时候,要自动重试。

一般来说,会在公共方法里面封装这个操作。

loadJs('//xxximg.cdn.com/sfslnaf.js') // loadJs 方法里会自动重试

对于图片,会在 onerror 上做这个动作。

<img src="//xxximg.cdn.com/test.png" onerror="__reloadResource(this)">

要求更严格的还会准备备用的 cdn,第二次加载会改为使用备用 cdn 的域名。

数据兜底:

当资源重加载还失败的时候,某些场景需要展示兜底数据。

比如默认图片。

代码回退:

严格来讲,这个不算兜底,比较像是运维方案。

在成熟的可运营系统当中,线上出现了问题或者疑似出现问题,第一个操作一定是回退。有什么问题回退了再慢慢去看,保证业务第一时间可用。

这里就要保证有随时回退的能力。比如保留每次发版的 backup,留作备用。最好可以搭建一个自动化回退,以防人肉操作时翻车。

没有什么问题是一次代码回退解决不了的,如果有就大家一起回退一次。

前端容灾

前端容灾常见于有一定规模的业务当中。目的是在"几近崩溃"的情况下,还能给用户提供服务。

那么问题来了,为啥都崩溃了,还要提供服务?

当然是 因为钱啊! 因为用户规模太大了呀,一崩起来,造成的负面影响不可估计。所以要想方设法的保证服务可用。

在非常大流量的业务中,会故意把服务都整挂了,研究在缺少接口服务的情况下,应该怎么做。

大部分方案都会选择再开一个备用服务,也就是 planB。颇有养兵千日用兵一时的味道。

服务降级:

保留一份只有基础功能的页面,比如产品最初的v1.x版本。当遇到业务大规模不可用的时候,切换到这个版本,可以减少服务的压力。

基础版服务
基础版服务

通俗来叫,就是“乞丐版”,文艺点叫做“容灾页”。这里面只有核心业务,可能丑了点,但是能保证上下游流程的正常,甚至还非常流畅。

在有一定规模量的核心业务中,这是非常重要的一个策略。只有确保了核心服务的稳定,才有挣钱服务发挥的余地。

有的公司还会定期测试,能否随时切换到容灾页,以确保服务健壮性得到保障。

数据缓存:

浏览器的 localStorage 可以储存非常多的东西,可以在这里缓存接口甚至脚本数据。如果遇到服务不可用的时候,可以考虑使用这个方法。

另外 PWA 可以拦截请求,也可以在这里做接口数据的缓存,甚至是前端页面的缓存,让用户感受不到有出问题的迹象。

灰度&开关:

有一些预期不明朗的功能,或者有风险的特性,可以做灰度或者弄个开关。

这里的灰度类似 A/B test,一部分用户看的是新特性 A,另一部分还在原来的 B 上。

一出问题,马上停止灰度或者关闭开关。

很多时候我们是被动做这个事情,大部分来自于产品的要求。相对于产品对数据的负责,这里是对我们自己的业务负责。

定期测试/演习:

养兵千日用兵一时。花钱花精力养了这么久已经这么辛苦了,如果突然要用但是用不了就更悲剧了。因此要保持一定频率的测试,确保这些容灾方案可用。有的人称这个过程为:演习。

大概就是在夜深人静,用户量少的时候,按照清单一项项停掉服务,捣乱业务。检查前端的业务能不能按照预期进行,然后记录。当然演习期间也会要求服务最低限度可用。

如果能够预见流量的高峰,还可以非常有规律的按照计划去进行,比如618和双11的前一两个月的时候。

这和我有什么关系

兜底指的是:当数据异常时,依然保证服务可用。

容灾指的是:发生灾害时,依然可以保证服务可用。

文章开头老赵遇到的这两类问题,前者很多前端er都遇到过,后者需要业务有一定的规模量才会遇到。问题不一定是自己直接造成的,但是这个锅总是要挨点儿边。

技术服务于业务,抛开场景谈技术都是耍流氓。

一开始我们相信后台的队友,服务会很稳定,response 也按照约定的那样。然而理想很丰满,现实很骨感。经历一次又一次的被队友伤害后,渐渐地我们谁也不相信了。头秃了,但是也变强了。

另一方面。小规模的企业急于发展业务和活下来,线上只要不是无法访问,出了问题也不是那么重视。中等、大规模的公司要求就不一样了,用户不能跑,铁饭碗不能丢。甚至还要求当出现故障的时候,系统还能提供接近完整的服务。做不到就拿你的 kpi 来“祭天”。

本质还是因为业务需要,某一个阶段做某些事情。可能目前所在的业务并不关注这些"补丁",但是假设 跳槽 去一个日活千万的业务,进门之后,你猜他们会问些什么?[手动狗头]