Several scenes
First, let me take you through a few changingthepresent.org pages. I will show several places in the site that need to be cached. Then, we'll point out the choices we make for each of them and the code or strategy that we use to implement those pages. In particular, it will focus on the following topics:
- Full static page
- Completely dynamic page with almost no change
- Dynamic page Fragment
- Application Data
Take a look at the static page first. Almost every site will have a static page, as shown in Figure 1, which has our terms and conditions. You can browse the page by clicking the Register and then selecting whether to accept the user agreement. For changingthepresent, we removed all of the dynamic content from this page so that Apache could cache it. These will never be generated by the Rails server, according to the rules we have configured in Apache. So I don't have to think about the Rails cache at all.
Figure 1. User Agreement
Next, take a look at the full dynamic page. Theoretically, changingthepresent can have some dynamically built pages, but these pages generally rarely change. Since almost all pages will show whether the user is logged in, we are not paying much attention to the cache.
Again, look at the page fragment cache. The home page shown in Figure 2 is completely static, and now there are some elements that have become dynamic. Every day, the page displays a series of gifts, either randomly selected or selected by our administrator. Note Those gifts under the title "A Few of our Special Gifts for Mother's Day", and also note the link to the rightmost display as "login." This link depends on whether the user is logged on. We can't cache the entire page. Pages can only be changed once a day.
Figure 2. Home
Finally, consider the application. Unless you're surfing the web before 15, all the interesting sites you're experiencing are dynamic. Modern applications are mostly layered and can be made more efficient by adding caches between tiers. Changingthepresent uses some caching in the database layer. Next, I'll delve into the different types of caching and describe what caching we have for changingthepresent.
Caching Static Content
Mongrel is a WEB server that is written by Zed Shaw using 2500 lines of Ruby and C. This small server consumes very little memory and is ideal for Ruby Web applications such as Rails, Nitro, Iowa, and so on. Mongrel can run on UNIX? and Linux? , you can also run on the Win32. Mongrel can also often run as an agent on the back end of another WEB server (such as Apache or Litespeed), but this is not necessary-because Mongrel is an HTTP server that can be used in conjunction with all of your preferred HTTP tools.
In addition to the image, there is not much to say about caching the contents of static data. Since our site is a charitable portal, it means that we need to focus more on the user's feelings, such as adding more images or videos. But our Web server Mongrel does not serve static data very well, so we use Apache to service image content.
We are now moving towards using the graphics accelerator Panther Express to cache the most frequently used images so that they can be accessed by our customers faster. To take this strategy, we will need a subdomain images.changingThePresent.org. Panther Express provides image services directly in the image's local cache, and then sends the request to us. Since the Panther service does not know when we will change the image, we use the HTTP header to expire it, as follows:
HTTP Cache Invalidation Header
http/1.1 OK
cache-control:max-age=86400, must-revalidate
expires:tues, Apr 2007 11:43:51 GMT
Last-modified:mon, APR 2007 11:43:51 GMT
Note that these are not HTML headers. They are built independently of Web page content. The WEB server will be responsible for building these HTTP headers. A series of articles about Rails like this one is a bit off to detail the Web server configuration, so I'll go directly to the topic of cached content that can be controlled by the rails framework (see Resources for more on Web server configuration).
Page Caching
If dynamic pages do not change frequently, you can use page-level caching. For example, blogs and bulletin boards Use this kind of caching. With page caching, Rails can be used to build dynamic HTML pages and store this page in a public directory so that the application server can serve the dynamic page just like any other static page.
If the page is already cached, then there is no need to introduce rails, which is the fastest cache in rails. At the bottom, page caching is actually very easy to implement in Rails. Both the page and the segmented cache occur at the controller level. You need to tell Rails the following:
- Which pages do you want to cache?
- When the page content changes, how can you make the page expire in the cache?
You can enable page caching by using the Caches_page directive in the controller class. For example, to cache Privacy_policy and user_agreement pages in About_us_controller, you can enter the following code:
Listing 2. Enable page Caching
Class Aboutcontroller < Applicationcontroller
caches_page:p rivacy_policy,: user_agreement
End
The expiration of the page expires can be achieved by expire_page instructions. To make the page expiration expire when Rails invokes the New_pages action, you can use the following code:
Listing 3. Make page invalid
Class Aboutcontroller < Applicationcontroller
caches_page:p rivacy_policy: user_agreement
def new_pages
expire_page:action =>:p rivacy_policy
expire_page:action =>: user_agreement
End
In addition, there are a few minor issues to note, such as URLs. URLs cannot depend on URL parameters. For example, you should use GIFTS/WATER/1 rather than gifts/water?page=1. Using this type of URL in ROUTES.RB will be very easy. For example, there is always a tab parameter in our page to show which tab is currently selected. To use this tab as part of the URL, we will have the following routing rules:
Listing 4. Routing Rules for Tabs
Copy Code code as follows:
Map.connect ' Member/:id/:tab ',: Controller => ' profiles ',: Action => ' show '
The same approach is required for those lists that have page parameters and other pages that depend on the URL parameters. In addition, you need to consider security issues.
If the page is already in the cache, the Rails framework is not used and the server does not manage security for you. The WEB server will be more willing to render any page within the cache, regardless of whether the user has permission to view it. So, if you're concerned about who the page is, then don't use the page cache.
If you just want to cache a simple static page, it should be sufficient to understand the above content. As long as the content is simple, it is not difficult to achieve.
When you want to cache more complex content, you need to make trade-offs. Because the page you want to cache is highly dynamic, the expiration logic becomes more complex. To handle complex expiration failure logic, you will need to write and configure a custom cleaner (sweeper). When some controllers are fired, the classes delete the selected elements from the cache.
Most custom cleaner will look at some model objects and cause one or more cached pages to expire due to a change in the firing logic. Listing 5 shows a typical cache cleaner. In this cleaner, the developer can define an activity record event, such as After_save. When this event is fired, The cleaner will also be fired and the expiration of a specific page in the cache is due to expire. The expiration failure shown by this case is based on the Expire_page method. Most strict applications use Ruby's excellent file system utility to explicitly delete cached pages.
Listing 5. A typical observer
Class Causecontroller < Applicationcontroller
cache_sweeper:cause_sweeper
...
Class Causesweeper < Actioncontroller::caching::sweeper
observe cause
def after_save (record)
expire_ Page (: Controller => ' causes ',: Action => ' Show ',
: IDs => record.id)
Cause.nonprofits.each do |nonprofit |
Expire_page (: Controller => ' nonprofits ',: Action => ' Show ',
: ID => nonprofit.id)
End
End
Now, you may start to feel a bit of a drawback to the page cache: complexity. While you can do a good job of page-level caching, inherent complexity makes it difficult to test your application, which in turn increases the likelihood of bugs in your system. Also, if the page is different for each user, or if you want to cache a page that has been authenticated, you will need to use a way other than page caching. For Changingthepresent, we have to deal with two cases because we have to change the links on the basic layout based on whether the user is logged on or not. For most pages, we don't even consider using page-level caching. To give you a deeper understanding of page-level caching, in the Resources section of this article, I have specifically given a series of links to excellent articles on page-level caching. Next, delve into another form of full-page caching, the action cache, in the future.
Action Caching
At this point, you've learned the main advantages and main drawbacks of page caching: For most page searches, you don't need to consider Rails at all. The advantage of page caching is that it is fast. The disadvantage is lack of flexibility. If you want to cache an entire page based on conditions within the application-for example, identity authentication-you can use the action cache.
The action cache works the same way as the page cache, but there is a slight difference in the process. Rails actually invokes the controller before rendering the action. If the page rendered by the action already exists in the cache, then Rails renders the page in the cache instead of rendering it again. Because Rails is now used, the speed of the action cache is slower than the page cache, but the advantages are obvious. Almost all Rails authentication modes use the before filter on the controller. The action cache allows you to take advantage of any filters on the authentication and controller.
From the perspective of statement composition, the action cache and page caching should be very similar, only the instructions are not the same. Listing 6 shows how to use the caches_action directive.
Listing 6. Enable action Caching
Class Aboutcontroller < Applicationcontroller
Caches_action:secret_page,: secret_list
End
Cache expiration failures and how the cleaner works should be the same. The reason we don't use the action cache is the same as the reason we don't use the page cache, but the segmented cache is more important to us.
Caching page Segments
With partial caching, you can cache part of the page, and the cached content is often layout-like. To use a segmented cache, developers need to determine the fragmentation by enclosing a piece of content directly on a Web page, as shown in Listing 7. On the changingthepresent.org, we use the segmented cache to cache the home page and several other pages. All of these pages use database intensive access and are mostly our most popular pages.
Listing 7. Determining Cache Fragmentation
<% cache ' Gifts_index ' do%>
In Listing 7, the cache helper identifies the partition to be cached. The first parameter is the unique name that identifies this cache partition. The second parameter contains the code block-the code between the first do and the last end-this code block accurately determines the RHTML partition to cache.
Our site has only one homepage, so it's very easy to name this page. Elsewhere, we use a specific method to determine the URL of this page to uniquely identify the cache fragment. For example, when we cache code for specific content, such as world peace or poverty reduction, we need to use the code in Listing 8. The code will look for a permanent URL, also known as permalink.
Listing 8. To identify a cache fragment by URL
<% cache @cause. Permalink (Params[:id]) do%>
Typically, when you cache a separate page, you need to use the cleaner to expire it. Sometimes, it's easier and simpler to use simple, time-based object expiration. By default, Rails does not provide such a mechanism, but there is a plug-in named Timed_fragment_cache to do this. With this plug-in, I can specify a time-out, either in the cached content or in the controller code that provides dynamic data for this page. For example, the code shown in Listing 9 allows you to build dynamic data for this page. The When_fragment_expired method executes only if the associated cache fragment expires. This method takes a parameter that specifies the time length of the timeout, and it also accepts a block of code that specifies which content needs to be rebuilt when content expires. I can also choose to specify timeouts and caching methods in the Rhtml page, but I prefer to use a controller based approach.
Listing 9. Time-based cache expiration
def index
when_fragment_expired ' causes_list ', 15.minutes.from_now do
@causes = cause.find_all_ordered
End End
If you can tolerate data being slightly stale, using timed expiration mechanisms can greatly simplify caching policies. For each cached element, you simply specify what you want to cache, any controller actions that generate dynamic content, and timeouts. Similar to the page cache, you can use the Expire_fragment:controller => controller, if necessary, by using the action => action: The ID => ID method explicitly makes the content expire. This method works the same way that the cached action and the cached page expiration are invalidated. Next, I'll describe how to configure the backend.
Memcached
So far, I've covered the page and fragment caching model for Ruby on Rails. Once you've seen the API, you can now define where the cached data will be. By default, Rails puts cached pages into the file system. Cached pages and actions go into the public directory. You can configure the storage location of the cached fragment. To do this, you need to use memory storage, a file system (in a defined directory), a database, or a service called memcached. For changingthepresent.org, we use memcached.
Memcached can be imagined as a large hash chart, which is available over the network. Memory-based caching is faster, and network-based caching is more scalable. With plug-in support, Rails can use memcached to cache segments and ActiveRecord models. To use it, you need to install memcached (see Resources for more information) and configure it in ENVIRONMENT.RB (or other environment profiles, such as PRODUCTION.RB).
Listing 10. Configuring caching
Config.action_controller.perform_caching = True
memcache_options = {
: c_threshold => 10_000,
: Compression => false,
:d Ebug => false,
: readonly => false,
: UrlEncode => false,
: TTL =>
: Namespace => ' Igprod ',
:d isabled => false
}
CACHE = Memcache.new memcache_options
Listing 10 shows a typical configuration in which the first line config.action_controller.perform_caching = True enables caching. The next line prepares the caching options. Note that many of the options are available to allow you to get more debug data, disable caching, and define the cached namespace. The memcached site, given in the Resources section, can find more information about configuration options.
Model Cache
The last cache we use is based on the model cache. We are using a custom version of the cache plug-in called Cachedmodel. Model caching is actually a limited form of database caching. Caching is easy to enable by model.
For a model to use a caching solution, simply extend the Cachedmodel class, not the extended ActiveRecord, as shown in Listing 11. Cachedmodel extended activerecord::base. ActiveRecord is not a whole-object relational mapping layer. This framework relies heavily on SQL to perform complex features, and users can easily descend to SQL if needed. Using SQL directly can cause caching problems because the caching layer must handle the full result set rather than a single database row. Processing a complete set of results is often problematic, and it is almost impossible without deep logic to support the application. For this reason, the focus of the Cachedmodel is placed on the cache of a single model object and only the query that returns a single row of results is accelerated.
Listing 11. Using Cachedmodel
Class Cause < Cachedmodel
Most Rails applications will repeatedly access multiple entries, such as user objects. The model slows down in many cases and can obviously speed up. For Changingthepresent, we are just beginning to speed up model-based caching.
Concluding remarks
Although Ruby is a highly productive language, the explanatory nature of the language makes it less desirable for performance reasons. Most of the major Rails applications will compensate for some deficiencies by effectively leveraging the cache. For changingthepresent.org, we mainly use segmented caching and use a time based method through the controller to invalidate the cache fragment expiration. This works well for our site, even though some of the pages will change based on the users who are logged in.
We also studied the effects of using Cachedmodel classes supported by memcached. While our research is limited to the impact of caching on database performance, early results are promising. In the next article, I'll introduce some practical tips that you can use to optimize your database for another real-world example of Rails.