Skip to content

Commit 51416c3

Browse files
authored
Use only 2xx http requests to compute req/s (the-benchmarker#3493)
1 parent 1910ad8 commit 51416c3

37 files changed

+549
-466
lines changed

.env

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DATABASE_URL=postgresql://postgres@localhost/benchmark
2+
DURATION=15
3+
CONCURRENCIES=64,256,512
4+
ROUTES=GET:/,GET:/user/0,POST:/user

Gemfile

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# frozen_string_literal: true
22

3-
source "https://rubygems.org"
3+
source 'https://rubygems.org'
44

5-
group :procuction do
6-
gem "mustache"
7-
gem "pg"
8-
gem "rake"
9-
gem "dotenv"
10-
gem 'activesupport'
11-
end
5+
gem 'activesupport'
6+
gem 'dotenv'
7+
gem 'mustache'
8+
gem 'pg'
9+
gem 'rake'
10+
11+
gem 'bigdecimal'
12+
gem 'open3'
1213

1314
group :development, :test do
14-
gem "rspec"
15-
gem "rubocop"
15+
gem 'rspec'
16+
gem 'rubocop'
1617
end

README.md

+211-269
Large diffs are not rendered by default.

README.mustache.md

+3-61
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ This project aims to be a load benchmarking suite, no more, no less
2525

2626
## Requirements
2727

28-
+ [Crystal](https://crystal-lang.org) as `built-in` tools are made in this language
28+
+ [Ruby](https://ruby-lang.org) as `built-in` tools are made in this language
2929
+ [Docker](https://www.docker.com) as **frameworks** are `isolated` into _containers_
3030
+ [wrk](https://github.com/wg/wrk) as benchmarking tool, `>= 4.1.0`
3131
+ [postgresql](https://www.postgresql.org) to store data, `>= 10`
@@ -44,67 +44,9 @@ eval $(docker-machine env default)
4444

4545
## Usage
4646

47-
+ Install all dependencies
47+
... to be documented ...
4848

49-
~~~sh
50-
shards install
51-
~~~
52-
53-
+ Build internal tools
54-
55-
~~~sh
56-
shards build
57-
~~~
58-
59-
+ Create and initialize the database
60-
61-
~~~sh
62-
createdb -U postgres benchmark
63-
psql -U postgres -d benchmark < dump.sql
64-
~~~
65-
66-
Docker can be used to set up the database:
67-
68-
~~~sh
69-
docker run -it --rm -d \
70-
-p 5432:5432 \
71-
-e POSTGRES_DB=benchmark \
72-
-e POSTGRES_HOST_AUTH_METHOD=trust \
73-
-v /tmp/pg-data:/var/lib/postgresql/data \
74-
--name pg postgres:12-alpine
75-
~~~
76-
77-
Wait several seconds for the container to start, then inject the dump:
78-
79-
~~~sh
80-
docker exec pg sh -c "echo \"$(cat dump.sql)\" | psql -U postgres -d benchmark"
81-
~~~
82-
83-
After creating the database, export its URL:
84-
85-
~~~sh
86-
export DATABASE_URL="postgresql://postgres@localhost/benchmark"
87-
~~~
88-
89-
+ Make configuration
90-
91-
~~~sh
92-
bin/make config
93-
~~~
94-
95-
+ Build containers
96-
97-
> jobs are either languages (example : crystal) or frameworks (example : router.cr)
98-
99-
~~~sh
100-
bin/neph [job1] [job2] [job3] ...
101-
~~~
102-
103-
+ Export all results readme
104-
105-
~~~sh
106-
bin/db to_readme
107-
~~~
49+
feel free to create an issue if you want to try this project
10850

10951
## Results
11052

Rakefile

+18-33
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ require 'dotenv'
44

55
Dir.glob('lib/tasks/*.rake').each { |r| load r }
66

7-
environment = ENV.fetch('ENV', 'development')
8-
97
MANIFESTS = {
108
container: '.Dockerfile',
119
build: '.Makefile'
1210
}.freeze
1311

14-
default_environment = File.join('.env', 'default')
15-
custom_environment = File.join('.env', environment)
16-
Dotenv.load(custom_environment, default_environment)
12+
Dotenv.load
1713

1814
class ::Hash
1915
def recursive_merge(h)
@@ -29,27 +25,20 @@ def default_provider
2925
end
3026
end
3127

32-
def commands_for(language, framework, **options)
28+
def commands_for(language, framework, provider)
3329
config = YAML.safe_load(File.read('config.yaml'))
3430

35-
directory = File.dirname(options[:path])
36-
main_config = YAML.safe_load(File.open(File.join(directory, '..', '..', 'config.yaml')))
37-
language_config = YAML.safe_load(File.open(File.join(directory, '..', 'config.yaml')))
38-
framework_config = YAML.safe_load(File.open(File.join(directory, 'config.yaml')))
31+
directory = Dir.pwd
32+
main_config = YAML.safe_load(File.open(File.join(directory, 'config.yaml')))
33+
language_config = YAML.safe_load(File.open(File.join(directory, language, 'config.yaml')))
34+
framework_config = YAML.safe_load(File.open(File.join(directory, language, framework, 'config.yaml')))
3935
app_config = main_config.recursive_merge(language_config).recursive_merge(framework_config)
40-
41-
options[:framework] = framework
42-
options[:language] = language
43-
44-
ENV.each do |key, value|
45-
options[key] = value unless options.key?(key)
46-
end
47-
36+
options = { language: language, framework: framework }
4837
commands = { build: [], collect: [], clean: [] }
4938

5039
# Compile first, only for non containers
5140

52-
if app_config.key?('binaries') && !(options[:provider].start_with?('docker') || options[:provider].start_with?('podman'))
41+
if app_config.key?('binaries') && !(provider.start_with?('docker') || provider.start_with?('podman'))
5342
commands << "docker build -f #{MANIFESTS[:container]} -t #{language}.#{framework} ."
5443
commands << "docker run -td #{language}.#{framework} > cid.txt"
5544
app_config['binaries'].each do |out|
@@ -62,36 +51,32 @@ def commands_for(language, framework, **options)
6251
end
6352
end
6453

65-
config['providers'][options[:provider]]['build'].each do |cmd|
54+
config['providers'][provider]['build'].each do |cmd|
6655
commands[:build] << Mustache.render(cmd, options.merge!(manifest: MANIFESTS[:container])).to_s
6756
end
6857

69-
config['providers'][options[:provider]]['metadata'].each do |cmd|
58+
config['providers'][provider]['metadata'].each do |cmd|
7059
commands[:build] << Mustache.render(cmd, options).to_s
7160
end
7261

73-
if app_config.key?('bootstrap') && config['providers'][options[:provider]].key?('exec')
74-
remote_command = config['providers'][options[:provider]]['exec']
62+
if app_config.key?('bootstrap') && config['providers'][provider].key?('exec')
63+
remote_command = config['providers'][[provider]]['exec']
7564
app_config['bootstrap'].each do |cmd|
7665
commands[:build] << Mustache.render(remote_command, options.merge!(command: cmd)).to_s
7766
end
7867
end
7968

80-
if config['providers'][options[:provider]].key?('reboot')
81-
commands[:build] << config['providers'][options[:provider]].fetch('reboot')
69+
if config.dig('providers', provider).key?('reboot')
70+
commands[:build] << config.dig('providers', provider, 'reboot')
8271
commands[:build] << 'sleep 30'
8372
end
8473

8574
commands[:build] << 'curl --retry 5 --retry-delay 5 --retry-max-time 180 --retry-connrefused http://`cat ip.txt`:3000 -v'
8675

87-
unless options[:collect] == 'off'
88-
commands[:collect] << "DATABASE_URL=#{ENV['DATABASE_URL']} ../../bin/client --language #{language} --framework #{framework} #{options[:sieger_options]} -h `cat ip.txt`"
89-
end
76+
commands[:collect] << "LANGUAGE=#{language} FRAMEWORK=#{framework} DATABASE_URL=#{ENV['DATABASE_URL']} bundle exec rake collect"
9077

91-
unless options[:clean] == 'off'
92-
config['providers'][options[:provider]]['clean'].each do |cmd|
93-
commands[:clean] << Mustache.render(cmd, options).to_s
94-
end
78+
config.dig('providers', provider, 'clean').each do |cmd|
79+
commands[:clean] << Mustache.render(cmd, options).to_s
9580
end
9681

9782
commands
@@ -170,7 +155,7 @@ task :config do
170155

171156
makefile = File.open(File.join(language, framework, MANIFESTS[:build]), 'w')
172157

173-
commands_for(language, framework, provider: provider, clean: clean, sieger_options: sieger_options, path: path, collect: collect).each do |target, commands|
158+
commands_for(language, framework, provider).each do |target, commands|
174159
makefile.write("#{target}:\n")
175160
commands.each do |command|
176161
makefile.write("\t #{command}\n")

lib/tasks/collect.rake

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require "open3"
2+
require "csv"
3+
require "etc"
4+
require "bigdecimal/util"
5+
6+
PIPELINE = {
7+
GET: File.join(Dir.pwd, "pipeline.lua"),
8+
POST: File.join(Dir.pwd, "pipeline_post.lua"),
9+
}
10+
11+
def insert(db, framework_id, metric, value, concurrency_level_id)
12+
res = db.query("INSERT INTO keys (label) VALUES ($1) ON CONFLICT (label) DO UPDATE SET label = $1 RETURNING id", [metric])
13+
14+
metric_id = res.first["id"]
15+
16+
res = db.query("INSERT INTO values (key_id, value) VALUES ($1, $2) RETURNING id", [metric_id, value])
17+
value_id = res.first["id"]
18+
19+
db.query("INSERT INTO metrics (value_id, framework_id, concurrency_id) VALUES ($1, $2, $3)", [value_id, framework_id, concurrency_level_id])
20+
end
21+
22+
task :collect do
23+
threads = ENV.fetch("THREADS") { Etc.nprocessors }
24+
duration = ENV.fetch("DURATION") { 10 }
25+
language = ENV.fetch("LANGUAGE") { raise "please provide the language" }
26+
framework = ENV.fetch("FRAMEWORK") { raise "please provide the target framework" }
27+
concurrencies = ENV.fetch("CONCURRENCIES") { "10" }
28+
routes = ENV.fetch("ROUTES") { "GET:/" }
29+
database = ENV.fetch("DATABASE_URL") { raise "please provide a DATABASE_URL (pg only)" }
30+
31+
hostname = File.read(File.join(Dir.pwd, language, framework, "ip.txt")).strip
32+
`wrk -H 'Connection: keep-alive' -d 5s -c 8 --timeout 8 -t #{threads} http://#{hostname}:3000`
33+
`wrk -H 'Connection: keep-alive' -d #{duration}s -c 256 --timeout 8 -t #{threads} http://#{hostname}:3000`
34+
35+
db = PG.connect(database)
36+
37+
res = db.query("INSERT INTO languages (label) VALUES ($1) ON CONFLICT (label) DO UPDATE SET label = $1 RETURNING id", [language])
38+
language_id = res.first["id"]
39+
40+
res = db.query("INSERT INTO frameworks (language_id, label) VALUES ($1, $2) ON CONFLICT (language_id, label) DO UPDATE SET label = $2 RETURNING id", [language_id, framework])
41+
framework_id = res.first["id"]
42+
43+
routes.split(",").each do |route|
44+
method, uri = route.split(":")
45+
46+
concurrencies.split(",").each do |concurrency|
47+
res = db.query("INSERT INTO concurrencies (level) VALUES ($1) ON CONFLICT (level) DO UPDATE SET level = $1 RETURNING id", [concurrency])
48+
49+
concurrency_level_id = res.first["id"]
50+
51+
command = format("wrk -H 'Connection: keep-alive' --connections %<concurrency>s --threads %<threads>s --duration %<duration>s --timeout 1 --script %<pipeline>s http://%<hostname>s:3000", concurrency: concurrency, threads: threads, duration: duration, pipeline: PIPELINE[method.to_sym], hostname: hostname)
52+
53+
Open3.popen3(command) do |_, _, stderr|
54+
lua_output = stderr.read
55+
56+
info = lua_output.split(",")
57+
["duration_ms", "total_requests", "total_requests_per_s", "total_bytes_received",
58+
"socket_connection_errors", "socket_read_errors", "socket_write_errors",
59+
"http_errors", "request_timeouts", "minimim_latency", "maximum_latency",
60+
"average_latency", "standard_deviation", "percentile_50",
61+
"percentile_75", "percentile_90", "percentile_99", "percentile_99.999"].each_with_index do |key, index|
62+
insert(db, framework_id, key, info[index].to_d, concurrency_level_id)
63+
end
64+
end
65+
end
66+
end
67+
db.close
68+
end

0 commit comments

Comments
 (0)