Golang vs Rust 퍼포먼스 벤치마킹 썰

Golang vs Rust

Golang vs Rust 퍼포먼스 벤치마킹 썰

나는 현재 stibee.com의 백엔드 엔지니어로 일하고 있다. 초기에는 Java와 Golang을 주 언어로 사용해서 구현했고 현재는 Golang으로만 구현하고 있다.

위에서 말한바와 같이 나는 고돌이며, 편파적인 시각으로 Rust와의 벤치마킹 결과를 공유하고자 한다. (물론 내 능력이 되는대로 최대한 공정함을 유지하려고 했다.)

Go와 Rust는 C/C++정도의 퍼포먼스를 내면서 더 좋은(?) 언어를 목표로 한다고 한다.

stibee.com과 같은 스타트업에게 생산성 좋고 퍼포먼스까지 월등한 언어는 EC2 인스턴스의 타입을 낮추고 서버 수를 줄여주어 금전적인 기여를 하고, 기능 개발의 시간도 줄여주니, 주력 언어를 선택하는 것은 매우 중요하다 하겠다.

Go로 매우 만족하며 지내던 와중, Rust 퍼포먼스가 그렇게 좋다더라. 어떤 오픈소스가 Go에서 Rust로 전환했다더라 라는 말을 듣게되어 호기심이 일게 되었다. 사실 stibee.com 초기에 대세는 폴리글랏이랍시고 Java/Go/Javascript(nodeJS)/Python 등 온갖 언어를 동원해 서버를 하나씩 구성하기도 했었다. 그리고 유지보수의 한계를 느끼고선 바로 Go로 대동단결 하였다. 하지만!! 그렇게 좋다면!! 아니 비교 해보지 않을 수 없지 않은가!!

신년이 도래하고, 약간의 여유가 생겨 rust-lang.org의 공식문서를 가볍게 훑어보니 오~ 하는 느낌이 생겼다. 확실히 “이놈은 퍼포먼스에 목숨정도는 아니더라도 손모가지 정도는 걸었다” 같은 느낌적인 느낌?

Rust와 Golang의 공정한 비교를 위해서 뭐를 비교할까 잠시 고민을 해보았다. 그런데 사실 퍼포먼스라는게 라이브러리를 뭘 쓰냐, 누가 어떻게 구현했냐에 따라 굉장한 차이를 내기 때문에, 뭔가 순수하게 Built-In정도의 함수만으로 벤치마킹을 해봐야하지 않을까? 라는 생각을 하게 되었고, HTTP API를 순수하게 소켓으로 짜보면 되겠다 라는 결론을 내렸다.

뭐 여기까지는 내 생각이고, 하루만에 내가 Rust로 HTTP Server를 짤 수 있겠는가!! 적어도 동시처리는 해줘야 벤치마킹 좀 했다 하는 소리를 듣지 않겠는가?! 라며 자학하던 중…rust-lang.org의 공식 도큐먼트 마지막 부분에 내가 원하던 수준의 딱 알맞은 예제가 있었다! (Thread Pool을 사용한 다중 요청처리)

이거다 싶어 바로 개발서버에 Rust를 설치,(윈도우 PC에 설치했다가 리눅스용 컴파일이 안되서…ㅠ 크로스컴파일이 잘안되는 점이 아쉬웠다) 빌드하고 실행시키는데 성공하였다. 곧장 최대한 비슷하게 Go로 해당 기능을 구현 하였다.

그러곤 저녁이면 할일없이 놀고만 있는(이메일 서비스 특성상) 발송서버들에게 임무를 부여해줬다. “ab(apache benchmark) 설치하고 저기에 부하 좀 때려봐!”

Rust에선 thread 수와 Golang에선 goroutine 수를 조정해가며 튜닝 좀 하고 각자 최대 성능을 낼 수 있는 수치에 맞춘 뒤 부하를 쏘아댔지만…서버(t2.micro) 10대를 동원해도 그를 만족시킬 순 없었다.(cpu를 20%도 안썼다.)

그래서 어쩔 수 없이 각 요청에 “cnt = cnt*cnt + cnt*2 + i” 와 같은 연산을 100만번씩 돌리는 로직을 추가했다. 사실 함수를 몇번 더 호출시켜서 Call Stack이 퍼포먼스에 영향을 주길 바랐다.

그리고 cpu를 95%까지 쥐어 짜내는 서버를 보며 흡족해 한 뒤, 결과를 비교해 보았다.

결과는??? 예상대로 Rust의 승리로 마무리 되었지만, 다시 한번 Go의 위대함을 느낄 수 있었다. 왜냐고?? 내가 실험한 환경에 국한하여 얘기하지만 Rust와 Go의 성능차이는 5%정도로 Rust가 우세하였지만, 동일한 기능을 구현하는데 작성된 코드 수는 Go는 60라인도 채 안되며, Rust는 3배정도 많았다. 그리고 goroutine과 channel의 조합은 진짜 말도 안되게 심플한 동시성 구현을 가능하게 해준다.

사실 Go로 Rust만큼 퍼포먼스를 내고자 여러가지 모드를 구현해서(goroutine pooling이나 channel을 통한 분산처리 등) 테스트 해보았지만 최신 버전의 Go는 단순 무식하게 Request Per Goroutine으로 짜도 별반 다르지 않은 퍼포먼스를 보여주었다. 뭐 말머리에서도 말한바와 같이 Go만 열심히 찬양하기 바빴는데…Rust는 Go보다 CPU를 조금 덜 먹긴 했다. cargo라는 멋진 툴도 있고, 함수형 프로그래밍도 가능하고! 뭔가 GC가 많이 발생할 것 같은 로직이 있으면 Rust의 강점이 좀 더 부각될 듯 싶다만…어려웡…단순 무식 Go가 최고!!

마지막으로 테스트한 코드를 첨부하오니 여러분들도 참고하시기 바랍니다!

https://gist.github.com/plumhj/e6bf749d5ee4e020e592e15f37ad9027