Skip to content

Commit 5d7d1dd

Browse files
authored
Add workers :auto (#3827)
* Add workers :auto * Docs: clarify workers :auto hook gotcha [ci skip] * Apply @MSP-Greg suggestion, but don't break tests
1 parent b8c4783 commit 5d7d1dd

5 files changed

Lines changed: 79 additions & 16 deletions

File tree

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,18 @@ Or with the `WEB_CONCURRENCY` environment variable:
115115
$ WEB_CONCURRENCY=3 puma -t 8:32
116116
```
117117

118+
When using a config file, most applications can simply set `workers :auto` (requires the `concurrent-ruby` gem) to match the number of worker processes to the available processors:
119+
120+
```ruby
121+
# config/puma.rb
122+
workers :auto
123+
```
124+
125+
See [`workers :auto` gotchas](lib/puma/dsl.rb).
126+
118127
Note that threads are still used in cluster mode, and the `-t` thread flag setting is per worker, so `-w 2 -t 16:16` will spawn 32 threads in total, with 16 in each worker process.
119128

120-
If the `WEB_CONCURRENCY` environment variable is set to `"auto"` and the `concurrent-ruby` gem is available in your application, Puma will set the worker process count to the result of [available processors](https://msp-greg.github.io/concurrent-ruby/Concurrent.html#available_processor_count-class_method).
129+
If `workers` is set to `:auto`, or the `WEB_CONCURRENCY` environment variable is set to `"auto"`, and the `concurrent-ruby` gem is available in your application, Puma will set the worker process count to the result of [available processors](https://msp-greg.github.io/concurrent-ruby/Concurrent.html#available_processor_count-class_method).
121130

122131
For an in-depth discussion of the tradeoffs of thread and process count settings, [see our docs](docs/deployment.md).
123132

docs/deployment.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ For the purposes of Puma provisioning, "CPU cores" means:
3535

3636
Set your config with the following process:
3737

38-
* Use cluster mode and set the number of workers to the same number of CPU cores on the machine (minimum 2, otherwise use single mode!)
38+
* Use cluster mode and set `workers :auto` (requires the `concurrent-ruby` gem) to match the number of CPU cores on the machine (minimum 2, otherwise use single mode!). If you can't add the gem, set the worker count manually to the available CPU cores.
3939
* Set the number of threads to desired concurrent requests/number of workers.
4040
Puma defaults to 5, and that's a decent number.
4141

42+
For most deployments, adding `concurrent-ruby` and using `workers :auto` is the right starting point.
43+
44+
See [`workers :auto` gotchas](../lib/puma/dsl.rb).
45+
4246
## Worker utilization
4347

4448
**How do you know if you've got enough (or too many workers)?**
@@ -72,7 +76,7 @@ Should you run 2 pods with 50 workers each? 25 pods, each with 4 workers? 100 po
7276
* **Increasing thread counts will increase throughput, but also latency and memory use** Unless you have a very I/O-heavy application (50%+ time spent waiting on IO), use the default thread count (5 for MRI). Using higher numbers of threads with low I/O wait (<50% of wall clock time) will lead to additional request latency and additional memory usage.
7377
* **Increasing worker counts decreases memory per worker on average**. More processes per pod reduces memory usage per process, because of copy-on-write memory and because the cost of the single master process is "amortized" over more child processes.
7478
* **Low worker counts (<4) have exceptionally poor throughput**. Don't run less than 4 processes per pod if you can. Low numbers of processes per pod will lead to high request queueing (see discussion above), which means you will have to run more pods and resources.
75-
* **CPU-core-to-worker ratios should be around 1**. If running Puma with `threads > 1`, allocate 1 CPU core (see definition above!) per worker. If single threaded, allocate ~0.75 cpus per worker. Most web applications spend about 25% of their time in I/O - but when you're running multi-threaded, your Puma process will have higher CPU usage and should be able to fully saturate a CPU core.
79+
* **CPU-core-to-worker ratios should be around 1**. If running Puma with `threads > 1`, allocate 1 CPU core (see definition above!) per worker. If single threaded, allocate ~0.75 cpus per worker. Most web applications spend about 25% of their time in I/O - but when you're running multi-threaded, your Puma process will have higher CPU usage and should be able to fully saturate a CPU core. Using `workers :auto` will size workers to this guidance on most platforms.
7680
* **Don't set memory limits unless necessary**. Most Puma processes will use about ~512MB-1GB per worker, and about 1GB for the master process. However, you probably shouldn't bother with setting memory limits lower than around 2GB per process, because most places you are deploying will have 2GB of RAM per CPU. A sensible memory limit for a Puma configuration of 4 child workers might be something like 8 GB (1 GB for the master, 7GB for the 4 children).
7781

7882
**Measuring utilization and queue time**

lib/puma/configuration.rb

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,18 +238,14 @@ def puma_options_from_env(env = ENV)
238238
min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
239239
max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
240240
persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
241-
workers = if env['WEB_CONCURRENCY'] == 'auto'
242-
require_processor_counter
243-
::Concurrent.available_processor_count
244-
else
245-
env['WEB_CONCURRENCY']&.strip
246-
end
241+
workers_env = env['WEB_CONCURRENCY']
242+
workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil
247243

248244
{
249245
min_threads: min && min != "" && Integer(min),
250246
max_threads: max && max != "" && Integer(max),
251247
persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
252-
workers: workers && workers != "" && Integer(workers),
248+
workers: workers,
253249
environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
254250
}
255251
end
@@ -380,12 +376,23 @@ def require_processor_counter
380376
require 'concurrent/utility/processor_counter'
381377
rescue LoadError
382378
warn <<~MESSAGE
383-
WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
379+
WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
384380
Please add "concurrent-ruby" to your Gemfile.
385381
MESSAGE
386382
raise
387383
end
388384

385+
def parse_workers(value)
386+
if value == :auto || value == 'auto'
387+
require_processor_counter
388+
Integer(::Concurrent.available_processor_count)
389+
else
390+
Integer(value)
391+
end
392+
rescue ArgumentError, TypeError
393+
raise ArgumentError, "workers must be an Integer or :auto"
394+
end
395+
389396
# Load and use the normal Rack builder if we can, otherwise
390397
# fallback to our minimal version.
391398
def rack_builder

lib/puma/dsl.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -669,21 +669,27 @@ def state_permission(permission)
669669
@options[:state_permission] = permission
670670
end
671671

672-
# How many worker processes to run. Typically this is set to
673-
# the number of available cores.
672+
# How many worker processes to run. Typically this is set to the number of
673+
# available cores.
674674
#
675675
# The default is the value of the environment variable +WEB_CONCURRENCY+ if
676-
# set, otherwise 0.
676+
# set, otherwise 0. Passing +:auto+ will set the value to
677+
# +Concurrent.available_processor_count+ (requires the concurrent-ruby gem).
678+
# On some platforms (e.g. under CPU quotas) this may be fractional, and Puma
679+
# will round down. If it rounds down to 0, Puma will run in single mode and
680+
# cluster-only hooks like +before_worker_boot+ will not execute.
681+
# If you rely on cluster-only hooks, set an explicit worker count.
677682
#
678-
# @note Cluster mode only.
683+
# A value of 0 or nil means run in single mode.
679684
#
680685
# @example
681686
# workers 2
687+
# workers :auto
682688
#
683689
# @see Puma::Cluster
684690
#
685691
def workers(count)
686-
@options[:workers] = count.to_i
692+
@options[:workers] = count.nil? ? 0 : @config.send(:parse_workers, count)
687693
end
688694

689695
# Disable warning message when running in cluster mode with a single worker.

test/test_config.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,43 @@ def test_config_loads_correct_max_threads
830830
assert_equal default_max_threads, conf.options.default_options[:max_threads]
831831
end
832832

833+
def test_config_workers_auto_from_dsl_and_env
834+
require 'concurrent/utility/processor_counter'
835+
836+
Concurrent.stub(:available_processor_count, 5) do
837+
conf = Puma::Configuration.new
838+
conf.configure { |c| c.workers :auto }
839+
conf.clamp
840+
assert_equal 5, conf.options[:workers]
841+
end
842+
843+
Concurrent.stub(:available_processor_count, 1.7) do
844+
conf = Puma::Configuration.new({}, {}, { "WEB_CONCURRENCY" => "auto" })
845+
conf.clamp
846+
assert_equal 1, conf.options.default_options[:workers]
847+
end
848+
end
849+
850+
def test_config_workers_auto_requires_concurrent_ruby
851+
conf = Puma::Configuration.new
852+
853+
def conf.require(path)
854+
raise LoadError, "Mocking system where concurrent-ruby is not available" if path == 'concurrent/utility/processor_counter'
855+
super(path)
856+
end
857+
858+
_, err = capture_io do
859+
assert_raises(LoadError) { conf.configure { |c| c.workers :auto } }
860+
end
861+
assert_includes err, 'Please add "concurrent-ruby" to your Gemfile'
862+
end
863+
864+
def test_config_workers_rejects_unknown_symbol
865+
conf = Puma::Configuration.new
866+
error = assert_raises(ArgumentError) { conf.configure { |c| c.workers :boom } }
867+
assert_includes error.message, 'Integer or :auto'
868+
end
869+
833870
def test_config_loads_workers_from_env
834871
env = { "WEB_CONCURRENCY" => "9" }
835872
conf = Puma::Configuration.new({}, {}, env)

0 commit comments

Comments
 (0)