원저자분의 허락을 얻어 Ruby의 GVL 관련한 글을 번역했습니다.
원문은 https://ivoanjo.me/blog/2022/07/17/tracing-ruby-global-vm-lock/ 이 블로그에서 확인할 수 있습니다. 훌륭한 글을 써준 Ivo Anjo씨에게 다시 한 번 감사드립니다.
최근에 루비의 gvl을 트레이싱하는 새 젬을 만들었다. 이 젬은 루비 스레드가 무엇을 하고 있는지 시각화한다.
원문의 Example 1 링크를 클릭해서 자세히 볼 수 있다. example1.json.gz을 다운 받아 Perfetto UI
로 직접 시각화할 수도 있다. 예시 코드는 example1.rb에서 확인할 수 있다.
흥미롭지 않나? 글의 나머지 부분에서는 위 트레이스에 대한 자세한 이해와 여러분의 코드도 위와 같이 시각화하는 법을 설명할 것이다.
Ruby VM은 주로 C로 작성한 거대한 프로그램이다. (JRuby
나 TruffleRuby
에 대한 논의는 다음 기회에 😁)
개발자가 루비에서 스레드를 만들면 Ruby VM은 이에 대응하는 운영체제의 실제 스레드를 만든다. (어느 시점에 이걸 변경하느냐에 대한 흥미로운 논의가 있다.)
결론적으로, Ruby VM은 거대한 멀티 스레드 C 프로그램이다. 그런 프로그램에서 복잡한 동시성 버그를 피하기 위해서는 여러 스레드가 동시에 동작할 때도 정확한 동작을 보장할 수 있는 전략을 택해야한다. Ruby 개발자들은 Global VM Lock 짧게, GVL이라고 부르는 전략을 선택했다.
잠시 다른 길로 새서,
Global Interpreter Lock
이나GIL
을 들어봤나? Python 커뮤니티에서 유래한 말인데 파이썬은 비슷한 이유로 GVL과 매우 유사한 전략을 선택했다. 단지 파이썬 개발자들은 다르게 이름붙였을 뿐이다.
[역주]
원 저자는 GVL 제거에 관한 논의도 있다고 했는데요, 파이썬 커뮤니티도 GIL 제거에 관한 이야기를 요새 많이 하는게 흥미롭습니다. https://github.com/colesbury/nogil 파이썬과 루비가 건전한 경쟁관계를 이루면서 서로 발전을 주고 받으면 좋겠습니다 ㅎㅎ
GVL은 어떤 스레드가 Ruby 코드를 실행하거나 Ruby VM 구조와 상호 작용해야 할 때마다 해당 스레드가 GVL을 보유해야 하며 다른 스레드는 동일 작업을 수행할 수 없다는 것이다. 따라서 스레드가 오브젝트나 다른 중요한 VM 구조를 동시에 수정하지 않도록 함으로써 Ruby VM의 정확성을 보장한다.
GVL의 결과로, 우리의 루비 애플리케이션에서 동시성(Concurrency)을 볼 수 있지만(한 번에 여러 루비 스레드가 여러 가지 작업을 수행할 수 있음) 병렬성(parallelism)은 볼 수 없다(한 번에 오직 하나의 루비 스레드만 작업을 수행할 수 있다).
[역주] 동시성과 병렬성에 대해서는 https://johngrib.github.io/wiki/concurrency-and-parallel/ 위 아티클을 참고하면 좋습니다.
Ruby 3.0은 동시성을 다루는 여러 변화를 수반한다. Ractor
가 그 중 하나다. 언어와 VM 양 쪽에 도입한 Ractor
는 동시성을 다루는 새로운 방식이다.
Ractor
의 도입과 함께 진행된 주된 리팩토링은 Global VM Lock를 Ractor당 하나의, 그러니까 전체로 보면 여러 개의 Global VM Lock으로 바꾸는 것이었다.
Global VM Lock인데 이제 Global이 아닌 VM Lock이라... 조금 혼란스럽다. 루비의 주요 개발자들도 이 개념과 용어가 이상하다고 생각했고, 이제 더 이상 Global VM Lock이라 부르지 않는다. Ractor
가 가지는 이걸 이제 thread_sched
라고 부르기로 했다. Internal Renaming PR
하지만 대부분의 애플리케이션처럼 Ractor
를 사용하지 않는다면 사실 달라지는 건 없다. Ruby 3 이전의 GVL과 동일한 하나의 thread_sched
가 좌우할 것이다.
소개는 끝났다. 이제 재미있는 부분으로 가보자.
최근, @_byroot (Jean Boussier)는 Ruby VM에 엄청난 기능을 추가했다. 유저들이 직접 GVL의 상태에 대해 볼 수 있도록 한 것인데, 이걸 GVL Instrumentation API
라고 소개했다. 이 API를 통해 우리는 GVL에서 어떤 일이 일어나고 있는지 명확하게 볼 수 있다. 스레드가 GVL을 언제 획득했고, 언제 놓아줬고, 얼마나 오래 잡고 있었고, 다른 스레드는 얼마나 오래 기다리고 있었는지 등을 말이다.
이걸 알게되고, 이 API가 주는 정보들을 시각화하고 싶었다.
이렇게 gvl-tracing 젬이 탄생했다.
매우 간단한 루비 애플리케이션을 예시로 들어, 어떤 식으로 작동하는지 확인해보자.
위는 아래 코드에 대한 시각화이다.
require "gvl-tracing"
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end
GvlTracing.start("example2.json")
other_thread = Thread.new { fib(37) } # 다른 스레드에서 실행
fib(37) # 메인 스레드에서 실행
other_thread.join
GvlTracing.stop
이 코드에서는 두 개의 스레드가 GVL을 위해 경쟁한다. 루비는 각각 100ms
씩 실행시간을 준 다음 정지시키고 다음 스레드로 전환한다. 실행 중인 스레드는 resumed
상태고 기다리는 스레드는 ready
상태임을 볼 수 있다. 실행해야 하는 작업이 있는 태스크가 GVL을 얻는 다음 기회를 기다리고 있다는 것이다.
이게 루비가 동시성을 수행하는 방식이다. 병렬성은 아니다. 두 쓰레드가 할 일이 아주 많아도 번갈아 일을 하기 때문에 오히려 오래 걸리는 결과를 초래한다.
[역주]
동일한 일을 하는데 왜 스레드를 나눈다고 해서 더 오래 걸리는지 궁금하다면 Context Switching에 대해 찾아보길 권장합니다.
스레드의 수를 놀리면, 모든 스레드가 순서가 돌아올 때까지의 간격은 길어진다. 같은 예시를 3개의 스레드로 만들어보자. 각 스레드는 작업에 100ms을 쓰기 때문에, 다른 스레드는 다시 작업 순서가 돌아올 때까지 200ms을 기다려야 한다.
만약 Ractor
를 사용해서 다시 구현해보면 어떨까? 생각대로, 진정한 병렬성을 얻을 수 있다. Ractor 안의 스레드는 독립적이기 때문이다.
Ractor
가 잘 작동한다는 걸 확인할 수 있다. 다른 Ractor
에 속한 스레드는 서로 방해하지 않고 병렬로 동작한다.
이 글은 2022년 7월에 쓰여졌다. gvl-tracing 젬을 쓰는데 가장 큰 어려움은 루비 3.2의 최신 빌드가 필요하다는 것이다. (preview 1은 이미 너무 오래됐다)
Ruby 3.2는 2022년 12월에 출시되므로 그 이후에 이 글을 보고 있다면 stable release를 사용하면 된다. 하지만 그 때까지는, 다음 커맨드를 참고해 개발 버전을 사용해야 한다.
# rvm
rvm install ruby-head
# rbenv
rbenv install 3.2.0-dev
아니면 다음과 같이 도커 이미지를 활용할 수도 있다.
$ cd my_ruby_app/
$ docker run -v $(pwd):/app -it rubylang/ruby:master-focal
root@0e0b07edf906:/# cd app/
root@0e0b07edf906:/app# ruby -v
ruby 3.2.0dev (2022-07-23T12:42:05Z master 721d154e2f) [x86_64-linux]
root@0e0b07edf906:/app# gem install gvl-tracing
Building native extensions. This could take a while...
Successfully installed gvl-tracing-0.1.1
1 gem installed
root@0e0b07edf906:/app# ruby <your app>
위 명령은 현재 폴더를 도커 컨테이너와 공유하므로 평소처럼 앱이나 폴더 내 파일을 사용할 수 있다. (bundler
도!)
gvl-tracing
이용하기gvl-tracing
젬은 start
와 stop
두 개의 API만을 제공한다.
인자로 파일명을 넘겨 트레이싱을 시작할 수 있다. GvlTracing.start('my-test-trace.json')
트레이싱을 멈추기 위해서는 stop
을 호출하면 된다. GvlTracing.stop()
트레이스 결과는 Perfetto UI등에 파일을 업로드해서 볼 수 있다.
gvl tracing은 걸음마 단계에 있고 우린 그걸 통해 루비의 내부에 대해 많은 걸 배울 수 있을지도 모른다. 흥미로운게 있다면 github gist에 올리고 공유하자. @KnuX나 이메일을 통해 편하게 연락해달라.
마지막으로, 멋진 API를 제공해준 @_byroot (Jean Boussier)에게 감사의 말을 전하고 싶다. gvl을 확인하는 다른 방법인 Shopify의 gvltools
젬도 확인해보는 걸 추천한다.
번역에 대해서는 댓글로 달아주시거나 블로그 프로필의 메일로 말씀해주시면 감사하겠습니다. 원 저자분의 블로그에 가면 루비에 관한 다른 흥미로운 글들도 많이 볼 수 있습니다.