การตรวจสอบแคช Rails ระดับฟิลด์: โซลูชัน DSL
เผยแพร่แล้ว: 2022-03-11ในการพัฒนาเว็บสมัยใหม่ การแคชเป็นวิธีที่รวดเร็วและมีประสิทธิภาพในการเร่งความเร็วของสิ่งต่างๆ เมื่อทำถูกต้องแล้ว การแคชจะช่วยปรับปรุงประสิทธิภาพโดยรวมของแอปพลิเคชันได้อย่างมาก เมื่อทำผิด ย่อมจบลงด้วยความพินาศอย่างแน่นอน
การทำให้แคชใช้งานไม่ได้อย่างที่คุณทราบ เป็นหนึ่งในสามปัญหาที่ยากที่สุดในวิทยาการคอมพิวเตอร์—อีกสองปัญหาคือการตั้งชื่อสิ่งต่าง ๆ และข้อผิดพลาดแบบแยกส่วน วิธีง่ายๆ วิธีหนึ่งคือการทำให้ทุกอย่างเป็นโมฆะ ด้านซ้ายและขวา เมื่อมีการเปลี่ยนแปลง แต่นั่นขัดต่อจุดประสงค์ของการแคช คุณต้องการทำให้แคชใช้ไม่ได้เมื่อจำเป็นเท่านั้น
หากคุณต้องการใช้ประโยชน์สูงสุดจากการแคช คุณต้องมีความเฉพาะเจาะจงมากเกี่ยวกับสิ่งที่คุณทำให้ใช้งานไม่ได้ และบันทึกแอปพลิเคชันของคุณจากการสิ้นเปลืองทรัพยากรอันมีค่าไปกับการทำงานซ้ำๆ
ในบล็อกโพสต์นี้ คุณจะได้เรียนรู้เทคนิคในการควบคุมการทำงานของแคช Rails ได้ดีขึ้น: โดยเฉพาะการใช้การทำให้แคชในระดับฟิลด์ใช้งานไม่ได้ เทคนิคนี้อาศัย Rails ActiveRecord และ ActiveSupport::Concern
ตลอดจนการจัดการลักษณะการทำงานของวิธีการ touch
โพสต์บล็อกนี้อิงจากประสบการณ์ล่าสุดของฉันในโครงการที่เราเห็นว่าประสิทธิภาพดีขึ้นอย่างมากหลังจากใช้การทำให้แคชระดับฟิลด์ใช้งานไม่ได้ ช่วยลดการใช้แคชที่ไม่จำเป็นและการเรนเดอร์เทมเพลตซ้ำๆ
ราง ทับทิม และประสิทธิภาพ
Ruby ไม่ใช่ภาษาที่เร็วที่สุด แต่โดยรวมแล้ว เป็นตัวเลือกที่เหมาะสมซึ่งคำนึงถึงความเร็วในการพัฒนา นอกจากนี้ ความสามารถด้าน metaprogramming และภาษาเฉพาะโดเมน (DSL) ในตัวทำให้นักพัฒนามีความยืดหยุ่นอย่างมาก
มีการศึกษาเช่นการศึกษาของ Jakob Nielsen ที่แสดงให้เราเห็นว่าถ้างานใช้เวลามากกว่า 10 วินาที เราจะเสียสมาธิ และการกลับมาโฟกัสของเราอีกครั้งต้องใช้เวลา ดังนั้นจึงอาจมีค่าใช้จ่ายสูงอย่างไม่คาดคิด
น่าเสียดายที่ Ruby on Rails สร้างเทมเพลตเกินขีดจำกัด 10 วินาทีนั้นง่ายมาก คุณจะไม่เห็นสิ่งนั้นเกิดขึ้นในแอพ "hello world" หรือโปรเจ็กต์สัตว์เลี้ยงขนาดเล็ก แต่ในโครงการในโลกแห่งความเป็นจริงที่มีการโหลดสิ่งต่างๆ มากมายลงในหน้าเดียว เชื่อฉัน การสร้างเทมเพลตสามารถเริ่มลากได้ง่ายมาก
และนั่นคือสิ่งที่ฉันต้องแก้ไขในโครงการของฉัน
การเพิ่มประสิทธิภาพอย่างง่าย
แต่คุณจะเร่งความเร็วของสิ่งต่าง ๆ ได้อย่างไร?
คำตอบ: เกณฑ์มาตรฐานและเพิ่มประสิทธิภาพ
ในโครงการของฉัน สองขั้นตอนที่มีประสิทธิภาพมากในการเพิ่มประสิทธิภาพคือ:
- ขจัดข้อความค้นหา N+1
- แนะนำเทคนิคการแคชที่ดีสำหรับเทมเพลต
แบบสอบถาม N+1
การแก้ไขข้อความค้นหา N+1 นั้นง่ายมาก สิ่งที่คุณสามารถทำได้คือตรวจสอบไฟล์บันทึกของคุณ เมื่อใดก็ตามที่คุณเห็นการสืบค้น SQL หลายรายการเช่นด้านล่างในบันทึกของคุณ ให้กำจัดไฟล์เหล่านี้โดยแทนที่ด้วยการโหลดอย่างรวดเร็ว:
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
มีอัญมณีสำหรับสิ่งนี้ซึ่งเรียกว่ากระสุนเพื่อช่วยตรวจจับความไร้ประสิทธิภาพนี้ คุณยังดูกรณีการใช้งานแต่ละกรณีได้ และในระหว่างนี้ ให้ตรวจสอบบันทึกโดยตรวจสอบตามรูปแบบข้างต้น ด้วยการกำจัดความไร้ประสิทธิภาพของ N+1 ทั้งหมด คุณจะมั่นใจได้มากพอว่าจะไม่โอเวอร์โหลดฐานข้อมูลของคุณ และเวลาที่คุณใช้กับ ActiveRecord จะลดลงอย่างมาก
หลังจากทำการเปลี่ยนแปลงเหล่านี้ โปรเจ็กต์ของฉันก็ดำเนินไปอย่างรวดเร็วมากขึ้นแล้ว แต่ฉันตัดสินใจที่จะยกระดับขึ้นไปอีกระดับและดูว่าฉันจะลดเวลาในการโหลดลงได้อีกหรือไม่ ยังมีการแสดงผลที่ไม่จำเป็นเกิดขึ้นในเทมเพลตอยู่บ้าง และท้ายที่สุด นั่นคือสิ่งที่การแคชส่วนย่อยช่วยได้
Fragment Caching
โดยทั่วไปการแคช Fragment จะช่วยลดเวลาในการสร้างเทมเพลตได้อย่างมาก แต่พฤติกรรมแคชของ Rails ที่เป็นค่าเริ่มต้นนั้นไม่ได้ตัดสำหรับโปรเจ็กต์ของฉัน
แนวคิดเบื้องหลังการแคชแฟรกเมนต์ Rails นั้นยอดเยี่ยม มีกลไกการแคชที่ง่ายและมีประสิทธิภาพ
ผู้เขียน Ruby On Rails ได้เขียนบทความที่ดีมากใน Signal v. Noise เกี่ยวกับวิธีการทำงานของ Fragment caching
สมมติว่าคุณมีส่วนต่อประสานผู้ใช้เล็กน้อยซึ่งแสดงบางฟิลด์ของเอนทิตี
- ในการโหลดหน้าเว็บ Rails จะคำนวณ
cache_key
ตามคลาสของเอนทิตีและช่องupdated_at
- การใช้
cache_key
นั้นจะตรวจสอบเพื่อดูว่ามีอะไรในแคชที่เกี่ยวข้องกับคีย์นั้นหรือไม่ - หากไม่มีอะไรอยู่ในแคช โค้ด HTML สำหรับแฟรกเมนต์นั้นจะแสดงผลสำหรับมุมมอง (และเนื้อหาที่แสดงผลใหม่จะถูกเก็บไว้ในแคช)
- หากมีเนื้อหาอยู่ในแคชที่มีคีย์นั้น มุมมองจะแสดงด้วยเนื้อหาของแคช
นี่หมายความว่าแคชไม่จำเป็นต้องถูกทำให้ใช้งานไม่ได้อย่างชัดแจ้ง เมื่อใดก็ตามที่เราเปลี่ยนเอนทิตีและโหลดหน้าใหม่ เนื้อหาแคชใหม่จะถูกแสดงสำหรับเอนทิตี
ตามค่าเริ่มต้น Rails ยังเสนอความสามารถในการทำให้แคชของเอนทิตีหลักเป็นโมฆะในกรณีที่เด็กเปลี่ยนแปลง:
belongs_to :parent_entity, touch: true
เมื่อรวมอยู่ในโมเดลแล้ว จะ แตะ ผู้ปกครองโดยอัตโนมัติเมื่อ สัมผัส เด็ก คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ touch
ได้ที่นี่ ด้วยเหตุนี้ Rails จึงมีวิธีที่ง่ายและมีประสิทธิภาพในการทำให้แคชสำหรับเอนทิตีหลักเป็นโมฆะพร้อมกับแคชสำหรับเอนทิตีย่อย
การแคชใน Rails
อย่างไรก็ตาม การแคชใน Rails ถูกสร้างขึ้นเพื่อให้บริการอินเทอร์เฟซผู้ใช้ โดยที่ส่วนย่อย HTML ที่เป็นตัวแทนของเอนทิตีหลักประกอบด้วยแฟรกเมนต์ HTML ที่แสดงถึงเอนทิตีย่อยของพาเรนต์เท่านั้น กล่าวอีกนัยหนึ่ง ชิ้นส่วน HTML ที่แสดงถึงเอนทิตีย่อยในกระบวนทัศน์นี้ไม่สามารถมีฟิลด์จากเอนทิตีหลักได้
แต่นั่นไม่ใช่สิ่งที่เกิดขึ้นในโลกแห่งความเป็นจริง คุณอาจต้องทำสิ่งต่าง ๆ ในแอปพลิเคชัน Rails ของคุณที่ละเมิดเงื่อนไขนี้
คุณจะจัดการกับสถานการณ์ที่อินเทอร์เฟซผู้ใช้แสดงฟิลด์ของเอนทิตีหลักภายในแฟรกเมนต์ HTML ที่แสดงถึงเอนทิตีย่อยได้อย่างไร
หากรายการย่อยประกอบด้วยฟิลด์จากเอนทิตีหลัก แสดงว่าคุณกำลังประสบปัญหากับพฤติกรรมการยกเลิกแคชเริ่มต้นของ Rails
ทุกครั้งที่มีการปรับเปลี่ยนฟิลด์ที่แสดงจากเอนทิตีหลัก คุณจะต้องแตะเอนทิตีย่อยทั้งหมดที่เป็นของพาเรนต์นั้น ตัวอย่างเช่น หากมีการแก้ไข Parent1
คุณจะต้องตรวจสอบให้แน่ใจว่าแคชสำหรับมุมมอง Child1
และ Child2
นั้นใช้งานไม่ได้

เห็นได้ชัดว่าสิ่งนี้อาจทำให้เกิดปัญหาคอขวดด้านประสิทธิภาพอย่างมาก การสัมผัสทุกเอนทิตีย่อยเมื่อใดก็ตามที่ผู้ปกครองมีการเปลี่ยนแปลงจะส่งผลให้มีการสืบค้นฐานข้อมูลจำนวนมากโดยไม่มีเหตุผลที่ดี
สถานการณ์จำลองอื่นที่คล้ายคลึงกันอีกประการหนึ่งคือ เมื่อมีการนำเสนอเอนทิตีที่เกี่ยวข้องกับการเชื่อมโยง has_and_belongs_to
ในรายการ และการแก้ไขเอนทิตีเหล่านั้นจะเริ่มต้นการเรียงซ้อนของแคชที่ใช้ไม่ได้ผ่านสายการเชื่อมโยง
class Event < ActiveRecord::Base has_many :participants has_many :users, through: :participants end class Participant < ActiveRecord::Base belongs_to :event belongs_to :user end class User < ActiveRecord::Base has_many :participants has_many :events, through :participants end
ดังนั้น สำหรับอินเทอร์เฟซผู้ใช้ข้างต้น การสัมผัสผู้เข้าร่วมหรือเหตุการณ์เมื่อตำแหน่งของผู้ใช้เปลี่ยนแปลงไปจะไม่สมเหตุสมผล แต่เราควรสัมผัสทั้งเหตุการณ์และผู้เข้าร่วมเมื่อชื่อผู้ใช้เปลี่ยนไปใช่หรือไม่?
ดังนั้น เทคนิคในบทความ Signal v. Noise จึงไม่มีประสิทธิภาพสำหรับอินสแตนซ์ UI/UX บางรายการ ดังที่อธิบายไว้ข้างต้น
แม้ว่า Rails จะมีประสิทธิภาพสูงสุดสำหรับสิ่งง่ายๆ แต่โปรเจ็กต์จริงก็มีความยุ่งยากในตัวเอง
Field Level Rails Cache Invalidation
ในโครงการของฉัน ฉันใช้ Ruby DSL ขนาดเล็กเพื่อจัดการกับสถานการณ์ดังที่กล่าวมา ช่วยให้คุณสามารถระบุฟิลด์ได้อย่างชัดเจนซึ่งจะทำให้แคชใช้ไม่ได้ผ่านการเชื่อมโยง
มาดูตัวอย่างบางส่วนที่ช่วยได้จริง:
ตัวอย่างที่ 1:
class Event < ActiveRecord::Base include Touchable ... has_many :tasks ... touch :tasks, in_case_of_modified_fields: [:name] ... end class Task < ActiveRecord::Base belongs_to :event end
ข้อมูลโค้ดนี้ใช้ประโยชน์จากความสามารถในการเขียนโปรแกรมเมตาและความสามารถ DSL ภายในของ Ruby
เพื่อให้เฉพาะเจาะจงมากขึ้น เฉพาะการเปลี่ยนชื่อในเหตุการณ์จะทำให้แคชส่วนย่อยของงานที่เกี่ยวข้องเป็นโมฆะ การเปลี่ยนแปลงฟิลด์อื่นๆ ของกิจกรรม เช่น จุดประสงค์หรือสถานที่ จะไม่ทำให้แคชส่วนย่อยของงานเป็นโมฆะ ฉันจะเรียก การควบคุมการใช้แคชที่ละเอียดระดับฟิลด์ นี้
ตัวอย่างที่ 2:
มาดูตัวอย่างที่แสดงการทำให้แคชใช้ไม่ได้ผ่านสายการเชื่อมโยง has_many
ส่วนต่อประสานผู้ใช้ที่แสดงด้านล่างแสดงงานและเจ้าของ:
สำหรับส่วนติดต่อผู้ใช้นี้ แฟรกเมนต์ HTML ที่แสดงถึงงานควรถูกทำให้ใช้งานไม่ได้เฉพาะเมื่องานเปลี่ยนแปลงหรือเมื่อชื่อของเจ้าของเปลี่ยนแปลง หากฟิลด์อื่นๆ ทั้งหมดของเจ้าของ (เช่น เขตเวลาหรือค่ากำหนด) เปลี่ยนไป แคชงานก็ควรจะไม่เสียหาย
ซึ่งทำได้โดยใช้ DSL ที่แสดงไว้ที่นี่:
class User < ActiveRecord::Base include Touchable touch :tasks, in_case_of_modified_fields: [:first_name, :last_name] ... end class Task < ActiveRecord::Base has_one owner, class_name: :User end
การใช้งาน DSL
สาระสำคัญของ DSL คือวิธีการ touch
อาร์กิวเมนต์แรกคือการเชื่อมโยง และอาร์กิวเมนต์ถัดไปคือรายการฟิลด์ที่ทริกเกอร์การ touch
ในการเชื่อมโยงนั้น:
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
วิธีนี้จัดทำโดยโมดูล Touchable
:
module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end
ในโค้ดนี้ ประเด็นหลักคือเราเก็บอาร์กิวเมนต์ของการโทร touch
จากนั้น ก่อนบันทึกเอนทิตี เราจะทำเครื่องหมายความสัมพันธ์ที่สกปรก ถ้าฟิลด์ที่ระบุถูกแก้ไข เราสัมผัสหน่วยงานในการเชื่อมโยงนั้นหลังจากบันทึกหากการเชื่อมโยงนั้นสกปรก
ประเด็นส่วนตัวที่น่าเป็นห่วงคือ
... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present? end …
ในวิธี check_touchable_entities
เราตรวจสอบ ว่าฟิลด์ที่ประกาศเปลี่ยนแปลง หรือไม่ หากเป็นเช่นนั้น เราจะทำเครื่องหมายการเชื่อมโยงว่าสกปรกโดยการตั้งค่า meta_info[association]
true
จากนั้น หลังจากบันทึกเอนทิตี เราจะตรวจสอบการ เชื่อมโยงที่สกปรก ของเรา และแตะเอนทิตีในเอนทิตีหากจำเป็น:
… def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …
และนั่นก็คือ! ตอนนี้คุณสามารถทำให้แคชระดับฟิลด์ใช้งานไม่ได้ใน Rails ด้วย DSL แบบง่าย
บทสรุป
การแคช Rails สัญญาว่าจะปรับปรุงประสิทธิภาพในแอปพลิเคชันของคุณได้อย่างง่ายดาย อย่างไรก็ตาม การใช้งานจริงอาจซับซ้อนและมักก่อให้เกิดความท้าทายที่ไม่เหมือนใคร ลักษณะการทำงานแคชของ Rails เริ่มต้นทำงานได้ดีสำหรับสถานการณ์ส่วนใหญ่ แต่มีบางสถานการณ์ที่การปรับให้เหมาะสมในการทำให้แคชใช้ไม่ได้อีกเล็กน้อยสามารถไปได้ไกล
ตอนนี้ คุณรู้วิธีปรับใช้การทำให้แคชระดับฟิลด์ใช้งานไม่ได้ใน Rails แล้ว คุณสามารถป้องกันการทำให้แคชเป็นโมฆะโดยไม่จำเป็นในแอปพลิเคชันของคุณได้