How to optimize Unicorn processes in a Ruby on Rails application

Original author: Benjamin Tan
  • Transfer

If you are a rails developer, then you probably heard about Unicorn , an http server that can handle many requests at the same time.

To ensure concurrency, Unicorn uses multiple process creation. Because the created (forked) processes are copies of each other, which means that the rails application must be thread safe.

This is great because it's hard for us to be sure that our code is thread safe. If we cannot be sure of this, then there can be no talk of parallel web servers, such as Puma , or even alternative Ruby implementations that implement parallelism, such as JRuby and Rubinius .

Therefore, Unicorn provides parallelism to our rails applications, even if they are not thread safe. However, this requires a fee. Rails applications running on Unicorn require much more memory. Without paying any attention to the memory consumption of your application, you may eventually find that your cloud server is overloaded.

In this article, we will look at several ways to use Unicorn's concurrency while controlling the amount of memory consumed.

Use Ruby 2.0!


If you are using Ruby 1.9, you should seriously consider upgrading to 2.0. To understand why, we need to deal with process creation a bit.

Process Creation and Copy-on-Write

When a child process is created, it is exactly a copy of its parent process. However, there is no need to immediately copy physical memory. Being exact copies of each other, both child and parent processes can use the same physical memory. When the recording process occurs, only then do we copy the child process into physical memory.

How does all this relate to Ruby 1.9 / 2.0 and Unicorn?

I remind you that Unicorn uses forks. In theory, the operating system will be able to use Copy-on-Write. Unfortunately, Ruby 1.9 makes this impossible. More specifically, the implementation of the garbage collector in Ruby 1.9 makes this impossible. In the simplified version, it looks like this - when the garbage collector in 1.9 fires, it writes, which makes Copy-on-Write useless.

Without going into details, suffice it to say that the garbage collector in Ruby 2.0 eliminates this, and we can use Copy-on-Write.

Configure Unicorn


Here are a few settings we can set in config / unicorn.rb to get the most out of Unicorn.
worker_processes
Specifies the number of worker processes to start. It is important to know how much memory a process takes. This is necessary so that you can run the required number of workers without fear of overloading the RAM of your VPS.
timeout
Should be given a small number: usually 15 to 30 seconds is appropriate. A relatively small value is set so that time-consuming requests do not delay the processing of other requests.
preload_app
Should be set to true - this reduces the startup time of the worker. Thanks to Cope-on-Write, the application is loaded before the rest of the workers are launched. However, there is an important caveat. We need to make sure that all sockets (including database connections) are correctly closed and reopened. We will do this using before_fork and after_fork.
Example:
before_fork do|server, worker|# Disconnect since the database connection will not carry overifdefined? ActiveRecord::Base
    ActiveRecord::Base.connection.disconnect!
  endifdefined?(Resque)
    Resque.redis.quit
    Rails.logger.info('Disconnected from Redis')
  endend
after_fork do|server, worker|# Start up the database connection again in the workerifdefined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  endifdefined?(Resque)
    Resque.redis = ENV['REDIS_URI']
    Rails.logger.info('Connected to Redis')
  endend

In this example, we make sure that the connections are closed and reopened when creating workers. In addition to connecting to the database, we need to make sure that other connections that require working with sockets are handled the same way. Above is the configuration for Resque .

Unicorn Worker Memory Limitation


Obviously, there are not only rainbows but unicorns around. (there was an author’s pun 'rainbows and unicorns' - approx. translator). If your Rails application has memory leaks, Unicorn will make things worse.

Each of the created processes takes up memory, because is a copy of the rails application. Therefore, although the presence of more workers means that our application can process more incoming requests, we are limited by the physical RAM of our system.

Memory leaks in a rails application are very simple. But even if we manage to “plug” all memory leaks, we still have to deal with a slightly imperfect garbage collector (I mean the implementation in MRI).

The image above shows a memory leak rails application launched by Unicorn.

Over time, memory consumption will continue to grow. The use of many workers will only accelerate the speed of memory consumption, until the moment when there is no free memory left. The application will crash, leading to many unfortunate users and customers.

It is important to note that this is not Unicorn's fault. However, this is a problem that you will encounter sooner or later.

Meet Unicorn Worker Killer


One of the easiest solutions I've come across is the unicorn-worker-killer gem .
Quote from README :
the unicorn-worker-killer gem allows you to automatically restart Unicorn workers based on:
1) the maximum number of requests and
2) the size of the memory occupied by the process (RSS) that does not process the request.
This will greatly increase the stability of the site, avoiding unexpected memory shortages in the application nodes.

Please note that I assume that you already have Unicorn installed and running.
Step 1:
Add unicorn-worker-killer to your Gemfile lower than unicorn.
group :productiondo 
  gem 'unicorn'
  gem 'unicorn-worker-killer'end

Step 2:
Run bundle install.
Step 3:
Next, the fun part begins. Open the config.ru file.
# --- Start of unicorn worker killer code ---if ENV['RAILS_ENV'] == 'production'require'unicorn/worker_killer'
  max_request_min =  500
  max_request_max =  600# Max requests per worker
  use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max
  oom_min = (240) * (1024**2)
  oom_max = (260) * (1024**2)
  # Max memory size (RSS) per worker
  use Unicorn::WorkerKiller::Oom, oom_min, oom_max
end# --- End of unicorn worker killer code ---require::File.expand_path('../config/environment',  __FILE__)
run YourApp::Application

At the beginning we check that we are in a production environment. If so, we execute the rest of the code.
unicorn-worker-killer kills workers based on two conditions: the maximum number of requests and the maximum memory consumed.
  • maximum number of requests. In this example, the worker is killed if it processed from 500 to 600 requests. Notice that the interval is used. This minimizes situations where more than one worker stops at a time.
  • maximum memory consumption. Here the worker is killed if it takes from 240 to 260 MB of memory. The interval here is needed for the same reason as above.

Each application has its own memory requirements. You should have a general estimate of the memory consumption of your application during normal operation. This way you can better estimate the minimum and maximum amount of memory that your workers should occupy.

If during the deployment of your application you configured everything correctly, you will notice much less unstable memory behavior:

Pay attention to the excesses in the graph - this gem does its job!

Conclusion


Unicorn provides your rails application with a painless way to achieve concurrency, whether it is thread safe or not. However, this is achieved along with an increase in RAM consumption. Balancing memory consumption is very important for the stability and performance of your application.
We looked at 3 ways to tune your Unicorn workers for maximum performance:
  1. Using Ruby 2.0 gives us an improved garbage collector that allows us to take advantage of copy-on-write.
  2. Setting various configuration options in config / unicorn.rb.
  3. Using unicorn-worker-killer to solve the problem of stopping workers when they become too bloated.


Resources



Also popular now: