Давным-давно я начал задумываться о производительности технологических стеков, которые я использовал для 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.