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

ประสิทธิภาพของ Rails:การแคชเป็นทางเลือกที่เหมาะสมเมื่อใด

ในแง่ของการเขียนโปรแกรม การแคชหมายถึงการจัดเก็บค่า (หรือค่า) สำหรับการดึงข้อมูลอย่างรวดเร็วในอนาคต โดยปกติ คุณจะทำเช่นนี้กับค่าที่คำนวณได้ช้าด้วยเหตุผลบางประการ ตัวอย่างเช่น พวกเขาต้องการการกดปุ่ม API ภายนอกเพื่อดึงข้อมูล หรือต้องใช้การคำนวณจำนวนมากเพื่อสร้าง

ค่าที่แคชไว้มักจะถูกเก็บไว้ในเซิร์ฟเวอร์ที่แยกจากกัน เช่น memcached หรือ Redis สามารถเก็บไว้ในดิสก์หรือในแรม ในโค้ด เรามักจะ 'แคช' ข้อมูลภายในตัวแปรเพื่อหลีกเลี่ยงการเรียกใช้ฟังก์ชันราคาแพงหลายครั้ง

data = some_calculation()
a(data)
b(data)

ข้อเสียของความเร็วทั้งหมดที่คุณได้รับคือคุณกำลังใช้ข้อมูลเก่า จะเกิดอะไรขึ้นหากข้อมูลที่แคชกลายเป็น 'เก่า' และไม่ถูกต้องอีกต่อไป คุณจะต้องล้างแคชเพื่อ 'ทำให้เป็นโมฆะ'

อาร์กิวเมนต์ต่อต้านการแคช

ตามคำกล่าวที่ว่า วิทยาการคอมพิวเตอร์มีปัญหายากๆ เพียง 2 ข้อเท่านั้น:การตั้งชื่อสิ่งของ2. แคชใช้ไม่ได้3. ข้อผิดพลาดทีละรายการ

เหตุใดการทำให้แคชใช้ไม่ได้จึงเป็นเรื่องยาก ค่าที่เก็บไว้โดยธรรมชาติแล้วจะ 'ซ่อน' มูลค่าที่แท้จริง ทุกครั้งที่ค่า 'ของจริง' เปลี่ยนไป คุณ (ใช่ คุณคือโปรแกรมเมอร์) ต้องไม่ลืมที่จะ 'ทำให้แคชใช้ไม่ได้' เพื่อที่แคชจะได้รับการอัปเดต

สมมติว่าคุณกำลังเพิ่มวิดเจ็ต 'จำนวนคำ' ลงในโปรแกรมแก้ไขข้อความ คุณต้องอัปเดตจำนวนคำตามประเภทผู้ใช้ วิธีที่ง่ายที่สุดคือนับคำใหม่ทุกครั้งที่กดแป้นพิมพ์ แต่วิธีนี้ช้าเกินไป มีแนวทางอื่น:

  1. นับจำนวนคำเมื่อโหลดไฟล์
  2. บันทึกการนับคำนี้ลงในตัวแปร (หรือ 'แคช')
  3. แสดงเนื้อหาของตัวแปรบนหน้าจอ

การใช้งานนี้เร็วกว่ามาก แต่ 'จำนวนคำ' ที่แคชไว้จะไม่เปลี่ยนแปลงเมื่อเราพิมพ์ ในการทำเช่นนั้น เราจำเป็นต้อง 'ทำให้แคชใช้ไม่ได้' เมื่อใดก็ตามที่เราคาดว่าจำนวนคำจะเปลี่ยนไป

ตอนนี้ เมื่อมีการกดแป้นพิมพ์ คุณจะตรวจพบคำ (เช่น การเว้นวรรค) และเพิ่มตัวนับคำ แน่นอน คุณจะลดค่าลงเมื่อผู้ใช้ลบคำ ง่าย. เสร็จแล้ว. ตั๋วต่อไป

...แต่เดี๋ยวก่อน คุณลืมอัปเดตจำนวนคำเมื่อผู้ใช้ตัดข้อความไปที่คลิปบอร์ดหรือไม่ แล้วเมื่อพวกเขาวางข้อความล่ะ แล้วเมื่อเครื่องตรวจตัวสะกดแยกการพิมพ์ผิดออกเป็นสองคำล่ะ

ปัญหาในที่นี้ไม่ได้อัปเดตค่า ซึ่งค่อนข้างไม่สำคัญ ปัญหาคือคุณต้องไม่อัปเดตใน ทุกที่ . การอัปเดตเหล่านี้เพียงรายการเดียวที่ขาดหายไปทำให้เกิดปัญหาการทำให้แคชใช้ไม่ได้ ซึ่งหมายความว่าระบบจะแสดงค่าที่เก่าแก่ผู้ใช้

เมื่อคำนึงถึงสิ่งนี้ คุณจะเห็นว่าการเพิ่มแคชทำให้เกิดความซับซ้อนทางเทคนิคและแหล่งที่มาของข้อบกพร่องที่อาจเกิดขึ้น แน่นอน ปัญหาเหล่านี้สามารถแก้ไขได้ แต่ควรคำนึงถึงก่อนที่จะข้ามไปที่การแคชเป็นวิธีแก้ปัญหา

ความเร็วโดยไม่ต้องแคช

หากเรานำแคชออกจากตาราง การเพิ่มความเร็วให้กับแอปพลิเคชันของเราคือการระบุและแก้ไขปัญหาคอขวดด้านประสิทธิภาพ - ระบบที่ช้ากว่าที่ควรจะเป็น เราสามารถจัดกลุ่มได้เป็นสามประเภทโดยรวม:

  1. การสืบค้นฐานข้อมูล (มากหรือช้าเกินไป)
  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