上个月的一个晚上,线上突然报了一个Serverless相关的Bug,服务大面积报错。我从晚上十点开始排查,一直到第二天早上六点才找到根因并修复。这篇文章记录了这次排查的全过程,包括故障现象、排查思路、踩过的坑、最终的根因和解决方案,以及从这次故障中总结的经验教训。如果你也在使用Serverless架构,希望这篇文章能帮你避免类似的坑。

一、故障发生

那天是周五,本来是一个平静的夜晚。我刚洗完澡准备看会儿书就睡觉,手机突然响了。是运维群里的告警消息,线上服务报错率突然飙升,从正常的0.1%一下子跳到了30%多,而且还在持续上升。

我心里咯噔一下,赶紧打开电脑登录监控系统。一看确实出问题了,我们的核心接口大面积报错,错误类型主要是超时和502错误。用户反馈也开始进来了,很多人说页面打不开,功能用不了。

这个服务是我们半年前迁移到Serverless架构上的,用的是阿里云的函数计算。迁移之后一直运行得很稳定,从来没有出过这么大的问题。我第一反应是是不是流量突增了,因为周五晚上通常是流量高峰。但看了一下监控,流量并没有明显增加,和平时差不多。

那就奇怪了,流量没增加,为什么会突然大面积报错呢?我开始了漫长的排查过程。

二、第一轮排查:看日志

排查线上问题的第一步永远是看日志。我打开函数计算的日志面板,想看看到底报了什么错。

但一看日志我更懵了。日志里大部分请求都是正常的,只有少量报错,而且报错的原因五花八门。有的是连接数据库超时,有的是调用第三方API失败,有的是内存不足,有的是函数执行超时。看起来不像是一个统一的原因导致的,更像是系统整体出了问题。

我注意到一个细节,报错的请求大部分都是冷启动的请求。函数计算的日志里会标记每个请求是冷启动还是热启动,冷启动就是函数实例没有被复用,需要重新启动一个新实例。冷启动的请求耗时会比热启动长很多,因为需要初始化运行环境、加载代码、建立数据库连接等。

我统计了一下,冷启动的比例从平时的5%左右突然上升到了40%多。这就解释了为什么会有这么多超时错误,因为冷启动的请求本身就慢,再加上系统负载高,就更容易超时了。

但为什么冷启动比例会突然上升呢?冷启动比例上升通常意味着函数实例被大量回收了,或者请求的并发模式发生了变化,导致实例无法被复用。

我看了一下实例数监控,发现实例数确实在剧烈波动。平时实例数比较稳定,维持在一个相对固定的水平。但今天晚上实例数忽高忽低,一会儿飙升到几百个,一会儿又降到几十个。这种剧烈波动导致大量冷启动,进而导致大量超时错误。

那为什么实例数会剧烈波动呢?我开始怀疑是不是函数计算的自动扩缩容出了问题。

三、第二轮排查:自动扩缩容

函数计算的自动扩缩容是根据请求的并发量来调整实例数的。请求多了就增加实例,请求少了就减少实例。这个机制平时工作得很好,但今天晚上好像出了问题。

我仔细看了一下扩缩容的监控数据,发现了一个奇怪的现象。实例数的增加和减少都非常剧烈,而且频率很高。有时候几秒钟之内就从几十个实例增加到几百个,然后又在几分钟之内降回几十个。这种剧烈的扩缩容在平时是很少见的。

我开始研究函数计算的扩缩容策略。函数计算的自动扩缩容是基于指标的,主要看的是每个实例的并发请求数。当并发请求数超过阈值时就会扩容,低于阈值时就会缩容。

我看了一下我们配置的扩缩容阈值,发现了一个问题。我们配置的并发阈值是10,也就是说当每个实例的平均并发数超过10时就会扩容。但我们的函数执行时间比较长,平均执行时间在5秒左右。这意味着一个实例每秒只能处理0.2个请求。如果流量稍微增加一点,并发数就很容易超过阈值,触发扩容。

但这也不是什么新问题啊,我们的配置一直是这样的,以前为什么没出问题呢?

我继续往下查,发现了一个更关键的问题。函数计算的缩容策略是,当实例空闲超过一定时间后就会被回收。我们配置的空闲回收时间是60秒。也就是说如果一个实例在60秒内没有处理任何请求,就会被回收掉。

这个配置平时也没问题,因为流量比较均匀,实例很少会空闲超过60秒。但今天晚上流量模式发生了变化,变得很不均匀,一波一波的。一波流量过来,触发大量扩容,实例数飙升。然后流量低谷期,很多实例空闲超过60秒被回收,实例数骤降。然后下一波流量又来,又触发大量扩容,大量冷启动。如此循环往复,导致实例数剧烈波动,冷启动比例飙升,大量请求超时。

但为什么流量模式会突然变化呢?我们的流量一直比较均匀啊。

四、第三轮排查:流量模式

我开始分析流量模式。把今天晚上的流量数据和平时的流量数据做了对比。

一看对比我发现了问题。今天晚上的流量确实和平时不一样。平时的流量比较均匀,每秒的请求数波动不大。但今天晚上的流量呈现出明显的周期性波动,大概每5分钟一个周期,请求数在短时间内飙升到平时的3倍,然后又迅速降下来,过几分钟又来一波。

这种周期性的流量波动不像是正常的用户流量,更像是某种定时任务或者批量请求导致的。我开始查是什么导致了这种周期性流量。

我查了一下请求的来源IP和User-Agent,发现这些周期性的请求大部分都来自同一个IP段,而且User-Agent都是同一个,是我们内部的一个数据同步服务。

这个数据同步服务是我们另一个团队开发的,用来定时从我们的服务同步数据。它的逻辑是每隔5分钟调用一次我们的接口,一次性拉取大量数据。平时这个服务也在运行,但每次拉取的数据量不大,对我们的服务影响不大。

我联系了那个团队的负责人,问他们最近是不是改了什么。他们说今天下午刚上线了一个新版本,把数据同步的频率从每小时一次改成了每5分钟一次,而且每次拉取的数据量也增加了。他们说测试的时候没发现什么问题,就直接上线了。

我一听就明白了。就是这个改动导致了周期性的流量高峰。每5分钟一次的大批量请求,导致我们的服务流量瞬间飙升,触发函数计算的自动扩容,实例数暴增。然后请求结束,流量骤降,大量实例空闲超过60秒被回收。然后5分钟后下一波请求又来,又触发大量扩容,大量冷启动。如此循环,导致系统不稳定。

找到原因了,但问题是怎么解决呢?

五、尝试解决方案

找到原因之后我开始尝试解决方案。当时已经是凌晨两点了,我必须尽快恢复服务。

方案一:调整扩缩容配置

我首先想到的是调整函数计算的扩缩容配置。比如提高并发阈值,让它不那么容易扩容;或者延长空闲回收时间,让实例不那么容易被回收。

我先尝试把并发阈值从10提高到20。这样每个实例可以承受更多的并发请求,不容易触发扩容。改完之后观察了一会儿,发现扩容的频率确实降低了一些,但问题没有根本解决。因为数据同步服务的请求量太大了,即使阈值提高到20,还是会触发大量扩容。

然后我尝试把空闲回收时间从60秒延长到300秒。这样实例空闲之后不会马上被回收,而是会保留5分钟。这样下一波请求来的时候就可以复用这些实例,减少冷启动。改完之后效果好了很多,冷启动比例明显下降了,报错率也降下来了。

但这个方案有个问题,延长空闲回收时间意味着实例会保留更长时间,成本会增加。因为函数计算是按实例运行时间计费的,实例保留时间越长费用越高。而且如果流量持续很低,保留大量空闲实例会浪费很多钱。

所以这个方案只能作为临时方案,不能作为长期解决方案。

方案二:限制数据同步服务的请求速率

另一个思路是限制数据同步服务的请求速率,让它不要一次性发这么多请求。我联系了那个团队,让他们把数据同步的频率改回去,或者把大批量请求拆分成小批量,均匀地发送。

但那个团队说他们刚上线的新版本有业务需求,不能马上改回去,至少要等到下周一才能调整。而且他们说拆分请求需要改代码,也需要时间。

这个方案短期内行不通,我只能先靠调整扩缩容配置来扛着。

方案三:预留实例

函数计算还有一个功能叫预留实例,就是预先保留一定数量的实例,这些实例不会被回收,一直保持运行状态。这样即使流量波动,也有一定数量的实例可以直接处理请求,不需要冷启动。

我尝试开通了预留实例,预留了50个实例。这样不管流量怎么波动,至少有50个实例是一直在线的,可以处理大部分请求。剩下的流量再靠自动扩缩容来处理。

开通预留实例之后效果非常好,冷启动比例降到了5%以下,报错率也降到了正常水平。服务终于恢复了稳定。

但预留实例的成本比较高,因为这些实例是一直运行的,不管有没有请求都要计费。50个预留实例一个月要多花不少钱。所以这也只是一个临时方案,长期来看还是需要从根本上解决问题。

六、最终的根因和长期解决方案

服务恢复稳定的时候已经是早上六点了。我拖着疲惫的身体开始复盘这次故障,思考长期的解决方案。

这次故障的根本原因是什么呢?表面上看是数据同步服务的流量波动导致的,但深层次的原因是我们的Serverless架构对流量波动的容忍度太低了。

Serverless架构的特点是按需扩缩容,这个特点在流量均匀的时候非常好,可以节省成本。但在流量波动大的时候,频繁的扩缩容会导致大量冷启动,影响服务稳定性。这是Serverless架构的一个固有特性,也是我们在使用Serverless架构时必须考虑的问题。

基于这个认识,我制定了以下长期解决方案。

1. 优化函数启动速度

冷启动的影响大小取决于函数启动时间。函数启动越快,冷启动对请求的影响越小。所以我们要尽量优化函数的启动速度。

我们做了以下优化:

  • 减少代码包大小,去掉不必要的依赖,代码包从50MB减小到15MB
  • 优化初始化逻辑,把不需要在启动时做的事情延迟到第一次请求时做
  • 使用更轻量的运行时,从Node.js换成了更轻量的运行时
  • 预热机制,定时发送少量请求保持实例活跃

通过这些优化,函数的冷启动时间从原来的3秒降低到了800毫秒,冷启动对请求的影响大大减小。

2. 调整扩缩容策略

我们重新调整了扩缩容策略,让它更适合我们的流量模式。

  • 并发阈值根据函数执行时间动态调整,执行时间长的函数阈值设高一些
  • 空闲回收时间延长到120秒,减少实例频繁回收
  • 开启按量实例的缓冲模式,让扩缩容更平滑
  • 设置最大实例数,防止异常流量导致实例数无限飙升

通过这些调整,扩缩容变得更加平滑,实例数波动减小,冷启动比例降低。

3. 流量削峰

对于数据同步服务这种周期性的大批量请求,我们在API网关层加了流量削峰功能。把短时间内的大批量请求缓存起来,然后以一个稳定的速率转发给后端服务。这样后端服务看到的流量就是均匀的,不会出现剧烈波动。

流量削峰功能上线之后,数据同步服务的请求对我们后端服务的影响就很小了。即使数据同步服务一次性发大量请求,API网关也会把它们均匀地分发出去,后端服务的流量始终保持平稳。

4. 服务降级和限流

我们还加了服务降级和限流机制。当系统负载过高时,自动降级非核心功能,保证核心功能的可用性。同时对异常的大批量请求进行限流,防止单个调用方把整个服务打垮。

这些机制虽然不能完全避免故障,但可以在故障发生时减小影响范围,保证核心功能的可用性。

5. 完善监控和告警

最后我们完善了监控和告警系统。增加了冷启动比例、实例数波动、扩缩容频率等监控指标。设置了更合理的告警阈值,在问题刚出现时就能发现,而不是等到大面积报错了才知道。

同时我们建立了故障演练机制,定期模拟各种故障场景,检验系统的容错能力,确保在真正的故障发生时能够快速响应和恢复。

七、从这次故障中学到的经验

这次排查了一夜的故障,让我学到了很多经验教训。

1. Serverless不是银弹

很多人觉得Serverless架构很美好,不用管服务器,不用管扩缩容,一切都自动搞定。但这次故障让我深刻认识到,Serverless不是银弹,它有自己的适用场景和局限性。

Serverless适合流量比较均匀、函数执行时间短、对冷启动不敏感的场景。如果流量波动大、函数执行时间长、对延迟敏感,Serverless架构可能会带来一些问题。在选择架构的时候要根据业务场景来决定,不能盲目跟风。

即使使用了Serverless架构,也要深入理解它的工作原理,了解它的扩缩容机制、冷启动特性、计费方式等。只有深入理解了,才能在出问题的时候快速定位和解决。

2. 变更管理很重要

这次故障的直接原因是另一个团队的服务上线了新版本,改变了流量模式。但他们在上线之前没有通知我们,也没有做充分的测试,导致我们的服务受到了影响。

这暴露了我们在变更管理上的问题。跨团队的服务调用,变更之前应该通知依赖方,评估影响范围,做充分的测试。不能自己觉得没问题就直接上线,因为你的变更可能会影响到其他服务。

后来我们建立了跨团队的变更通知机制。任何服务的重要变更,都要提前通知所有依赖方,评估影响,共同测试。这样可以避免很多因为变更导致的故障。

3. 监控要全面

这次故障刚开始的时候,我们只看到了报错率上升,但没有及时发现冷启动比例上升和实例数波动。因为我们的监控指标不够全面,没有覆盖到这些关键指标。

后来我们完善了监控系统,增加了很多Serverless相关的监控指标,比如冷启动比例、实例数、扩缩容频率、实例空闲时间等。有了这些指标,我们就能更早地发现问题,更快地定位根因。

监控是运维的眼睛。监控越全面,发现问题就越早,定位问题就越快。不要等到出了大问题才发现监控不够用。

4. 预案很重要

这次故障发生的时候,我们没有现成的应急预案,只能临时想办法,走了很多弯路。如果有应急预案,就可以更快地恢复服务,减少故障影响时间。

后来我们针对各种可能的故障场景制定了应急预案,包括流量突增、数据库故障、第三方服务不可用、Serverless扩缩容异常等。每个预案都明确了故障现象、排查步骤、处理方法、负责人等。定期进行故障演练,确保每个人都熟悉应急预案。

有了应急预案之后,再遇到类似的故障,就可以按预案快速处理,不用临时想办法,大大缩短了故障恢复时间。

5. 身体是革命的本钱

最后一点也是最重要的一点,身体是革命的本钱。这次排查了一夜,第二天整个人都昏昏沉沉的,连续好几天才恢复过来。长期这样熬夜排查故障,对身体的伤害很大。

作为技术人员,我们要注意保护自己的身体。合理安排工作时间,尽量避免熬夜。加强锻炼,保持健康的生活习惯。只有身体好了,才能更好地工作和生活。

同时公司也要注意保护员工的健康,合理安排值班,建立完善的故障处理机制,减少深夜排查故障的情况。不要让员工用健康来换系统的稳定性。

八、写在最后

这次Serverless故障从晚上十点排查到第二天早上六点,整整八个小时。虽然过程很辛苦,但也让我学到了很多东西。对Serverless架构有了更深的理解,对线上故障排查有了更多的经验,对系统设计和运维也有了更多的思考。

Serverless是一个很好的架构,它让我们从繁琐的服务器管理中解放出来,可以更专注于业务逻辑的开发。但它不是完美的,也有自己的问题和局限性。我们要理性看待它,在适合的场景使用它,同时要深入理解它的原理,做好监控和预案,确保系统的稳定性。

线上故障是每个技术人员都会遇到的事情。重要的不是避免所有故障,因为那是不可能的,而是在故障发生时能够快速发现、快速定位、快速恢复,并且从每次故障中学习和成长,避免同样的故障再次发生。

最后我想说,技术之路漫漫,我们都在不断学习和成长。每一次故障都是一次宝贵的学习机会,每一次排查都是一次能力的提升。愿我们都能在一次次的故障中变得更加强大,构建出更加稳定可靠的系统。

用一句话结束本文:"线上故障不可怕,可怕的是不从故障中学习。"愿每一个技术人都能从每次故障中汲取经验,不断成长,不断进步。