วิธีทั่วไปในการอธิบาย caching คือการเก็บข้อมูลบางอย่างไว้ เพื่อที่เราจะดึงมันกลับมาได้อย่างรวดเร็วในภายหลัง บางครั้ง หมายถึงการเก็บข้อมูลที่คำนวณแล้ว เพื่อไม่ต้องคำนวณใหม่ แต่อาจหมายถึงการเก็บข้อมูลในท้องถิ่น (locally) เพื่อหลีกเลี่ยงการต้องดึงมันอีกครั้ง คอมพิวเตอร์ของคุณทำสิ่งนี้ตลอดเวลา เนื่องจากระบบปฏิบัติการพยายามเก็บข้อมูลที่เข้าถ่ายบ่อยใน RAM เพื่อไม่ให้ต้องดึงมันอีกครั้งจากฮาร์ดไดรฟ์หรือ SSD
เช่นเดียวกัน เบราว์เซอร์ของคุณพยายามนำทรัพยากรที่ดาวน์โหลดแล้วมาใช้ใหม่ คุณอาจเคยเห็นเองเมื่อไปเยี่ยมชมเว็บไซต์ใหม่ครั้งแรก การโหลดครั้งแรกใช้เวลานานกว่า เพราะเบราว์เซอร์ต้องดึง ทุกอย่าง ที่มันต้องการ รวมถึงรูปภาพทั้งหมด javascript และ stylesheets ข้อเท็จจริงที่น่าสนใจคือ เมื่อคุณดาวน์โหลดหน้า CNN homepage ใหม่ๆ เบราว์เซอร์ของคุณจะดึงข้อมูลมากกว่าเกม Doom ต้นฉบับในปี 1993 สำหรับผู้ที่สนใจ ณ เวลาเขียนบล็อกโพสต์นี้ CNN ดาวน์โหลดเพิ่งเกิน 3MB บนเครื่องของผม บีบอัดจาก ~15MB และนั่นกับ ad blocker เปิดอยู่ ในขณะที่ติดตั้ง Doom ต้นฉบับประมาณ ~2.2MB
เพื่อให้เบราว์เซอร์ cache ข้อมูลนี้ จำเป็นต้องมีการประสานงานกับเซิร์ฟเวอร์ เบราว์เซอร์ต้องรู้ว่าสิ่งใดสามารถ cache ได้ และนานแค่ไหน มิฉะนั้น มันอาจแสดงเนื้อหาเก่าเมื่อเซิร์ฟเวอร์มีเวอร์ชันใหม่พร้อมใช้งาน ในบทความนี้ เราจะดูว่าการประสานงาน caching ระหว่างไคลเอนต์-เซิร์ฟเวอร์ ถูกดำเนินการอย่างไร และ Rails ให้อะไรเพื่อปรับเปลี่ยนมัน
แม้โฟกัสจะอยู่ที่ Ruby on Rails จัดการอย่างไร กลไกจริงเป็นส่วนหนึ่งของ HTTP specifications กล่าวอีกนัยหนึ่ง caching ที่เราพูดถึงที่นี่ บูรณาญัตอยู่ในโครงสร้างพื้นฐานของอินเทอร์เน็ต ซึ่งทำให้มันเป็นหินมุมของการพัฒนาเว็บไซต์และเฟรมเวิร์กสมัยใหม่ เฟรมเวิร์กต่างๆ เช่น Rails, single-page applications (SPAs) และแม้แต่ static sites สามารถใช้กลไกเหล่านี้ทั้งหมดเพื่อช่วยปรับปรุงประสิทธิภาพ
HTTP Request-Response (คำขอ-ตอบสนอง HTTP)
คุณอาจคุ้นเคยกับ request-response lifecycle อย่างน้อยที่ระดับสูง: คุณคลิกลิงก์บนเว็บไซต์ เบราว์เซอร์ของคุณส่งคำขอไปที่เซิร์ฟเวอร์สำหรับเนื้อหานั้น และเซิร์ฟเวอร์ส่งเนื้อหานั้นกลับมา (สังเกตว่าผมข้ามความซับซ้อนมากไปโดยเจตนา)
มา खोจลึกลงไปนิดหน่อยในข้อมูลจริงที่ส่งในธุรกรรม back-and-forth นี้ แต่ละ HTTP message มี header และ body (ไม่สับสนกับแท็ก HTML <head> และ <body>) Request header บอกเซิร์ฟเวอร์ว่าคุณพยายามเข้าถึง path ไหน และใช้ HTTP method ไหน (เช่น GET/PUT/PATCH/POST) หากต้องการ คุณสามารถขุดลึกลงใน headers นี้ได้โดยใช้ developer tools ของเบราว์เซอร์ หรือเครื่องมือคำสั่งบรรทัด เช่น curl:
# curl -v honeybadger.io ... > GET / HTTP/1.1 > Host: honeybadger.io > User-Agent: curl/7.64.1 > Accept: */* ส่วนแรกของ output นี้คือ request header เรากำลังออก GET ไปที่ honeybadger.io ตามมาด้วยสิ่งที่เซิร์ฟเวอร์ส่งกลับ ( "response header" ):
> < 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 Response รวมถึง HTTP code (เช่น 200 สำหรับ success หรือ 404 สำหรับ not found) ในตัวอย่างนี้ เป็น permanent redirect (301) เพราะ curl พยายามติดต่อ URL http ซึ่ง redirect ไปที่ https ที่ปลอดภ Response header ยังรวม content type ซึ่งเป็น text/plain ที่นี่ แต่ตัวเลือกทั่วไปอื่นๆ คือ text/html, text/css, text/javascript, และ application/json
Response body ตามหลัง header ในกรณีของเรา body ว่างเปล่า เพราะ 301 redirect ไม่ต้องการ body หากเราลองอีกครั้งด้วย curl -v https://www.honeybadger.io คุณจะเห็นเนื้อหาหน้าแรกที่นี่ เหมือนกับการดู source ในเบราว์เซอร์
หากคุณต้องการทดลองด้วยตัวเอง นี่คือสองเคล็ดลับ:
- เพื่อแสดง response header เท่านั้นกับ curl (เช่น ไม่มี request headers หรือ response body) ใช้ option
-Iเช่นในcurl -I localhost:3000 - โดยค่าเริ่มต้น Rails ไม่ cache ในสภาพแวดล้อม development คุณอาจต้องรัน
rails dev:cacheก่อน
HTTP Header Cache-Control (The Cache-Control HTTP Header)
Header หลักที่เราสนใจ สำหรับ caching คือ header Cache-Control นี่ช่วยกำหนดว่าเครื่องไหนสามารถ cache response จากเซิร์ฟเวอร์ Rails ของเราได้ และข้อมูลที่ cache ไว้จะหมดอายุเมื่อไหร่ ภายใน header Cache-Control มีหลายฟิลด์ ส่วนใหญ่เป็น optional เราจะผ่านฟิลด์ที่เกี่ยวข้องที่สุดที่นี่ แต่สำหรับข้อมูลเพิ่มเติม คุณสามารถตรวจสอบ HTTP spec อย่างเป็นทางการที่ w3.org
นี่คือตัวอย่างจาก response header ของ Rails พื้นฐาน out-of-the-box:
< Content-Type: text/html; charset=utf-8 < Etag: W/"b41ce6c6d4bde17fd61a09e36b1e52ad" < Cache-Control: max-age=0, private, must-revalidate max-age
ฟิลด์ max-age เป็นจำนวนเต็มที่มีจำนวนวินาทีที่ response ถูกต้อง โดยค่าเริ่มต้น response ของ Rails สำหรับ view จะตั้งค่านี้เป็น 0 (คือ response หมดอายุทันที และเบราว์เซอร์ควรได้เวอร์ชันใหม่เสมอ)
public/private
การรวม public หรือ private ใน header ตั้งค่าว่าเซิร์ฟเวอร์ไหนได้รับอนุญาตให้ cache response หาก header รวม private มันจะถูก cache เฉพาะโดย requesting client (เช่น เบราว์เซอร์) ไม่ใช่เซิร์ฟเวอร์อื่นๆ ที่มันอาจผ่านไปเพื่อไปถึงที่นั่น เช่น content delivery networks (CDNs) หรือ proxies หาก header รวม public เซิร์ฟเวอร์กลางเหล่านี้จึงได้รับอนุญาตให้ cache response Rails ตั้งค่าแต่ละ header เป็น private โดยค่าเริ่มต้น
must-revalidate
Rails ยังตั้งค่าฟิลด์ must-revalidate โดยค่าเริ่มต้น นี่หมายความว่า client ต้องติดต่อเซิร์ฟเวอร์เพื่อยืนยันว่าเวอร์ชันที่ cache ไว้อยู่ยังถูกต้องก่อนที่จะใช้งาน เพื่อกำหนดว่า cached version ยังถูกต้องอยู่ client และเซิร์ฟเวอร์ใช้ ETags
ETags
ETags เป็น HTTP header เพิ่มเติมที่เพิ่มโดยเซิร์ฟเวอร์เมื่อมันส่ง response ไปที่ client ปกติแล้ว นี่คือ checksum บางอย่างบน response เอง เมื่อ client (คือ เบราว์เซอร์ของคุณ) ต้องการขอทรัพยากรนี้อีกครั้ง มันจะรวม Etag ที่มันได้รับ (สมมติว่ามันมี response ก่อนหน้า cached) ใน HTTP header If-None-Match เซิร์ฟเวอร์สามารถตอบกลับด้วย HTTP code 304 ("Not Modified") และ body ว่างเปล่า นี่หมายความว่าเวอร์ชันบนเซิร์ฟเวอร์ไม่ได้เปลี่ยนแปลง ดังนั้น client ควรใช้ cached version ของมัน
มี ETags สองประเภท: strong และ weak (weak tag แสดงด้วย prefix W/) พวกมันทำงานเหมือนกัน แต strong ETag หมายความว่าสองสำเนาของทรัพยากร (เวอร์ชันบนเซิร์ฟเวอร์และตัวใน local cache) เหมือนกัน 100% byte-for-byte Weak ETags อย่างไรก็ตาม บ่งชี้ว่าสองสำเนาอาจไม่เหมือนกัน byte-for-byte แต cached version ยังใช้ได้ ตัวอย่างทั่วไปคือ helper csrf_meta_tags ของ Rails ซึ่งสร้าง token ที่เปลี่ยนแปลงตลอดเวลา ดังนั้น แม้คุณจะมีหน้า static ในแอปของคุณ มันจะไม่ byte-for-byte identical เมื่อ refresh เนื่องจาก Cross-Site-Request-Forgery (CSRF) token Rails ใช้ weak ETags โดยค่าเริ่มต้น
ETags ใน Rails (ETags in Rails)
Rails จัดการ ETags อัตโนมัติบน views มันรวม ETag ใน outgoing headers และมี middleware เพื่อตรวจสอบ incoming ETag headers และคืนค่า 304 (Not Modified) codes เมื่อเหมาะสม สำคัญอย่างไรก็ตาม เนื่องจาก Rails สร้าง views แบบไดนามิก มันยังต้องทำ rendering work ทั้งหมดก่อนที่จะคำนวณ ETag สำหรับ view นั้นได้ นี่หมายถึง แม้ ETags จะตรงกัน คุณก็ประหยัดได้แค่เวลาที่ใช้ส่งข้อมูลข้ามเครือข่าย เทียบกับอย่าง view caching ที่คุณสามารถข้ามขั้นตอน rendering ได้สมบูรณ์ หากมี cached version อย่างไรก็ตาม Rails ให้วิธีสองสามอย่างในการปรับ ETag ที่สร้างขึ้น
stale?
วิธีหนึ่งในการเอาชนะ CSRF token ที่เปลี่ยนแปลงตลอดเวลาจากการเปลี่ยน ETag คือด้วย helper stale? ใน ActionController นี่อนุญาตให้คุณตั้งค่า ETag (ไม่ว่าจะ strong หรือ weak) โดยตรง อย่างไรก็ตาม คุณสามารถเพียงแค่ส่งผ่านออบเจ็กต์ เช่น ActiveRecord model และมันจะคำนวณ ETag จาก timestamp updated_at ของออบเจ็กต์ หรือใช้ updated_at สูงสุด หากคุณส่ง collection:
class UsersController < ApplicationController def index @users = User.includes(:posts).all render :index if stale?(@users) end end โดยการ hit หน้าด้วย 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 นี่ให้เราควบคุมมากขึ้นว่าลูกค้าต้องดาวน์โหลด full payload ใหม่เมื่อไหร่ แต่ยังคงต้องตรวจสอบกับเซิร์ฟเวอร์ ทุกครั้ง เพื่อกำหนดว่า cache ของมันยังถูกต้องอยู่ สิ่งจะเกิดขึ้นหากเราต้องการข้ามการตรวจสอบนั้นโดยสิ้นเชิง? นี่คือที่ที่ฟิลด์ max-age ใน header เข้ามา
expires_in และ http_cache_forever
Rails ให้เรา helper methods สองตัวใน 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 ได้โดยการส่ง named argument public: true
http_cache_forever เป็นเพียง wrapper รอบ expires_in ที่ตั้ง max-age เป็น 100 ปี และรับ block:
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 การ cache แบบยาวนานอย่างสุดโหดนี้คือเหตุผลที่ Rails assets มี "fingerprint" ต่อท้าย ซึ่งเป็นแฮชของเนื้อหาไฟล์ และสร้าง filenames อย่าง packs/js/application-4028feaf5babc1c1617b.js "Fingerprint" ที่ท้ายอย่างมีประสิทธิภาพเชื่อมเนื้อหาของไฟล์กับชื่อไฟล์ หากเนื้อหาเปลี่ยนแปลง ชื่อไฟล์จะเปลี่ยน นี่หมายความว่าเบราว์เซอร์สามารถ cache ไฟล์นี้ ตลอดกาล ได้อย่างปลอดภ เพราะหากมันเปลี่ยนแปลงแม้แต่น้อย นิ้วมือ (fingerprint) จะเปลี่ยน ในที่ที่เบราว์เซอร์เกี่ยวข้อง มันเป็นไฟล์ที่แยกจากกันโดยสิ้นเชิงที่ต้องดาวน์โหลด
Spheres of Influence (ลัทธิอำนาจ)
ตอนนี้เราครอบคลุมตัวเลือก caching บางอย่าง คำแนะนำของผมอาจดูแปลกนิดหน่อย แต่ขอแนะนำว่า พยายามหลีกเลี่ยงการใช้เมธอดใดๆ ในบทความนี้! ETags และ HTTP caching เหมาะที่จะรู้จัก และ Rails ให้เครื่องมือเฉพาะสำหรับแก้ปัญหาเฉพาะ ข้อขัดแย้ง อย่างไรก็ตาม และมันใหญ่โต คือ caching ทั้งหมดนี้เกิดขึ้นภายนอกแอปพลิเคชันของคุณ และดังนั้นส่วนใหญ่อยู่นอกเหนือจากการควบคุมของคุณ หากคุณใช้ view caching หรือ low-level caching ใน Rails ตามที่ครอบคลุมในส่วนก่อนหน้าของซีรีส์นี้ และพบปัญหา invalidation คุณมีตัวเลือก คุณสามารถ touch models, push updated code หรือแม้แต่ reach เข้า Rails.cache โดยตรงจากคอนโซล หากคุณต้อง แต่ไม่ได้กับ HTTP caching ส่วนตัว ผมชอจะต้องรัน Rails.cache.clear ใน production มากกว่าการเผชิญปัญหาที่ไซต์แตกสำหรับผู้ใช้จนกว่าพวกเขาจะ clear browser cache (ทีม customer service ของคุณจะรักคุณสำหรับมันด้วย)
สรุป (Conclusion)
นี่คือจุดจบของซีรีส์ caching ใน Rails หวังว่ามันมีประโยชน์และให้ข้อมูล คำแนะนำของผมสำหรับ caching ยังคงเป็น: ทำน้อยที่สุดเท่าที่คุณทำได้ แต่มากที่สุดเท่าที่คุณต้อง หากคุณประสบปัญหาประสิทธิภาพ เริ่มด้วยการมองหาเมธอดที่ถูก hit บ่อย อาจจะสามารถ memoized ได้ ต้องการให้ค่านั้นคงอยู่ข้าม requests? อาจจะเมธอดที่ใช้บ่อยนั้นสามารถใช้ low-level caching ได้ หรือ อาจจะไม่ใช่เมธอดเฉพาะเจาะจง ใคร่แค่การบดบั้งผ่าน partials ซ้อนกันทั้งหมด ซึ่งทำให้ช้า ในกรณีนี้ อาจจะ view level caching ช่วยได้ Rails ให้เรา "มีดคม" เพื่อจัดเป้าปัญหาแต่ละอย่าง และเราแค่ต้องรู้เมื่อไหร่ควรใช้พวกมัน