作为一名自由WEB开发工作者,我对各种框架和技术的性能非常感兴趣,但是我们在网络上看到的测试大多数都只是考虑到 Hello World 的例子。
当我们构建真实的应用程序的时候,有更多的方面需要考虑,因此我决定在最流行的框架和技术之间运行一个完整的基准测试。
除了性能,我还对在每个框架中实现特定任务的容易程度,以及扩展应用程序性能的成本感兴趣。
候选框架和技术
•Laravel 5, PHP 7.0, Nginx•Lumen 5, PHP 7.0, Nginx•Express JS 4, Node.js 8.1, PM2[1]•Django, Python 2.7, Gunicorn•Spring 4, Java, Tomcat•.NET Core, Kestrel
我们测试什么
我们主要关注的是在不同的服务器配置下每个框架在一秒内完成的请求次数,以及实现代码的简洁或冗长度。
服务器配置
我们还关注每个框架的扩展性如何,以及实现该性能的代价,这就是我们在三台不同配置的 DigitalOcean 服务器上测试它们的原因。
•1 CPU, 512 MB – $5 / month•4 CPU, 8 GB – $80 / month•12 CPU, 32 GB – $320 / month
我们构建的应用程序
我们想要测试真实应用程序的情况,所以我们提供了4个不同的 WEB REST API,每一个都有不同的复杂度:
•Hello World – 返回一个包含 Hello World 字符串的 JSON。•计算 – 计算前10000个斐波那契数。•简单列表 – 我们有一个包含了 countries 表的 MySQL 数据库,我们要返回表中所有的数据。•复杂的列表 – 我们添加一个 users 表,与 countries 构成多对多的关系,我们要返回去过 France 这个国家的用户,以及这些用户去过的所有国家。
最后两个接口,我们通过使用框架提供的工具以最简单的方式来实现我们的目标。
我们如何测试
为了测试它们,我们将同时使用wrk[2]和ab[3]两种HTTP性能测试工具,以检查是否得到类似的结果,并改变请求的并发量,从而使每种技术都能达到最大的潜力。
这些工具运行在独立的服务器上,所以他们不会同应用程序API争抢服务器资源。
此外,测试请求的服务器与应用程序服务器通过内网IP连接,所以不会有明显的网络延迟。
性能测试结果
下面我们以每个接口的测试结果,并且我们可以从图中看到不同框架在不同服务器上的性能扩展情况如何。
API 如何实现
下面是每个框架所使用的实际控制器,您还可以检查 GitHub[4]上可用的整个代码。
Laravel and Lumen with PHP
namespace AppHttpControllers;
use IlluminateRoutingController as BaseController;
use AppUser;
use AppCountry;
class Controller extends BaseController
{
public function hello()
{
return response()->json(['hello' => 'world']);
}
public function compute()
{
$x = 0; $y = 1;
$max = 10000 + rand(0, 500);
for ($i = 0; $i <= $max; $i++) {
$z = $x + $y;
$x = $y;
$y = $z;
}
return response()->json(['status' => 'done']);
}
public function countries()
{
$data = Country::all();
return response()->json($data);
}
public function users()
{
$data = User::whereHas('countries', function($query) {
$query->where('name', 'France');
})
->with('countries')
->get();
return response()->json($data);
}
}
Express JS with Node.js
const Country = require('../Models/Country');
const User = require('../Models/User');
class Controller{hello(req, res) {return res.json({ hello: 'world' });}
compute(req, res) {let x = 0, y = 1;let max = 10000 + Math.random() * 500;
for (let i = 0; i <= max; i++) {let z = x + y;x = y;y = z;}
return res.json({ status: 'done' })}
async countries(req, res) {let data = await Country.fetchAll();return res.json({ data });}
async users(req, res) {let data = await User.query(q => {q.innerJoin('UserCountryMapping', 'User.id', 'UserCountryMapping.userId');q.innerJoin('Country', 'UserCountryMapping.countryId', 'Country.id');q.groupBy('User.id');q.where('Country.name', 'France');}).fetchAll({withRelated: ['countries']})
return res.json({ data });}}
module.exports = new Controller();
Django with Python
from django.http import JsonResponsefrom random import randintfrom models import Country, User, UserSerializer, CountrySerializerdef hello(req):return JsonResponse({ 'hello': 'world' })
def compute(req):x = 0y = 1max = 10000 + randint(0, 500)
for i in range(max):z = x + yx = yy = z
return JsonResponse({ 'status': 'done' })
def countries(req):data = Country.objects.all()data = CountrySerializer(data, many=True).data
return JsonResponse({ 'data': list(data) })
def users(req):data = User.objects.filter(usercountrymapping__countries__name='France').all()data = UserSerializer(data, many=True).data
return JsonResponse({ 'data': list(data) })
Spring with Java
package app;import java.util.List;import java.util.Random;import java.util.concurrent.atomic.AtomicLong;
import org.hibernate.Criteria;import org.hibernate.SessionFactory;import org.hibernate.criterion.Restrictions;import org.json.JSONException;import org.json.JSONObject;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
import model.Country;import model.User;
@RestControllerpublic class Controller {private SessionFactory sessionFactory;
public Controller(SessionFactory sessionFactory) {this.sessionFactory = sessionFactory;}
@RequestMapping(value = "/hello", produces = "application/json")public String hello() throws JSONException {return new JSONObject().put("hello", "world").toString();}
@RequestMapping(value = "/compute", produces = "application/json")public String compute() throws JSONException {long x = 0, y = 1, z, max;Random r = new Random();max = 10000 + r.nextInt(500);
for (int i = 0; i <= max; i++) {z = x + y;x = y;y = z;}
return new JSONObject().put("status", "done").toString();}
@RequestMapping(value = "/countries", produces = "application/json")@Transactionalpublic List countries() throws JSONException {List data = (List) sessionFactory.getCurrentSession().createCriteria(Country.class).list();return data;}
@RequestMapping(value = "/users", produces = "application/json")@Transactionalpublic List users() throws JSONException {List data = (List) sessionFactory.getCurrentSession().createCriteria(User.class).createAlias("countries", "countriesAlias").add(Restrictions.eq("countriesAlias.name", "France")).list();return data;}}
.NET Core
using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using dotnet_core.Models;using dotnet_core.Data;namespace dotnet_core.Controllers{public class MyController : Controller{private readonly ApplicationDbContext context;
public MyController(ApplicationDbContext context){this.context = context;}
[HttpGet][Route("/hello")]public IEnumerable hello(){return new string[] { "hello", "world" };}
[HttpGet][Route("/compute")]public IEnumerable compute(){int x = 0, y = 1, z, max;Random r = new Random();max = 10000 + r.Next(500);
for (int i = 0; i <= max; i++) {z = x + y;x = y;y = z;}
return new string[] { "status", "done" };}
[HttpGet][Route("/countries")]public IEnumerable countries(){return context.Country.ToList();}
[HttpGet][Route("/users")]public object users(){return context.UserCountryMapping.Where(uc => uc.country.name.Equals("France")).Select(uc => new {id = uc.user.id,firstName = uc.user.firstName,lastName = uc.user.lastName,email = uc.user.email,countries = uc.user.userCountryMappings.Select(m => m.country)}).ToList();}}}
总结
记住,在真实的环境中,几乎所有的请求都会与数据库交互,选择哪个框架都没问题,他们都可以处理绝大多数WEB应用程序的需求。
然而,Node.js 的 Express JS 的性能还是非常显著的,它甚至可以与 Java 和 .Net Core 相竞争,甚至超过他们。
应用程序的伸缩性方面,中间那组服务器是最佳的选择,12核和 32G 内存并没有太多的提升。这种情况下,瓶颈可能在其他方面,或者需要微调以释放整个服务器的潜力。
References
[1] PM2: https://github.com/Unitech/pm2
[2] wrk: https://github.com/wg/wrk
[3] ab: https://httpd.apache.org/docs/2.4/programs/ab.html
[4] GitHub: https://github.com/mihaicracan/web-rest-api-benchmark