Skia Python 적용기


Skia 와의 첫 만남

모씨 서버 아키텍쳐는 기본적으로 Python + Flask 로 구성되어 있으며, 그 중 카드 생성 프로세스는 Celery 와 ImageMagick + Wand 로 구성되어 있습니다.

모씨를 방문하는 이용자들은 매일 수 백만장의 카드를 업로드 합니다. 실제 모씨가 오픈한 2014년 11월 이후 약 1년 6개월이 넘는 동안 이용자들이 업로드한 카드는 약 4억장에 이릅니다. 이용자들간의 주요 소통 수단이 카드인 만큼 카드 업로드 관련 코드는 꽤나 중요한 부분입니다만 별 탈 없이 안정적으로 운용되어 왔기에 크게 개선의 필요성을 느끼지 못하고 있던 상황이었습니다.

그러던 중 5월 중순에 VCNC 엔지니어링 블로그에 올라온 글을 보게 되었습니다. 비트윈 아키텍쳐에 Skia 를 적용하며 4배 정도의 성능 개선을 이루었다는 말에 굉장히 큰 자극을 받고 모씨 아키텍쳐에도 적용하면 좋겠다…라는 생각을 하게 되었습니다. 하지만 당시 모씨 클라이언트에 굉장히 큰 기능 추가가 있었던 관계로 칸반 보드에 관련 이슈를 붙여놓기만 하고 후일을 기약하기로 했습니다.

시간이 지나고 어느 정도 중요한 작업들을 끝낸 후에 약 1주일 정도 Skia 를 적용할 수 있는 시간을 확보할 수 있었고, 작업을 시작하기로 하였습니다.

작업 시작, 이름 짓기

실제로 Skia 적용 작업을 하기 전에 틈틈이 Skia 관련된 자료들을 찾아보고, C++ 프로젝트를 Python 으로 쉽게 바인딩 할 수 있는 방법들에 대한 조사를 진행했습니다. 그리고 작업을 시작하기 전 다음과 같은 규칙을 정했습니다.

  • Skia 는 문서가 상대적으로 빈약한 관계로, 1주일 안에 완성하지 못할 경우 프로젝트를 포기한다.
  • Python - C++ 바인딩을 해 본 경험이 없으므로 마찬가지로 1주일 안에 완성하지 못할 경우 프로젝트를 포기한다.
  • 꽤나 어려운 작업이 될 것 같으므로 작업이 완료되면 코드를 공개한다.

진행하기로 결정한 만큼 무엇보다 먼저 프로젝트의 이름을 지어야 했습니다. 잠깐 생각한 후 프로젝트 이름을 다음 두 가지로 압축 하였습니다.

  • SIPSkia - Simple Image Processing by Skia
  • GAESkia - Graphic Analyzing Engine by Skia

후보군을 정하고 다음날 오전 스탠드 미팅 때 모씨 팀원들의 의견을 물어보았는데 호불호가 적당히 반반인 것 같아서 SIPSkia 로 그냥 제가 정했습니다.

클론, 테스트

Skia 소스코드를 먼저 다운 받아서 빌드를 진행해 보았습니다. 제가 경험한 빌드 툴은 make/cmake 정도인데 난생 처음보는 gypninja 가 등장하니 정말 시작부터 정신을 못차리겠더군요. 궁금한 부분들은 덮어두고 일단 xcode 빌드와 커맨드라인 빌드를 번갈아 하면서 Skia 프로젝트 자체에 익숙해지려는 시도를 계속 했습니다. 한 두시간 코드를 노려보고 있으니 조금씩 구성이나 상황이 눈에 익기 시작했습니다. 시작이 반이라는데 참 다행이라는 생각이 들더군요.

일단 SampleApp 이라는 프로젝트를 실행해 테스트 해 보면서 예제 중 하나를 골라 그곳에 코드를 심어 테스트를 시작했습니다. 간단하게는 로컬 이미지 파일 로딩 부터 시작하여 필요로 하는 crop, resize 테스트를 금방 해 볼 수 있었습니다. 문제는 그 뒤에 일어났습니다.

마지막으로 변환된 이미지를 jpeg 으로 인코딩 해야 하는데 아무리 코드를 실행해도 제대로 수행이 되지 않는 상황을 마주하게 되었습니다. 디버깅을 진행해도 문제점을 몰라서 이틀 정도 고생을 했는데요, Skia 프로젝트는 master 브랜치를 develop 브랜치로 이용하고 있다는 사실을 뒤늦게 알게 됩니다;; git-flow 브랜치 모델에 익숙해져 있는 상황에서 master 가 당연히 최신 릴리즈겠거니 생각한 저의 실수였습니다. chrome/m52 릴리즈(당시 가장 최신) 로 체크아웃 하여 빌드한 후 jpeg 인코딩이 무리없이 진행되는 것을 확인하였습니다.

당연한 것이긴 하지만 제가 필요로 하는 기능들을 직접 수행해 보았으니 본격적인 작업에 들어가게 됩니다.

모씨 서버에 맞도록

하지만 1주일의 시간 안에 잘 정의된 문서 없이 40만 라인이 넘는 Skia 코드를 분석한 후 관련 코드 작성 방법이나 룰을 파악하는 것은 불가능합니다. 두서 없이 코드를 계속 보다가 skhello.cpp 파일을 발견하고 해당 코드를 기반으로 sipskia 개발을 진행하기로 결정 하였습니다. 간단하게 API 인터페이스를 명세한 후 코드 작성을 진행 하였습니다. 앞서 워낙 삽을 많이 떠서 그런지 이 부분은 크게 어려운 점이 없이 수월하게 진행이 완료 되었습니다. 어느 정도 API 인터페이스가 나온 후에 boost-Python 을 통한 Python 바인딩 작업으로 넘어가게 됩니다.

boost-Python, distutils

흔히들 Python 은 C 와의 접착성이 매우 좋다고들 합니다. 그런데 C++ 라이브러리를 Python 에 붙이는 것은 생각보다 쉽지 않은 것 같더군요. 조사해 보니 많은 대안 중에 boost-Python 을 확인할 수 있었고 잘 모르는 저는 그냥 덥썩 사용하기로 결정 하였습니다. 그와 동시에 distutils 를 이용해서 손쉽게 Python 라이브러리를 만드는 것도 같이 조사해야 했습니다.

API 구조 자체가 복잡하지 않아서(C like) boost-Python 을 적용하는 것은 크게 문제가 되지 않았습니다. 다만 jpeg 바이너리의 경우 바이너리 스트림 중간에 ‘\0’ 문자가 포함되는 경우가 있는데 이를 무시하는 전체 데이터를 PyObject 형태로 넘기는 부분에서 조금 헤맸습니다만 다행히 Python 공식 문서를 보고 해결할 수 있었습니다.

가장 큰 문제는 distutils 를 이용한 cpp 코드 빌드였는데요, 일단 skhello 자체가 ninja 를 통해 빌드되다 보니 빌드 옵션을 전혀 모른다는 문제가 있었습니다. 그렇다고 gyp 가 생성한 Makefile 을 볼 생각은 죽어도 없고 그냥 xcode 를 통한 빌드 메시지를 확인해서 한 줄 한 줄 분석하여 그대로 setup.py 에 때려박아 버렸습니다. 문제는 계속 이어졌는데요, setup.py 를 통한 빌드 시에 정의하지 않은 컴파일 옵션이 계속 붙어다녀서 확인해 보니 distutils 의 버그 아닌 버그라는 것을 웹과 distutils 코드 리뷰를 통해 확인할 수 있었습니다. 약간의 trick 을 써서 컴파일 옵션을 커스터마이징 하였고 빌드에 성공 하였습니다.

마지막으로 릴리즈 빌드 옵션까지 모두 확인한 후에 릴리즈 모드로 빌드 후 최종적으로 sipskia.so 파일을 얻어내는데 성공하였습니다.

성능 테스트

가장 두근거리는 순간이었습니다. ImageMagick 에 비해 얼마나 성능 개선이 있을까? 모씨 이미지 사이즈가 크지 않아서 성능 차이가 없으면 어쩔까? 여러 걱정을 하면서 테스트를 진행 하였는데, 평균 약 4배, 빠를 때는 5배 이상의 성능 차이가 나는 것을 확인할 수 있었습니다.

다음은 메모리 누수 테스트였습니다. objective-C 조차 몇 년 전에 ARC 가 적용된 만큼 회사에서 사용하는 언어들은 모두 managed 언어인 관계로 제가 작성한 코드에 대한 검증을 진행해야 했습니다. 무한 루프로 이미지 변환 작업을 반복시키며 프로세스의 메모리 점유율을 확인하니 역시 메모리가 미친듯이 새어나가더군요. 기분좋게 메모리 누수를 모두 잡고 개발의 8부 능선을 돌파하는데 성공했습니다.

이미지 최적화

다만 금방 해결할 수 있을 줄 알았던 이미지 최적화 문제가 마지막에 발목을 잡았습니다. SkPaint 에 안티 얼라이싱 옵션을 줘도 실제 이미지에 적용이 되지 않는 문제였습니다. 관련된 문서도 전무한 상황이라 상당히 골치아픈 문제였는데 인터넷 어딘가에 굴러다니는 아주 예전 Skia 코드에서 단서를 얻어 imageFilterQuality 를 조정하는 것으로 문제를 겨우 해결할 수 있었습니다.

이때 사실 심적으로 굉장히 쫓기고 있었던 시점이었습니다. 스스로 약속한 1주일이 되었기 때문입니다. 당시 수요일이었고, 일단 8부 능선을 넘었다고 판단한 만큼 금요일까지 2일만 더 작업을 진행해 보고 마무리 되지 않으면 드랍하기로 결정 하였습니다.

포팅, 마지막 난관

대부분의 작업이 완료되었으니 우분투로의 포팅은 단지 시간의 문제라고만 생각했습니다만 꼬박 하루가 걸렸습니다. Skia 는 기본적으로 -fPIC 옵션으로 빌드가 되지 않은데 gyp 와 ninja 기반의 빌드 환경이다 보니 -fPIC 옵션을 어디서 쑤셔 넣어야 할 지 도무지 짐작도 가지 않는 상황이었습니다. 반쯤 의심하며 해 본 GYP_DEFINES=”skia_shared_lib=1” 옵션을 통해 겨우 -fPIE 옵션으로 Skia 를 빌드하는데 성공했으며, boost-Python 도 마찬가지로 힘들게 -fPIC 옵션을 주고 빌드할 수 있었습니다.

마지막으로 오브젝트 파일들을 링크해서 생성한 sipskia.so 파일을 여는데 Bad Value 가 계속 발생할 때는 정말 다 집어치우고 싶었습니다만 어찌어찌 빌드를 마무리 할 수 있었습니다. 거기에 맞추어 setup.py 역시 변경 작업을 마무리 하였습니다.

그 외에 CPU 바이트 오더와 관련된 Skia 의 버그가 하나 있었는데 이 부분까지 설명하자면 너무 길어지므로 생략하겠습니다….

최종 테스트, 그리고 배포

모씨 서버의 celery 코드에 sipskia 관련 코드를 머징하는 작업을 진행하는 것으로 모두 마무리 되었습니다. 최종적으로 목요일 저녁에 모든 작업이 끝났으며, 금요일에 오후에 성공적으로 서버에 배포 하였습니다.

카드 변환-업로드 루틴에서 변환이 차지하는 성능 비중은 약 10% 에 불과하기 때문에 4배의 성능 향상이 있다 하더라도 이게 전체 업로드 퍼포먼스에는 큰 영향을 끼치진 못합니다. 다만 놀라운 것은 이미지 변환 서버의 CPU 사용률 변화입니다. 기존 이미지 변환 서버 대비 약 30 ~ 40% 의 CPU 사용률 감소가 있었습니다. 이 효율이 그대로 비용절감으로 이어진다는 것을 생각한다면 꽤나 놀라운 수치라고 볼 수 있습니다. 다음은 Newrelic 에서 캡쳐한 각각 ImageMagick/Skia 가 적용된 서버들의 CPU 사용률입니다.

ImageMagick 기반 서버의 CPU 사용률

Skia 기반 서버의 CPU 사용률

전체적으로 Skia 가 적용된 서버가 훨씬 여유있는 모습을 보여주고 있습니다.

회고, 오픈 소스

Skia 라이브러리의 놀라운 성능은 CPU 인스트럭션 수준에서의 최적화에 기인합니다. 실제로 Skia 코드를 조금 뜯어보면 어셈블리 코드를 굉장히 많이 발견할 수 있습니다. 개인적으로 시간에 여유가 좀 더 있었다면 더욱 완성도 있는 라이브러리를 만들 수 있지 않았나 생각해 봅니다만 우선 이 정도에도 만족하고 있습니다. 앞으로도 지속적인 성능 개량을 해 나가고자 합니다.

본 프로젝트를 진행하며 참고 문서가 부족하여 벽에 부딪힐 때 마다 새삼 그 동안 이름도 얼굴도 모르는 수 많은 개발자들에게 기대어 개발을 해 왔다는 생각을 하곤 했습니다. 그리고 이번 프로젝트의 결과물이 아무리 하찮고 볼 것 없더라도 코드를 공개하여 혹시 모를 사람에게 도움이 되었으면 하는 마음을 가지게 되었습니다. Skia 에 관심을 가지고 있는 개발자들이라면 이 글과 소스코드가 조금이라도 도움이 되었으면 합니다.

아래 링크를 통해 소스코드를 확인할 수 있습니다.

SIPSkia : Simple Image Processing by Skia

문의 사항이 있으면 moonsoo.kim at nrise.net 으로 메일 주세요.

참고