страничное кэширование мультидоменного сайта на Rails 3+Nginx
«Наиболее эффективными считаются те запросы, которые никогда не выдаются», ― говорит Сэм Руби. Страничное кэширование с этой точки зрения не максимально эффективно, но близко к максимуму т.к. страница загруженная полностью из кэша не делает ни одного запроса к БД. Однако, в случае если сайт имеет поддомены, например, для определения локали (en.site.com, ru.site.com), кэш страницы в одной локале будет затираться кэшом в другой. В этой статье мой опыт борьбы с этой проблемой.
Если загуглить решение этой проблемы, то одно из первых, что выскакивает это статья Ryan Stout «Page Caching by Subdomain in Rails and Nginx». Собственно, по ней я и пытался кэшировать на поддоменах, но не все так гладко как там описано. Поэтому я кое-что добавил от себя, а кое-что изменил.
ApplicationController
Прежде всего, как и описано в статье надо добавить кое-что в ApplicationController
, но тут помимо указанного у Ryan’а метода cache_page
надо будет еще перегрузить expire_page
иначе обновить кэш из под рельс можно будет только на велосипеде.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class ApplicationController < ActionController::Base ... def cache_page(content = nil, options = nil) path = "/#{request.host}/" path << case options when Hash url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])) when String options else if request.path.empty? || request.path == '/' '/index' else request.path end end super(content, path) end def expire_page(options = {}) path = "/#{request.host}/" params[:format]=:html if options[:format] path << case options when Hash url_for(options.merge(:only_path => true, :format => params[:format])) when String options else if request.path.empty? || request.path == '/' '/index' else request.path end end self.class.expire_page(path) end ... end |
настройка самого rails-приложения
Согласно статье, кэш будет храниться в директории public/cache
. Чтобы так и было нужно, во-первых, создать папку cache
в public
, а, во-вторых, сообщить о пути кэша самому приложению. Для этого можно, например, в config/environments/production.rb
дописать:
MyApp::Application.configure do ... config.action_controller.page_cache_directory = Rails.root.join("public/cache") end |
nginx
В настройке сайта в nginx.conf
нужно указать, что и в оригинальной статье:
# If the file exists in the public folder, send it if (-f $request_filename) { break; } # Check / files with index.html if (-f $document_root/cache/$host/$uri/index.html) { rewrite (.*) /cache/$host/$1/index.html break; } # Check the path + .html if (-f $document_root/cache/$host/$uri.html) { rewrite (.*) /cache/$host/$1.html break; } # Check directly if (-f $document_root/cache/$host/$uri) { rewrite (.*) /cache/$host/$1 break; } |
Добавление этих условников говорит Nginx’у куда записывать кэш если его нет и откуда его читать если для данного адреса есть кэш. Вот тут и возникает проблема, Nginx не будет различать это GET-запрос или POST. И если в случае с GET, все хорошо, то когда поступает PUT-запрос на обновление модели (экшены update
и show
имеют один и тот же URL по умолчанию) Nginx с радостью обнаружит закэшированную страницу и выдаст ошибку 505.
Язык конфигурации Nginx не поддерживает вложенные условники и не понимает конъюнкцию (&& или AND или еще как угодно), все, что остается это строить велосипед что-то типа:
if ( $request_method = GET ) { set $method_and_file "method"; } if (-f $request_filename) { set $method_and_file $method_and_file+"&file"; } if ($method_and_file = "method&file") { break; } |
Но эти костыли у меня не завелись с первого раза и я забил на эти грязные мысли.
как же тогда обновить кэш?
Для обновления кэша я предлагаю такой, на мой взгляд, не самый кровавый вариант. Надо делать форму модели AJAX-вую и в вызове expire_cache
явно указывать формат кэша.
пример на модели Page
app/controllers/pages_controller.rb:
class PagesController < ApplicationController ... caches_page :show caches_page :index ... def update respond_to do |format| if @page.update_attributes(params[:page]) expire_page({action: :show, id: @page.id, format: :html}) expire_page({action: :index, format: :html}) format.html { redirect_to @page, notice: 'Page was successfully updated.' } format.js else format.html { render action: "edit" } format.js #format.json { render json: @page.errors, status: :unprocessable_entity } end end end ... end |
/app/views/pages/update.js:
setTimeout( 'window.location.replace("<%=page_path(@page)%>")', 1500); |
/app/views/pages/_form.html.erb:
<%= form_for(@page, remote: true, format: :js) do |f|%> ... <%end%> |