วิธีทั่วไปในการอธิบายการแคชคือการจัดเก็บข้อมูลบางอย่างเพื่อให้เราสามารถเรียกค้นข้อมูลในภายหลังได้อย่างรวดเร็ว บางครั้ง นี่หมายถึงการจัดเก็บข้อมูลที่คำนวณแล้วเพื่อไม่ให้ต้องคำนวณใหม่ แต่ยังสามารถอ้างอิงถึงการจัดเก็บข้อมูลในเครื่องเพื่อหลีกเลี่ยงการดึงข้อมูลอีกครั้ง คอมพิวเตอร์ของคุณทำเช่นนี้อย่างต่อเนื่อง เนื่องจากระบบปฏิบัติการของคุณพยายามเก็บข้อมูลที่เข้าถึงบ่อยใน RAM เพื่อไม่ให้ต้องดึงข้อมูลอีกครั้งจากฮาร์ดไดรฟ์หรือ SSD
ในทำนองเดียวกัน เบราว์เซอร์ของคุณพยายามใช้ทรัพยากรที่ดาวน์โหลดไปแล้วกลับมาใช้ซ้ำ คุณอาจเคยเห็นสิ่งนี้ด้วยตัวเองเมื่อเข้าชมเว็บไซต์ใหม่เป็นครั้งแรก การโหลดครั้งแรกใช้เวลานานขึ้นเนื่องจากเบราว์เซอร์ของคุณต้องดึง ทุกอย่าง มันต้องการ รวมทั้งรูปภาพ จาวาสคริปต์ และสไตล์ชีตทั้งหมด ข้อเท็จจริงที่น่าสนุกคือเมื่อคุณดาวน์โหลดหน้าแรกของ CNN ใหม่ เบราว์เซอร์ของคุณจะดึงข้อมูลมากกว่าเกม Doom ดั้งเดิมประมาณปี 1993 สำหรับคนที่อยากรู้อยากเห็น ในขณะที่เขียนบล็อกโพสต์นี้ CNN ดาวน์โหลดมากกว่า 3MB บนเครื่องของฉัน โดยบีบอัดจาก ~15MB และเปิดใช้งานตัวบล็อกโฆษณา ในขณะที่ตัวติดตั้ง Doom ดั้งเดิมคือ ~2.2MB
เพื่อให้เบราว์เซอร์แคชข้อมูลนี้ จำเป็นต้องมีการประสานงานกับเซิร์ฟเวอร์ เบราว์เซอร์จำเป็นต้องรู้ว่าสามารถแคชอะไรได้บ้างและนานแค่ไหน ไม่เช่นนั้น ระบบอาจแสดงเนื้อหาเก่าแก่คุณเมื่อเซิร์ฟเวอร์มีเวอร์ชันที่ใหม่กว่า ในบทความนี้ เราจะมาดูกันว่าการประสานงานแคชระหว่างไคลเอ็นต์กับเซิร์ฟเวอร์นี้ดำเนินการอย่างไร และสิ่งที่ Rails มีให้เพื่อแก้ไข
แม้ว่าการมุ่งเน้นจะอยู่ที่วิธีที่ Ruby on Rails จัดการกับสิ่งนี้ แต่กลไกที่แท้จริงนั้นเป็นส่วนหนึ่งของข้อกำหนด HTTP กล่าวอีกนัยหนึ่ง การแคชที่เรากำลังพูดถึงในที่นี้ถูกรวมเข้ากับโครงสร้างพื้นฐานของอินเทอร์เน็ต ซึ่งทำให้เป็นรากฐานที่สำคัญของการพัฒนาเว็บไซต์และเฟรมเวิร์กที่ทันสมัย กรอบงานต่างๆ เช่น Rails แอปพลิเคชันหน้าเดียว (SPA) และแม้แต่ไซต์แบบคงที่ ล้วนใช้กลไกเหล่านี้เพื่อช่วยปรับปรุงประสิทธิภาพได้
การตอบกลับคำขอ HTTP
คุณอาจคุ้นเคยกับวงจรการตอบกลับคำขอ อย่างน้อยก็ในระดับสูง:คุณคลิกลิงก์บนเว็บไซต์ เบราว์เซอร์ของคุณส่งคำขอไปยังเซิร์ฟเวอร์สำหรับเนื้อหานั้น และเซิร์ฟเวอร์ส่งเนื้อหานั้นกลับ (โปรดทราบว่าฉัน ฉันตั้งใจกลบความซับซ้อนมากมายที่นี่)
มาเจาะลึกข้อมูลจริงที่ส่งในธุรกรรมไปมานี้กัน แต่ละข้อความ HTTP มีส่วนหัวและเนื้อหา (เพื่อไม่ให้สับสนกับ <head>
และ <body>
แท็ก HTML) ส่วนหัวของคำขอจะบอกเซิร์ฟเวอร์ว่าคุณกำลังพยายามเข้าถึงเส้นทางใดและใช้วิธี HTTP ใด (เช่น GET/PUT/PATCH/POST) หากจำเป็น คุณสามารถเจาะลึกเข้าไปในส่วนหัวเหล่านี้ได้โดยใช้เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ของเบราว์เซอร์หรือเครื่องมือบรรทัดคำสั่ง เช่น curl
:
# curl -v honeybadger.io
...
> GET / HTTP/1.1
> Host: honeybadger.io
> User-Agent: curl/7.64.1
> Accept: */*
ส่วนแรกของผลลัพธ์นี้เป็นส่วนหัวของคำขอ เรากำลังออก GET
ไปยัง honeybadger.io
. ตามด้วยสิ่งที่เซิร์ฟเวอร์ส่งกลับ ("ส่วนหัวการตอบสนอง"):
>
< HTTP/1.1 301 Moved Permanently
< Cache-Control: public, max-age=0, must-revalidate
< Content-Length: 39
< Content-Security-Policy: frame-ancestors 'none'
...
< Content-Type: text/plain
การตอบสนองมีรหัส HTTP (เช่น 200
เพื่อความสำเร็จ หรือ 404
เพราะหาไม่เจอ) ในตัวอย่างนี้ เป็นการเปลี่ยนเส้นทางถาวร (301) เนื่องจาก curl พยายามติดต่อ http
URL ซึ่งเปลี่ยนเส้นทางไปยัง https
. ที่ปลอดภัย URL ส่วนหัวของการตอบสนองยังมีประเภทเนื้อหาด้วย ซึ่งก็คือ text/plain
ที่นี่ แต่ตัวเลือกทั่วไปอื่นๆ อีกสองสามตัวคือ text/html
, text/css
, text/javascript
และ application/json
.
เนื้อหาการตอบสนองตามส่วนหัว ในกรณีของเรา เนื้อความว่างเปล่าเพราะ 301
การเปลี่ยนเส้นทางไม่จำเป็นต้องมีเนื้อหา หากเราลองอีกครั้งด้วย curl -v https://www.honeybadger.io
คุณจะเห็นเนื้อหาของหน้าแรกที่นี่ เหมือนกับว่าคุณกำลังดูแหล่งที่มาในเบราว์เซอร์
หากคุณต้องการทดลองด้วยตัวเอง ต่อไปนี้เป็นเคล็ดลับสองประการ:
- หากต้องการแสดงเฉพาะส่วนหัวของการตอบสนองที่มี curl (เช่น ไม่มีส่วนหัวของคำขอหรือเนื้อหาการตอบกลับ) ให้ใช้
-I
ตัวเลือกเช่นเดียวกับในcurl -I localhost:3000
. - โดยค่าเริ่มต้น Rails จะไม่แคชในสภาพแวดล้อมการพัฒนา คุณอาจต้องเรียกใช้
rails dev:cache
ก่อน.
ส่วนหัว HTTP ของการควบคุมแคช
ส่วนหัวหลักที่เราสนใจเกี่ยวกับแคชคือ Cache-Control
หัวข้อ. ซึ่งจะช่วยกำหนดว่าเครื่องใดสามารถแคชการตอบกลับจากเซิร์ฟเวอร์ Rails ของเรา และเมื่อใดข้อมูลที่แคชนั้นจะหมดอายุ ภายใน Cache-Control
ส่วนหัว มีหลายฟิลด์ ซึ่งส่วนใหญ่ไม่บังคับ เราจะพูดถึงรายการที่เกี่ยวข้องมากที่สุดบางส่วนที่นี่ แต่สำหรับข้อมูลเพิ่มเติม คุณสามารถตรวจสอบข้อกำหนด HTTP อย่างเป็นทางการได้ที่ w3.org
นี่คือตัวอย่างจากส่วนหัวการตอบสนอง Rails แบบพื้นฐานที่พร้อมใช้งานทันที:
< Content-Type: text/html; charset=utf-8
< Etag: W/"b41ce6c6d4bde17fd61a09e36b1e52ad"
< Cache-Control: max-age=0, private, must-revalidate
อายุสูงสุด
max-age
field คือจำนวนเต็มที่มีจำนวนวินาทีที่การตอบสนองถูกต้อง ตามค่าเริ่มต้น การตอบสนองของ Rails สำหรับมุมมองจะมีการตั้งค่านี้เป็น 0 (เช่น การตอบกลับจะหมดอายุทันที และเบราว์เซอร์ควรได้รับเวอร์ชันใหม่เสมอ)
สาธารณะ/ส่วนตัว
รวมถึง public
หรือ private
ในชุดส่วนหัวซึ่งเซิร์ฟเวอร์ได้รับอนุญาตให้แคชการตอบสนอง หากส่วนหัวมี private
เป็นเพียงเพื่อแคชโดยไคลเอนต์ที่ร้องขอเท่านั้น (เช่น เบราว์เซอร์) ไม่ใช่เซิร์ฟเวอร์อื่นใดที่อาจส่งผ่านเพื่อไปยังที่นั่น เช่น เครือข่ายการจัดส่งเนื้อหา (CDN) หรือพร็อกซี หากส่วนหัวมี public
จากนั้นเซิร์ฟเวอร์ตัวกลางเหล่านี้จะได้รับอนุญาตให้แคชการตอบกลับ Rails ตั้งค่าส่วนหัวแต่ละส่วนเป็น private
โดยค่าเริ่มต้น
ต้องตรวจสอบใหม่
Rails ยังตั้งค่า must-revalidate
ฟิลด์โดยค่าเริ่มต้น ซึ่งหมายความว่าไคลเอ็นต์ต้องติดต่อเซิร์ฟเวอร์เพื่อยืนยันว่าเวอร์ชันแคชยังใช้งานได้ก่อนที่จะใช้งาน เพื่อตรวจสอบว่าเวอร์ชันแคชถูกต้องหรือไม่ ไคลเอ็นต์และเซิร์ฟเวอร์ใช้ ETags
ETags
ETags เป็นส่วนหัว HTTP เสริมที่เพิ่มโดยเซิร์ฟเวอร์เมื่อส่งการตอบกลับไปยังไคลเอนต์ โดยทั่วไป นี่คือผลรวมการตรวจสอบบางอย่างในการตอบสนอง เมื่อไคลเอนต์ (เช่น เบราว์เซอร์ของคุณ) ต้องการขอทรัพยากรนี้อีกครั้ง มันจะรวม Etag ที่ได้รับ (สมมติว่ามีการแคชการตอบสนองก่อนหน้านี้) ใน If-None-Match
ส่วนหัว HTTP เซิร์ฟเวอร์สามารถตอบกลับด้วย 304
รหัส HTTP ("ไม่ได้แก้ไข") และเนื้อหาที่ว่างเปล่า ซึ่งหมายความว่าเวอร์ชันบนเซิร์ฟเวอร์ไม่มีการเปลี่ยนแปลง ดังนั้นไคลเอ็นต์ควรใช้เวอร์ชันที่แคชไว้
ETags มีสองประเภท:แข็งแกร่งและอ่อนแอ (แท็กอ่อนแอแสดงด้วย W/
คำนำหน้า) พวกเขาทำงานในลักษณะเดียวกัน แต่ ETag ที่แข็งแกร่งหมายความว่าสำเนาของทรัพยากรสองชุด (เวอร์ชันบนเซิร์ฟเวอร์และหนึ่งในแคชในเครื่อง) มีความเหมือนกัน 100% แบบไบต์ต่อไบต์ อย่างไรก็ตาม ETags ที่อ่อนแอระบุว่าสำเนาทั้งสองชุดอาจไม่เหมือนกันแบบไบต์ต่อไบต์ แต่เวอร์ชันแคชยังคงใช้งานได้ ตัวอย่างทั่วไปของสิ่งนี้คือ csrf_meta_tags
. ของ Rails ผู้ช่วยซึ่งสร้างโทเค็นที่เปลี่ยนแปลงตลอดเวลา ดังนั้น แม้ว่าคุณจะมีหน้าสแตติกในแอปพลิเคชันของคุณ หน้านั้นจะไม่เหมือนกันแบบไบต์ต่อไบต์เมื่อมีการรีเฟรชเนื่องจากโทเค็น Cross-Site-Request-Forgery (CSRF) Rails ใช้ ETags ที่อ่อนแอโดยค่าเริ่มต้น
ETags ใน Rails
Rails จัดการ ETags โดยอัตโนมัติในมุมมอง รวม ETag ในส่วนหัวขาออกและมีมิดเดิลแวร์เพื่อตรวจสอบส่วนหัว ETag ขาเข้าและส่งคืน 304
(ไม่ดัดแปลง) รหัสตามความเหมาะสม อย่างไรก็ตาม น่าสังเกต เนื่องจาก Rails สร้างมุมมองแบบไดนามิก มันยังคงต้องทำงานการเรนเดอร์ทั้งหมดก่อนที่จะสามารถหา ETag สำหรับมุมมองนั้นได้ ซึ่งหมายความว่าแม้ว่า ETags จะตรงกัน คุณจะประหยัดเวลาและแบนด์วิดท์ที่ใช้ในการส่งข้อมูลผ่านเครือข่ายเท่านั้น เมื่อเทียบกับการแคชมุมมอง ซึ่งคุณสามารถข้ามขั้นตอนการแสดงผลได้อย่างสมบูรณ์หากมีเวอร์ชันที่แคชไว้ อย่างไรก็ตาม Rails มีวิธีการสองสามวิธีในการปรับแต่ง ETag ที่สร้างขึ้น
เก่าหรือไม่
วิธีหนึ่งในการเอาชนะโทเค็น CSRF ที่เปลี่ยนแปลงตลอดเวลาจากการเปลี่ยน ETag คือการใช้ stale?
ตัวช่วยใน ActionController
. วิธีนี้ทำให้คุณสามารถตั้งค่า ETag (ไม่ว่าจะแข็งแกร่งหรืออ่อน) ได้โดยตรง อย่างไรก็ตาม คุณยังสามารถส่งผ่านวัตถุ เช่น โมเดล ActiveRecord และมันจะคำนวณ ETag ตาม updated_at
ของวัตถุ ประทับเวลาหรือใช้สูงสุด updated_at
หากคุณผ่านคอลเล็กชัน:
class UsersController < ApplicationController
def index
@users = User.includes(:posts).all
render :index if stale?(@users)
end
end
โดยการกดที่หน้าด้วย curl เราจะเห็นผลลัพธ์:
# curl -I localhost:3000 -- first page load
ETag: W/"af9ae8f2d66b9b6c4d0513f185638f1a"
# curl -I localhost:3000 -- reload (change due to CSRF token)
ETag: W/"f06158417f290334f47ea2124e08d89d"
-- Add stale? to controller code
# curl -I localhost:3000 -- reload
ETag: W/"04b9b99835c359f36551720d8e3ca6fe" -- now using `@users` to generate ETag
# curl -I localhost:3000 -- reload
ETag: W/"04b9b99835c359f36551720d8e3ca6fe" -- no change
สิ่งนี้ทำให้เราควบคุมได้มากขึ้นเมื่อไคลเอนต์ต้องดาวน์โหลดเพย์โหลดแบบเต็มอีกครั้ง แต่ก็ยังต้องตรวจสอบกับเซิร์ฟเวอร์ ทุกครั้ง เพื่อตรวจสอบว่าแคชยังใช้ได้อยู่หรือไม่ เกิดอะไรขึ้นถ้าเราต้องการข้ามการตรวจสอบนั้นทั้งหมด? นี่คือที่ที่ max-age
ฟิลด์ในส่วนหัวเข้ามา
expires_in และ http_cache_forever
Rails มีวิธีช่วยเหลือสองสามวิธีใน ActionController
เพื่อปรับ max-age
ฟิลด์:expires_in
และ http_cache_forever
. พวกเขาทั้งคู่ทำงานอย่างที่คุณคาดหวังตามชื่อของพวกเขา:
class UsersController < ApplicationController
def index
@users = User.includes(:posts).all
expires_in 10.minutes
end
end
# curl -I localhost:3000
Cache-Control: max-age=600, private
Rails ได้ตั้งค่า max-age
เป็น 600 (10 นาทีในไม่กี่วินาที) และลบ must-revalidate
สนาม. คุณยังสามารถเปลี่ยน private
โดยส่ง public: true
ชื่ออาร์กิวเมนต์
http_cache_forever
ส่วนใหญ่เป็นเพียงเสื้อคลุมรอบ ๆ expires_in
ที่กำหนด max-age
ถึง 100 ปีและใช้เวลาบล็อก:
class UsersController < ApplicationController
def index
@users = User.includes(:posts).all
http_cache_forever(public: true) do
render :index
end
end
end
# curl -I localhost:3000
Cache-Control: max-age=3155695200, public
การแคชระยะยาวอย่างยิ่งประเภทนี้เป็นสาเหตุที่ทำให้เนื้อหา Rails มี "ลายนิ้วมือ" ต่อท้าย ซึ่งเป็นแฮชของเนื้อหาของไฟล์และสร้างชื่อไฟล์ เช่น packs/js/application-4028feaf5babc1c1617b.js
. "ลายนิ้วมือ" ในตอนท้ายจะเชื่อมโยงเนื้อหาของไฟล์กับชื่อไฟล์ได้อย่างมีประสิทธิภาพ หากเนื้อหามีการเปลี่ยนแปลง ชื่อไฟล์จะเปลี่ยนไป ซึ่งหมายความว่าเบราว์เซอร์สามารถแคชไฟล์นี้ได้อย่างปลอดภัย ตลอดไป เพราะถ้ามันเปลี่ยนไปแม้เพียงเล็กน้อยลายนิ้วมือก็จะเปลี่ยนไป เท่าที่เกี่ยวข้องกับเบราว์เซอร์ มันเป็นไฟล์ที่แยกจากกันโดยสิ้นเชิงซึ่งจำเป็นต้องดาวน์โหลด
ทรงกลมแห่งอิทธิพล
ตอนนี้เราได้ครอบคลุมตัวเลือกการแคชแล้ว คำแนะนำของฉันอาจดูแปลกไปบ้าง แต่ฉันแนะนำให้คุณ พยายามหลีกเลี่ยงการใช้วิธีการใดๆ ในบทความนี้! การแคช ETags และ HTTP เป็นข้อมูลที่น่ารู้ และ Rails มีเครื่องมือเฉพาะสำหรับแก้ไขปัญหาเฉพาะ ถึงแม้ว่าข้อแม้และเป็นเรื่องใหญ่คือการที่แคชทั้งหมดนี้เกิดขึ้นนอกแอปพลิเคชันของคุณและส่วนใหญ่อยู่นอกเหนือการควบคุมของคุณ หากคุณกำลังใช้การแคชมุมมองหรือการแคชระดับต่ำใน Rails ตามที่อธิบายไว้ในส่วนก่อนหน้าของซีรีส์นี้ และพบปัญหาการทำให้ใช้งานไม่ได้ คุณมีตัวเลือก คุณสามารถ touch
โมเดล พุชโค้ดที่อัปเดต หรือแม้แต่เข้าถึง Rails.cache
โดยตรงจากคอนโซลหากคุณต้องการ แต่ไม่ใช่ด้วยการแคช HTTP โดยส่วนตัวแล้ว ฉันค่อนข้างจะต้องเรียกใช้ Rails.cache.clear
ในการผลิตมากกว่าประสบปัญหาที่ไซต์ใช้งานไม่ได้สำหรับผู้ใช้จนกว่าพวกเขาจะล้างแคชของเบราว์เซอร์ (ทีมบริการลูกค้าของคุณจะรักคุณเช่นกัน)
บทสรุป
นี่คือจุดสิ้นสุดของซีรีส์เรื่องแคชใน Rails; หวังว่าจะเป็นประโยชน์และให้ข้อมูล คำแนะนำของฉันสำหรับการแคชยังคงเป็นดังนี้:ทำน้อยที่สุดเท่าที่คุณจะทำได้ แต่มากที่สุดเท่าที่คุณต้องทำ หากคุณประสบปัญหาด้านประสิทธิภาพ ให้เริ่มด้วยการมองหาวิธีการที่โดนบ่อย บางทีพวกเขาสามารถจดจำได้ ต้องการค่าที่จะคงอยู่ในคำขอต่างๆ หรือไม่ บางทีวิธีที่ใช้กันมากอาจใช้การแคชระดับต่ำ หรืออาจไม่ใช่วิธีการเฉพาะใดๆ มันเป็นเพียงการกระทืบผ่านบางส่วนที่ซ้อนกันเหล่านั้นซึ่งทำให้สิ่งต่าง ๆ ช้าลง ในกรณีนี้ การแคชระดับการดูอาจช่วยได้ Rails ให้ "มีดที่คม" แก่เราเพื่อกำหนดเป้าหมายแต่ละปัญหาเหล่านี้ และเราแค่ต้องรู้ว่าควรใช้เมื่อใด