You've built and deployed your app, and you're now starting to experience heavier traffic as it gains traction in the market. Your cloud servers will be experiencing a heavier load than in the past, perhaps leading to poor performance. Or maybe you want to proactively take the steps needed to prepare your application or API for heavier traffic requests.
There are some commonly known techniques you can apply to improving the performance of our application, so let's take a look at some of these in more detail:
Move Non-critical Tasks as Background Processes
As I recently wrote in my article "Getting the most out of Cloud Servers without additional Scripting", one of the best ways to optimize your page and API request handling is to offload as much work as possible to the background. Tasks such as sending an email or making API calls for integration/sync can sometimes take longer than expected. It can slow down your application performance for the user, along with the overall performance of the app.
For frameworks such as Rails, one solution is using ActiveJob. ActiveJob has been around since Rails 4.2 and offers an easy way to offload code to the background. The background worker is called a Job and generally looks like this:
class WelcomeEmailJob < ApplicationJob queue_as :default def perform(user) # Send an email in the background... end end
Then, you can request the job to be pushed to the background:
With ActiveJob, you can also request that the job be performed at a specific time:
ActiveJob allows you to choose what background job processor you wish to use. I would recommend Resque or Sidekiq, as other solutions may not support more than one process running or be able to recover from failed jobs easily.
To learn more about messaging queues and how they can be used to reliably conduct background processing, check out the Realscale article on the subject.
Apply Caching Optimizations
Caching allows content to be stored and read later without requiring computation to be performed again. It is common to cache data, computationally heavy results, and even part or all of a response to speed up web applications and APIs. Often, caches use a key/value approach to store the data. Using a dedicated cache, such as memcached, will prevent each server from having to perform the same work.
Frameworks such as Rails offer several levels of caching built-in, including:
- Fragment caching for reducing the database queries and content generation of a portion of a web page.
- Low-level caching for developers that need to store data or expensive computational results.
- SQL caching to prevent a single request from needing to re-execute the exact same query more than once.
You may choose to implement one or more than one level of caching to achieve the results needed. If you have a Rails application, the Rails Caching Guide provides a nice overview of each type of caching strategy and how to make it work for your particular application.
Note: When moving computation work to background jobs, as we discussed earlier, be sure to invalidate any related cache entries if the job changes any of your cached data.
Use CDNs to Offload Static and Cache Entire API/Page Responses
However, some CDN services will even cache dynamic content as well, helping to offload your app and API server work. Your app or API will need to be setup with proper cache headers, including how long to cache the response, an ETag to help identify when data has changed, and other HTTP-related details. It can take some time to get everything working properly, but once you do, CDNs can offer significant performance boosts. They can also protect your application from DDoS attacks as well.
Improving Database Performance
If your website is read-heavy, then your database is likely experiencing (or will experience) a high number of read requests as traffic increases. While caching can offload some of the work by limiting or preventing the need for database access, sometimes that isn't enough. In those cases, the application needs to increase the amount of read-based queries the app can handle.
Scaling read-heavy applications is typically handled through the use of read replicas, but may also involve strategies such as data partitioning or sharding. By adding more database servers to support read-based queries, you can increase the number of requests/sec that your app or API can handle. Applying this technique also provides higher availability for your application, by preventing a database failure from taking down your entire app.
Refer to our Realscale article on Database Server Scaling Strategies for more detail on how database replication works.
Optimize Performance Bottlenecks
Frameworks and third-party libraries are often useful and save us time, but they can also hide performance issues. Profile your application using ruby-prof, or a tool such as New Relic, to find the hotspots where you're experiencing poor performance. The results from profiling your application will give you hints as to where you may need to optimize first. This may require steps such as dropping down to SQL in areas where performance is critical, and/or removing third-party gems that offer limited help but may negatively impact performance.
Poor choices in database design, access strategies, and ORM frameworks can severely limit your application's performance as well. Refer to the article, "Getting the Most out of your Database with Ruby on Rails", for more details on optimizing your Rails app for database access, including ways to use database indexes and how you can find faster ways to access your database.