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

การแก้ไขปัญหาประสิทธิภาพ ActiveRecord

ActiveRecord เป็นคุณสมบัติมหัศจรรย์ที่สุดของ Ruby on Rails ปกติแล้วเราไม่จำเป็นต้องกังวลเกี่ยวกับการทำงานภายใน แต่เมื่อดำเนินการ ต่อไปนี้คือวิธีที่ AppSignal จะช่วยให้เราทราบว่าเกิดอะไรขึ้นภายใต้ประทุน

ActiveRecord คืออะไร

ในการพูดคุยเกี่ยวกับ ActiveRecord เราต้องคิดถึงเฟรมเวิร์กก่อน โดยเฉพาะเกี่ยวกับเฟรมเวิร์ก MVC MVC ย่อมาจาก Model-View-Controller และเป็นรูปแบบการออกแบบซอฟต์แวร์ยอดนิยมสำหรับแอปพลิเคชันกราฟิกและเว็บ

เฟรมเวิร์ก MVC ประกอบด้วย:

  • รุ่น :จัดการตรรกะทางธุรกิจและความคงอยู่ของข้อมูล
  • ดู :ขับเคลื่อนเลเยอร์การนำเสนอและวาดอินเทอร์เฟซผู้ใช้
  • ตัวควบคุม :ผูกทุกอย่างเข้าด้วยกัน

ActiveRecord คือ รุ่น คอมโพเนนต์ในเฟรมเวิร์ก Ruby in Rails มันแนะนำชั้นนามธรรมระหว่างโค้ดและข้อมูล ดังนั้นเราจึงไม่ต้องเขียนโค้ด SQL ด้วยตัวเอง แต่ละรุ่นถูกแมปกับตารางเดียวและมีวิธีการต่างๆ ในการดำเนินการ CRUD (สร้าง อ่าน อัปเดต และลบ)

การตรวจสอบ ActiveRecord ด้วย AppSignal

นามธรรมรู้สึกมหัศจรรย์ — ช่วยให้เราเพิกเฉยต่อรายละเอียดที่เราไม่จำเป็นต้องรู้และมุ่งความสนใจไปที่งานที่ทำอยู่ แต่เมื่อสิ่งต่าง ๆ ไม่ทำงานตามที่คาดไว้ ความซับซ้อนที่พวกเขาเพิ่มเข้าไปอาจทำให้ระบุสาเหตุที่แท้จริงได้ยากขึ้น AppSignal สามารถให้รายละเอียดเกี่ยวกับสิ่งที่เกิดขึ้นจริงใน Rails ได้

กราฟเวลาตอบสนอง

มาดูปัญหากันก่อน การแก้ไขปัญหาประสิทธิภาพการทำงานเป็นกระบวนการที่ทำซ้ำได้ เมื่อคุณมีแอปพลิเคชัน Rails รายงานไปยัง AppSignal แล้ว ให้ไปที่ เหตุการณ์> ประสิทธิภาพ .

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

ถัดไป ดูกราฟกลุ่มเหตุการณ์ มันแสดงให้เห็นว่าใช้เวลาเท่าใดตามหมวดหมู่ ตรวจสอบระยะเวลาที่เกี่ยวข้องโดย active_record . ตรวจสอบการใช้งานในเนมสเปซทั้งหมดของคุณ

กราฟจะบอกเราทันทีว่าเราควรมุ่งเน้นที่ใดในการเพิ่มประสิทธิภาพโค้ด

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

แดชบอร์ดการสืบค้นข้อมูลช้า

ตอนนี้เราได้ระบุแล้วว่าปัญหาเกิดจากข้อมูลผูกมัด เรามาดูกันว่าเราจะขยายภาพเพื่อหาสาเหตุที่แท้จริงได้หรือไม่

เปิด ปรับปรุง> แบบสอบถามช้า แผงควบคุม. หน้านี้แสดงรายการแบบสอบถาม SQL ที่จัดอันดับตามผลกระทบต่อเวลาโดยรวม ข้อความค้นหาที่สร้างจาก ActiveRecord ทั้งหมดจะแสดงเป็น sql.active_record เหตุการณ์

ลองคลิกที่ข้อความค้นหาระดับบนสุดเพื่อดูรายละเอียด แดชบอร์ดแสดงระยะเวลาเฉลี่ยและข้อความค้นหา

การเลื่อนด้านล่างจะแสดงเวลาตอบกลับของคำค้นหาในไม่กี่ชั่วโมงที่ผ่านมาและการดำเนินการเริ่มต้น

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

แดชบอร์ดการวัดประสิทธิภาพ

เหตุการณ์การวัดประสิทธิภาพจะเปิดขึ้นเมื่อ AppSignal บันทึกปลายทางใหม่หรืองานพื้นหลัง

เหตุการณ์อยู่ที่ประสิทธิภาพ> รายการปัญหา แดชบอร์ด

หน้าเหตุการณ์แสดงเวลาที่ผ่านไปและจำนวนการจัดสรรสำหรับส่วนประกอบ MVC แต่ละรายการ ปัญหา ActiveRecord จะแสดงระยะเวลานานใน active_record หมวดหมู่.

ไทม์ไลน์ของกิจกรรมแสดงให้เห็นว่ากิจกรรมดำเนินไปอย่างไรเมื่อเวลาผ่านไป

การค้นหาปัญหา ActiveRecord

ในส่วนนี้ เราจะมาดูกันว่า AppSignal สามารถช่วยเราระบุปัญหา ActiveError ทั่วไปได้อย่างไร

การเลือกคอลัมน์ที่เกี่ยวข้อง

ภูมิปัญญาฐานข้อมูลบอกว่าเราควรดึงคอลัมน์ที่จำเป็นสำหรับงานเสมอ ตัวอย่างเช่น แทนที่จะ SELECT * FROM people เราควร SELECT first_name, surname, birthdate FROM people . นั่นเป็นทั้งหมดที่ดีและดี แต่เราจะทำอย่างไรกับ Rails?

โดยค่าเริ่มต้น ActiveRecord จะดึงข้อมูลคอลัมน์ทั้งหมด

Person.all.each {
      # process data
}

โชคดีที่เรามี select วิธีการเลือกและเลือกคอลัมน์ที่ต้องการ:

Person.select(:name, :address, :birthdate).each {
      # process data
}

อาจดูเหมือนฉันกำลังหมกมุ่นอยู่กับรายละเอียดเล็กๆ น้อยๆ แต่สำหรับตารางกว้าง การเลือกคอลัมน์ทั้งหมดนั้นเป็นการสิ้นเปลือง คุณจะสังเกตเห็นว่าเมื่อสิ่งนี้เกิดขึ้น ActiveRecord จะจัดสรรหน่วยความจำขนาดใหญ่:

ปัญหา N+1

ปัญหา N+1 เกิดขึ้นเมื่อแอปพลิเคชันได้รับชุดระเบียนจากฐานข้อมูลและวนซ้ำ ซึ่งทำให้แอปพลิเคชันดำเนินการค้นหา N+1 โดยที่ N คือจำนวนแถวที่ได้รับในตอนแรก อย่างที่คุณอาจจินตนาการ รูปแบบนี้จะขยายได้ไม่ดีเมื่อโต๊ะโตขึ้น เป็นปัญหาที่สร้างความเสียหายอย่างมากที่ AppSignal เตือนคุณโดยเฉพาะ:

ปัญหา N+1 มักปรากฏขึ้นพร้อมกับโมเดลที่เกี่ยวข้อง ลองนึกภาพว่าเรามีโมเดลบุคคล:

class Person < ApplicationRecord
    has_many :addresses
end

แต่ละคนสามารถมีที่อยู่ได้หลายที่อยู่:

class Address < ApplicationRecord
    belongs_to :person
end

วิธีที่ตรงไปตรงมาที่สุดในการดึงข้อมูลนำไปสู่ปัญหา N+1:

class RelatedTablesController < ApplicationController
    def index
        Person.all.each do |person|
            person.addresses.each do |address|
            address.address
            end
        end
    end
end

คุณสามารถเห็นใน AppSignal ว่าแอปพลิเคชันกำลังเรียกใช้ SELECT ต่อคน:

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

class RelatedTablesController < ApplicationController
    def index
        Person.all.includes(:addresses).each do |person|
            person.addresses.each do |address|
            address.address
            end
        end
    end
end

ตอนนี้เรามีข้อความค้นหาสองรายการแทนที่จะเป็น N+1:

Processing by RelatedTablesController#index as HTML
   (0.2ms)  SELECT sqlite_version(*)
  ↳ app/controllers/related_tables_controller.rb:12:in `index'
  Person Load (334.6ms)  SELECT "people".* FROM "people"
  ↳ app/controllers/related_tables_controller.rb:12:in `index'

  Address Load (144.4ms)  SELECT "addresses".* FROM "addresses" WHERE "addresses"."person_id" IN (1, 2, 3, . . .)

ดำเนินการแบบสอบถามที่จำเป็นน้อยที่สุดต่อตาราง

บางครั้งอาจสับสนกับปัญหา N+1 แต่ก็แตกต่างกันเล็กน้อย เมื่อเราสืบค้นตาราง เราควรดึงข้อมูลทั้งหมดที่เราคิดว่าเราจะต้องใช้ในการย่อขนาดการดำเนินการอ่าน อย่างไรก็ตาม มีโค้ดที่ดูไร้เดียงสาจำนวนมากที่ทำให้เกิดการสืบค้นซ้ำซ้อน ตัวอย่างเช่น ดูว่า count . เป็นอย่างไร ให้ผลลัพธ์เป็น SELECT COUNT(*) . เสมอ แบบสอบถามในมุมมองต่อไปนี้:

<ul>
    <% @people.each do |person| %>
        <li><%= person.name %></li>
    <% end %>
</ul>
 
<h2>Number of Persons: <%= @people.count %></h2>

ตอนนี้ ActiveRecord ทำการสืบค้นสองรายการ:

Rendering duplicated_table_query/index.html.erb within layouts/application
  (69.1ms)  SELECT COUNT(*) FROM "people" WHERE "people"."name" = ?  [["name", "John Waters"]]
↳ app/views/duplicated_table_query/index.html.erb:3
Person Load (14.6ms)  SELECT "people".* FROM "people" WHERE "people"."name" = ?  [["name", "John Waters"]]
↳ app/views/duplicated_table_query/index.html.erb:6

ใน AppSignal อาการที่คุณจะสังเกตเห็นคือมี active_record . สองตัว เหตุการณ์บนโต๊ะเดียวกัน:

ความจริงก็คือเราไม่ต้องการคำถามสองข้อ เรามีข้อมูลทั้งหมดที่เราต้องการในหน่วยความจำอยู่แล้ว ในกรณีนี้ วิธีแก้ไขคือสลับ count ด้วย size :

<ul>
<% @people.each do |person| %>
<li><%= person.name %></li>
<% end %>
</ul>
 
<h2>Number of Persons: <%= @people.size %></h2>

ตอนนี้เรามี SELECT . ตัวเดียว อย่างที่ควรจะเป็น:

Rendering duplicated_table_query/index.html.erb within layouts/application
Person Load (63.2ms)  SELECT "people".* FROM "people" WHERE "people"."name" = ?  [["name", "Abdul Strosin"]]
↳ app/views/duplicated_table_query/index.html.erb:5

อีกวิธีหนึ่งคือใช้พรีโหลดเพื่อแคชข้อมูลในหน่วยความจำ

การคำนวณข้อมูลรวมใน Rails

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

เรากำลังทำการรวมใน Rails ทุกครั้งที่เราใช้ฟังก์ชัน Ruby เช่น max , min , หรือ sum เหนือองค์ประกอบ ActiveRecord หรือรายการอื่นๆ

class AggregatedColumnsController < ApplicationController
    def index
        @mean = Number.pluck(:number).sum()
    end
end

โชคดีที่โมเดล ActiveRecord มีเมธอดเฉพาะที่แมปกับฟังก์ชันการรวมในฐานข้อมูล ตัวอย่างเช่น เคียวรีต่อไปนี้จะจับคู่กับ SELECT SUM(number) FROM ... ซึ่งเร็วกว่าและถูกกว่ามากในการรันกว่าตัวอย่างก่อนหน้า:

# controller
 
class AggregatedColumnsController < ApplicationController
    def index
        @mean = Number.sum(:number)
    end
end
Processing by AggregatedColumnsController#index as */*
  (2.4ms)  SELECT SUM("numbers"."number") FROM "numbers"

หากคุณต้องการฟังก์ชันการรวมที่ซับซ้อนหรือรวมกันมากขึ้น คุณอาจต้องใส่โค้ด SQL ดิบบางส่วน:

sql = "SELECT AVG(number), STDDEV(number), VAR(number) FROM ..."
@results = ActiveRecord::Base.connection.execute(sql)

การจัดการธุรกรรมขนาดใหญ่

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

เราสามารถรวมกลุ่มการเปลี่ยนแปลงไว้ในธุรกรรมเดียวด้วย ActiveRecord::Base.transaction .

class BigTransactionController < ApplicationController
    def index
        ActiveRecord::Base.transaction do
            (1..1000).each do
                Person.create(name: 'Les Claypool')
            end
        end
    end
end

มีหลายกรณีที่ถูกต้องตามกฎหมายสำหรับการใช้ธุรกรรมขนาดใหญ่ อย่างไรก็ตาม สิ่งเหล่านี้มีความเสี่ยงที่จะทำให้ฐานข้อมูลช้าลง นอกจากนี้ ธุรกรรมที่เกินขีดจำกัดบางอย่างจะส่งผลให้ฐานข้อมูลปฏิเสธธุรกรรมเหล่านั้น

สัญญาณแรกของการทำธุรกรรมที่ใหญ่เกินไปคือการใช้เวลามากกับ commit transaction เหตุการณ์:

ยกเว้นปัญหาการกำหนดค่าในฐานข้อมูลเอง วิธีแก้ไขคือแบ่งธุรกรรมออกเป็นชิ้นเล็กๆ

การตรวจสอบฐานข้อมูล

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

หากเราใช้ฐานข้อมูลของเราเอง เราสามารถติดตั้งเอเจนต์แบบสแตนด์อโลนเพื่อบันทึกเมตริกของโฮสต์ได้ หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้เอเจนต์แบบสแตนด์อโลน โปรดอ่าน:การมอนิเตอร์ทุกระบบด้วย StatsD และเอเจนต์แบบสแตนด์อโลนของ AppSignal

สัญญาณต่อไปนี้อาจแสดงว่ามีบางอย่างเกิดขึ้นกับฐานข้อมูล ไปที่ ตรวจสอบ> ตัวชี้วัดโฮสต์ แดชบอร์ดเพื่อดูการใช้ทรัพยากรในเซิร์ฟเวอร์ของคุณ:

  • การใช้หน่วยความจำสูง :ฐานข้อมูลต้องการหน่วยความจำจำนวนมาก — มากกว่าระบบอื่นๆ ส่วนใหญ่ — เพื่อให้ทำงานได้อย่างถูกต้อง เมื่อชุดข้อมูลเติบโตขึ้น ความต้องการหน่วยความจำก็มักจะขยายตามไปด้วย เราจำเป็นต้องเพิ่มหน่วยความจำหรือแบ่งชุดข้อมูลระหว่างเครื่องต่างๆ เป็นครั้งคราว
  • สลับการใช้งาน :ตามหลักการแล้วเครื่องฐานข้อมูลไม่ควรต้องการหน่วยความจำแบบสลับเลย การสลับฆ่าประสิทธิภาพของฐานข้อมูล การปรากฏตัวของมันหมายความว่ามีการกำหนดค่าเชิงลึกหรือปัญหาหน่วยความจำมากขึ้น
  • การใช้งาน I/O สูง :จุดสูงสุดในกิจกรรมดิสก์อาจเกิดจากงานบำรุงรักษา เช่น การทำดัชนีใหม่หรือการสำรองข้อมูลฐานข้อมูล การทำงานเหล่านี้ในช่วงชั่วโมงเร่งด่วนจะส่งผลต่อประสิทธิภาพอย่างแน่นอน

👋 ถ้าคุณชอบบทความนี้ ยังมีอีกมากมายที่เราเขียนเกี่ยวกับประสิทธิภาพของ Ruby (on Rails) ให้ดูที่รายการตรวจสอบการตรวจสอบประสิทธิภาพของ Ruby

บทสรุป

การวินิจฉัยปัญหาด้านประสิทธิภาพไม่ใช่เรื่องง่าย วันนี้ เราได้เรียนรู้วิธีใช้ Appsignal เพื่อระบุแหล่งที่มาของปัญหาอย่างรวดเร็ว

มาเรียนรู้เกี่ยวกับ AppSignal และ Ruby on Rails กัน:

  • ประสิทธิภาพ ActiveRecord:แอนตี้แพทเทิร์นการสืบค้น N+1
  • Rails นั้นเร็ว:เพิ่มประสิทธิภาพการดูของคุณ
  • แนะนำ Ruby on Rails Patterns และ Anti-patterns

ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!