2017年,Serverless架构开始火起来,阿里云也推出了函数计算服务,让开发者不用管理服务器,只需要写函数代码就能部署应用,按需付费,自动扩缩容。我最近在研究阿里云函数计算,用它做了几个项目,对Serverless架构有了一些实践经验。

函数计算确实有很多优点,比如不用运维、弹性伸缩、按需付费等等,但是要做好高可用高并发的架构设计,还是有很多需要注意的地方。今天就来分享一下阿里云函数计算的架构设计经验,从高可用和高并发两个维度,聊聊我踩过的坑和总结的最佳实践。

一、什么是函数计算

在讲架构设计之前,先简单介绍一下什么是函数计算。函数计算是阿里云推出的一个Serverless计算服务,它的核心思想是,开发者只需要写函数代码,上传到函数计算平台,然后配置触发器,平台就会自动运行你的函数,你完全不需要管理服务器,不需要关心运维,不需要关心扩缩容,平台会自动帮你处理。

函数计算的计费方式是按需付费,按函数的执行次数和执行时间计费,没有请求的时候不收费,有请求的时候才收费,而且有免费额度,小流量的应用基本上不用花钱。这对于初创公司和个人开发者来说非常友好,不用再为了偶尔的高峰流量去买高配服务器了。

函数计算支持多种语言,包括Node.js、Python、Java、PHP、C#等等,基本上主流的语言都支持。而且它和阿里云的其他服务集成得很好,比如API网关、对象存储OSS、消息队列MNS、日志服务SLS、表格存储TableStore等等,可以很方便地搭建完整的应用。

函数计算的使用场景也很广泛,比如Web应用后端、API服务、数据处理、定时任务、事件驱动的应用、IoT后端等等。特别是对于流量波动大的应用,函数计算的自动扩缩容能力非常有优势,流量来了自动扩容,流量走了自动缩容,不用再担心服务器扛不住或者资源浪费了。

当然,函数计算也不是银弹,它也有一些局限性,比如函数有执行时间限制(最长10分钟)、有内存限制、冷启动延迟、状态管理困难、调试和排错比较麻烦等等。所以在选择函数计算之前,要先评估自己的应用是不是适合用Serverless架构,不要为了用而用。

二、高可用架构设计

高可用是架构设计的基本要求,用函数计算虽然不用管理服务器了,但是高可用还是需要自己设计的,因为函数计算平台本身也可能出问题,而且你的函数代码、依赖的服务也可能出问题。下面从几个方面聊聊函数计算的高可用设计。

1. 多可用区部署

阿里云函数计算默认是多可用区部署的,平台会自动把你的函数部署到多个可用区,单个可用区出问题了,其他可用区还能继续服务。这一点平台已经帮我们做好了,我们不需要额外配置,但是要知道有这回事,在设计架构的时候要考虑到。

不过要注意的是,如果你依赖的其他服务(比如数据库、缓存)是单可用区的,那函数计算多可用区也没用,因为依赖的服务挂了整个应用还是挂了。所以要保证整个链路的高可用,依赖的服务也要做多可用区或者主从备份。

2. 错误处理和重试机制

函数执行过程中可能会出现各种错误,比如代码异常、依赖的服务不可用、网络超时等等。对于这些错误,要有完善的错误处理和重试机制。

首先,函数代码里要做好异常捕获,不要让异常直接抛出去导致函数执行失败。对于可以重试的错误(比如网络超时、依赖服务暂时不可用),可以在代码里做重试,但是要注意重试次数和重试间隔,避免无限重试把依赖服务打挂。

其次,函数计算平台本身也支持重试配置,对于异步调用的函数,可以配置重试次数和重试间隔,平台会自动重试失败的函数。但是同步调用的函数(比如API网关触发的),平台不会自动重试,需要客户端自己处理重试。

另外,对于重要的任务,建议用消息队列来解耦,把任务先发到消息队列,然后函数消费消息队列来执行任务。这样即使函数执行失败了,消息还在队列里,可以重新消费,不会丢失任务。而且消息队列本身也有重试机制和死信队列,可以保证任务最终被执行。

3. 降级和熔断机制

当依赖的服务不可用的时候,要有降级和熔断机制,不要让整个应用都挂掉。比如你的函数依赖了一个第三方API,当这个API不可用的时候,可以降级返回缓存的数据,或者返回一个默认值,而不是直接报错。

熔断机制也很重要,当依赖的服务连续失败多次之后,就熔断,暂时不再调用这个服务,直接返回降级结果,等一段时间之后再尝试恢复。这样可以避免因为依赖服务不可用而导致大量请求超时,把函数计算的资源都占满了,影响其他功能。

可以在代码里自己实现简单的熔断逻辑,也可以用阿里云的其他服务来做,比如用API网关的限流和熔断功能。

4. 监控和告警

高可用离不开完善的监控和告警,你要时刻知道你的函数运行得怎么样,有没有错误,有没有性能问题,出了问题要能第一时间知道。

阿里云函数计算和日志服务SLS集成得很好,函数的执行日志会自动收集到SLS里,可以很方便地查询和分析。函数计算控制台也有监控面板,可以看到函数的调用次数、错误次数、执行时间、内存使用等等指标。

除了平台自带的监控,建议自己在代码里加一些业务监控,比如关键操作的日志、性能指标的上报、异常的告警等等。可以用阿里云的云监控来配置告警规则,当错误率超过阈值、执行时间超过阈值、调用量异常的时候,自动发短信或者邮件告警。

监控和告警是高可用的最后一道防线,出了问题不可怕,可怕的是出了问题你不知道,等用户反馈了才发现,那就晚了。

三、高并发架构设计

函数计算的一大优势就是自动扩缩容,能应对高并发场景,但是要真正做到高并发,还是有很多需要注意的地方,不是说用了函数计算就天然高并发了。下面聊聊高并发架构设计的几个关键点。

1. 冷启动优化

冷启动是Serverless架构的一个痛点,当函数很长时间没有被调用的时候,平台会把函数的运行环境回收掉,下次再调用的时候需要重新启动运行环境、加载代码、初始化依赖,这个过程就是冷启动,会有几百毫秒到几秒的延迟。对于高并发场景,冷启动会影响响应时间,特别是流量突然上来的时候,大量的冷启动会导致响应变慢。

冷启动优化有几个方法:

  • 减少函数代码包的大小,代码包越小,加载越快。可以把不需要的依赖去掉,把大的静态资源放到OSS上,不要打包到代码里。
  • 减少初始化的工作量,把不需要在初始化阶段做的事情放到函数执行的时候做,或者延迟加载。
  • 选择合适的语言和运行时,不同语言的冷启动时间不一样,一般来说Node.js和Python的冷启动比较快,Java比较慢。
  • 用预留实例,阿里云函数计算支持预留实例,可以预先保留一定数量的函数实例,避免冷启动。但是预留实例是要收费的,相当于一直占用着资源,适合对延迟要求高的应用。
  • 定时预热,可以用定时触发器定时调用一下函数,保持函数的热度,避免被回收。但是这个方法不是很可靠,因为平台的回收策略可能会变。

2. 并发度控制

函数计算有并发度的概念,就是一个函数实例同时能处理多少个请求。默认情况下,一个函数实例同时只处理一个请求,也就是说,有多少个并发请求,就需要启动多少个函数实例。这样虽然隔离性好,但是对于IO密集型的应用来说,资源利用率不高,因为函数实例大部分时间都在等IO。

可以通过配置并发度来提高资源利用率,让一个函数实例同时处理多个请求。比如设置并发度为10,那么一个函数实例就能同时处理10个请求,这样需要的函数实例数就少很多,冷启动的概率也小很多,成本也更低。

但是要注意,提高并发度要求你的函数代码是线程安全的,因为多个请求会在同一个进程里并发执行,如果代码里有全局变量或者共享状态,就可能出现数据错乱的问题。所以在配置并发度之前,要确保你的代码是无状态的,是线程安全的。

另外,函数计算还有账号级别的并发度限制,默认是1000,也就是说你的所有函数加起来最多同时有1000个并发实例。如果你的应用需要更高的并发,可以提工单申请提高配额。

3. 数据库连接池

高并发场景下,数据库连接是一个大问题。传统的应用里,数据库连接池是在应用进程里维护的,但是函数计算是无状态的,每个函数实例都是独立的,而且会自动扩缩容,如果每个函数实例都维护自己的数据库连接,那么当并发很高的时候,函数实例数很多,数据库连接数就会爆掉,数据库扛不住。

解决这个问题有几个方法:

  • 用数据库代理,比如阿里云的RDS代理,或者自己部署一个数据库中间件(比如MyCat、ProxySQL),由代理来维护数据库连接池,函数实例只连接代理,这样数据库连接数就是可控的。
  • 用表格存储TableStore或者其他NoSQL数据库,这些数据库是分布式的,能支持高并发,没有连接数的问题。
  • 用缓存,把热点数据放到Redis里,减少数据库的访问,大部分请求都走缓存,只有少部分请求打到数据库,这样数据库的压力就小很多。
  • 限制函数的并发度,通过配置函数的最大并发实例数,来控制数据库连接数,避免把数据库打挂。

我个人比较推荐的方案是,用Redis做缓存,加上数据库代理,这样既能保证性能,又能保证数据库的安全。

4. 异步化处理

对于不需要同步返回结果的操作,尽量异步化处理,不要在函数里同步执行,这样可以减少函数的执行时间,提高并发能力,也能提高用户体验。

比如用户注册之后发欢迎邮件,这个操作不需要同步执行,可以把发邮件的任务扔到消息队列里,然后函数立即返回,由另一个函数异步消费消息队列来发邮件。这样用户注册的响应时间就很快,不用等发邮件完成。

常见的可以异步化的操作包括:发邮件、发短信、推送通知、数据统计、日志记录、生成报表、调用第三方接口等等。只要不是必须同步返回结果的,都可以异步化。

异步化不仅能提高响应速度,还能起到削峰填谷的作用,当流量突然上来的时候,先把任务存到消息队列里,然后函数慢慢消费,避免把后端服务打挂。

5. 缓存设计

缓存是高并发的利器,函数计算场景下也不例外。可以用Redis做缓存,把热点数据、经常查询的数据、计算成本高的数据都缓存起来,大部分请求直接走缓存,不用查数据库,也不用重复计算,这样性能会好很多,数据库的压力也小很多。

缓存设计要注意几个问题:

  • 缓存的过期时间,要根据数据的更新频率来设置,更新频繁的数据过期时间短一点,更新不频繁的数据过期时间长一点。
  • 缓存穿透,对于不存在的数据,也要缓存一个空值,避免大量请求都打到数据库。
  • 缓存雪崩,缓存的过期时间要加一个随机值,避免大量缓存同时过期,导致请求都打到数据库。
  • 缓存更新,数据更新的时候要及时更新缓存,或者删除缓存,避免缓存和数据库不一致。
  • 缓存降级,当Redis不可用的时候,要有降级方案,比如直接查数据库,或者返回默认值,不要因为Redis挂了整个应用就挂了。

四、踩坑总结

用阿里云函数计算做了几个项目,踩了不少坑,这里总结几个常见的坑,希望大家能避开。

第一个坑是冷启动。刚开始用的时候没注意冷启动的问题,结果上线之后发现响应时间有时候特别慢,查了半天才发现是冷启动导致的。后来做了代码包优化、减少初始化工作、配置预留实例,才把冷启动的问题解决了。所以大家用函数计算的时候,一定要重视冷启动的问题,特别是对延迟要求高的应用。

第二个坑是数据库连接数。刚开始没考虑到函数实例会自动扩容,结果流量一上来,函数实例数暴增,每个实例都建数据库连接,数据库连接数直接爆了,数据库挂了,整个应用都用不了。后来加了Redis缓存,又加了数据库代理,限制了函数的最大并发数,才解决这个问题。所以高并发场景下,数据库连接数一定要提前规划好。

第三个坑是函数执行时间限制。函数计算默认的函数执行时间是30秒,最长可以配置到10分钟。有一次我写了一个数据处理的函数,处理的数据量比较大,执行时间超过了10分钟,结果函数被强制终止了,数据处理到一半就停了,导致数据不一致。后来把任务拆分成了多个小任务,用消息队列分批处理,才解决这个问题。所以用函数计算的时候,要注意执行时间的限制,长时间的任务要拆分或者用其他方式处理。

第四个坑是调试和排错。函数计算的调试比本地调试麻烦很多,因为函数运行在云端,不能直接在本地打断点调试。刚开始出了问题只能看日志,但是日志有时候不够详细,排错效率很低。后来我学会了用函数计算的本地运行环境来调试,在本地跑函数,复现问题,然后再部署到云端。另外,在代码里加详细的日志也很重要,出了问题能通过日志快速定位。

第五个坑是状态管理。函数计算是无状态的,函数实例会被创建和销毁,所以不能在函数的全局变量里存状态,因为下次调用的时候可能是一个新的实例,全局变量就没了。刚开始我没注意这个问题,把一些配置信息存在了全局变量里,结果有时候能读到,有时候读不到,出了很诡异的bug。后来把状态都存到了外部存储(比如Redis、数据库)里,才解决这个问题。所以用函数计算的时候,一定要记住函数是无状态的,所有状态都要存到外部存储里。

五、写在最后

阿里云函数计算架构设计:高可用高并发。

函数计算和Serverless架构是未来的趋势,它能让开发者从繁琐的运维中解放出来,专注于业务逻辑的开发,而且按需付费、自动扩缩容的特性,对于初创公司和个人开发者来说非常友好。但是要用好函数计算,做好高可用高并发的架构设计,还是有很多需要学习和注意的地方。

这篇文章分享了我对阿里云函数计算架构设计的一些理解和实践经验,包括高可用设计、高并发设计、以及踩过的一些坑,希望能帮到大家。当然我也还在学习中,有说得不对的地方欢迎指正。

函数计算还在快速发展中,很多功能还在不断完善,未来会越来越强大。我相信Serverless架构会越来越普及,会有越来越多的应用跑在函数计算上。作为开发者,我们要保持学习,跟上技术的发展,用好新技术,提升开发效率,做出更好的产品。

最后用一句话结尾:"Serverless不是银弹,但是它能让你更专注于业务,更快地交付价值。用好它,能让你的架构更弹性、更可靠、更省钱。"

祝大家都能用好函数计算,搭建出高可用高并发的应用!