我们的Serverless应用线上出了一个奇怪的Bug,函数偶尔会超时,排查了整整一夜才找到原因。本文记录了这次故障的排查过程,包括问题现象、排查思路、Serverless环境的调试难点、最终找到的根因、以及解决方案。如果你也在用Serverless架构,或者对云原生故障排查感兴趣,希望这篇文章能帮到你。

一、背景

先说说事情的背景。

我们有一个业务用了Serverless架构,部署在阿里云的函数计算(FC)上。这个业务是一个图片处理服务,用户上传图片之后,函数自动处理(压缩、裁剪、加水印),然后存储到OSS。用Serverless的好处是不用管理服务器,按需付费,自动扩缩容。

这个服务已经跑了大半年了,一直很稳定。但是有一天晚上,监控告警突然响了,说函数调用超时率升高了。我们打开监控一看,超时率从平时的0.1%升到了5%左右,而且还在波动。

刚开始我们以为是流量突增导致的,但是看了一下流量,并没有明显增加。而且超时的函数都是随机的,不是某个特定的函数或者特定的输入。

这个问题看起来很简单,但是排查起来非常困难,因为Serverless环境和传统的服务器环境不一样,调试工具很少,日志也不全。我们花了整整一夜,才终于找到了原因。

二、问题现象

先说说问题的具体现象。

我们的图片处理函数,设置的超时时间是30秒。正常情况下,处理一张图片只需要1到3秒。但是从某天晚上开始,偶尔会有函数调用超过30秒,被平台强制终止。

超时的特点:

  • 随机发生,没有明显的规律
  • 有时候连续好几次都超时,有时候几个小时都不超时
  • 超时的函数输入都是正常的图片,没有特别大或者特别奇怪的
  • 超时的时候,函数的日志很少,好像刚开始执行就卡住了
  • 重试之后大部分都能成功

这种随机的、偶发的问题最难排查,因为很难稳定复现。

三、Serverless调试的难点

在说排查过程之前,先说说Serverless环境调试的难点。

1. 无法登录服务器

Serverless环境下,你没有服务器可以登录,不能像传统服务器那样用top、ps、netstat等命令查看系统状态。函数运行在平台管理的容器里,你只能通过日志和监控来了解情况。

2. 调试工具少

Serverless环境下能用的调试工具很少。不能用gdb、strace、tcpdump这些传统的调试工具。大部分时候只能靠打日志来排查。

3. 冷启动问题

Serverless函数有冷启动的问题,第一次调用的时候需要初始化环境,会比较慢。但是冷启动的慢是有规律的,和我们遇到的随机超时不一样。

4. 环境不透明

Serverless平台的底层环境对你是不透明的。你不知道底层的CPU、内存、网络是什么情况,也不知道平台在做什么调度。有时候问题可能出在平台侧,而不是你的代码。

5. 日志有限

函数的日志有大小限制和保留时间限制,而且函数被强制终止的时候,可能日志都没来得及输出。

这些难点导致Serverless环境的故障排查比传统服务器困难很多。

四、排查过程

下面详细说说排查的过程。

第一步:看监控和日志

遇到问题,第一步当然是看监控和日志。我们看了函数的监控指标,包括调用次数、超时率、内存使用、CPU使用、执行时间等。

发现超时的时候,内存使用正常,CPU使用也不高,但是执行时间突然飙升到30秒(超时时间)。这说明函数不是在做计算,而是在等什么东西。

我们又看了函数的日志,发现超时的函数日志很少,有的只有开头的几行,然后就没有了。这说明函数在执行早期就卡住了,可能是在初始化的时候,或者在调用某个外部服务的时候。

第二步:检查外部依赖

函数卡住,最常见的原因是外部依赖慢或者不可用。我们的函数依赖几个外部服务:OSS(存储图片)、一个内部的API服务、Redis(缓存)。

我们检查了这几个服务的监控,发现它们都很正常,没有延迟升高或者错误率升高的情况。而且如果是外部服务的问题,应该是所有函数都受影响,而不是随机的几个。

我们又在函数里加了更详细的日志,记录每个外部调用的开始和结束时间。结果发现,超时的函数,有的是在调用OSS的时候卡住,有的是在调用内部API的时候卡住,还有的甚至还没开始调用外部服务就卡住了。

这说明不是某个特定外部服务的问题。

第三步:检查代码逻辑

我们仔细review了函数的代码,没有发现明显的死循环或者阻塞操作。代码逻辑很简单:下载图片、处理图片、上传图片、返回结果。

我们也检查了依赖的库,都是常用的库,没有已知的Bug。

第四步:本地复现

我们尝试在本地复现这个问题,但是本地跑了几百次都没有超时。本地环境和线上环境不一样,可能复现不了。

我们又在测试环境部署了同样的函数,跑了压力测试,也没有复现。

第五步:怀疑是平台问题

代码和外部依赖都没问题,我们开始怀疑是Serverless平台的问题。可能是平台的某个Bug,或者是底层资源的问题。

我们联系了云厂商的技术支持,他们查了一下,说平台侧没有异常,让我们再检查一下代码。

这时候已经是凌晨了,我们有点绝望,因为所有能查的都查了,还是找不到原因。

第六步:关键发现

就在我们准备放弃的时候,一个同事注意到了一个细节:超时的函数,都是在函数实例被复用的时候发生的。

Serverless平台为了提升性能,会复用函数实例。一个函数执行完之后,实例不会立刻销毁,而是保留一段时间,如果有新的调用,就直接用这个实例,避免冷启动。

我们看了一下实例的生命周期,发现超时的函数,都是在实例已经运行了一段时间(比如几个小时)之后发生的。新启动的实例从来没有超时过。

这个发现很关键,说明问题和实例的运行时间有关,可能是某种资源泄漏或者状态累积。

第七步:找到根因

有了这个线索,我们重新审查了代码,重点关注那些可能在实例复用的时候出问题的地方。

终于,我们找到了问题所在。

我们的函数用了一个HTTP客户端库来调用外部API。这个库在初始化的时候会创建一个连接池,复用HTTP连接。但是这个库有一个Bug:当连接池中的连接因为网络原因被断开之后,库不会自动清理这些断开的连接。下次从连接池取连接的时候,可能会取到一个已经断开的连接,然后就会卡住,直到超时。

为什么是随机的呢?因为连接是否断开是随机的,取决于网络状况和平台的调度。为什么新实例不会超时?因为新实例的连接池是空的,每次都会新建连接,不会取到断开的连接。为什么实例运行时间长了容易超时?因为运行时间越长,连接池中累积的断开连接越多,取到坏连接的概率越大。

这个Bug在传统服务器环境下也会出现,但是传统服务器的连接池通常有健康检查机制,会自动清理坏连接。而我们用的这个库的连接池没有健康检查,所以在Serverless环境下,因为实例复用时间长,问题就暴露出来了。

五、解决方案

找到根因之后,解决方案就简单了。

临时方案

我们的临时方案是在函数每次执行结束的时候,关闭HTTP客户端,销毁连接池。这样每次函数执行都会新建连接,不会取到坏连接。

虽然这样会有一点性能损失(每次都要建连接),但是至少不会超时了。

长期方案

长期方案是换一个有连接健康检查的HTTP客户端库,或者在现有库的基础上加上连接健康检查的逻辑。同时,我们也给这个库提了Issue,希望官方能修复这个Bug。

另外,我们也调整了函数的实例最大存活时间,让实例不要运行太久,定期回收,减少坏连接累积的概率。

六、经验总结

这次排查花了整整一夜,但是也让我学到了很多。总结一下经验。

1. Serverless环境下要特别注意实例复用

Serverless函数的实例是会被复用的,所以代码中不要假设每次执行都是全新的环境。全局变量、连接池、缓存这些,在实例复用的时候会保留下来,如果处理不好,就可能出问题。

写Serverless函数的时候,要考虑实例复用的情况,做好资源的初始化和清理。

2. 连接池在Serverless环境下要小心

连接池可以提升性能,但是在Serverless环境下,因为实例复用时间长,连接池中的连接可能会因为各种原因断开。如果连接池没有健康检查,就可能取到坏连接,导致超时。

用连接池的时候,一定要确认它有健康检查机制,或者自己加上健康检查。

3. 偶发问题要找规律

偶发的、随机的问题最难排查,但是只要仔细观察,总能找到一些规律。比如我们这次发现超时只发生在运行时间长的实例上,这个规律就是找到根因的关键。

遇到偶发问题,不要只看单个案例,要把所有案例放在一起分析,找共同点和不同点。

4. Serverless调试要多打日志

Serverless环境下调试工具少,日志是最重要的排查手段。要在关键的地方打日志,记录时间戳、输入输出、外部调用的耗时等。日志越详细,排查越容易。

但是也要注意日志不要太多,否则会影响性能,也会增加日志费用。

5. 不要忽视平台侧的问题

虽然大部分问题都是代码的问题,但是有时候问题确实出在平台侧。如果代码和依赖都查了没问题,要及时联系云厂商的技术支持,让他们帮忙查平台侧的情况。

但是也要注意,不要一遇到问题就甩锅给平台,先把自己能查的都查了。

七、写在最后

Serverless架构很美好,不用管理服务器,按需付费,自动扩缩容。但是它也有自己的特点和坑,特别是在调试和故障排查方面,比传统服务器困难很多。

这次排查虽然辛苦,但是让我对Serverless架构有了更深的理解,也提升了在Serverless环境下排查问题的能力。

希望我们的排查经验能帮到正在遇到类似问题的你。记住,遇到问题不要慌,一步一步来,从现象到规律,从规律到根因,总能找到答案。

最后用一句话结束本文:"Serverless不是没有服务器,而是你不需要管服务器。但是出了问题,你还是得管。"愿每一个用Serverless的开发者都能少踩坑,多睡好觉。