REST API производительность в реальных веб-приложениях

И так наши кандидаты на тестирование:

  • Laravel 5, PHP 7.0, Nginx
  • Lumen 5, PHP 7.0, Nginx
  • Express JS 4, Node.js 8.1, PM2
  • Django, Python 2.7, Gunicorn
  • Spring 4, Java, Tomcat
  • .NET Core, Kestrel

Что мы будем тестировать?

Нас интересует количество запросов в секунду, которое каждая платформа получает на разных конфигурациях серверов, а также то, насколько чистым или подробным выглядит код.

Как выглядят конфигурации сервера?

Мы также заинтересованы в том, как каждая структура масштабирует производительность и какова цена достижения такой производительности. Вот почему мы будем тестировать их на 3 различных конфигурациях серверов с помощью хостинга DigitalOcean:

  • 1 CPU, 512 MB - $5 / месяц - примерно 345 рублей в месяц по текущему курсу
  • 4 CPU, 8 GB - $80 / месяц - примерно 5500 рублей в месяц по текущему курсу
  • 12 CPU, 32 GB - $320 / месяц - примерно 22.000 рублей в месяц по текущему курсу

Что мы будем создавать?

Мы хотим протестировать пример реального приложения, поэтому мы создадим Web rest API, который предоставляет 4 конечные точки, каждая из которых имеет разную сложность:

  1. Hello World - простое приложение которое будет отдавать строку в формате Json "Hello World"
  2. Вычисление первых 10.000 чисел Фибоначчи. (Computation)
  3. Простой листинг - у нас есть база данных MySQL, содержащая таблицу стран, и мы перечислим все страны. (Simple Listing)
  4. Сложный список - мы добавляем таблицу пользователей наряду с отображением "многие ко многим" между пользователями и странами, и мы перечислим всех пользователей, которые посетили Францию, наряду со всеми странами, которые каждый посетил. (Complex Listing)

Для построения последних двух конечных точек мы будем использовать инструменты, предоставляемые каждой платформой, чтобы достичь нашей цели самым простым способом.

Как мы будем их тестировать?

Для их тестирования мы будем использовать инструменты wrk и AB HTTP benchmarking, чтобы проверить, получаем ли мы аналогичные результаты, а также варьировать параллелизм запросов, чтобы каждая технология могла достичь своего максимального потенциала.

Эти инструменты будут работать на своей собственной ноде, созданной на DigitalOcean, чтобы они не конкурировали на ресурсах сервера с фактическим приложением API.

Кроме того, сервер, используемый для размещения тестовых запросов, и тот, который используется для запуска приложения, связаны с помощью их частного IP-адреса, поэтому не будет заметной задержки в сети.

Результаты тестов

Ниже Вы можете увидеть результаты, сгруппированные по каждой конечной точке, а также вы можете проверить на одной диаграмме, как каждая структура масштабируется на различных конфигурациях серверов.

Как построен API?

Ниже приведены фактические контроллеры, используемые для каждой платформы, чтобы составить представление о том, как выглядит код. Вы также можете посмотреть весь код в репозитории на github.

Laravel и Lumen (PHP)

<?php
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use App\User;
use App\Country;
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 с 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 с Python

from django.http import JsonResponse
from random import randint
from models import Country, User, UserSerializer, CountrySerializer

def hello(req):
    return JsonResponse({ 'hello': 'world' })

def compute(req):
    x = 0
    y = 1
    max = 10000 + randint(0, 500)

    for i in range(max):
        z = x + y
        x = y
        y = 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 с 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;

@RestController
public 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")
    @Transactional
    public List<Country> countries() throws JSONException {
        List<Country> data = (List<Country>) sessionFactory.getCurrentSession()
                .createCriteria(Country.class)
                .list();
        return data;
    }
    
    @RequestMapping(value = "/users", produces = "application/json")
    @Transactional
    public List<User> users() throws JSONException {
        List<User> data = (List<User>) 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<string> hello()
        {
            return new string[] { "hello", "world" };
        }

        [HttpGet]
        [Route("/compute")]
        public IEnumerable<string> 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<Country> 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();
        }
    }
}

Вывод:

Имея в виду, что в реальном приложении почти все запросы взаимодействуют с базой данных, ни один из вариантов не является плохим, и все они могут обрабатывать требования большинства веб-приложений.

Однако производительность node.js с Express JS весьма быстрая. Node.js конкурирует с такими технологиями, как Java и .NET Core или даже превосходит их и в сочетании с Javascript ES6, который Вы можете сразу использовать с Node.js 8.

Что касается масштабируемости приложений, то наилучшая производительность по стоимости была получена на сервере среднего размера. Добавление 12 ядер и 32 ГБ памяти не сильно помогло. Возможно, в этом случае узкое место находится где-то в другом месте или требуется тонкая настройка, чтобы полностью раскрыть потенциал сервера.

А Вы что думаете?

Если вы нашли результаты интересными, пожалуйста пишите свои комментарии.