서버의 성능 최적화

지난 회사에서는 게임 서버 프로그래머로, 이번 회사에서는 웹 서비스의 서버사이드 엔지니어로 일하고 있습니다. 여러가지 업무 중에 개인적으로 제일 재미있어 하는 부분은 성능을 최적화하는 일입니다. 정확히 무엇이라 설명하기는 어렵지만, 10의 자원을 소모하고 있던 작업을 이제는 1의 자원만 소모해도 처리할 수 있도록 수정하는 것은 코드 품질 개선을 위해 구조를 개선하는 작업과는 또다른 굉장히 재미있는 경험이었기 때문입니다.

게임 서버와 웹 서비스는 얼핏보면 굉장히 달라보이지만, 당연하게도, 컴퓨터의 자원을 소모하며 수행되는 프로그램이라는 점에서 별로 다르지 않은 경험이었습니다. 도메인에 의한 차이로 인해 발생하는 성능 문제보다는 결국 그걸 만드는 사람들이 하게 되는 비슷한 수준의 성능적 실수나 암묵적인 기대로 인해 그런 것은 아닐까요 ;p

면피를 위해 이것이 다시 한 번 제 경험담임을 강조하고 싶습니다. 주제가 주제인만큼 무서운 분들로부터 공격을 많이 받게 될 것 같아서요 :$

  • 대부분은 로직 작성 시 크게 고민하지 않은 채 반복문을 중첩하여 사용하는게, 서비스 기간이 길어짐에 따라 이 수치가 늘어나면서 이 쪽에서 순수히 CPU 자원을 많이 소모하게 되며 발생하는 문제가 있었습니다. 이 정도는 CPU profiling을 통해 쉽게 찾아낼 수 있는 사안이지만, 결국 그 로직이 그 정도의 규모로 실행될 수 있는 시나리오를 돌릴 수 있는 상황이어야겠지요.
  • 위 내용과 비슷합니다만, bulk로 처리할 수 있는 것들을 single로 처리할 수 있는 interface를 먼저 만든 탓에, 혹은 그것이 쉽기 때문에 외부에서 반복문을 통해 collection을 한 건 한 건 처리하게 되는 경우입니다. 이는 persistent layer와 데이터를 교환하는 등의 외부 시스템과 대량의 데이터로 통신해야 하는 경우에 큰 성능 손실을 유발할 수 있습니다.
  • 가끔은 이웃집 주민에게 공격을 받기도 합니다. 갑자기 백신의 구동으로 인해 시스템 자원을 선점 당해 게임에서 지연이 발생한다거나 혹은 모니터링 프로그램이 과도한 보고를 시작하게 되면서 서버가 유저와 통신하기 위한 네트워크 대역폭을 빼앗기는 경우도 있었습니다. Java로 시스템을 구축할 때에는 객체의 생명 주기를 잘못 관리하는 이웃집 스레드에 의해 다같이 GC의 공격을 받게 되는 경우도 있고요.
  • 당연한 이야기이지만 어설픈 최적화 때문에 화를 부르는 경우도 봤었습니다. 단일 요청을 처리할 때의 효율이 최고가 될 수 있도록 많은 부분을 cache로 관리하다가 다같이 stop the world에 갖힌 적도 있고, 조금 더 편리하게 작업하기 위해 메모리를 과도하게 할당했다가 작업량이 늘어나자 out of memory가 발생한 적도 있고, 심지어 작업을 병렬로 처리하기 위해 fork-join을 썼는데 thread pool을 제대로 관리하지 않아 thread가 넘쳐버린 적도 있었지요.
  • 아군인지 적군인지 참으로 판단하기 어려운 외부 라이브러리도 있습니다. 믿었던 json serializer가 특정 객체를 serialization할 때 내부에서 엄청난 자원을 소모하게 된다거나, 믿고 썼던 common utils나 java lambda의 polyfill들이 내부에서 class 수준의 lock을 잡아 전체적인 병렬성을 해쳤던 적도 있었습니다. 결국은 몇 차례의 성능 검사를 통해 각각의 경우들이 밝혀져 각 라이브러리의 release history를 확인하여 수정된 버전으로 바꿨지만 사실 아직도 많은 부분이 그대로 남아있을 지도 모릅니다.

자기가 작성한 부분, 남이 작성한 부분, 가져와서 사용하는 부분, 이미 작성된 기반적인 부분, 그리고 심지어 가끔은 하드웨어까지 모두가 하나로 어우러져 발생하는 문제가 성능이다보니 모든 것은 케바케요 참으로 답이 없는 문제이지 싶습니다. 그나마 다행이란 것은 직접 작성한 부분에서 많은 문제가 발생한다는 것이겠지요 :$

그렇다면 어떻게 하면 이런 것들을 좀 잘 발견할 수 있을까요? 대충 이렇게 생각합니다.

  • 성능을 확인하기 위한 모니터링 도구를 적절히 잘 설치하고 아예 걱정되는 부분에는 직접 측정 코드를 추가합니다. 예를 드러 전에는 vs profiler나 dotTrace를 주로 썼었습니다.
  • 시스템이 제공할 수 있는 기능을 최대한 잘 섞어서, 실 서비스에서 발생할 수 있는 수준의 간섭을 유지한 채로 과부하를 발생할 수 있도록 시뮬레이터를 작성합니다. 어떤 한 가지 동작을 아주 잘못 작성하지 않았다면, 실 서비스에서는 하나의 요청에 의해서보다는 다양한 요청들의 간섭에 의해 과부하가 발생하는 경우가 많기 때문입니다.
  • 제일 중요한 부분인데, 기반 지식을 많이 공부합니다. 그리고 특히 사람들이 주로 실수할 수 있는 부분을 아주 많이 공부합니다. 훌륭한 모니터링 도구와 시뮬레이터의 도움을 받는다고 해도 실제 문제가 되는 부분을 찾으려면 이러한 지식들의 도움을 받아 가설을 세우고 끊임없이 검증을 해야 합니다. 가끔은 소설을 쓴다 싶을 정도로 과도해지기도 하지만, 결국 그렇게 추리와 증명을 반복하다 정말 문제가 되는 지점으로 수렴해 들어갈 수 있다고 생각하기 때문입니다.

기반 지식은 학교 다닐 때 열심히 공부하게 되는 내용을 포함하여, 서점에 많이 있는 소위 성능 관련된 책들에서 많이 학습을 할 수 있습니다. Java로 치자면 GC의 내용부터 시작해서 JIT나 ClassLoader의 부담같은, 정말 살면서 몇 번 겪어보지 않게 될 것 같지만 어느 순간 몇 달을 밤새게 만드는 내용에 대해서까지 잘 공부할 수 있는 책들이 잘 정리되어 있습니다.

그리고 계속 해당 작업을 수행하면서 경험을 쌓으면 그나마 조금씩 더 잘하게 되는 것 같은 느낌을 받습니다. 그리고 그냥 코딩을 진행할 때에도 자연스럽게 좀 더 신경을 쓰면서 작성하게 되는 것 같고, 조금은 모순적이지만, 너무 최적화를 고려하며 코딩하는 것을 경계하고 차라리 추후 더 최적화할 수 있는 요소를 심어두며 적절한 선에서 작업을 그치는 쪽으로 발전해나가게 되는 것 같습니다.

당연히 개발을 진행하다보면 일정 등의 이유로 인해 성능에 대해 제대로 신경을 쓰지 못하게 되는 경우도 많고, 심지어 성능을 신경 쓸 바에야 머신을 더 투입하는 쪽으로 결정하는 경우도 있습니다. 머신을 더 투입해서 문제를 해결하는 것이 쉽지 않은 것을 차치하더라도 기본적인 성능 개선에 그리 신경을 쓰지 않는 것, 혹은 못하는 것은 가슴 아픈 일이라고 생각합니다. 예전 Intel의 틱톡 전략처럼 한 번은 기능 개발을 했으면 한 번은 그것을 최적화하는 데에 자원을 할당하는 것도 괜찮을 것이라고 생각하는데 말이죠.

저는 다행히 운이 좋아 개발 후에 최적화를 수행할 수 있는 기회를 적절히 얻고 있습니다. 유의미한 결과를 낼 때도 있고 그렇지 못할 때도 있지만, 이런 경험을 계속할 수 있다는 것은 정말 큰 행운인 것 같습니다. 이 재미있는 것을 언젠가 깔끔하게 정리해서 하나씩 올려보도록 하겠습니다.


Leave a Reply

Your email address will not be published. Required fields are marked *