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