루비 2.7.x 버전의 EOL 기간이 2023년 5월 말까지였고 더이상 유지보수를 하지 않는다. 루비 온 레일즈로 서비스를 운영중이기에 해당 소식을 접한 작년 말부터 루비 버전을 3.x 로 업그레이드 하는 준비를 하였고, 2월 말부터 작업착수하여 5월 초-중 안정적으로 서비스에 적용시켰다. 특히 이번 루비 3.x 버전에서는 주버전이 바뀐 만큼 신경써야되는 부분이 많았을 뿐더러 해당 블로그를 참고하였을 때 퍼포먼스 개선에 대한 궁금증이 있었다. 루비 3.x 버전 업그레이드하면서 겪었던 트러블 슈팅 및 벤치마킹 테스트를 통해 성능을 측정한 과정을 공유하고자 한다.
간단하게 루비 버전이 바뀌면서 변경 사항들을 정리해보았다.
루비 3.0 으로 버전업하면서 대체적으로 퍼포먼스가 매우 개선되었다고 한다.
루비 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.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(**) 을 사용할 것
추가로 보면 좋을 사이트도 소개합니다.
unsafe_load
로 불완전한 신택스도 허용했으나 이제는 alias 와 같은 문법도 사용할 수 없습니다.저는 요약한 루비 버전업 내용을 토대로 코드 수정 작업을 진행하였고 다음과 같은 이슈를 겪었습니다.
YAML.load
에서 발생한 에러를 모두 수정하였습니다.3.1 버전업한 뒤 사이드킥에서 메모리 누수가 발생하였습니다. 원인을 찾기 위해 heapy 젬을 사용하였고, 그 결과 이전 세대에서 메모리가 해제되지 않는다는 것을 확인하였습니다. 조금 더 자세한 내용을 파악하기 위해 heapy 젬에서 제공하는 특정 세대의 디테일을 볼 수 있는 기능을 사용하였고, 전체적으로 살펴보았을 때 duplicated string 및 메모리가 할당된 코드 위치가 마치 스택 트레이서처럼 찍혀있었습니다. (해당 스크린샷은 사내 보안을 위해 올리지 않겠습니다)
이와 관련하여 제가 작업한 코드 중 debug
라는 젬을 추가했던 것을 발견하였습니다. 이는 3.1 버전업하면서 필수 젬이 되어 추가했던 것으로 해당 젬을 별도의 그룹에 속하지 않고 필수 젬으로 인스톨 하였습니다. 이를 development 그룹에 넣어 다시 빌드하였고 배포한 결과 사이드킥 메모리 누수가 잡힌 것을 확인하였습니다.
젬을 업그레이드 및 인스톨할 때는 확실한 테스트를 한 뒤 적용하자!
저희 회사는 온라인 테스트 서비스를 제공합니다. 대규모 시험의 경우 사전에 어느 정도의 서버가 필요한지 측정하는 BMT 환경을 구성하였습니다. 이를 활용하여 이번 루비 3.2 버전업 + YJIT 적용 + 사이드킥 6.5.x 버전업이 퍼포먼스에 얼마나 영향을 미쳤는지 측정을 해보았습니다.
BMT 는 응시자가 시험에 들어가고(enter), 시험을 시작한 뒤(start), 시험을 마칠 때(finish)까지 총 3단계로 나눠 측정합니다. 이 중 가장 동시 접속자가 몰리는 start 단계를 집중적으로 살펴보았습니다.
위 이미지는 응시자가 시험 시작할 때를 측정한 결과이며, 평균적으로 얼마나 걸리는 지, 그리고 가장 오래 걸렸던 응시자와 약 90~95퍼의 응시자가 얼마나 걸렸는지를 보여줍니다.
제가 중점적으로 봤던 항목은 http_req_duration
로서 3.2 버전 + YJIT 를 통해 2~3배 성능 향상을 이끌어낸 것을 확인할 수 있었습니다.
패키지를 업그레이드하는 것을 몇 번 해보았지만 이번과 같이 언어의 주버전 업그레이드를 하는 경우는 처음 해보았습니다. 수많은 테스트 및 정성평가를 했음에도 불구하고 배포를 한 뒤 미처 발견하지 못했던 이슈도 많았으며 이로 인해 롤백도 해보았고, 이 후 사이드킥 메모리 이슈와 같은 서비스에 타격이 큰 이슈도 처음 겪어보았습니다. 앞으로도 메이저한 이슈 및 업그레이드를 다뤄야 할 일이 많을텐데 충분히 좋은 거름이 될 경험을 한 거 같습니다. 실무에서 개발한 지 1년 반 정도 밖에 안되었지만 이전에 다뤘던 크론 사이드킥 이전과 더불어 언어 주버전 업그레이드 작업을 할 수 있도록 기회를 주고 믿어준 팀원분들께 감사합니다!