Node.js服务器实际上能处理多少请求?
Node.js 服务器究竟能处理多少请求?在 Heroku 和 AWS 上对 Express 进行负载测试,对比 Django 的表现,其吞吐量约为 40K 次/秒,以及是什么因素拖慢了 API 的速度。
来源: How Many Requests Can a Real-World Node.js Server-Side App Handle?
任何刚接触 Node.js 的人最终都会遇到同一个实际问题:一个生产环境中的 Node.js 应用能处理多少请求,以及特定流量需要多少服务器实例?答案涉及的因素远不止框架本身。
值得追求的目标
据称,以主要为 Node.js 构建的后端为基础,设计平台 PicsArt 每秒可处理约 40,000 个请求。
这个数字相当惊人,而且它还取决于许多后端服务器之外的因素。在本次讨论中,这些其他因素被暂且忽略,仅考虑后端硬件与软件,尤其是 Node.js。
合成基准测试的结果
Fastify 的开发团队发布了一项基准测试,结果显示 Express 每秒可处理约 15,000 个请求,而纯 Node HTTP 模块则能达到约 70,000 个请求。
2015款15英寸MacBook Pro的测试结果几乎相同。即便Express能达到15000次/秒的响应速度,也很不错,但这类基准测试使用的只是微型处理程序,远非真正的API。决定API是否“具备实际应用价值”的是数据本身、数据的传输方式以及各组件之间的交互方式,因此真实的处理程序必须能够与数据库或其他服务进行通信。
原始实现与Express之间的差异其实并不重要。像Express这样的成熟框架通常更为合适,因为在实际应用中,单个实例根本无法处理每秒10万次请求。因此这些测试中使用了Express,再搭配一个小型处理程序来执行各种操作。
测试处理程序
该处理程序会执行六步操作:
- 将新用户插入数据库。
- 从数据库中获取20位用户信息。
- 将这20位用户的信息序列化后再反序列化。
- 递归地将那20个用户对象转换为驼峰命名法。
- 对数组进行排序。
- 执行一些简单的计算和标准输出操作,比如
Date.now和console.log,这些操作也会消耗计算资源。
响应数据大小约为12KB。每个用户对象的大小为609字节,结构如下:
结果1:使用AWS RDS的Heroku上的Node.js
该应用部署在每月费用为25美元的Heroku硬件上,请求以不会增加平均延迟的最高频率发送。该实例在60秒内处理了31,000次请求,平均每秒515次请求。
结果2:使用AWS RDS的Heroku上的Django
为了解析非单线程技术的表现,我们在相同条件下于Django中构建了类似的处理程序,并将其部署在同一台服务器上。其性能略逊于Node.js,60秒内仅能处理23,000次请求,即每秒389次。
结果3:AWS Elastic Beanstalk上的三个Node.js实例
第三种配置模拟了在AWS上的实际生产环境部署。数据库和后端服务均位于距离测试者约3,000公里的最近区域——法兰克福(eu-central-1)。三个Node.js实例通过AWS负载均衡机制协同工作,10秒内可处理19,000次请求,相当于每秒1,858次请求。后续的持续负载测试中,13分钟内共完成了150万次成功请求。
扩展至每秒40,000次请求
一个实例大约能处理600次每秒的请求,那么40,000次请求需要多少个实例呢?假设大型平台上的平均接口处理负载是测试用处理器的两倍,那么对于一个全球性的独角兽级应用来说,就需要40,000 ÷ (600 ÷ 2) = 133个分布在世界各地的实例,这并非什么难事。
如果接口处理负载更大,需求可能会提高三倍,但通过缓存和基于事件的通信可以恢复一定的吞吐量,因此两倍是一个合理的中间估算值。
什么会降低API的速度
无数因素都可能拖慢API的速度。这些都是在大多数服务器端Web应用中会出现的与性能相关的问题。
数据大小
存储在数据库中或从数据库中获取的数据量非常重要,尤其是在关系型数据库中,较大的数据量往往会导致复杂的查询,以及数据库管理系统与磁盘内存之间需要处理更多的数据。无论何种情况,数据量越大,传输时间就越长。将文件存储在数据库中是一种不良设计,对象存储才是更合适的选择。理想情况下,一条记录的大小应控制在数十千字节以内,通常为1到20KB。
与数据库的距离
后端与数据库之间的距离越远,查询和获取结果所需的时间就越长。假设有两个后端,一个位于弗吉尼亚州,另一个位于法兰克福,而数据库只设在弗吉尼亚州。美国用户的请求大约在10到40毫秒内到达美国后端,而后端与数据库之间的数据传输则需要1到5毫秒,这还算可以接受。德国用户的请求在10到20毫秒内到达法兰克福后端,但向弗吉尼亚州的数据库发送查询则需要120到130毫秒。因此,单区域应用应将数据和后端逻辑部署在相近位置,而全球应用则必须通过复杂的工程手段来保证运行速度。
数据库硬件
硬件也会产生很大影响。小型应用应根据自身的流量需求购买更优质的硬件,否则就不得不依靠复杂的工程解决方案。
数据库配置
数据库出厂时带有默认设置,因此无需额外操心管理,但在大规模使用时仍需关注。最大连接数是值得留意的一个设置——它可能会成为应用程序的瓶颈或使数据库服务器不堪重负,而像ORM这样的客户端库常常会更改这一数值。连接超时则是另一个需要考虑的因素。
数据库调用次数
一个真正的API为完成某项任务可能需要发起数十次独立的数据库调用,而且它还可能调用其他服务,而这些服务各自又会发起数十次调用。有一个值得记住的案例是某个银行系统的单体API,在云开发环境中的响应时间为9秒。其处理程序包含了300行代码,大约进行了30次数据库调用以及10次内部服务调用。这种情况往往出现在“先写代码”而非“先设计”的开发方式中。
解决方案取决于业务逻辑。队列和其他异步技术可以将部分数据库操作放到后台处理,这需要将大型服务拆分为小型服务。联合查询也能减少调用次数。
钩子
常见的ODM和ORM提供了钩子或中间件,便于处理特定逻辑。但在批量操作时,它们可能会成为隐藏的瓶颈,因为诸如camelCase或pascal_case转换器之类的插件也会占用计算资源与时间。钩子确实有帮助,但其中的内容及其放置位置仍需仔细考虑。
高CPU消耗的任务
占用大量CPU的计算任务会降低服务器性能,同时也会让最终用户等待。在Node.js中,许多这类任务依赖于线程池。虽然这不会阻塞异步I/O,但仍会拖慢API的响应速度。例如,点击注册按钮时需要等待线程池对密码进行哈希处理,这一过程可能需要100毫秒以上。
事件循环瓶颈
Node.js在单个线程上的事件循环中执行代码,因此代码的性能会影响到整个应用程序。JSON.parse和JSON.stringify操作可能会降低API的响应速度,而嵌套循环和递归函数则可能带来严重问题。如果不了解Node.js的工作原理,根据数据量和流量不同,应用程序可能会出现无法使用的状况。
糟糕的基础设施设计
大型基础设施不仅比单体架构存在更多问题,而且问题的类型也不同,往往需要复杂的架构方案来解决。由于组件众多,糟糕的架构会严重影响API速度,并导致系统资源利用效率低下,因此人们不得不为本可以更低成本解决的问题支付更多费用。在设计复杂应用时,要思考某项技术能解决哪些问题,并了解其他替代方案。还要考虑需求在两月或六个月后可能会如何变化,并根据这些预测来检验每一个决策。同时要关注服务等级协议以及非功能需求,包括流量增加时系统会如何表现。
组件规模与配置
添加到基础设施中的组件可能会拖慢整个平台的运行速度。以RabbitMQ为例,和所有技术一样,它也有众多配置选项。刚开始使用默认设置即可,但通过深入研究特定组件、进行架构审查或重构配置,能够显著提升基础设施的性能。此外,这些组件还必须能够随使用量扩展,以避免成为性能瓶颈。