[번역] tracing ruby’s (global) vm lock

구경회·2022년 10월 16일
2
post-thumbnail

원저자분의 허락을 얻어 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에서 확인할 수 있다.

흥미롭지 않나? 글의 나머지 부분에서는 위 트레이스에 대한 자세한 이해와 여러분의 코드도 위와 같이 시각화하는 법을 설명할 것이다.

global vm lock이란?

Ruby VM은 주로 C로 작성한 거대한 프로그램이다. (JRubyTruffleRuby에 대한 논의는 다음 기회에 😁)

개발자가 루비에서 스레드를 만들면 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/ 위 아티클을 참고하면 좋습니다.

더 이상 GVL은 없다

Ruby 3.0은 동시성을 다루는 여러 변화를 수반한다. Ractor가 그 중 하나다. 언어와 VM 양 쪽에 도입한 Ractor는 동시성을 다루는 새로운 방식이다.

Ractor에 대한 설명은 다음글을 참고하라. 게시글 1, 게시글 2

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가 좌우할 것이다.

소개는 끝났다. 이제 재미있는 부분으로 가보자.

GVL 트레이싱

최근, @_byroot (Jean Boussier)는 Ruby VM에 엄청난 기능을 추가했다. 유저들이 직접 GVL의 상태에 대해 볼 수 있도록 한 것인데, 이걸 GVL Instrumentation API라고 소개했다. 이 API를 통해 우리는 GVL에서 어떤 일이 일어나고 있는지 명확하게 볼 수 있다. 스레드가 GVL을 언제 획득했고, 언제 놓아줬고, 얼마나 오래 잡고 있었고, 다른 스레드는 얼마나 오래 기다리고 있었는지 등을 말이다.

이걸 알게되고, 이 API가 주는 정보들을 시각화하고 싶었다.

이렇게 gvl-tracing 젬이 탄생했다.

매우 간단한 루비 애플리케이션을 예시로 들어, 어떤 식으로 작동하는지 확인해보자.

Example 2 json, code

위는 아래 코드에 대한 시각화이다.

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 안의 스레드는 독립적이기 때문이다.


Example 3: json, code

Ractor가 잘 작동한다는 걸 확인할 수 있다. 다른 Ractor에 속한 스레드는 서로 방해하지 않고 병렬로 동작한다.

using gvl-tracing: you need ruby 3.2

이 글은 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 젬은 startstop 두 개의 API만을 제공한다.

인자로 파일명을 넘겨 트레이싱을 시작할 수 있다. GvlTracing.start('my-test-trace.json')

트레이싱을 멈추기 위해서는 stop을 호출하면 된다. GvlTracing.stop()

트레이스 결과는 Perfetto UI등에 파일을 업로드해서 볼 수 있다.

share your traces and learnings!

gvl tracing은 걸음마 단계에 있고 우린 그걸 통해 루비의 내부에 대해 많은 걸 배울 수 있을지도 모른다. 흥미로운게 있다면 github gist에 올리고 공유하자. @KnuX나 이메일을 통해 편하게 연락해달라.

마지막으로, 멋진 API를 제공해준 @_byroot (Jean Boussier)에게 감사의 말을 전하고 싶다. gvl을 확인하는 다른 방법인 Shopify의 gvltools 젬도 확인해보는 걸 추천한다.

  • 이 글에 대해 더 이야기해보고 싶다면? 트위터나 이메일(ivo@원글의 도메인)
  • 최신 글들을 놓치기 싫다면? 이메일 구독

번역에 대해서는 댓글로 달아주시거나 블로그 프로필의 메일로 말씀해주시면 감사하겠습니다. 원 저자분의 블로그에 가면 루비에 관한 다른 흥미로운 글들도 많이 볼 수 있습니다.

profile
즐기는 거야

0개의 댓글