A Ruby on Rails application built with component based architecture serving multiple portions to different audiences for example: public, admin and API will install and require them all but you can require only the necessary portion to reduce memory usage and speed up the bundling process.
I leverage bundler groups to define the portions of my application inside the Gemfile
for example an app serving public and admin:
# Gemfile
group :public_app do
path 'components' do
gem 'public_ui'
end
end
group :admin_app do
path 'components' do
gem 'admin_ui'
gem 'legacy_migration'
end
end
The admin application is deployed with some legacy migration daemons.
Speed up deployment
Running bundle
on your workstation installs all the gems and locks your Gemfile.lock
with all the components but when you deploy to the admin server you can (and should) ignore the public portion that isn’t required. For example deploying the public application with:
bundle --without 'public_app'
You will see a confirming message: Gems in the group public_app were not installed.
and probably shave off a few seconds from installing unnecessary dependencies.
If you don’t like the inline option you can use an environment variable BUNDLE_WITHOUT
for more information see bundler excellent documentation. Heroku supports that environment variable.
Decrease memory usage
Inside config/application.rb
change Bundler.require
to:
Bundler.require(*Rails.groups + AppRunningMode.bundler_groups)
AppRunningMode
is aware of the APP_RUNNING_MODE env variable and translates that in to a bundler group.
Here’s an example:
class AppRunningMode
class << self
def value
case ENV['APP_RUNNING_MODE']
when 'workshop'
return :workshop
when 'playground'
return :playground
else
return :development
end
end
def bundler_groups
case value
when :workshop
return [:workshop_app]
when :playground
return [:playground_app]
else
return [:workshop_app, :playground_app]
end
end
end
end
This extends the class I use to allow components routes to be mounted programmatically when serving multiple portions.
Benchmarking a practical example
I created a test example app with a component based app running two portions: playground and workshop. I’ve setup workshop to depend and require these gems at startup:
s.add_dependency "chronic"
s.add_dependency "state_machine"
s.add_dependency "carrierwave"
s.add_dependency 'prawn'
s.add_dependency 'cinch'
s.add_dependency 'nokogiri'
to keep the example simple playground doesn’t have any dependency.
The benchmark is a simple ps
for the current ruby process in a controller like this:
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i
render text: "Ruby process memory usage #{memory_usage} KB"
and rendered it out on a /benchmark
.
Running playground portion only:
Ruby process memory usage 73924 KB
Running both playground and workshop:
Ruby process memory usage 83148 KB
I’ve uploaded a github repo here with this example as well as a simpler one file test outside of Rails.
Conclusion
If your Ruby on Rails application holds multiple portions in a single repository requiring only the served portion will help reducing memory usage as well as enforcing your dependency structure.
I checked on the public portion of an app deployed on Heroku running a 2400 requests per minute load test from an EC2 instance with Vegeta before the change the 10 2X dynos memory usage on Newrelic would go from a minimum of 422 up to 674MB. After the change and applying the same load test we’re starting from 377 up to 588MB. You milage might vary but I’d be surprised if you have no gain.
The Gemfile.lock
remains the manifest of what your apps are locked to. If for some reason you need different versions of the same gems in your components I’d love to hear from you, perhaps you should revisit the monolithic approach.