Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ทุกสิ่งที่คุณอยากรู้เกี่ยวกับการดูแคชใน Rails

การแคชเป็นคำทั่วไปที่หมายถึงการจัดเก็บผลลัพธ์ของโค้ดบางตัว เพื่อให้เราสามารถเรียกค้นคืนในภายหลังได้อย่างรวดเร็ว วิธีนี้ช่วยให้เราสามารถหลีกเลี่ยงการกดฐานข้อมูลซ้ำแล้วซ้ำอีกเพื่อรับข้อมูลที่ไม่ค่อยเปลี่ยนแปลง แม้ว่าแนวคิดทั่วไปจะเหมือนกันสำหรับการแคชทุกประเภท แต่ Rails ก็มีตัวช่วยที่แตกต่างกันไปตามสิ่งที่เราพยายามแคช

สำหรับนักพัฒนา Rails รูปแบบการแคชทั่วไปรวมถึงการจำ การแคชระดับต่ำ (ทั้งสองครอบคลุมในส่วนก่อนหน้าของชุดการแคชนี้) และดูการแคช ซึ่งเราจะกล่าวถึงที่นี่

วิธีที่ Ruby on Rails แสดงผล

ก่อนอื่น มาพูดถึงคำศัพท์ที่สับสนเล็กน้อยกันก่อน สิ่งที่ชุมชน Rails เรียกว่า "มุมมอง" คือไฟล์ที่อยู่ใน app/views . ของคุณ ไดเรกทอรี โดยทั่วไป สิ่งเหล่านี้คือ .html.erb ไฟล์ แม้ว่าจะมีตัวเลือกอื่นๆ (เช่น .html plain ธรรมดา) , .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>

ต้องรันโค้ด Ruby ในไฟล์เพื่อแสดงมุมมอง (สำหรับ 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 ใช้ในการตอบกลับเบราว์เซอร์คือ 68 มิลลิวินาที ซึ่งใช้เวลา 40 มิลลิวินาทีในการเรนเดอร์ erb ไฟล์และ 15.7ms ในการประมวลผลการสืบค้น ActiveRecord

แม้ว่าจะเป็นเพียงตัวอย่างเล็กๆ น้อยๆ แต่ก็แสดงให้เห็นด้วยว่าเหตุใดเราจึงอาจต้องการดูการแคชเลเยอร์การดู แม้ว่าเราจะสามารถทำให้การสืบค้น 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 ครั้ง จากตัวอย่างเล็กๆ น้อยๆ ของเรา เรื่องนี้ไม่ได้เป็นเรื่องที่น่ากังวลมากนัก ในเครื่องของฉัน บางส่วนใช้เวลาเพียง 15 มิลลิวินาทีในการแสดงดัชนีทั้งหมด แน่นอน ตัวอย่างในโลกแห่งความเป็นจริงอาจซับซ้อนกว่าและอาจรวมถึงบางส่วนภายในตัวอย่างเหล่านั้นด้วย เวลาในการแสดงผลจะเพิ่มขึ้นได้ไม่ยาก แม้ว่าจะใช้เวลาเพียง 1-2ms ในการแสดงผล _email . ของเรา บางส่วน จะใช้เวลา 100-200 มิลลิวินาทีในการรวบรวมทั้งหมด

โชคดีที่ Rails มีฟังก์ชันในตัวเพื่อช่วยให้เราเพิ่มการแคชเพื่อแก้ปัญหานี้ได้อย่างง่ายดาย ไม่ว่าเราจะต้องการแคชเฉพาะ __email บางส่วน ดัชนี หรือทั้งสองอย่าง

การดูแคชคืออะไร

ดูแคชใน Ruby on Rails ใช้ HTML ที่มุมมองสร้างและเก็บไว้ใช้ในภายหลัง แม้ว่า Rails จะรองรับการเขียนสิ่งเหล่านี้ไปยังระบบไฟล์หรือเก็บไว้ในหน่วยความจำ แต่สำหรับการใช้งานจริง คุณแทบจะต้องการเซิร์ฟเวอร์แคชแบบสแตนด์อโลน เช่น Memcached หรือ Redis memory_store ของ Rails มีประโยชน์สำหรับการพัฒนาแต่ไม่สามารถแชร์ข้ามกระบวนการได้ (เช่น หลายเซิร์ฟเวอร์/ไดโน หรือเซิร์ฟเวอร์การฟอร์คกิ้ง เช่น ยูนิคอร์น) ในทำนองเดียวกัน 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(@user) do %>
<div>
  Hi <%= @user.name %>!
</div>
...
<% sleep(5) #artificial delay %>
<% end %>

บทความก่อนหน้านี้ในซีรีส์นี้เกี่ยวกับการแคชระดับต่ำครอบคลุมรายละเอียดของคีย์แคช ดังนั้นฉันจะไม่พูดถึงเรื่องนี้อีกที่นี่ สำหรับตอนนี้ ก็พอจะรู้ว่าถ้าเราส่ง model ไปที่ cache() จะใช้ updated_at . ของโมเดลนั้น คุณลักษณะเพื่อสร้างคีย์เพื่อค้นหาในแคช กล่าวคือ เมื่อใดก็ตามที่ @user ได้รับการอัปเดต หน้าแคชนี้จะหมดอายุ และ Rails จะแสดง HTML อีกครั้ง

เราได้ดูแลกรณีที่ผู้ใช้เปลี่ยนชื่อ แต่โพสต์ของพวกเขาเป็นอย่างไร การเปลี่ยนโพสต์ที่มีอยู่หรือสร้างใหม่จะไม่เปลี่ยน updated_at เวลาประทับของ ผู้ใช้ ดังนั้น หน้าแคชของเราจะไม่หมดอายุ นอกจากนี้ หากผู้ใช้เปลี่ยนชื่อ เราจะแสดงผล ทั้งหมด . อีกครั้ง ของโพสต์ของพวกเขาแม้ว่าโพสต์ของพวกเขาจะไม่เปลี่ยนแปลง เพื่อแก้ปัญหาทั้งสองนี้ เราสามารถใช้ "การแคชตุ๊กตารัสเซีย" (เช่น แคชภายในแคช):

<% 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 ได้รับการอัปเดตแล้ว เราไม่ต้องแสดงโพสต์ซ้ำ เราสามารถใช้ค่าแคชได้ เรายังมีอีกปัญหาหนึ่งแม้ว่า หากเป็น โพสต์ มีการเปลี่ยนแปลง เรายังคงไม่แสดงการอัปเดตเนื่องจาก @user.update_at ไม่มีการเปลี่ยนแปลง ดังนั้นบล็อกภายใน cache(@user) ทำ จะไม่ดำเนินการ

ในการแก้ไขปัญหานี้ เราต้องเพิ่ม touch:true ไปที่ Post . ของเรา รุ่น:

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

โดยเพิ่ม สัมผัส:จริง ที่นี่ เรากำลังบอก ActiveRecord ว่า ทุกครั้ง โพสต์ได้รับการอัปเดตเราต้องการ updated_at เวลาประทับของผู้ใช้ "เป็นของ" ที่จะได้รับการอัปเดตด้วย

ฉันควรเพิ่มด้วยว่า Rails มีตัวช่วยเฉพาะสำหรับการแสดงผลคอลเล็กชันบางส่วน โดยพิจารณาจากความธรรมดา:

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

ซึ่งเทียบเท่ากับการใช้งานดังต่อไปนี้:

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

ไม่ใช่แค่ render บางส่วน:... cached:true ในรูปแบบที่ละเอียดน้อยกว่า และยังให้ประสิทธิภาพเพิ่มเติมแก่คุณด้วยเพราะ Rails สามารถออก multiget ให้กับที่เก็บแคชได้ (เช่น การอ่านคู่คีย์/ค่าจำนวนมากในการไปกลับครั้งเดียว) แทนที่จะกดไปที่ที่เก็บแคชของคุณสำหรับแต่ละรายการในคอลเล็กชัน

เนื้อหาของหน้าไดนามิก

เป็นเรื่องปกติที่หน้าบางหน้าจะรวมเนื้อหา 'ไดนามิก' จำนวนหนึ่งซึ่งเปลี่ยนแปลงในอัตราที่เร็วกว่าหน้าอื่นๆ ที่อยู่รอบๆ หน้านั้นมาก โดยเฉพาะอย่างยิ่งในโฮมเพจหรือแดชบอร์ด ซึ่งคุณอาจมีฟีดกิจกรรม/ข่าวสาร การรวมสิ่งเหล่านี้ไว้ในหน้าแคชของเราอาจทำให้แคชของเราต้องใช้งานไม่ได้บ่อยครั้ง ซึ่งจำกัดประโยชน์ที่เราได้รับจากการแคชตั้งแต่แรก

ยกตัวอย่างง่ายๆ ให้เพิ่มวันปัจจุบันในมุมมองของเรา:

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

  ...
<% end %>

เราอาจทำให้แคชใช้ไม่ได้ทุกวัน แต่นั่นก็ใช้ไม่ได้จริงด้วยเหตุผลที่ชัดเจน ทางเลือกหนึ่งคือการใช้ค่าตัวยึดตำแหน่ง (หรือแม้แต่ . ที่ว่างเปล่า ) และเติมด้วยจาวาสคริปต์ วิธีการประเภทนี้มักถูกเรียกว่า "javascript sprinkles" และเป็นแนวทางที่ Basecamp ชื่นชอบเป็นส่วนใหญ่ ซึ่งมีการพัฒนาโค้ดหลักของ Rails จำนวนมาก ผลลัพธ์จะเป็นดังนี้:

<% 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 ทำให้มัน เหลือเชื่อ ง่ายต่อการแคชมุมมองและบางส่วน แม้ว่าจะซ้อนกันอยู่ลึก ในบทความแรกในชุดนี้ ฉันได้กล่าวถึงปัญหาที่อาจเกิดขึ้นเมื่อคุณเพิ่มการแคชลงในระบบของคุณ แต่ฉันคิดว่านี่เป็นเรื่องจริงโดยเฉพาะอย่างยิ่งกับการแคชระดับการดู

เหตุผลก็คือว่าโดยธรรมชาติแล้ว มุมมองมักจะมีปฏิสัมพันธ์กับข้อมูลพื้นฐานของระบบมากกว่า เมื่อคุณใช้ memoization หรือ low-level caching ใน Rails คุณมักจะไม่ต้องมองออกไปนอกไฟล์ที่คุณอยู่เพื่อพิจารณาว่าควรรีเฟรชค่าที่แคชไว้เมื่อใดและเพราะเหตุใด ในทางกลับกัน มุมมองอาจมีการเรียกแบบจำลองที่แตกต่างกันหลายแบบ และหากไม่มีการวางแผนอย่างรอบคอบ ก็อาจเป็นเรื่องยากที่จะดูว่าแบบจำลองใดควรทำให้เกิดการแสดงผลส่วนใดของมุมมองใหม่ ณ เวลาใด

เช่นเดียวกับการแคชระดับต่ำ คำแนะนำที่ดีที่สุดคือการวางกลยุทธ์เกี่ยวกับตำแหน่งและเวลาที่คุณใช้ ใช้การแคชให้น้อยที่สุดเท่าที่จะทำได้ เพื่อให้ได้ประสิทธิภาพในระดับที่ยอมรับได้

การแคชของ Rails โดยค่าเริ่มต้น

จนถึงตอนนี้ในซีรีส์เรื่องการแคช เราได้กล่าวถึงวิธีการแคชสิ่งต่าง ๆ ด้วยตนเอง แต่ถึงแม้จะไม่มีการกำหนดค่าด้วยตนเอง ActiveRecord ก็ทำการแคชบางส่วนภายใต้ประทุนเพื่อเพิ่มความเร็วการสืบค้น (หรือข้ามไปทั้งหมด) ในบทความถัดไปของชุดข้อมูลแคชนี้ เราจะมาดูกันว่า ActiveRecord กำลังแคชอะไรสำหรับเรา และเราจะเก็บ "แคชตัวนับ" ไว้ได้อย่างไรด้วยการทำงานเพียงเล็กน้อย .size ไม่ต้องกดฐานข้อมูลเลยเพื่อรับข้อมูลล่าสุด