Seongsiks

Being A DevOpser. Powered by
Obtvse, highlight.js, theme toc Creative Commons License
Seongsiks Twitter Github Email
DevOps Ruby On Rails Chef Projects Misc Movies & Drama ME

Rails4-6 Etags n Cache

Overview

원래 zombie outlaw코스 상에는 중간에 Test부분이 있습니다만, 이 부분은 BDD인 [RSPEC]으로 자세해 다루기로 하고, 이번에 드디어 Rails4가 되면서 Rails가 매번 까이던 성능을 개선하기 위한 노력을 보여드릴 기회인것 같습니다.

ETags

ETag은 Entity tag의 약자로 서버가 Response를 "200 OK"를 보낼 것인가 아니면 "304 NOT MODIFIED"를 보낼 것인가를 확인하는데 사용되는 값입니다. ETag의 원리를 좀 더 자세히 보면,

  1. client가 Rails에게 request를 보냅니다.
  2. Rails는 응답할 웹페이지를 만들어내고, 이 결과물을 기준으로 MD5로 인코딩해서 ETag을 만들어 응답합니다.
  3. client는 받은 웹페이지와 ETag을 기억해놓습니다.
  4. 같은 페이지를 client가 다시 요청합니다. 이때, If-None-Match 헤더를 달고 아까 받은 ETag을 Rails에 보냅니다.
  5. Rails는 헤더를 확인하고 페이지를 만들어서 MD5로 인코딩해서 ETag값이 같으면 바뀐것이 없다고 판단하고 304 response를 하는 것이죠.
  6. client는 304네 하면서 로컬에 있는 캐시에서 페이지를 가져옵니다.

이 과정에서 줄일 수 있는 시간은 네트워크 시간뿐입니다. Rails입장에서는 이미 페이지를 만드는데 시간을 다 썻고, 보내기만 하면되는데 ETag을 확인하고 안보내는거죠. 실은 ETag을 만들고 비교도 해야되서 약간의 오버헤드까지 있어서 온전히 네트워크시간을 다 번 것도 아닙니다.

flesh_when 메소드

Rails3에서는 ETag을 아무 생각없이 다음과 같이 만듭니다.

headers['ETag'] = Digest::MD5.hexdigest(body)

body전체에 대해서 ETag을 만들고 있죠. Rails4에서는 어느 정보를 이용하여 Etag을 만들지 결정할 수 있습니다. 바로 #flesh_when메소드를 이용해서 말이죠.

class ItemsController < ApplcationController
  def show
    @item = Item.find(params[:id])
    fresh_when(@item)
  end
end

위에 fresh_when(@item)은 다음과 같이 @item의 cache_key에서 ETag을 생성하라는 의미입니다.

headers['Etag'] = Digest::MD5.hexdigest(@item.cache_key)

여기서 말하는 cache_key"item/2-201304010000"과 같은 모양으로 다음과 같은 규칙으로 만들어 집니다.
<model name>/<id>-<updated_at>

이렇게 모델에서 Etag을 만듦으로써, Rails는 모델까지만 조회하고 view를 렌더하는 시간을 벌 수 있게되는 거죠.

Declarative Etags

실제로 컨트롤러에 여러개의 액션이 존재하면 이런 Etag관련 코드가 계속 반복해서 나타나게 됩니다.

class ItemsController < ApplcationController
  def show
    @item = Item.find(params[:id])
    fresh_when([@item, current_user.id])
  end

  def edit
    @item = Item.find(params[:id])
    fresh_when([@item, current_user.id])
  end

  def most_recent
    @item = Item.find(params[:id])
    fresh_when([@item, current_user.id])
  end
end

이걸 하나로 모아서 컨트롤서 상단에 묶어서 적습니다.

class ItemController < ApplicationController
  etag { current_user.id }
  etag { current_user.age }

  def show
    @item = Item.find(params[:id])
    fresh_when(@item)
  end
  .....

end

Dalli

Dalli는 새로 Rails4부터 default memcached클라이언트로 기존 memcached클라이언트보다 다음과 같은 장점이 있답니다.

  • 기존 memcahed 클라이언트보다 약 20% 빠르다.
  • 타임아웃을 컨트롤할 수 있고, 좀 더 우아한 failover를 제공합니다.
  • Threadsafe
  • 기존 memcached클라이언트랑 호환됨

Rails4에서 이를 사용하기 위해 특별히 해줘야 되는 일은 그냥 Gemfile에 gem 'dalli'를 추가해 주는것 밖에는 없습니다. 물론 Rails3에서 하던 mecached 캐시설정은 해줘야합니다. 이부분은 Rails3와 동일합니다.

Caching Digests

Rails 4에서는 Caching Digests라는 새로운 캐싱 기능이 추가 되었습니다. 이를 제대로 쓸려면 Fragment Caching이라던가 Russian Doll Caching같은것을 이해할 필요가 있습니다.

russian dolls

< russian dolls from timrich26's Flickr >

Fragment Caching

Partial에서 caching하기 위해서는 다음과 같이 cache키워드와 같이 코드블럭을 이용합니다. 다음 샘플코드는 post에 belongs_to하고 있는 comment 모델의 partial입니다.

# File name: app/views/comments/_comment.html.erb
<% cache comment do %>
  <li><%= comment %></li>
<% end %>

이렇게 작성하면 comment.cache_key를 확인해서 comment가 변경되었으면 캐시를 생성하여 저장하고, 변경이 없다면 해당 partial을 다시 렌더링하지 않고 캐시에서 읽어와 서비스하게 됩니다.
그런데 만약 저 partial을 아래 코드와 같이 "작성자"를 추가했다면 어떻게 될까요?

# File name: app/views/comments/_comment.html.erb
<% cache comment do %>
  <li><%= comment %> - <%= comment.author %></li>
<% end %>

cache_key는 바뀐것이 없습니다. 그래서 기존 캐시를 사용하겠죠? 그럼 변경사항은 반영되지 않겠죠. 기존에 Rails3에서 이 문제를 해결하기 위해선 다음과 같이 버전을 추가하는 방법을 사용했습니다.

# File name: app/views/comments/_comment.html.erb
<% cache ['v1', comment] do %>
  <li><%= comment %> - <%= comment.author %></li>
<% end %>

그러나 Rails4에서는 이런 버전을 추가해주지 않아도 알아서 템플릿이 변경되었는지 알고, 캐시를 다시 생성합니다. 이렇게 할 수 있는 이유는 Rails3에서는 cachekey가 *[model name]/[id]-[updated\at]*였지만, Rails4에서는 템플릿 자체의 md5 해시가 추가되었기 때문입니다.

<model name>/<id>-<updated_at>/<md5 of template>

Russian-doll caching

Post가 comment를 has_many하고 있다고 말씀드렸죠. 이 Post의 View는 comment partial을 포함하고 있습니다. 이렇게 캐싱하는 템플릿 안에 다른 캐싱대상을 포함하고 있는 것을 Nested fragment caching또는 Russian-doll caching이라고 부릅니다. 예제를 보시죠.

# File name : app/views/blog/show.html.erb
<%= render @blog.posts %>
--------------------------------------------
# File name : app/views/posts/_post.html.erb
<% cache post do %>
<article>
  <h3><%= post.title %></h3>
  <ul>
    <%= render post.comments %>
  </ul>
</article>
<% end %>
--------------------------------------------
# File name: app/views/comments/_comment.html.erb
<% cache  comment do %>
  <li><%= comment %></li>
<% end %>

위 예제에서 comment가 수정된 상황을 생각해 보겠습니다. Rail4가 템플릿의 MD5 해시를 cachekey에 추가 해주었다고 하더라도 post의 cachekey입장에서는 변경된것이 없어 보입니다. 그래서 변경사항이 보이지 않게 되죠. 이문제는 model에서 touch 키워드로 해결 가능합니다.

# File name : app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :post, touch: true
end 

touch 키워드를 사용하면 comment가 새로 추가되거나 수정될때 comment의 updatedat뿐만 아니라 자신이 belongsto하고 있는 Post의 updatedat도 같이 업데이트 해주는 겁니다. 결론적으로 comment가 변경되면 post의 cachekey도 같이 변경되는거죠.

Cache Digests

자 위에 예제에서 comment의 변경은 Model에서 touch키워드를 이용해서 해결했습니다. 그럼 comment partial템플릿이 변경된것도 알 수 있을까요?
대답은 YES입니다. Rails 4에서는 partial이 partial을 포함하고 있는 경우에도 이를 포함시켜서 cache_key을 생성하여 포함되는 partial의 변경사항도 감지할 수 있습니다.

그러나 하나 주의 할것이 있습니다!! 바로 Helper Method을 이용했을 경우에는 Cache Digest가 그 Helper을 찾지 못한다는 군요.

# File name : app/views/posts/_post.html.erb
<% cache post do %>
<article>
  <h3><%= post.title %></h3>
  <ul>
    <%= render post.recent_comments %>
  </ul>
</article>
<% end %>

방법이 없는 것은 아닙니다. Helper method을 찾을 수 있도록 도와주면 됩니다.

방법 1. 명시적인 Partial Syntax

<%= render partial: "comments/comment", collection: post.recent_comments %>

방법 2. 특수한 주석 달아주기

<%# Template Dependency: comments/comment %>
<%= render  post.recent_comments %>

이것으로 휴우~ 가장 난이도 있었던, 포스트를 다 썼습니다. Rails 4로 업데이트 되면서 기존의 캐싱을 많이 보완한것을 느꼈습니다. 실제로 개발자 입장에서는 용법에 있어서 크게 변경되지 않으면서도, 섬세하게 캐시를 컨트롤할 수 있는 방법을 제공하기 시작했습니다. Ruby 2.0의 성능향상, 섬세한 캐싱컨트로, 멀티스레딩 서버 puma들의 도움으로 성능이슈에 대해서 조금은 자유로워 졌음면 좋겠습니다.


[이전 Rails4-5 View]
[다음 Rails4-7 Streaming]
comments powered by Disqus
Back to Ruby On Rails