웹사이트 검색

Ruby on Rails 앱에서 Unicorn 작업자를 최적화하는 방법


유니콘 소개

Rails 개발자라면 여러 요청을 동시에 처리할 수 있는 HTTP 서버인 Unicorn에 대해 들어보셨을 것입니다.

Unicorn은 분기 프로세스를 사용하여 동시성을 달성합니다. 분기된 프로세스는 기본적으로 서로의 복사본이기 때문에 Rails 애플리케이션이 스레드로부터 안전할 필요가 없음을 의미합니다.

우리 자체 코드가 스레드로부터 안전한지 확인하기 어렵기 때문에 이것은 훌륭합니다. 코드가 스레드로부터 안전한지 확인할 수 없다면 Rubinius와 같은 동시 웹 서버는 문제가 되지 않습니다.

따라서 Unicorn은 스레드로부터 안전하지 않은 경우에도 Rails 앱에 동시성을 제공합니다. 그러나 이것은 비용이 듭니다. Unicorn에서 실행되는 Rails 앱은 훨씬 더 많은 메모리를 사용하는 경향이 있습니다. 앱의 메모리 사용량에 주의를 기울이지 않으면 클라우드 서버에 과부하가 걸릴 수 있습니다.

이 기사에서는 Unicorn의 동시성을 활용하는 동시에 메모리 소비를 제어하는 몇 가지 방법을 살펴보겠습니다.

루비 2.0을 사용하세요!

Ruby 1.9를 사용하고 있다면 Ruby 2.0으로 전환하는 것을 진지하게 고려해야 합니다. 그 이유를 이해하려면 포크에 대해 조금 이해해야 합니다.

분기 및 기록 중 복사(CoW)

자식 프로세스가 분기되면 부모 프로세스와 정확히 동일한 복사본입니다. 그러나 복사된 실제 물리적 메모리를 만들 필요는 없습니다. 그것들은 정확한 복사본이기 때문에 자식 프로세스와 부모 프로세스 모두 동일한 물리적 메모리를 공유할 수 있습니다. 쓰기가 이루어진 경우에만-- 우리는 자식 프로세스를 물리적 메모리에 복사합니다.

그렇다면 이것이 Ruby 1.9/2.0 및 Unicorn과 어떤 관련이 있습니까?

유니콘이 포크를 사용한다는 것을 기억하십시오. 이론적으로 운영 체제는 CoW를 활용할 수 있습니다. 유감스럽게도 Ruby 1.9에서는 이를 가능하게 하지 않습니다. 더 정확히 말하면 Ruby 1.9의 가비지 수집 구현은 이를 가능하게 하지 않습니다. 극도로 단순화된 버전은 다음과 같습니다. Ruby 1.9의 가비지 수집기가 시작되면 쓰기가 이루어지고 CoW가 쓸모 없게 됩니다.

너무 자세히 설명하지 않고 Ruby 2.0의 가비지 수집기가 이를 수정했으며 이제 CoW를 활용할 수 있다고 말하는 것으로 충분합니다.

Unicorn의 구성 조정

Unicorn에서 최대한 많은 성능을 끌어내기 위해 config/unicorn.rb에서 조정할 수 있는 몇 가지 설정이 있습니다.

작업자 프로세스

실행할 작업자 프로세스 수를 설정합니다. 하나의 프로세스가 얼마나 많은 메모리를 차지하는지 아는 것이 중요합니다. 이는 VPS의 RAM을 소진하지 않도록 작업자 수를 안전하게 예산할 수 있도록 하기 위한 것입니다.

타임아웃

이 값은 작은 숫자로 설정해야 합니다. 일반적으로 15~30초가 적당한 숫자입니다. 이 설정은 작업자가 시간 초과되기 전까지의 시간을 설정합니다. 상대적으로 낮은 수를 설정하려는 이유는 장기 실행 요청이 다른 요청 처리를 방해하지 않도록 하기 위함입니다.

preload_app

true로 설정해야 합니다. 이것을 true로 설정하면 Unicorn 작업자 프로세스를 시작하는 시작 시간이 줄어듭니다. 이는 CoW를 사용하여 다른 작업자 프로세스를 포크하기 전에 애플리케이션을 미리 로드합니다. 그러나 문제가 있습니다. 모든 소켓(예: 데이터베이스 연결)이 적절하게 닫혔다가 다시 열리도록 특별한 주의를 기울여야 합니다. before_forkafter_fork를 사용하여 이 작업을 수행합니다.

예를 들면 다음과 같습니다.

before_fork do |server, worker|
  # Disconnect since the database connection will not carry over
  if defined? ActiveRecord::Base
    ActiveRecord::Base.connection.disconnect!
  end
  
  if defined?(Resque)
    Resque.redis.quit
    Rails.logger.info('Disconnected from Redis')
  end
end

after_fork do |server, worker|
  # Start up the database connection again in the worker
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
  
  if defined?(Resque)
    Resque.redis = ENV['REDIS_URI']
    Rails.logger.info('Connected to Redis')
  end
end

이 예제에서는 작업자가 포크될 때 연결이 닫혔다가 다시 열리는지 확인합니다. 데이터베이스 연결 외에도 소켓이 필요한 다른 연결이 유사하게 처리되는지 확인해야 합니다. 위는 Resque에 대한 구성을 포함합니다.

유니콘 작업자의 메모리 소비 길들이기

분명히 모든 무지개와 유니콘이 아닙니다(말장난 의도!). Rails 앱에서 메모리 누수가 발생하면 Unicorn이 상황을 악화시킵니다.

이렇게 분기된 각 프로세스는 Rails 애플리케이션의 복사본이므로 메모리를 소비합니다. 따라서 더 많은 작업자가 있다는 것은 애플리케이션이 더 많은 수신 요청을 처리할 수 있음을 의미하지만 시스템에 있는 물리적 RAM의 양에 구속됩니다.

Rails 애플리케이션에서 메모리 누수가 발생하기 쉽습니다. 우리가 모든 메모리 누수를 막을 수 있더라도 여전히 경쟁해야 할 이상적인 가비지 수집기가 적습니다(MRI 구현을 언급하고 있습니다).

위는 메모리 누수와 함께 Unicorn을 실행하는 Rails 애플리케이션을 보여줍니다.

시간이 지남에 따라 메모리 소비는 계속 증가할 것입니다. 여러 개의 Unicorn 작업자를 사용하면 더 이상 여유 RAM이 없을 때까지 메모리 소비 속도가 빨라집니다. 그런 다음 응용 프로그램이 중단되어 불행한 사용자와 고객 무리로 이어집니다.

이것은 Unicorn의 잘못이 아닙니다는 점에 유의하는 것이 중요합니다. 그러나 이것은 조만간 직면하게 될 문제입니다.

유니콘 일꾼 살인마를 입력하십시오

내가 접한 가장 쉬운 솔루션 중 하나는 unicorn-worker-killer gem입니다.

README에서:

unicorn-worker-killer gem은 다음을 기반으로 Unicorn 작업자의 자동 재시작을 제공합니다.

  1. 최대 요청 수
  2. 요청에 영향을 주지 않고 프로세스 메모리 크기(RSS).

이렇게 하면 응용 프로그램 노드에서 예기치 않은 메모리 고갈을 방지하여 사이트의 안정성이 크게 향상됩니다.

나는 당신이 이미 Unicorn을 설정하고 실행 중이라고 가정합니다.

1 단계:

Gemfile에 unicorn-worker-killer를 추가하세요. 이것을 유니콘 보석 아래에 두세요.

group :production do 
  gem 'unicorn'
  gem 'unicorn-worker-killer'
end

2 단계:

번들 설치를 실행합니다.

3단계:

여기에 재미있는 부분이 있습니다. config.ru 파일을 찾아 엽니다.

# --- Start of unicorn worker killer code ---

if ENV['RAILS_ENV'] == 'production' 
  require 'unicorn/worker_killer'

  max_request_min =  500
  max_request_max =  600

  # Max requests per worker
  use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max

  oom_min = (240) * (1024**2)
  oom_max = (260) * (1024**2)

  # Max memory size (RSS) per worker
  use Unicorn::WorkerKiller::Oom, oom_min, oom_max
end

# --- End of unicorn worker killer code ---

require ::File.expand_path('../config/environment',  __FILE__)
run YourApp::Application

먼저 production 환경에 있는지 확인합니다. 그렇다면 계속해서 다음 코드를 실행할 것입니다.

unicorn-worker-killer는 최대 요청과 최대 메모리라는 두 가지 조건이 주어지면 작업자를 죽입니다.

최대 요청

이 예에서 작업자는 500~600개의 요청 사이를 처리한 경우 종료됩니다. 이것은 범위라는 점에 유의하십시오. 이는 1명 이상의 작업자가 동시에 종료되는 발생을 최소화합니다.

최대 메모리

여기에서 작업자는 240~260MB의 메모리를 소비하는 경우 종료됩니다. 위와 같은 이유로 범위입니다.

모든 앱에는 고유한 메모리 요구 사항이 있습니다. 정상적인 작동 중에 애플리케이션의 메모리 소비를 대략적으로 측정해야 합니다. 이렇게 하면 작업자의 최소 및 최대 메모리 소비량을 더 잘 추정할 수 있습니다.

모든 것을 적절하게 구성하고 나면 앱을 배포할 때 훨씬 덜 불규칙한 메모리 동작을 확인할 수 있습니다.

그래프의 꼬임을 확인하십시오. 그것이 제 역할을 하는 보석입니다!

결론

Unicorn은 스레드 안전 여부에 관계없이 Rails 애플리케이션이 동시성을 달성할 수 있는 쉬운 방법을 제공합니다. 그러나 RAM 소비가 증가하는 비용이 발생합니다. RAM 소비의 균형은 애플리케이션의 안정성과 성능에 절대적으로 중요합니다.

최대 성능을 위해 Unicorn 작업자를 조정하는 3가지 방법을 확인했습니다.

  1. Ruby 2.0을 사용하면 copy-on-write 의미 체계를 활용할 수 있는 훨씬 향상된 가비지 수집기가 제공됩니다.\n
  2. config/unicorn.rb에서 다양한 구성 옵션을 조정합니다.\n
  3. unicorn-worker-killer를 사용하여 작업자가 너무 커지면 종료하고 다시 시작하여 정상적으로 문제를 해결합니다.\n

자원

  • Ruby 2.0 가비지 수집기와 copy-on-write 의미 체계가 작동하는 방식에 대한 훌륭한 설명입니다.\n
  • Unicorn 구성 옵션의 전체 목록입니다.

제출자: Benjamin Tan