
Давным-давно я начал задумываться о производительности технологических стеков, которые я использовал для Backend-разработки. Я хотел сравнить Spring Boot, Ruby on Rails и Express / Node.js при выполнении простых операций REST с MongoDB.
Я написал простой REST API, который потреблял и создавал JSON с использованием этих фреймворков, и измерил количество запросов, которые они способны обработать.
Настраивать
Я использовал следующие версии, которые были доступны на тот момент:
- MongoDB v4 работает на Ubuntu 16
- Spring Boot 2 работает на JVM 11
- Рельсы 5 + Ruby 2.6 + Mongoid
- Node.js 11 + Экспресс + Мангуст
Я использовал конфигурацию по умолчанию для всех компонентов, потому что я хотел протестировать начальное состояние без какой-либо производственной настройки.
Исходные коды доступны на GitHub:
https://github.com/Lameaux/rest-benchmark
Я использовал wrk - инструмент тестирования HTTP из: https://github.com/wg/wrk
В их документации есть базовый пример, который я использовал:
wrk -t12 -c400 -d30s https://127.0.0.1:8080/index.html
Сценарий 0 - Манекен читает
Давайте измерим производительность фиктивной конечной точки GET, которая просто возвращает метку времени в виде JSON.
Ruby on Rails
class TimestampController < ApplicationController
def timestamp
render json: { timestamp: Time.now.to_i }
end
end
Полученные результаты:
wrk -t12 -c400 -d30s https://127.0.0.1:3000/timestamp Running 30s test @ https://127.0.0.1:3000/timestamp 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 9.64ms 3.09ms 34.62ms 78.32% Req/Sec 260.29 158.28 565.00 47.17% 15557 requests in 30.06s, 4.42MB read Requests/sec: 517.54 Transfer/sec: 150.61KB
Expess и Node.js
const express = require('express');
const app = express();
const port = 3001;
app.get('/timestamp',
(_req, res) => res.json(
{
timestamp: new Date().getTime()
}
)
);
app.listen(port, () => console.log(`Listening on port ${port}`));
Полученные результаты:
wrk -t12 -c400 -d30s https://127.0.0.1:3001/timestamp Running 30s test @ https://127.0.0.1:3001/timestamp 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 35.50ms 2.06ms 86.90ms 80.10% Req/Sec 0.93k 111.91 1.21k 83.39% 333933 requests in 30.06s, 76.11MB read Requests/sec: 11110.03 Transfer/sec: 2.53MB
Spring Boot
@RestController
public class TimestampRestController {
@GetMapping("/timestamp")
public TimestampResponse timestamp() {
return new TimestampResponse(System.currentTimeMillis());
}
class TimestampResponse {
private final long timestamp;
TimestampResponse(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
}
}
Полученные результаты:
wrk -t12 -c400 -d30s https://127.0.0.1:8080/timestamp Running 30s test @ https://127.0.0.1:8080/timestamp 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 21.70ms 17.44ms 274.30ms 65.75% Req/Sec 1.63k 480.17 3.94k 71.97% 582651 requests in 30.10s, 92.34MB read Requests/sec: 19356.66 Transfer/sec: 3.07MB
Резюме
Перед каждым тестом я прогревал приложения, отправляя запросы на конечные точки в течение 5 минут.
Rails показал худший результат с ~ 500 запросов в секунду.
Express смог обработать 11 КБ запросов / сек.
Spring показал 19 КБ запросов / Сек.
Я действительно не понимал, почему Rails такой медленный. Может быть, причина в дефолтной конфигурации Puma?
Puma starting in single mode... * Version 3.12.1 (ruby 2.6.1-p33), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: production
По умолчанию Rails запускает Puma в одиночном режиме с 5 потоками.
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
Я попытался запустить больше обсуждений Puma, переопределив RAILS_MAX_THREADS:
RAILS_MAX_THREADS=20 rails s -e production ... * Min threads: 20, max threads: 20 ...
Но никакого эффекта это не дало.
Я дал Rails еще один шанс. На этот раз я начал больше рабочих.
Я обновил config / puma.rb, разрешив предварительную загрузку приложений и запустив 4 воркера Puma:
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
workers ENV.fetch("WEB_CONCURRENCY") { 4 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
preload_app!
Вывод из rails s:
[7678] Puma starting in cluster mode... [7678] * Version 3.12.1 (ruby 2.6.1-p33), codename: Llamas in Pajamas [7678] * Min threads: 5, max threads: 5 [7678] * Environment: production [7678] * Process workers: 4 [7678] * Preloading application
Окончательный результат для Rails…
wrk -t12 -c400 -d30s https://127.0.0.1:3000/timestamp
Running 30s test @ https://127.0.0.1:3000/timestamp
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 9.61ms 11.85ms 131.84ms 84.99%
Req/Sec 1.04k 0.94k 2.31k 47.67%
62364 requests in 30.08s, 17.72MB read
Requests/sec: 2072.93
Transfer/sec: 603.26KB
2 КБ запросов в секунду, что в 4 раза лучше, чем для одного экземпляра Puma.
Я продолжал использовать Rails в этой настройке, хотя это было несправедливо, потому что вначале я обещал использовать конфигурацию по умолчанию.
А пока предположим, что Rails чертовски медленный, и перейдем к следующим сценариям.
Сценарий 1 - Запись: заполнение MongoDB новыми документами
В этом сценарии мы собираемся заполнить MongoDB документами.
Ruby on Rails
# Gemfile
gem 'mongoid', '~> 6.1.0'
# models/document.rb
class Document
include Mongoid::Document
field :title, type: String
end
# routes.rb
resources :documents
# controllers/documents_controller.rb
class DocumentsController < ApplicationController
def create
document = Document.new(title: "Document #{SecureRandom.uuid}")
document.save!
render json: document.to_json
end
end
Полученные результаты:
wrk -t12 -c400 -d30s -s post.lua https://localhost:3000/documents Running 30s test @ https://localhost:3000/documents 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 22.81ms 12.88ms 124.29ms 73.70% Req/Sec 293.23 202.42 630.00 39.44% 26320 requests in 30.09s, 9.36MB read Requests/sec: 874.57 Transfer/sec: 318.57KB > db.documents.count() 26343
Express / Node.js
const mongoose = require('mongoose');
const url = 'mongodb://localhost:27017/nodejs';
mongoose.connect(url, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
const Schema = mongoose.Schema;
const DocumentSchema = new Schema({ title: String });
const DocumentModel = mongoose.model('Document', DocumentSchema );
app.post('/documents', (req, res) => {
const document = new DocumentModel({
title: `Document ${new Date().getTime()}`
});
document.save().catch(error => { console.log(error) });
res.json(document);
});
Полученные результаты:
wrk -t12 -c400 -d30s -s post.lua https://localhost:3001/documents Running 30s test @ https://localhost:3001/documents 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 299.59ms 32.46ms 638.44ms 89.11% Req/Sec 119.39 91.18 590.00 63.08% 39406 requests in 30.07s, 10.48MB read Requests/sec: 1310.60 Transfer/sec: 357.09KB > db.documents.count() 39802
Spring Boot
// build.gradle
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
// Document.java
public class Document {
@Id
public String id;
public String title;
public Document() {}
public Document(String title) {
this.title = title;
}
}
// DocumentRepository.java
public interface DocumentRepository extends MongoRepository<Document, String> {
}
// DocumentRestController.java
@PostMapping("/documents")
public Document create() {
Document doc = new Document("Document " + UUID.randomUUID().toString());
repository.save(doc);
return doc;
}
// application.properties
spring.data.mongodb.database=springboot
Полученные результаты:
wrk -t12 -c400 -d30s -s post.lua https://localhost:8080/documents Running 30s test @ https://localhost:8080/documents 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 61.34ms 27.44ms 332.76ms 73.69% Req/Sec 540.65 98.04 1.39k 76.57% 193356 requests in 30.09s, 42.07MB read Requests/sec: 6426.18 Transfer/sec: 1.40MB > db.documents.count() 181712
Сценарий 2 - Чтение: получение документа из MongoDB
В этом сценарии мы собираемся прочитать документ из MongoDB.
Ruby on Rails
# controllers/documents_controller.rb def index document = Document.first render json: document.to_json end
Полученные результаты:
wrk -t12 -c400 -d30s https://localhost:3000/documents Running 30s test @ https://localhost:3000/documents 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 22.24ms 12.34ms 113.50ms 73.97% Req/Sec 451.12 409.20 0.97k 50.17% 26981 requests in 30.10s, 9.60MB read Requests/sec: 896.37 Transfer/sec: 326.51KB
Express / Node.js
app.get('/documents', (req, res) => {
DocumentModel
.findOne({})
.exec(function (err, document) {
if (err) console.log(err);
res.json(document);
});
});
Полученные результаты:
wrk -t12 -c400 -d30s https://localhost:3001/documents Running 30s test @ https://localhost:3001/documents 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 156.10ms 37.30ms 558.64ms 87.29% Req/Sec 218.31 100.88 333.00 52.83% 76140 requests in 30.06s, 20.84MB read Requests/sec: 2533.12 Transfer/sec: 709.97KB
Spring Boot
@GetMapping("/documents")
public Document index() {
Pageable pageableRequest = PageRequest.of(0, 1);
Page<Document> page = repository.findAll(pageableRequest);
List<Document> document = page.getContent();
return document.get(0);
}
Полученные результаты:
wrk -t12 -c400 -d30s https://localhost:8080/documents Running 30s test @ https://localhost:8080/documents 12 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 68.51ms 35.07ms 441.45ms 72.05% Req/Sec 485.78 92.96 1.50k 74.96% 173931 requests in 30.10s, 37.85MB read Requests/sec: 5777.83 Transfer/sec: 1.26MB
Заключение
Вот результаты:
Итак, Java по-прежнему на высоте! Spring Boot в два раза быстрее других фреймворков. Я просто не понимаю, почему чтение медленнее, чем запись. Какого черта?
Вы можете использовать Express / Node.js для Backend-разработки, если Javascript - ваш любимый язык.
Ruby - хороший язык. Особенно классные RSpec. С Ruby on Rails разработка идет очень быстро. С другой стороны, кажется, что производительности Rails может быть недостаточно, если вы собираетесь создать высоконагруженный REST API.