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

การแคช HTTP ในแอปพลิเคชัน Ruby on Rails

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

หากคุณต้องการทดลองด้วยตัวเอง ต่อไปนี้เป็นเคล็ดลับสองประการ:

  1. หากต้องการแสดงเฉพาะส่วนหัวของการตอบสนองที่มี curl (เช่น ไม่มีส่วนหัวของคำขอหรือเนื้อหาการตอบกลับ) ให้ใช้ -I ตัวเลือกเช่นเดียวกับใน curl -I localhost:3000 .
  2. โดยค่าเริ่มต้น 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 ให้ "มีดที่คม" แก่เราเพื่อกำหนดเป้าหมายแต่ละปัญหาเหล่านี้ และเราแค่ต้องรู้ว่าควรใช้เมื่อใด