미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
아시다시피 루비에는 MRI, JRuby, Rubinius, Opal, RubyMotion 등과 같은 몇 가지 구현이 있으며 각 구현은 서로 다른 코드 실행 패턴을 사용할 수 있습니다. 이 문서에서는 이 중 처음 세 가지에 초점을 맞추고 MRI를 비교합니다.
아시다시피 루비에는 MRI, JRuby, Rubinius, Opal, RubyMotion 등과 같은 몇 가지 구현이 있으며, 각각 다른 패턴을 사용할 수 있습니다. 코드 실행을 살펴봅니다. 이 글에서는 이 중 처음 세 가지에 초점을 맞추고 CPU 집약적인 알고리즘 처리, 파일 복사 등 다양한 상황에서 포크 및 스레딩의 적합성을 평가하는 몇 가지 샘플 스크립트를 실행하여 MRI(현재 가장 많이 사용되는 구현)를 JRuby 및 Rubinius와 비교합니다."실습을 통한 학습"을 시작하기 전에 몇 가지 기본 용어를 수정해야 합니다.
포크
스레드
포크와 스레드를 사용하는 많은 도구가 있으며, 애플리케이션 서버 수준에서는 Unicorn(포크) 및 Puma(스레드), 백그라운드 작업 수준에서는 Resque(포크) 및 Sidekiq(스레드) 등이 일상적으로 사용되고 있습니다.
다음 표는 주요 루비 구현에서 포크 및 스레딩에 대한 지원을 보여줍니다.
루비 구현 | 포크 | 스레딩 |
MRI | 예 | 예(GIL**에 의해 제한됨) |
JRuby | – | 예 |
루비니우스 | 예 | 예 |
이 주제에서는 병렬성과 동시성이라는 두 가지 마법의 단어가 부메랑처럼 돌아오고 있으므로 이에 대해 조금 더 설명할 필요가 있습니다. 우선, 이 두 용어는 같은 의미로 사용할 수 없습니다. 간단히 말해 병렬성은 두 개 이상의 작업이 정확히 동시에 처리되는 경우에 대해 이야기할 수 있습니다. 동시성은 두 개 이상의 작업이 겹치는 시간대에 처리될 때 발생합니다(반드시 동시에 처리될 필요는 없음). 예, 광범위한 설명이지만 차이점을 알아차리고 이 글의 나머지 부분을 이해하는 데 도움이 되기에 충분합니다.
다음 표에는 병렬 처리 및 동시성 지원이 나와 있습니다.
루비 구현 | 병렬 처리(포크를 통한) | 병렬 처리(스레드를 통한) | 동시성 |
MRI | 예 | 아니요 | 예 |
JRuby | – | 예 | 예 |
루비니우스 | 예 | 예(버전 2.X부터) | 예 |
이론은 여기까지입니다 - 실제로 확인해 보겠습니다!
루비의 구현에서 포크와 스레딩이 어떻게 작동하는지 보여주기 위해 다음과 같은 간단한 클래스를 만들었습니다. 테스트
를 상속하는 몇 가지 클래스가 있습니다. 각 클래스에는 처리할 작업이 다릅니다. 기본적으로 모든 작업은 루프에서 네 번 실행됩니다. 또한 모든 작업은 순차, 포크, 스레드의 세 가지 코드 실행 유형에 대해 실행됩니다. 또한, Benchmark.bmbm
는 코드 블록을 두 번 실행합니다. 첫 번째는 런타임 환경을 설정하고 실행하기 위해, 두 번째는 측정하기 위해 실행합니다. 이 문서에 제시된 모든 결과는 두 번째 실행에서 얻은 것입니다. 물론, 심지어 bmbm
메서드가 완벽한 격리를 보장하지는 않지만 여러 코드 실행 간의 차이는 미미합니다.
"벤치마크" 필요
클래스 Test
AMOUNT = 4
def run
Benchmark.bmbm do |b|
b.report("sequential") { sequential }
b.report("포킹") { 포킹 }
b.report("스레딩") { 스레딩 }
end
end
private
def sequential
AMOUNT.times { 수행 }
end
def 포크
AMOUNT.times do
포크 do
perform
end
end
Process.waitall
구조 NotImplementedError => e
# 포크 메서드는 JRuby에서 사용할 수 없습니다.
넣어
end
def 스레딩
threads = []
AMOUNT.times do
threads << Thread.new do
perform
end
end
threads.map(&:join)
end
def perform
raise "구현되지 않음"
end
end
계산을 루프에서 실행하여 큰 CPU 부하를 생성합니다.
LoadTest 클래스 < Test
def perform
1000.times { 1000.times { 2**3**4 } }
end
end
실행해 보겠습니다...
LoadTest.new.run
...그리고 결과 확인
MRI | JRuby | 루비니우스 | |
순차적 | 1.862928 | 2.089000 | 1.918873 |
포크 | 0.945018 | – | 1.178322 |
스레딩 | 1.913982 | 1.107000 | 1.213315 |
보시다시피 순차적으로 실행한 결과는 비슷합니다. 물론 솔루션 간에 약간의 차이가 있지만 이는 다양한 인터프리터에서 선택한 방법을 기본적으로 구현하기 때문에 발생하는 문제입니다.
이 예제에서 포크는 상당한 성능 향상(코드가 거의 두 배 빠르게 실행됨)을 가져옵니다.
스레딩은 포크와 비슷한 결과를 제공하지만 JRuby와 Rubinius에만 해당됩니다. MRI에서 스레드를 사용하여 샘플을 실행하면 순차적 방법보다 시간이 조금 더 걸립니다. 적어도 두 가지 이유가 있습니다. 첫째, GIL은 스레드를 순차적으로 실행하도록 강제하므로 완벽한 세계에서는 실행 시간이 순차 실행과 동일해야 하지만, GIL 작업(스레드 간 전환 등)에 대한 시간 손실도 발생합니다. 둘째, 스레드를 생성하는 데 약간의 오버헤드 시간도 필요합니다.
이 예는 MRI에서 사용 스레드의 의미에 대한 질문에 대한 답을 제공하지 않습니다. 다른 예시를 살펴봅시다.
절전 모드를 실행합니다.
클래스 SnoozeTest < Test
def perform
sleep 1
end
end
결과는 다음과 같습니다.
MRI | JRuby | 루비니우스 | |
순차적 | 4.004620 | 4.006000 | 4.003186 |
포크 | 1.022066 | – | 1.028381 |
스레딩 | 1.001548 | 1.004000 | 1.003642 |
보시다시피, 각 구현은 순차 및 포크 실행뿐만 아니라 스레딩 실행에서도 비슷한 결과를 제공합니다. 그렇다면 왜 MRI가 JRuby 및 Rubinius와 동일한 성능 향상을 가져올까요? 그 답은 다음과 같은 구현에 있습니다. 수면
.
MRI 수면
메서드는 다음과 같이 구현됩니다. rb_thread_wait_for
라는 다른 함수를 사용하는 native_sleep
. 구현을 간단히 살펴 보겠습니다 (코드는 단순화되었으며 원래 구현은 다음에서 찾을 수 있습니다. 여기):
정적 void
native_sleep(rb_thread_t *th, 구조체 timeval *timeout_tv)
{
...
GVL_UNLOCK_BEGIN();
{
// 여기서 몇 가지 작업을 수행합니다.
}
gvl_unlock_end();
thread_debug("native_sleep donen");
}
이 함수가 중요한 이유는 엄격한 루비 컨텍스트를 사용하는 것 외에도 일부 작업을 수행하기 위해 시스템 컨텍스트로 전환하기 때문입니다. 이런 상황에서는 루비 프로세스가 할 일이 없습니다... 시간 낭비의 좋은 예시인가요? 아니요, GIL에 이런 말이 있습니다: "이 스레드에서 할 일이 없나요? 다른 스레드로 전환하고 잠시 후에 다시 돌아오자"라는 말이 있기 때문입니다. 이 작업은 다음을 사용하여 GIL을 잠금 해제하고 잠그면 가능합니다. gvl_unlock_begin()
그리고 gvl_unlock_end()
함수.
상황은 분명해졌지만 수면
메서드는 거의 유용하지 않습니다. 더 많은 실제 사례가 필요합니다.
파일을 다운로드하고 저장하는 프로세스를 실행합니다.
"net/http" 필요
다운로드 파일 테스트 클래스 < 테스트
def perform
Net::HTTP.get("upload.wikimedia.org", "/wikipedia/commons/thumb/7/73/Ruby_logo.svg/2000px-Ruby_logo.svg.png")
end
end
다음 결과에 대해서는 따로 설명할 필요가 없습니다. 위의 예와 매우 유사합니다.
1.003642 | JRuby | 루비니우스 | |
순차적 | 0.327980 | 0.334000 | 0.329353 |
포크 | 0.104766 | – | 0.121054 |
스레딩 | 0.085789 | 0.094000 | 0.088490 |
또 다른 좋은 예로는 파일 복사 프로세스나 기타 I/O 작업을 들 수 있습니다.