asyncio 실운영 1년 반 되어가는 시점의 회고. 3.6에서 시작해서 지금 3.7로 올라와있음. aiohttp 서버, 크롤러, 외부 API 게이트웨이 — 성격 다른 세 개 굴려봤다. 솔직한 얘기 적음.
잘한 것
외부 API를 대량으로 호출하는 게이트웨이 성격의 서비스는 asyncio가 정답이었다. 이전엔 gevent로 돌렸는데 monkey patch 들어가는 순간부터 스택트레이스가 망가지고, 서드파티 라이브러리 중 일부가 patch와 궁합이 나쁨. asyncio로 가니 속 편함. 단일 프로세스에서 동시 5000 커넥션 무리 없음.
잘 못한 것
첫째, 섞은 것. 처음에 "async로 전환" 하려다가 결국 sync 라이브러리 일부 남겨둔 채로 배포했다. 그러면 한 코루틴이 sync I/O로 블록되는 순간 이벤트 루프 전체가 멈춘다. psycopg2 그대로 쓰다가 장애 한 번 났고, 그 후 aiopg로 다 바꿨음. 교훈: 반쯤 async는 async가 아니다.
둘째, CPU bound 작업을 그대로 async 함수로. JSON 파싱, 큰 리스트 정렬 같은 걸 async def에 넣고 await 없이 처리. 이건 그냥 sync 함수보다 느리기만 함. run_in_executor로 빼야 한다는 걸 뒤늦게 학습.
셋째, 에러 핸들링. create_task로 띄운 태스크의 예외를 놓치면 "Task exception was never retrieved" 경고가 로그에 뜨면서 조용히 묻힌다. 태스크 레퍼런스 안 들고 있으면 GC 타이밍에 따라 취소도 됨. 결국 태스크는 전부 등록/트래킹하는 헬퍼 하나 만들어서 돌림.
다시 선택한다면
여전히 asyncio 쓸 것. 다만 프로젝트 초기에 "이 서비스가 async여야 하는 이유"를 명확히 하고 간다. 그냥 "요즘 거니까"로 쓰면 낭패 본다. I/O 바인드면 맞고, CPU 바운드거나 sync DB 락이 중요하면 멀티프로세스 + sync가 낫다.
3.8에 asyncio.run(), named task 기능 추가된다고. 꾸준히 좋아지고 있다.