Serverless架构用了一年多,功能越来越成熟,但性能问题也逐渐暴露:冷启动慢、调用延迟高、并发受限。经过系统性的性能优化,我们把平均响应时间从1.2秒降到了200毫秒以内。本文分享完整的优化过程,包括冷启动优化、代码优化、架构调整和监控体系建设。
一、背景和问题
我们的业务从2019年开始全面转向Serverless架构,用的是阿里云函数计算,前端是小程序和Web应用,后端全部跑在函数计算上。一年多下来,业务功能从几个函数发展到了上百个函数,日调用量从几千涨到了几百万。
功能越来越多,但性能问题也越来越明显:
- 冷启动慢:用户第一次访问某个函数时,经常要等1-3秒才能得到响应,体验很差。尤其是低频函数,几乎每次调用都是冷启动。
- 平均延迟高:整体平均响应时间在1.2秒左右,有些复杂函数甚至超过3秒,用户投诉越来越多。
- 并发受限:大促或活动期间,并发量上来之后,函数实例启动跟不上,出现大量超时和报错。
- 性能不稳定:同样的函数,有时候快有时候慢,延迟波动很大,难以预测。
这些问题在业务量小的时候不明显,但随着用户增长,已经严重影响了用户体验。我们决定做一次系统性的性能优化,目标是把平均响应时间降到300毫秒以内,冷启动时间控制在500毫秒以内。
二、第一步:建立性能监控体系
优化之前,我们对性能的了解很模糊,只知道"慢",但不知道慢在哪里。所以第一步是建立完善的性能监控体系。
我们做了以下几件事:
- 全链路追踪:接入了链路追踪系统,给每个函数调用加上traceId,记录从用户请求到函数执行完成的完整链路,包括网关耗时、函数初始化耗时、业务逻辑耗时、下游服务耗时等。
- 性能指标采集:在函数代码中加入详细的性能打点,记录每个关键步骤的耗时,比如数据库连接、缓存读取、外部API调用、数据处理等。
- 冷启动监控:专门监控冷启动的频率和耗时,区分冷启动和热启动的性能表现。
- 告警和仪表盘:建立性能仪表盘,实时展示P50、P95、P99延迟、冷启动率、错误率等关键指标,设置告警阈值。
监控体系建立之后,我们对性能问题有了清晰的认识。数据分析显示:
- 平均响应时间1.2秒,其中冷启动占了约600毫秒,业务逻辑约400毫秒,网关和网络约200毫秒
- 约30%的调用是冷启动,主要集中在低频函数
- 业务逻辑中,数据库查询占了约50%的时间,外部API调用占了约30%
- P99延迟超过5秒,主要是冷启动加上慢查询
定位了问题之后,我们开始针对性优化。
三、冷启动优化
冷启动是Serverless最大的性能痛点,也是我们优化的重点。
1. 减少函数代码包体积
我们的函数代码包普遍偏大,平均有50MB,最大的超过200MB。代码包大意味着下载和解压时间长,冷启动自然慢。
我们做了以下优化:
- 移除不必要的依赖,只保留运行时必需的包
- 对Node.js函数使用webpack打包和tree-shaking,去掉未使用的代码
- 把大型依赖(如图片处理库、机器学习模型)放到层(Layer)中,或者改用更轻量的替代方案
- 静态资源放到对象存储,不打进代码包
经过优化,平均代码包体积从50MB降到了15MB,冷启动时间减少了约200毫秒。
2. 优化初始化逻辑
很多函数在初始化阶段做了太多事情:建立数据库连接池、加载配置文件、初始化各种客户端、预加载数据等。这些都在冷启动时执行,直接增加了冷启动时间。
我们的优化策略:
- 延迟初始化:把非必需的初始化推迟到第一次使用时再做,比如某些不常用的客户端
- 全局复用:把数据库连接池、HTTP客户端等放在函数全局作用域,热启动时复用,避免每次调用都重建
- 异步初始化:一些不影响主流程的初始化放到后台异步执行
- 精简配置加载:只加载当前函数需要的配置,不加载全部配置
优化后,初始化时间从平均300毫秒降到了80毫秒。
3. 预留实例和定时预热
对于核心业务函数,我们使用了预留实例(预留模式),保证一定数量的实例常驻,避免冷启动。预留实例虽然成本高一些,但对于对延迟敏感的核心函数很值得。
对于无法使用预留实例的函数,我们做了定时预热:用定时触发器每隔几分钟调用一次函数,保持实例活跃。预热请求做了特殊标记,函数内部识别后直接返回,不执行业务逻辑,开销很小。
4. 选择合适的运行时和内存
不同的运行时冷启动时间差异很大。我们对比了Node.js、Python、Java、Go几种运行时,发现Go和Node.js的冷启动最快,Java最慢。对于性能要求高的函数,我们尽量用Node.js或Go开发。
另外,函数的内存配置也会影响冷启动速度,因为CPU和内存是绑定的,内存越大CPU越强。我们把核心函数的内存从256MB提升到了512MB,冷启动速度提升了约30%,而成本只增加了不到20%(因为执行时间缩短了)。
经过以上优化,冷启动平均时间从600毫秒降到了250毫秒,冷启动率从30%降到了8%。
四、业务逻辑优化
冷启动优化之后,业务逻辑的耗时占比上升了,成为新的瓶颈。
1. 数据库优化
数据库查询是业务逻辑中最耗时的部分,我们做了以下优化:
- 连接池复用:数据库连接放在全局作用域,函数热启动时复用连接,避免每次调用都新建连接。新建连接的开销很大(约100-200毫秒),复用之后这部分开销基本消除。
- 增加缓存:把高频查询的数据放到Redis缓存中,减少数据库查询。我们用了两级缓存:函数内的内存缓存(热启动时有效)和Redis分布式缓存。缓存命中率达到了85%,数据库查询量减少了70%。
- 优化SQL:对慢查询做了优化,增加索引,避免全表扫描,减少不必要的JOIN和子查询。最慢的几个查询从2秒降到了100毫秒以内。
- 读写分离:读请求走只读库,写请求走主库,减轻主库压力。
2. 外部API调用优化
我们的函数会调用很多外部API,这些调用经常成为瓶颈。优化措施:
- 并行调用:原来串行调用的多个API改成并行调用,用Promise.all或goroutine同时发起,总耗时从多个API耗时之和变成最慢的那个的耗时。
- 超时控制:给每个外部API调用设置合理的超时时间,避免一个慢接口拖垮整个函数。
- 结果缓存:对于不常变化的外部API结果,缓存起来,减少重复调用。
- 降级策略:外部API不可用时,有降级方案,比如返回缓存数据或默认值,不影响主流程。
3. 代码层面优化
- 减少不必要的计算和数据拷贝
- 大数据处理用流式处理,避免一次性加载到内存
- 对重复计算的结果做缓存
- 用更高效的数据结构和算法
经过业务逻辑优化,平均业务逻辑耗时从400毫秒降到了100毫秒。
五、架构调整
除了单点优化,我们还做了一些架构层面的调整。
1. 函数拆分和合并
原来的函数粒度过大,一个函数做了太多事情,导致代码包大、初始化慢、执行时间长。我们把大函数拆成了多个小函数,每个函数只做一件事,职责单一,更容易优化。
同时,有些函数粒度过小,调用链路过长,每次调用都有网络开销。我们把一些高频连续调用的小函数合并成一个,减少调用链路。
2. 异步化处理
对于不需要同步返回结果的操作(比如发送通知、写日志、数据统计),改成异步处理。主函数只做核心逻辑,其他操作通过消息队列异步执行,用户不需要等待,响应速度大幅提升。
3. 边缘计算
对于静态资源和简单的接口,放到CDN边缘节点或边缘函数中,用户就近访问,延迟更低。
六、优化效果
经过三个月的持续优化,我们的Serverless性能有了质的提升:
- 平均响应时间:从1.2秒降到了180毫秒
- P95延迟:从3.5秒降到了500毫秒
- P99延迟:从5秒降到了1.2秒
- 冷启动平均时间:从600毫秒降到了250毫秒
- 冷启动率:从30%降到了8%
- 错误率:从2.5%降到了0.3%
- 成本:因为执行时间缩短和缓存命中率提升,月成本反而降低了约15%
用户体验明显改善,投诉量下降了80%,页面加载速度快了很多,转化率也有提升。
七、经验和教训
回顾这次优化,有几点经验。
1. 先监控再优化
没有数据支撑的优化是盲目的。一定要先建立完善的监控体系,搞清楚时间花在哪里,再针对性优化。我们最开始以为是代码写得慢,结果数据显示冷启动和数据库才是大头。
2. 冷启动是Serverless的核心挑战
Serverless的性能优化,冷启动占了很大比重。要从代码包、初始化、运行时、预热等多个方面综合优化,不能只靠一招。
3. 缓存是性价比最高的优化
不管是数据库查询还是外部API调用,加缓存都能显著提升性能,而且实现成本不高。我们的优化中,缓存贡献了约40%的性能提升。
4. 性能和成本要平衡
预留实例、大内存配置能提升性能,但会增加成本。要根据业务的重要性和延迟敏感度,选择合适的方案,不是所有函数都需要最高配置。
5. 持续优化,不是一劳永逸
性能优化不是做一次就完了,业务在发展,代码在变化,性能也会退化。要建立持续的性能监控和优化机制,定期 review 性能指标,及时发现和解决问题。
八、写在最后
Serverless架构让我们从服务器管理中解放出来,专注于业务开发,但它的性能问题也需要认真对待。通过系统性的监控和优化,我们把Serverless应用的性能做到了和传统架构相当甚至更好的水平,同时还降低了成本。
Serverless正在快速成熟,冷启动等问题在不断改善,但在可预见的未来,性能优化仍然是Serverless应用开发中不可或缺的一环。希望我们的实战经验能给正在使用或准备使用Serverless的朋友一些参考。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录