asyncio에서 gevent로

 

외주로 맡아서 하던 채팅 서비스의 오픈을 며칠 앞두고 asyncio로 짰던 서버를 버리고 gevent로 갈아탔다. websocket 서버 및 핵심 로직은 한 시간도 안되서 컨버팅이 끝났는데, broadcast 분산 처리를 위한 rabbitmq 연동 부분에서 몇 시간을 까먹었다. 낮에 작업 시작해서 이제 겨우 연동 성공. 이 부분은 의외로 asyncqp가 상당히 편리했던 반면, pika나 py-amqp는 계속 에러가 나서 삽질하다가 결국 kombu에 정착했다. kombu가 좀더 wrapping되어 있기도 하지만 무엇보다 문서가 가장 좋았다. pika는 이 오래된 프로젝트가 왜 이리 문서가 개판인지. py-amqp는 어차피 celery 프로젝트가 관리하는 거고 celery는 kombu 통해서 쓰니까 kombu만 제대로 문서화해놓으면 되겠다고 생각한 모양인지 문서화를 그냥 안한 수준이다. kombu 인터페이스가 꼭 좋은 건 아니었지만 문서가 알아먹기 쉬우니 개념 정리도 잘되었다.

비동기 문제는 그냥 사용자 하나당 rabbitmq 커넥션을 맺으면 별 문제 없는데, 아무리 gevent와 rabbitmq과 커넥션 숫자에 강하다고 해도 하나로 되는 걸 굳이 사용자 수대로 커넥션을 맺고 싶지 않았다. 올바른 코드가 아니니까. 그래서 삽질을 시작했는데, 문제는 커넥션을 이미 consume하고 있으면 다른 greenlet에서 exchange나 queue를 추가로 declare하거나 publish할 수 없다는 거였다.(py-amqp는 publish까지는 가능했다) 그래서 exchange나 queue를 추가할 때는 consume을 멈추고 publish는 별도 커넥션 하나 더 열어서 처리하는 식으로 구성했다. 아무래도 consume을 강제로 스톱했다가 다시 재개하는 코드는 맘에 들지 않지만, 동작은 할 것 같다. 

이 부분은 asyncio가 더 좋긴 하다. asyncio는 커넥션 하나 가지고 여러 개의 코루틴을 돌릴 수 있다. gevent는 io를 시키면 콜백을 걸어놓고 스위치를 한 다음 io 이벤트가 오면 콜백에서 다시 원래 greenlet으로 스위치를 하는 식이라 소켓 하나에 여러 개의 콜백을 걸 수 없으니 여러 greenlet에서 하나의 소켓에 io를 할 수 없는데, asyncio는 코루틴 단위로 suspend/resume이 되니까 여러 개의 이벤트를 걸어놓을 수 있는 것 같다. 그래서 asyncqp는 그냥 커넥션 하나를 자연스럽게 공유해서 쓸 수 있었다.

그래도, 이 삽질을 다 거치고 난 이후의 코드는 물론 gevent+kombu의 압도적인 승리다. asyncio.ensure_future 같은 거 없이 그냥 콜하고 consuming 하는 코드만 gevent.spawn으로 쓰면 되니까 훨씬 깔끔하다.

아직 모든 코드를 전환하지는 못했고, 성능 테스트도 안해봤지만, 코드 품질에서는 asyncio보다 gevent가 압도적으로 좋다.

이 과정을 거치게 된 스토리는 제법 복잡하다. 현재 파이썬은 비동기 기술의 과도기에 있다. 선사시대 twisted에서 시작해서 tornado로 이어지다가 greenlet 스타일의 gevent로 넘어오던 중에 node.js가 등장해서 파이썬 커뮤니티 전체가 자극을 받았고, 뒤이어 나온 asyncio로 비동기 기술 춘추전국시대를 통일하나 했으나, asyncio의 첫 메이저 프레임웍인 aiohttp는 성능이 구렸다. wsgi의 비동기 버전을 목표로 하는 asgi는 복잡하기 그지 없고 구현체인 django channels도 갈 길이 멀어보인다. 한 마디로 asyncio로의 전환은 예고되어 있는 듯해서 gevent를 위시한 예전 기술을 쓰고 싶지는 않지만 그렇다고 새 시대가 아직 와 있는 건 아니라는 것.

clear winner가 없을 때는 직접 만드는 것도 파이썬 개발자들이 해왔던 방식이 아닐까 싶어 awsgi를 만들었다. 의외로 성능도 잘 나오고 6개월간 프로젝트 하는데 별 문제 없이 돌아갔다. 근데 본격적인 로드 테스트를 하니까 여러 가지 문제가 쏟아져 나왔고, 하나하나 해결이 되긴 했지만 불안감이 커졌다. 그러던 와중에 sanic이 awsgi와 몹시 비슷한 목적으로 만들어졌다는 걸 알게 되었다. 해커뉴스에 올라오면서 제법 모멘텀을 얻었는데, 이리저리 살펴보니 아직 production ready라고 하기는 어려웠다. 많은 사람이 관심을 갖고 있고 더 오래된 프로젝트인데도 이 정도면 awsgi에 숨어 있는 문제는 더 많겠지. 그래서 결국 대안을 찾기 시작했다.

uwsgi의 웹소켓과 async 엔진을 써보려고 했지만 uwsgi는 어이 없게도 모듈을 그냥은 import할 수 없고 서버를 띄우면 uwsgi 모듈을 넣어주는 방식이었다. 뭐 그래도 쓸 수만 있으면 되지 않나 싶기도 했지만, 그냥 기분 나빠서 포기했다.

근데 채팅 성능 테스트를 하면서 쓰게 된 gevent를 다시 돌아보기로 했다. gevent는 쓰레드만큼 충분히 쉬웠다. gevent는 node.js가 libev에서 libuv로 넘어갈 때쯤에야 겨우 lilbevent에서 libev로의 전환을 해냈고, asyncio가 등장한 이후 모멘텀을 잃은 듯해서 왠지 쓰기 싫었는데, 코드 자체가 워낙 쉽다보니 다시 검토해볼 가치가 있다고 생각했다. 그렇게 한참을 들여다보다가 gevent와 유사한 방식을 libuv로 해보려고 하는 시도들이 많다는 걸 알게 되었다. 이런 시도들이 성숙해지면 단순히 gevent를 그런 걸로 교체하는 것만으로 성능을 올릴 수 있고, 그 전까지는 그냥 gevent로 행복한 코딩을 할 수 있지 않을까? 이런 고민으로 결국 gevent를 대안으로 선택했다. gunicorn에 올리기도 좋고, uwsgi에도 올릴 수 있을 것 같다.

5~6년 쯤 전인가, node.js로 했던 채팅촌 서비스를 파이썬으로 갈아엎고자 gevent-socketio를 쓴 적이 있었다. 그 때는 상당히 좋다고 생각했는데, 그 뒤로 버전업도 잘 안되는 것 같고 해서 안 썼는데, 사실은 그때 잘 모르고 했던 선택이 제법 최적에 가까웠던 것 같다. node.js에서 cps, promise, fiber를 다 써봤고 파이썬에서도 꽤 여러 가지 비동기 기술을 경험한 셈인데, 이제는 말할 수 있을 것 같다. asyncio는 똥이다. gevent가 정답인지는 모르겠고, 프론트엔드에서의 vue.js만큼의 확신은 없지만, 그래도 비동기 파이썬의 정답에 꽤 근접한 것 같긴 하다. 모멘텀을 잃어가고 있는지라 남에게 추천하기는 어렵지만, 적어도 나는 앞으로 비동기 기술이 필요할 때 계속 gevent를 쓸 것 같다.