When you build a great product, sooner or later, it will be attracting more and more users who will expect a performant, irreproachable app. As the app grows in time it handles more requests per minute. If you’re not prepared for this the app performance will drop and you will potentially lose your audience. In this blog post we explain what you should pay attention to when building a scalable app.
What is Application Scalability?
An application scalability is the potential of an application to grow in time, being able to efficiently handle more and more requests per minute (RPM). It’s not just a simple tweak you can turn on/off, it’s a long-time process that touches almost every single item in your stack, including both hardware and software sides of the system.
In case of problems you can keep adding new CPUs or increase memory limits, but by doing so, you’re just increasing the throughput, not the app performance. It’s not the way you should stick to when you see your app is starting to have efficiency problems. Scaling the app is not an easy thing and thus you should know your app very well before starting to think about how and when to scale it.
What are the Problems with App Scaling
Quite often you can hear that Rails introduces scalability issues when your project grows too large, but the application ability to scale is more about the whole system architecture, not only the framework itself. Building the project using The Rails Way is certainly not the best approach when your app is evolving rapidly, but it doesn’t mean that scaling a Rails app is always a pain. Of course, Twitter moved from Rails to Scala, but on the other hand, Shopify is constantly growing with Rails at the backend for about 10 years, with over 50,000 requests per minute and 45ms response time.
Even if you don’t have performance or scalability problems like Twitter or Shopify, planning and developing the application in a proper way is priceless, regardless of the nature of potential problems. You may face dozens of different issues when it comes to scaling. A few general sources of your problems may be related to:
limited physical resources like memory, CPUs etc.,
wrong memory management,
inefficient database engine,
complicated database schema, bad indexing,
poorly performed database queries,
wrong server configuration,
app server limitations,
overall spaghetti code,
lack of monitoring tools,
too many external dependencies,
improper background jobs design,
more, and more, and more.
Despite all the opinions, Rails is a great framework with an incredibly huge community and millions of already answered questions online. It has hundreds of great open-source tools you may instantly build into your stack and a lot of profiling and analyzing tools that help you to identify the bottlenecks of your system.
Tips for Efficient Scaling
Keep your code clean - it seems to be obvious, but we just can’t omit to point it out here. Writing good code is the key to your app scalability. When your app is full of spaghetti code, it becomes a big ball of mud and it's really hard to maintain and scale it.
Leverage 12factor - it’s a great methodology that focuses on the best practices of developing a scalable application. It’s language-independent, so implementing each of these 12 factors depends on the developers. You really should follow these rules if you want your app to scale flexibly over time.
Take care of your database - to let the system grow smoothly, you have to take care of database. Choosing a proper DB engine and designing best possible schema you’re able to handle increasing transactions per second efficiently.
Prevent problems with queries - every single web application performs huge amounts of database queries, even if you have awesomely designed relations with proper indexing, running on the best engine on the market, remember to ask your database for the information efficiently, avoiding N+1 queries, unnecessary eager loading etc.
Choose the right hosting - you have to bare in mind that scalability is not only about your code. It’s very important to have proper server infrastructure and configuration. You can save a lot of time just by selecting proper tools and providers. For example - Amazon EC2 provides auto-scaling features, Docker offers docker-compose scale etc.
Track caching - you don’t need a dedicated machine to handle your caching when you start your project, but it’s definitely a good practice to implement, track and modify your caching needs over time.
Prepare for load balancing - if you’re not using a `ready to go` infrastructure like AWS or Heroku, remember that one day you have to prepare yourself to switch from a single-server architecture to a multi-server one with load balancing and reverse proxying.
Relieve the backend - if you get a chance to move some code to the front-end - do it. Thanks to that your backend will become less overloaded, as more and more things will be computed on the client side.
Test and monitor - even if your codebase is clean and perfectly maintainable, you need some tools to monitor it and identify the problems as soon as possible.
Optimize - identifying the problems is one thing, but you have to take advantage of the tools you have and optimize your code and configuration to minimize the bottlenecks of your app.
Separate code - it’s also a part of code cleanliness - try not to mix too many parts of your system in one place. Separate frontend and backend layers, detach background jobs from the main system, use design patterns wisely.
Update on regular basis - keep all the stuff up to date to avoid blocking moments due to outdated parts of your system (e.g. old ruby version).
Ruby/Rails Quick Wins
Use common gems like bullet or rack-mini-profiler to identify the problems instead of paying for some extra memory in advance.
Use update_all for bulk updates with the same value (bare in mind that `update_all` does not perform any callbacks nor validations) or consider writing a custom SQL query on your own. Using AR methods (e.g. update) for thousands of objects in a loop is not the best practice.
Consider using UUIDs instead of IDs. When you use standard, incremental IDs as primary keys, all database writes in most cases have to go through a single database. When you use UUIDs, you can you can spread them out across many servers easier in the future
Make use of database-level functions, like SQL SUM, COPY or DISTINCT. Example: Use `Model.sum(:value)` instead of `Model.sum(&:value)`. The first one performs an SQL sum function and returns the result; the second one fetches all the records, maps the collection, goes through every single item and takes the `value` attribute. Then it uses Array’s sum method to return the result we’re waiting for. Guess which one is faster?
Use `pluck` instead of `map` to fetch the same attributes from multiple objects.
Don’t use `length` to count records - 'ActiveRecord::Relation#length' will always load up all the records and then call Ruby’s `#length` on the resulting array. Use `size`, which will touch the database only when needed. `count` is also often a better solution, as it performs a single count query.
There isn’t any magic formula for scaling a Rails app. Every single application is actually a unique system. If you pay attention while developing it, it can be surprisingly easy to handle its growth. Thanks to the great set of available tools you don’t have to be an expert to scale your application. In most of the cases, optimized code, efficient SQL queries, proper DB indexing and some caching should be enough for the beginning.