Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Rails의 뷰 캐싱에 대해 알고 싶었던 모든 것

캐싱은 나중에 빠르게 검색할 수 있도록 일부 코드의 결과를 저장하는 것을 의미하는 일반적인 용어입니다. 이를 통해 예를 들어 거의 변경되지 않는 데이터를 얻기 위해 데이터베이스에 계속 부딪치는 것을 피할 수 있습니다. 일반적인 개념은 모든 유형의 캐싱에 대해 동일하지만 Rails는 캐싱하려는 항목에 따라 다양한 지원을 제공합니다.

Rails 개발자의 경우 일반적인 캐싱 형식에는 메모이제이션, 저수준 캐싱(둘 다 이 캐싱 시리즈의 이전 부분에서 다룸) 및 뷰 캐싱이 포함되며 여기에서 다룰 것입니다.

Ruby on Rails가 뷰를 렌더링하는 방법

먼저 약간 혼란스러운 용어를 살펴보겠습니다. Rails 커뮤니티에서 "views"라고 부르는 것은 app/views 내부에 있는 파일입니다. 예배 규칙서. 일반적으로 .html.erb입니다. 다른 옵션이 있지만(예:일반 .html , .js.erb 또는 Slim 및 haml과 같은 다른 전처리기를 사용하는 파일). 다른 많은 웹 프레임워크에서 이러한 파일을 "템플릿"이라고 하며, 이 파일이 용도를 더 잘 설명한다고 생각합니다.

Rails 애플리케이션이 GET를 수신할 때 요청이 있는 경우 특정 컨트롤러 작업(예:UsersController#index)으로 라우팅됩니다. . 그런 다음 작업은 데이터베이스에서 필요한 정보를 수집하고 보기/템플릿 파일을 렌더링하는 데 사용하기 위해 전달합니다. 이 시점에서 우리는 "뷰 레이어"로 들어가고 있습니다.

일반적으로 보기(또는 템플릿)는 하드 코딩된 HTML 마크업과 동적 Ruby 코드가 혼합되어 있습니다.

#app/views/users/index.html.erb

<div class='user-list'>
  <% @users.each do |user| %>
    <div class='user-name'><%= user.name %></div>
  <% end %>
</div>

보기를 렌더링하려면 파일의 루비 코드를 실행해야 합니다(erb <% %>의 모든 것입니다. 태그). 페이지를 100번 새로고침하고 @users.each... 100번 실행됩니다. 포함된 모든 부분에 대해서도 마찬가지입니다. 프로세서는 부분 html.erb를 로드해야 합니다. 파일에서 모든 Ruby 코드를 실행하고 결과를 단일 HTML 파일로 결합하여 요청자에게 다시 보냅니다.

저속 보기의 원인

개발 중에 페이지를 볼 때 Rails가 다음과 같은 많은 로그 정보를 출력한다는 사실을 눈치채셨을 것입니다.

Processing by PagesController#home as HTML
  Rendering layouts/application.html.erb
  Rendering pages/home.html.erb within layouts/application
  Rendered pages/home.html.erb within layouts/application (Duration: 4.0ms | Allocations: 1169)
  Rendered layouts/application.html.erb (Duration: 35.9ms | Allocations: 8587)
Completed 200 OK in 68ms (Views: 40.0ms | ActiveRecord: 15.7ms | Allocations: 14307)

마지막 줄은 이 단계에서 우리에게 가장 유용합니다. 왼쪽에서 오른쪽으로 시간을 따라가면 Rails가 브라우저에 응답을 반환하는 데 걸린 총 시간이 68ms였으며 그 중 erb를 렌더링하는 데 40ms가 소요되었음을 알 수 있습니다. 파일 및 ActiveRecord 쿼리 처리 시 15.7ms.

사소한 예이지만 뷰 레이어 캐싱을 살펴봐야 하는 이유도 보여줍니다. 마술처럼 ActiveRecord 쿼리가 즉시 발생하도록 할 수 있다고 해도 erb를 렌더링하는 데 두 배 이상 시간이 소요됩니다. .

뷰 렌더링이 느릴 수 있는 몇 가지 이유가 있습니다. 예를 들어 뷰 내에서 값비싼 DB 쿼리를 호출하거나 루프 내에서 많은 작업을 수행할 수 있습니다. 내가 본 가장 일반적인 상황 중 하나는 단순히 여러 수준의 중첩을 사용하여 많은 부분을 렌더링하는 것입니다.

개별 행을 처리하는 부분이 있을 수 있는 이메일 받은 편지함을 상상해 보십시오.

# app/views/emails/_email.html.erb

<li class="email-line">
  <div class="email-sender">
    <%= email.from_address %>
  </div>
  <div class="email-subject">
    <%= email.subject %>
  </div>
</div>

그리고 기본 받은 편지함 페이지에서 각 이메일에 대한 부분을 렌더링합니다.

# app/views/emails/index.html.erb

...
<% @emails.each do |email| %>
  <%= render email %>
<% end %>

받은 편지함에 100개의 메시지가 있는 경우 _email.html.erb를 렌더링합니다. 부분 100번. 우리의 사소한 예에서 이것은 크게 우려할 사항이 아닙니다. 내 컴퓨터에서 부분은 전체 인덱스를 렌더링하는 데 15ms만 걸립니다. 물론 실제 예제는 더 복잡하고 그 안에 다른 부분을 포함할 수도 있습니다. 렌더링 시간을 늘리는 것은 어렵지 않습니다. _email을 렌더링하는 데 1-2ms밖에 걸리지 않더라도 부분적인 경우 전체 수집을 수행하는 데 100-200ms가 걸립니다.

다행스럽게도 Rails에는 __email만 캐시할 것인지 여부와 상관없이 이 문제를 해결하기 위해 캐싱을 쉽게 추가하는 데 도움이 되는 몇 가지 내장 기능이 있습니다. 부분, index 페이지 또는 둘 다.

보기 캐싱이란 무엇입니까

Ruby on Rails의 뷰 캐싱은 뷰가 생성하는 HTML을 가져와 나중에 사용할 수 있도록 저장합니다. Rails는 이것을 파일 시스템에 쓰거나 메모리에 유지하는 기능을 지원하지만 프로덕션 용도로 사용하려면 Memcached 또는 Redis와 같은 독립 실행형 캐싱 서버가 필요할 것입니다. Rails의 memory_store 개발에는 유용하지만 프로세스 간에 공유할 수 없습니다(예:여러 서버/dyno 또는 유니콘과 같은 분기 서버). 마찬가지로 file_store 서버에 로컬입니다. 따라서 여러 상자에서 공유할 수 없으며 만료된 항목을 자동으로 삭제하지 않으므로 Rails.cache.clear를 주기적으로 호출해야 합니다. 서버의 디스크가 꽉 차는 것을 방지합니다.

캐싱 저장소를 활성화하려면 환경 구성 파일(예:config/environments/production.rb ):

  # memory store is handy for testing
  # during development but not advisable
  # for production
  config.cache_store = :memory_store

기본 설치에서 development.rb 컴퓨터에서 캐싱을 쉽게 토글할 수 있도록 일부 구성이 이미 완료되어 있습니다. rails dev:cache를 실행하기만 하면 됩니다. 캐싱을 켜고 끕니다.

Rails에서 뷰를 캐싱하는 것은 믿을 수 없을 정도로 간단하므로 성능 차이를 설명하기 위해 sleep(5)를 사용하겠습니다. 인위적 지연 생성:

<% cache do %>
  <div>
    <p>Hi <%= @user.name %>
    <% sleep(5) %>
  </div>
<% end %>

이 뷰를 처음 렌더링하는 데 예상대로 5초가 걸립니다. 그러나 두 번째 로드는 cache do 내부의 모든 것이 수행되기 때문에 몇 밀리초 밖에 걸리지 않습니다. 캐시에서 블록을 가져옵니다.

예시별 보기 캐싱 추가

작은 예제 보기를 사용하여 캐싱 옵션을 살펴보겠습니다. 이 보기가 실제로 몇 가지 성능 문제를 일으키는 것으로 가정합니다.

# app/views/user/show.html.erb
<div>
  Hi <%= @user.name %>!
<div>

<div>
  Here's your list of posts,
  you've written
  <%= @user.posts.count %> so far
  <% @user.posts.each do |post|
    <div><%= post.body %></div>
  <% end %>
</div>

<% sleep(5) #artificial delay %>

이것은 인공적인 5초 지연과 함께 작업할 기본 골격을 제공합니다. 먼저 전체 show.html.erb를 래핑할 수 있습니다. cache do의 파일 앞에서 설명한 대로 차단합니다. 이제 캐시가 따뜻해지면 멋지고 빠른 렌더링 시간을 얻을 수 있습니다. 하지만 이 계획에 문제가 발생하는 데는 오랜 시간이 걸리지 않습니다.

첫째, 사용자가 이름을 변경하면 어떻게 됩니까? 캐시된 페이지를 언제 만료할지 Rails에 알려주지 않았으므로 사용자는 업데이트된 버전을 볼 수 없습니다. 쉬운 해결책은 @user를 전달하는 것입니다. cache에 대한 개체 방법:

<% cache(@user) do %>
<div>
  Hi <%= @user.name %>!
</div>
...
<% sleep(5) #artificial delay %>
<% end %>

저수준 캐싱에 대한 이 시리즈의 이전 기사에서는 캐시 키에 대한 세부 정보를 다루었으므로 여기서 다시 다루지 않겠습니다. 지금은 모델을 cache()에 전달하면 충분합니다. , 해당 모델의 updated_at를 사용합니다. 캐시에서 조회할 키를 생성하는 속성입니다. 즉, @user 이 캐시된 페이지는 만료되고 Rails는 HTML을 다시 렌더링합니다.

사용자가 이름을 변경하는 경우를 처리했지만 게시물은 어떻습니까? 기존 게시물을 변경하거나 새 게시물을 생성해도 updated_at는 변경되지 않습니다. User의 타임스탬프 , 따라서 캐시된 페이지가 만료되지 않습니다. 또한 사용자가 이름을 변경하면 모두 게시물이 변경되지 않았더라도 이 두 가지 문제를 모두 해결하려면 "러시아 인형 캐싱"(즉, 캐시 내의 캐시)을 사용할 수 있습니다.

<% cache(@user) do %>
  <div>
    Hi <%= @user.name %>!
  <div>

  <div>
    Here's your list of posts,
    you've written
    <%= @user.posts.count %> so far<br>
    <% @user.posts.each do |post| %>
      <% cache(post) do %>
        <div><%= post.body %></div>
      <% end %>
    <% end %>
  </div>

  <% sleep(5) #artificial delay %>
<% end %>

이제 개별적으로 렌더링된 각각의 post를 캐싱합니다. (실제 세계에서 이것은 아마도 부분적일 것입니다). 따라서 @user 업데이트되면 게시물을 다시 렌더링할 필요가 없습니다. 캐시된 값만 사용할 수 있습니다. 하지만 아직 한 가지 문제가 더 있습니다. post 변경되었지만 @user.update_at 때문에 여전히 업데이트를 렌더링하지 않습니다. 변경되지 않았으므로 cache(@user) do 실행되지 않습니다.

이 문제를 해결하려면 touch: true를 추가해야 합니다. Post로 모델:

class Post < ApplicationRecord
  belongs_to :user, touch: true
end

touch: true 추가 여기서 우리는 ActiveRecord에 매번 게시물이 업데이트되었습니다. updated_at를 원합니다. 업데이트할 "소속" 사용자의 타임스탬프입니다.

또한 Rails는 얼마나 일반적인지 고려할 때 부분 컬렉션을 렌더링하기 위한 특정 도우미를 제공한다고 덧붙였습니다.

  <%= render partial: 'posts/post',
       collection: @posts, cached: true %>

다음과 기능적으로 동일합니다.

<% @posts.each do |post| %>
  <% cache(post) do %>
    <%= render post %>
  <% end %>
<% end %>

render partial: ... cached: true일 뿐만 아니라 덜 장황한 형태로, Rails가 컬렉션의 각 항목에 대한 캐시 저장소를 누르는 대신 캐시 저장소에 다중 가져오기를 발행할 수 있기 때문에(즉, 단일 왕복으로 많은 키/값 쌍을 읽을 수 있기 때문에) 추가 효율성을 제공합니다.

동적 페이지 콘텐츠

일부 페이지에는 주변 페이지의 나머지 부분보다 훨씬 빠른 속도로 변경되는 '동적' 콘텐츠가 포함되어 있는 것이 일반적입니다. 이는 활동/뉴스 피드가 있을 수 있는 홈 페이지 또는 대시보드에서 특히 그렇습니다. 캐시된 페이지에 이러한 항목을 포함하면 캐시를 자주 무효화해야 하므로 애초에 캐싱을 통해 얻을 수 있는 이점이 제한될 수 있습니다.

간단한 예로 현재 날짜를 보기에 추가해 보겠습니다.

<% cache(@user) do %>
  <div>
    Hi <%= @user.name %>,
    hope you're having a great
    <%= Date.today.strftime("%A") %>!
  <div>

  ...
<% end %>

우리는 매일 캐시를 무효화할 수 있지만 명백한 이유 때문에 그다지 실용적이지 않습니다. 한 가지 옵션은 자리 표시자 값(또는 빈 <span> ) 자바스크립트로 채웁니다. 이러한 종류의 접근 방식은 종종 "자바스크립트 스프링클"이라고 하며 많은 Rails의 핵심 코드가 개발되는 Basecamp에서 선호하는 접근 방식입니다. 결과는 다음과 같을 것입니다:

<% cache(@user) do %>
  <div>
    Hi <%= @user.name %>,
    hope you're having a great
    <span id='greeting-day-name'>Day</span>!
  <div>

  ...
<% end %>

<script>
 // assuming you're using vanilla JS with turbolinks
 document.addEventListener(
   "turbolinks:load", function() {
   weekdays = new Array('Sunday', 'Monday',
     'Tuesday', 'Wednesday', 'Thursday',
     'Friday', 'Saturday');
     today = weekdays[new Date().getDay()];
   document.getElementById("greeting-day-name").textContent=today;
 });
</script>

또 다른 접근 방식은 보기의 일부만 캐시하는 것입니다. 이 예에서 인사말은 페이지 상단에 있으므로 다음 항목만 캐시하는 것은 매우 간단합니다.

<div>
  Hi <%= @user.name %>,
  hope you're having a great
  <%= Date.today.strftime("%A") %>!
<div>

<% cache(@user) do %>
  ...
<% end %>

분명히 이것은 실제 세계에서 볼 수 있는 레이아웃으로 간단하지 않은 경우가 많으므로 캐싱을 적용하는 위치와 방법에 대해 신중해야 합니다.

경고 문구

뷰 캐싱을 성능 문제에 대한 빠르고 쉬운 만병통치약으로 보는 것은 쉽습니다. 실제로 Rails는 이를 믿을 수 없을 정도로 만듭니다. 뷰와 부분이 깊게 중첩된 경우에도 캐시하기 쉽습니다. 이 시리즈의 첫 번째 기사에서 시스템에 캐싱을 추가할 때 발생할 수 있는 문제를 설명했지만, 특히 보기 수준 캐싱의 경우에 그렇습니다.

그 이유는 뷰가 본질적으로 시스템의 기본 데이터와 더 많은 상호 작용을 하는 경향이 있기 때문입니다. Rails에서 메모이제이션이나 저수준 캐싱을 적용할 때 캐시된 값을 새로 고쳐야 하는 시기와 이유를 결정하기 위해 파일 외부를 볼 필요가 없는 경우가 많습니다. 반면에 보기에는 여러 다른 모델이 호출될 수 있으며 의도적인 계획 없이는 어느 모델이 보기의 어느 부분을 어느 시점에 다시 렌더링해야 하는지 확인하기 어려울 수 있습니다.

저수준 캐싱과 마찬가지로 가장 좋은 조언은 언제 어디서 사용하는지에 대해 전략적으로 정하는 것입니다. 허용 가능한 수준의 성능을 달성하려면 가능한 한 적은 위치에서 가능한 한 적은 캐싱을 사용하십시오.

Rails의 기본 캐싱

지금까지 이 캐싱 시리즈에서 수동으로 캐싱하는 방법을 다루었지만 수동 구성 없이도 ActiveRecord는 이미 쿼리 속도를 높이기 위해(또는 완전히 건너뛰기 위해) 내부에서 일부 캐싱을 수행합니다. 캐싱에 관한 이 시리즈의 다음 기사에서는 ActiveRecord가 우리를 위해 캐싱하는 것이 무엇인지, 그리고 적은 양의 작업으로 thing.children.size 최신 카운트를 얻기 위해 데이터베이스를 조회할 필요가 전혀 없습니다.