Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

ActiveRecord 성능 문제 해결

ActiveRecord는 Ruby on Rails의 가장 마법 같은 기능입니다. 우리는 일반적으로 내부 작동에 대해 걱정할 필요가 없지만 그럴 때 AppSignal이 내부에서 무슨 일이 일어나고 있는지 알 수 있도록 도와줍니다.

ActiveRecord란 무엇입니까?

ActiveRecord에 대해 이야기하려면 먼저 프레임워크, 특히 MVC 프레임워크에 대해 생각해야 합니다. MVC는 Model-View-Controller의 약자로 그래픽 및 웹 애플리케이션을 위한 인기 있는 소프트웨어 디자인 패턴입니다.

MVC 프레임워크는 다음으로 구성됩니다.

  • 모델 :비즈니스 로직 및 데이터 지속성을 처리합니다.
  • 보기 :프레젠테이션 레이어를 구동하고 사용자 인터페이스를 그립니다.
  • 컨트롤러 :모든 것을 하나로 묶습니다.

ActiveRecord는 모델입니다. Ruby in Rails 프레임워크의 구성 요소. 코드와 데이터 사이에 추상화 계층을 도입하므로 SQL 코드를 직접 작성할 필요가 없습니다. 각 모델은 하나의 테이블에 매핑되며 CRUD 작업(생성, 읽기, 업데이트 및 삭제)을 수행하는 다양한 방법을 제공합니다.

AppSignal로 ActiveRecord 모니터링

추상화는 마법처럼 느껴집니다. 우리가 알 필요가 없는 세부 사항을 무시하고 당면한 작업에 집중하는 데 도움이 됩니다. 그러나 일이 예상대로 작동하지 않을 때 추가되는 복잡성으로 인해 근본 원인을 파악하기가 더 어려워질 수 있습니다. 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 문제는 일반적으로 관련 모델과 함께 나타납니다. Person 모델이 있다고 상상해 보세요.

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를 실행하고 있음을 볼 수 있습니다. 1인당:

이 특정 경우에 대한 수정은 간단합니다. 필요한 열에 대한 쿼리를 최적화하도록 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를 사용하여 집계하면 데이터베이스에서 모든 레코드를 가져와 메모리에 보관한 다음 고급 코드를 사용하여 계산해야 하므로 확장되지 않습니다.

max와 같은 Ruby 함수에 의존할 때마다 Rails에서 집계를 수행합니다. , 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 패턴 및 안티 패턴 소개

추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!