ในแง่ของการเขียนโปรแกรม การแคชหมายถึงการจัดเก็บค่า (หรือค่า) สำหรับการดึงข้อมูลอย่างรวดเร็วในอนาคต โดยปกติ คุณจะทำเช่นนี้กับค่าที่คำนวณได้ช้าด้วยเหตุผลบางประการ ตัวอย่างเช่น พวกเขาต้องการการกดปุ่ม API ภายนอกเพื่อดึงข้อมูล หรือต้องใช้การคำนวณจำนวนมากเพื่อสร้าง
ค่าที่แคชไว้มักจะถูกเก็บไว้ในเซิร์ฟเวอร์ที่แยกจากกัน เช่น memcached หรือ Redis สามารถเก็บไว้ในดิสก์หรือในแรม ในโค้ด เรามักจะ 'แคช' ข้อมูลภายในตัวแปรเพื่อหลีกเลี่ยงการเรียกใช้ฟังก์ชันราคาแพงหลายครั้ง
data = some_calculation()
a(data)
b(data)
ข้อเสียของความเร็วทั้งหมดที่คุณได้รับคือคุณกำลังใช้ข้อมูลเก่า จะเกิดอะไรขึ้นหากข้อมูลที่แคชกลายเป็น 'เก่า' และไม่ถูกต้องอีกต่อไป คุณจะต้องล้างแคชเพื่อ 'ทำให้เป็นโมฆะ'
อาร์กิวเมนต์ต่อต้านการแคช
ตามคำกล่าวที่ว่า วิทยาการคอมพิวเตอร์มีปัญหายากๆ เพียง 2 ข้อเท่านั้น:การตั้งชื่อสิ่งของ2. แคชใช้ไม่ได้3. ข้อผิดพลาดทีละรายการ
เหตุใดการทำให้แคชใช้ไม่ได้จึงเป็นเรื่องยาก ค่าที่เก็บไว้โดยธรรมชาติแล้วจะ 'ซ่อน' มูลค่าที่แท้จริง ทุกครั้งที่ค่า 'ของจริง' เปลี่ยนไป คุณ (ใช่ คุณคือโปรแกรมเมอร์) ต้องไม่ลืมที่จะ 'ทำให้แคชใช้ไม่ได้' เพื่อที่แคชจะได้รับการอัปเดต
สมมติว่าคุณกำลังเพิ่มวิดเจ็ต 'จำนวนคำ' ลงในโปรแกรมแก้ไขข้อความ คุณต้องอัปเดตจำนวนคำตามประเภทผู้ใช้ วิธีที่ง่ายที่สุดคือนับคำใหม่ทุกครั้งที่กดแป้นพิมพ์ แต่วิธีนี้ช้าเกินไป มีแนวทางอื่น:
- นับจำนวนคำเมื่อโหลดไฟล์
- บันทึกการนับคำนี้ลงในตัวแปร (หรือ 'แคช')
- แสดงเนื้อหาของตัวแปรบนหน้าจอ
การใช้งานนี้เร็วกว่ามาก แต่ 'จำนวนคำ' ที่แคชไว้จะไม่เปลี่ยนแปลงเมื่อเราพิมพ์ ในการทำเช่นนั้น เราจำเป็นต้อง 'ทำให้แคชใช้ไม่ได้' เมื่อใดก็ตามที่เราคาดว่าจำนวนคำจะเปลี่ยนไป
ตอนนี้ เมื่อมีการกดแป้นพิมพ์ คุณจะตรวจพบคำ (เช่น การเว้นวรรค) และเพิ่มตัวนับคำ แน่นอน คุณจะลดค่าลงเมื่อผู้ใช้ลบคำ ง่าย. เสร็จแล้ว. ตั๋วต่อไป
...แต่เดี๋ยวก่อน คุณลืมอัปเดตจำนวนคำเมื่อผู้ใช้ตัดข้อความไปที่คลิปบอร์ดหรือไม่ แล้วเมื่อพวกเขาวางข้อความล่ะ แล้วเมื่อเครื่องตรวจตัวสะกดแยกการพิมพ์ผิดออกเป็นสองคำล่ะ
ปัญหาในที่นี้ไม่ได้อัปเดตค่า ซึ่งค่อนข้างไม่สำคัญ ปัญหาคือคุณต้องไม่อัปเดตใน ทุกที่ . การอัปเดตเหล่านี้เพียงรายการเดียวที่ขาดหายไปทำให้เกิดปัญหาการทำให้แคชใช้ไม่ได้ ซึ่งหมายความว่าระบบจะแสดงค่าที่เก่าแก่ผู้ใช้
เมื่อคำนึงถึงสิ่งนี้ คุณจะเห็นว่าการเพิ่มแคชทำให้เกิดความซับซ้อนทางเทคนิคและแหล่งที่มาของข้อบกพร่องที่อาจเกิดขึ้น แน่นอน ปัญหาเหล่านี้สามารถแก้ไขได้ แต่ควรคำนึงถึงก่อนที่จะข้ามไปที่การแคชเป็นวิธีแก้ปัญหา
ความเร็วโดยไม่ต้องแคช
หากเรานำแคชออกจากตาราง การเพิ่มความเร็วให้กับแอปพลิเคชันของเราคือการระบุและแก้ไขปัญหาคอขวดด้านประสิทธิภาพ - ระบบที่ช้ากว่าที่ควรจะเป็น เราสามารถจัดกลุ่มได้เป็นสามประเภทโดยรวม:
- การสืบค้นฐานข้อมูล (มากหรือช้าเกินไป)
- ดูการแสดงผล
- รหัสแอปพลิเคชัน (เช่น การคำนวณจำนวนมาก)
เมื่อทำงานกับประสิทธิภาพ มีสองเทคนิคที่คุณต้องรู้เพื่อให้ก้าวหน้า:การทำโปรไฟล์และการเปรียบเทียบ
การทำโปรไฟล์
การทำโปรไฟล์คือวิธีที่คุณรู้ว่า ที่ไหน ปัญหาอยู่ในแอปของคุณ:หน้านี้ช้าเพราะการแสดงเทมเพลตช้าหรือไม่ หรือมันช้าเพราะกดฐานข้อมูลล้านครั้ง?
สำหรับ Ruby on Rails ฉันขอแนะนำ rack-mini-profiler ซึ่งเพิ่มวิดเจ็ตเล็กๆ น้อยๆ ให้กับแอปของคุณ ซึ่งจะให้ภาพรวมที่ดีแก่คุณเกี่ยวกับสิ่งที่ต้องทำในการแสดงผลหน้าเว็บที่คุณกำลังดู เช่น จำนวนการสืบค้นฐานข้อมูลที่ถูกไล่ออก ระยะเวลาที่ใช้ และการแสดงผลบางส่วน
สำหรับการผลิต (เคล็ดลับมืออาชีพ:rack-mini-profiler
ทำงานได้ดีในการผลิต เพียงตรวจสอบให้แน่ใจว่าปรากฏสำหรับผู้ใช้บางรายเท่านั้น เช่น ผู้ดูแลระบบหรือนักพัฒนา) มีบริการออนไลน์ เช่น Skylight, New Relic และ Scout ที่คอยตรวจสอบประสิทธิภาพของเพจ
เป้าหมายที่อ้างถึงโดยทั่วไป <= 100ms
เหมาะอย่างยิ่งสำหรับการแสดงหน้าเว็บ เนื่องจากสิ่งที่น้อยกว่านี้เป็นเรื่องยากสำหรับผู้ใช้ที่จะตรวจพบในการใช้งานอินเทอร์เน็ตในโลกแห่งความเป็นจริง เป้าหมายของคุณจะแตกต่างกันไปตามปัจจัยหลายประการ จนถึงจุดหนึ่ง เมื่อทำงานกับแอปพลิเคชันรุ่นเก่าที่มีประสิทธิภาพต่ำ ฉันสร้างเป้าหมาย <=1 วินาที ซึ่งไม่ได้ดีนักแต่ก็ดีกว่าตอนที่ฉันเริ่มต้นมาก
การเปรียบเทียบ
เมื่อเราหาได้ว่า ที่ไหน ปัญหาคือ จากนั้นเราสามารถใช้การวัดประสิทธิภาพเพื่อดูว่า (ถ้ามี) มีผลกระทบต่อประสิทธิภาพการทำงานอย่างไร (ถ้ามี) โดยส่วนตัวแล้ว ฉันชอบใช้เบนช์มาร์ก-ips gem สำหรับงานประเภทนี้ เนื่องจากเป็นวิธีที่มนุษย์สามารถอ่านได้ง่ายเพื่อดูความแตกต่างที่โค้ดของคุณสร้างขึ้น
เป็นตัวอย่างเล็กน้อย ต่อไปนี้คือการเปรียบเทียบการต่อสตริงกับการแก้ไขสตริง:
require 'benchmark/ips'
@a = "abc"
@b = "def"
Benchmark.ips do |x|
x.report("Concatenation") { @a + @b }
x.report("Interpolation") { "#{@a}#{@b}" }
x.compare!
end
และผลลัพธ์:
Warming up --------------------------------------
Concatenation 316.022k i/100ms
Interpolation 282.422k i/100ms
Calculating -------------------------------------
Concatenation 10.353M (± 7.4%) i/s - 51.512M in 5.016567s
Interpolation 6.615M (± 6.8%) i/s - 33.043M in 5.023636s
Comparison:
Concatenation: 10112435.3 i/s
Interpolation: 6721867.3 i/s - 1.50x slower
สิ่งนี้ทำให้เราได้ผลลัพธ์ที่มนุษย์อ่านได้อย่างดี และการแก้ไขนั้นช้ากว่าการต่อเรียงกัน 1.5 เท่า (อย่างน้อยก็สำหรับสตริงขนาดเล็กของเรา) ด้วยเหตุผลนี้ เราขอแนะนำให้คุณคัดลอกวิธีการที่คุณพยายามปรับปรุงและตั้งชื่อใหม่ จากนั้นคุณสามารถเรียกใช้การเปรียบเทียบอย่างรวดเร็วเพื่อดูว่าคุณกำลังปรับปรุงประสิทธิภาพหรือไม่
การแก้ไขปัญหาด้านประสิทธิภาพ
ณ จุดนี้ เรารู้ว่าส่วนใดของแอปของเราช้า เรามีเกณฑ์มาตรฐานเพื่อวัดการปรับปรุงเมื่อเกิดขึ้น ตอนนี้ เราแค่ต้องทำงานจริงเพื่อเพิ่มประสิทธิภาพ เทคนิคที่คุณเลือกจะขึ้นอยู่กับว่าปัญหาของคุณอยู่ที่ใด:ในฐานข้อมูล มุมมอง หรือแอปพลิเคชัน
ประสิทธิภาพของฐานข้อมูล
สำหรับปัญหาด้านประสิทธิภาพที่เกี่ยวข้องกับฐานข้อมูล มีบางสิ่งที่ต้องพิจารณา ขั้นแรก ให้หลีกเลี่ยง 'N+1 แบบสอบถาม' สถานการณ์เช่นนี้มักเกิดขึ้นในการแสดงผลคอลเลกชันในมุมมอง ตัวอย่างเช่น คุณมีผู้ใช้ที่มีโพสต์บล็อก 10 รายการ และคุณต้องการแสดงผู้ใช้และโพสต์ทั้งหมดของเขาหรือเธอ การตัดครั้งแรกที่ไร้เดียงสาอาจเป็นแบบนี้:
# Controller
def show
@user = User.find(params[:id])
end
# View
Name: <%= @user.name %>
Posts:
<%= @user.posts each do |post| %>
<div>Title: <%= post.title %></div>
<% end %>
วิธีการที่แสดงด้านบนจะได้รับผู้ใช้ (1 ข้อความค้นหา) จากนั้นจึงปิดการสืบค้นสำหรับแต่ละโพสต์ (N=10 ข้อความค้นหา) ส่งผลให้มีทั้งหมด 11 รายการ (หรือ N+1) โชคดีที่ Rails มีวิธีแก้ไขปัญหานี้อย่างง่ายโดยการเพิ่ม .includes(:post)
กับแบบสอบถาม ActiveRecord ของคุณ ในตัวอย่างข้างต้น เราก็แค่เปลี่ยนโค้ดคอนโทรลเลอร์เป็นดังนี้:
def show
@user = User.includes(:post).find(params[:id])
end
ตอนนี้ เราจะดึงผู้ใช้ และ โพสต์ทั้งหมดของเขาหรือโพสต์ในฐานข้อมูลเดียว
อีกสิ่งหนึ่งที่ควรมองหาคือตำแหน่งที่คุณสามารถส่งการคำนวณไปยังฐานข้อมูล ซึ่งมักจะเร็วกว่าการดำเนินการเดียวกันในแอปพลิเคชันของคุณ รูปแบบทั่วไปของสิ่งนี้คือการรวมกลุ่มดังต่อไปนี้:
total = Model.all.map(&:somefield).sum
นี่คือการดึงบันทึกทั้งหมดจากฐานข้อมูล แต่ผลรวมของค่าที่เกิดขึ้นจริงเกิดขึ้นใน Ruby เราสามารถเร่งความเร็วได้โดยให้ฐานข้อมูลทำการคำนวณให้เราดังนี้:
total = Model.sum(:somefield)
บางทีคุณอาจต้องการอะไรที่ซับซ้อนกว่านี้ เช่น การคูณสองคอลัมน์:
total = Model.sum('columna * columnb')
ฐานข้อมูลทั่วไปรองรับเลขคณิตพื้นฐานเช่นนี้และการรวมทั่วไป เช่น ผลรวมและค่าเฉลี่ย ดังนั้นโปรดระวัง map(...).sum
โทรใน codebase ของคุณ
ดูประสิทธิภาพ
แม้ว่าฉันจะบอกว่าปัญหาด้านประสิทธิภาพที่เกี่ยวข้องกับเทมเพลตช่วยให้แคชเป็นวิธีแก้ปัญหาได้มากกว่า แต่ก็ยังมีผลไม้ที่แขวนอยู่ต่ำที่คุณอาจต้องการแยกแยะก่อน
สำหรับเวลาโหลดเพจทั่วไป คุณสามารถตรวจสอบว่าคุณกำลังใช้ซอร์สแบบย่อสำหรับไลบรารี Javascript หรือ CSS ใดๆ (อย่างน้อยบนเซิร์ฟเวอร์ที่ใช้งานจริง)
นอกจากนี้ ให้ระวังการรวมบางส่วนจำนวนมาก หาก _widget.html.erb
. ของคุณ เทมเพลตใช้เวลาในการประมวลผล 1 มิลลิวินาที แต่คุณมีวิดเจ็ต 100 อันบนหน้า ซึ่งนั่นก็หายไปแล้ว 100 มิลลิวินาที ทางออกหนึ่งคือพิจารณา UI ของคุณใหม่ การมีวิดเจ็ต 100 รายการบนหน้าจอพร้อมกันมักจะไม่ใช่ประสบการณ์ที่ดีของผู้ใช้ และคุณอาจต้องการพิจารณาใช้รูปแบบการแบ่งหน้า หรือบางทีอาจต้องยกเครื่อง UI/UX ที่รุนแรงยิ่งขึ้นไปอีก
ประสิทธิภาพของรหัสแอปพลิเคชัน
หากปัญหาด้านประสิทธิภาพของคุณอยู่ที่โค้ดของแอปพลิเคชันเอง (เช่น การจัดการข้อมูล) แทนที่จะเป็นมุมมองหรือเลเยอร์ฐานข้อมูล คุณมีตัวเลือกสองทาง หนึ่งคือการดูว่างานบางอย่างสามารถผลักเข้าไปในฐานข้อมูลเป็นอย่างน้อย ไม่ว่าจะเป็นแบบสอบถาม ตามที่อธิบายไว้ข้างต้น หรือเป็นมุมมองฐานข้อมูลด้วยบางที อาจเหมือนกับอัญมณีที่สวยงาม)
อีกทางเลือกหนึ่งคือการย้าย 'การยกของหนัก' ไปที่งานพื้นหลัง แม้ว่าอาจจำเป็นต้องเปลี่ยนแปลง UI ของคุณเพื่อจัดการกับข้อเท็จจริงที่ว่าตอนนี้ค่าจะถูกคำนวณแบบอะซิงโครนัส
ฉันยังต้องการแคช; ตอนนี้คืออะไร?
เมื่อผ่านสิ่งเหล่านี้มา คุณอาจตัดสินใจว่าใช่ การแคชคือโซลูชันที่คุณต้องการ ดังนั้นคุณควรทำอย่างไร? คอยติดตามเพราะนี่เป็นบทความแรกในชุดบทความที่ครอบคลุมรูปแบบแคชต่างๆ ที่มีอยู่ใน Ruby on Rails