루비 3.x 버전업 및 BMT

uchan·2023년 5월 28일
0

배경

루비 2.7.x 버전의 EOL 기간이 2023년 5월 말까지였고 더이상 유지보수를 하지 않는다. 루비 온 레일즈로 서비스를 운영중이기에 해당 소식을 접한 작년 말부터 루비 버전을 3.x 로 업그레이드 하는 준비를 하였고, 2월 말부터 작업착수하여 5월 초-중 안정적으로 서비스에 적용시켰다. 특히 이번 루비 3.x 버전에서는 주버전이 바뀐 만큼 신경써야되는 부분이 많았을 뿐더러 해당 블로그를 참고하였을 때 퍼포먼스 개선에 대한 궁금증이 있었다. 루비 3.x 버전 업그레이드하면서 겪었던 트러블 슈팅 및 벤치마킹 테스트를 통해 성능을 측정한 과정을 공유하고자 한다.

루비 버전업

간단하게 루비 버전이 바뀌면서 변경 사항들을 정리해보았다.

2.7 -> 3.0

루비 3.0 으로 버전업하면서 대체적으로 퍼포먼스가 매우 개선되었다고 한다.

  • MJIT 를 통해 퍼포먼스 향상
    • 단, 레일즈에서는 오히려 캐시미스 이슈로 성능이 악화되는 이슈가 있었다고 보고되었기에 적용하기 힘들것으로 보인다.
  • 동시성 개선
    • Ractor 도입
      • ruby 에서 actor 모델을 간편하게 구현할 수 있도록 지원하는 인터페이스다. 아직은 실험단계라서 프로덕션에 적용하기까지 많은 테스트가 필요하다.
    • Fiber 스케쥴러
      • 보통 I/O 작업할 때 해당 작업이 끝날때까지 블로킹에 의해 다른 작업으로 스위치가 일어나지 않는데 이번에 Fiber 스케쥴러를 통해 특정 파이버에서 다른 파이버로 블로킹 작업 중 끼어들 수 있도록 스케쥴링할 수 있다고한다.
  • 정적타입 도입
    • 루비는 파이썬과 같이 런타임 언어기 때문에 별도의 컴파일을 하지 않아 배포전까지 에러를 잡거나 디버깅이 어렵다. 또한 다른 개발자가 작성한 함수에 리턴 및 인자 타입도 알 수가 없기 때문에 개발 소통에서도 어려움이 있었다.
    • 이번에 RBS 를 통해 정저긍로 타입을 분석하여 이를 극복하려고 하는 거 같다.
  • 키워드 인자 전달 방식 변경(중요)
    • 이번 버전업에서 가장 중요한 변경사항으로서 해당 사항으로 인하여 많은 수정이 필요하였다. 요약하면 다음과 같다.

2.7.6 키워드 전달 방식

루비 2.x 버전에 도입됐던 keyword argument 는 다음과 같이 직접 전달과 Hash 를 이용한 전달 방법이 있습니다.

def func(arg1:, arg2:)
	p arg1, arg2
end

func(arg1: 3, arg2: 5)
# arg1 = 3, arg2 = 5 으로 들어갑니다.

func({arg1: 3, arg2: 5})
# arg1 = 3, arg2 = 5 으로 들어갑니다.

그러나 이러한 방식은 혼란을 야기하였습니다. 아래 예시를 보도록 하겠습니다.

def func(arg1, arg2: 10)
	p arg1, arg2
end

func({arg2: 5})
# arg1 = {arg2:5}, arg2 = 10 으로 들어갑니다.

3.0.0 키워드 전달 방식

루비 3.x 에서는 더이상 Hash 로 키워드를 받지 않습니다. 이전 2.7.6 버전부터 Hash 를 이용한 키워드 전달 방식은 루비 3.x 에서 deprecate 된다고 경고하였습니다. 아래 코드와 코드 결과 이미지를 살펴보겠습니다.

  • 코드
def func(arg1:, arg2:)
    p arg1, arg2
end

func({arg1: 3, arg2:5})
  • 코드 결과

루비 2.7 은 Hash 로 키워드 전달이 가능하나 루비 3.x 에서는 argumentError 가 발생합니다. 루비 3.x 에서 이를 해결하기 위해서는 다음과 같이 변경해주어야 합니다.

def func(arg1:, arg2:)
    p arg1, arg2
end

func(**{arg1: 3, arg2:5}) # splat(**) 을 사용할 것

추가로 보면 좋을 사이트도 소개합니다.

3.0 -> 3.1

  • YJIT 도입
    • 이전 3.0 버전에서 소개되었던 MJIT 가 레일즈와 같이 프로덕션 어플리케이션에서 좋은 성능을 이뤄내지 못한 한계가 존재하였습니다. 이를 극복하고자 shotify 에서 많은 기여를 하여 새로 추가된 YJIT 가 있습니다. 실제 레일즈 벤치마킹 테스트에서도 20~30% 성능 개선을 이뤄냈다고 보고되었습니다.
    • YJIT 는 3.1 에서 실험적으로 도입되었으나 3.2 에서는 프로덕션 단계로서 준비가 되었기에 기본적으로 제공한다고 합니다
  • Psych 젬을 기본적으로 4.0 버전으로 사용합니다.
    • 이로 인하여 기본적으로는 YAML 파일을 로드할 때 unsafe_load 로 불완전한 신택스도 허용했으나 이제는 alias 와 같은 문법도 사용할 수 없습니다.
    • 실제로 이로 인해 사이드킥에서는 delay extension 을 더이상 지원하지 않습니다. (관련 이슈)

3.1 -> 3.2

  • YJIT 가 정식으로 도입되었습니다.
  • ReDos 정규표현식 개선
    • 정규표현식 계산에 memoization 알고리즘을 도입하여 퍼포먼스를 향상시켰습니다.

트러블 슈팅

저는 요약한 루비 버전업 내용을 토대로 코드 수정 작업을 진행하였고 다음과 같은 이슈를 겪었습니다.

간단한(?) 이슈들

  • 키워드 전달 방식 변경
    • 루비마인의 inspect code 기능과 CI 에서 테스트 실패한 케이스들을 보고 수정이 필요한 코드를 찾아 모두 **(splat) 을 사용하여 해결하였습니다.
  • sprocket 젬을 업그레이드하여 루비 3.x 버전과 맞췄습니다.
  • psych 젬 업그레이드로 인하여 YAML.load 에서 발생한 에러를 모두 수정하였습니다.
  • 이외 생략...

중요 이슈

3.1 버전업한 뒤 사이드킥에서 메모리 누수가 발생하였습니다. 원인을 찾기 위해 heapy 젬을 사용하였고, 그 결과 이전 세대에서 메모리가 해제되지 않는다는 것을 확인하였습니다. 조금 더 자세한 내용을 파악하기 위해 heapy 젬에서 제공하는 특정 세대의 디테일을 볼 수 있는 기능을 사용하였고, 전체적으로 살펴보았을 때 duplicated string 및 메모리가 할당된 코드 위치가 마치 스택 트레이서처럼 찍혀있었습니다. (해당 스크린샷은 사내 보안을 위해 올리지 않겠습니다)
이와 관련하여 제가 작업한 코드 중 debug 라는 젬을 추가했던 것을 발견하였습니다. 이는 3.1 버전업하면서 필수 젬이 되어 추가했던 것으로 해당 젬을 별도의 그룹에 속하지 않고 필수 젬으로 인스톨 하였습니다. 이를 development 그룹에 넣어 다시 빌드하였고 배포한 결과 사이드킥 메모리 누수가 잡힌 것을 확인하였습니다.

젬을 업그레이드 및 인스톨할 때는 확실한 테스트를 한 뒤 적용하자!

퍼포먼스 측정

저희 회사는 온라인 테스트 서비스를 제공합니다. 대규모 시험의 경우 사전에 어느 정도의 서버가 필요한지 측정하는 BMT 환경을 구성하였습니다. 이를 활용하여 이번 루비 3.2 버전업 + YJIT 적용 + 사이드킥 6.5.x 버전업이 퍼포먼스에 얼마나 영향을 미쳤는지 측정을 해보았습니다.

resource 스펙

  • redis: t3.medium
  • db: db.r5.4xlarge
  • host: 75대
  • websocket: 36대
  • sidekiq: 75대

테스트 세팅

  • 응시인원: 2500명
  • 캠모드: cam_and_mobile
  • 알고리즘 3문제 + 객/주관식 24문제

3.1 버전

3.2 버전 + YJIT

결과 해석

BMT 는 응시자가 시험에 들어가고(enter), 시험을 시작한 뒤(start), 시험을 마칠 때(finish)까지 총 3단계로 나눠 측정합니다. 이 중 가장 동시 접속자가 몰리는 start 단계를 집중적으로 살펴보았습니다.
위 이미지는 응시자가 시험 시작할 때를 측정한 결과이며, 평균적으로 얼마나 걸리는 지, 그리고 가장 오래 걸렸던 응시자와 약 90~95퍼의 응시자가 얼마나 걸렸는지를 보여줍니다.
제가 중점적으로 봤던 항목은 http_req_duration 로서 3.2 버전 + YJIT 를 통해 2~3배 성능 향상을 이끌어낸 것을 확인할 수 있었습니다.

마무리

패키지를 업그레이드하는 것을 몇 번 해보았지만 이번과 같이 언어의 주버전 업그레이드를 하는 경우는 처음 해보았습니다. 수많은 테스트 및 정성평가를 했음에도 불구하고 배포를 한 뒤 미처 발견하지 못했던 이슈도 많았으며 이로 인해 롤백도 해보았고, 이 후 사이드킥 메모리 이슈와 같은 서비스에 타격이 큰 이슈도 처음 겪어보았습니다. 앞으로도 메이저한 이슈 및 업그레이드를 다뤄야 할 일이 많을텐데 충분히 좋은 거름이 될 경험을 한 거 같습니다. 실무에서 개발한 지 1년 반 정도 밖에 안되었지만 이전에 다뤘던 크론 사이드킥 이전과 더불어 언어 주버전 업그레이드 작업을 할 수 있도록 기회를 주고 믿어준 팀원분들께 감사합니다!

reference

0개의 댓글