회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
본문은 요즘IT와 번역가 David가 함께 칼 패터슨(Cal paterson)의 글 <Async Python is not faster>을 번역한 글입니다. 필자인 칼 패터슨은 데이터 처리, 마이크로서비스 아키텍처에 강점이 있는 시니어 개발자로, 금융, 에너지 분야에서의 풍부한 경험을 보유하고 있습니다. 이 글에서는 파이썬 비동기 방식의 성능에 관해 이야기합니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
본문은 요즘IT와 번역가 David가 함께 칼 패터슨(Cal paterson)의 글 <Async Python is not faster>을 번역한 글입니다. 필자인 칼 패터슨은 데이터 처리, 마이크로서비스 아키텍처에 강점이 있는 시니어 개발자로, 금융, 에너지 분야에서의 풍부한 경험을 보유하고 있습니다. 이 글에서는 파이썬 비동기 방식의 성능에 관해 이야기합니다.
필자에게 허락을 받고 번역했으며, 글에 포함된 각주(*표시)는 ‘번역자주’입니다. 글에 포함된 링크는 원문에 따라 표시했습니다.
실제 벤치마크 결과를 보면, 비동기 파이썬이 일반 ‘동기식’ 파이썬보다 오히려 더 느립니다. 더 큰 문제는 비동기 프레임워크들이 부하가 걸리면 불안정해진다는 점이죠.
대부분의 개발자들은 비동기 파이썬이 더 높은 동시성을 제공한다는 것을 알고 있죠. 그래서 동적 웹사이트나 웹 API*를 서빙하는 것과 같은 일반적인 작업에서 더 좋은 성능을 보일 거라고 생각하기 쉽습니다.
*API: 서로 다른 프로그램들이 소통하기 위한 약속된 방식
하지만 안타깝게도 비동기는 파이썬 인터프리터의 성능을 마법처럼 높여주는 해결책이 아닙니다. 실제 상황(아래 참조)에서 테스트해 보면, 비동기 웹 프레임워크는 처리량(초당 요청 수) 면에서 오히려 약간 더 안 좋은 성능을 보이고, 지연 시간의 변동 폭은 훨씬 더 큰 것으로 나타났습니다.
다양한 동기식과 비동기식 웹서버 설정으로 테스트를 진행해 봤습니다.
50번째와 99번째 백분위 응답 시간은 밀리초 단위로, 처리량은 초당 요청 수로 측정했습니다. 실제 서비스 운영에서 가장 중요하다고 생각되는 P99 기준으로 표를 정렬했습니다.
주목할 만한 점들을 정리해 보면:
2. 가장 성능이 안 좋은 것들은 전부 비동기 프레임워크였습니다.
3. 비동기 프레임워크들은 지연 시간의 변동 폭이 훨씬 컸습니다.
4. Uvloop 기반 옵션들은 기본 asyncio 루프보다 더 나은 성능을 보였습니다.
저는 가능한 한 실제 환경과 유사하게 벤치마크를 구성하고자 했습니다. 다음은 제가 사용한 아키텍처입니다.
실제 배포 환경을 최대한 현실적으로 구현했습니다. 리버스 프록시, 파이썬 코드(변수), 그리고 데이터베이스가 포함되어 있습니다. 또한 실제 웹 애플리케이션 배포 환경(특히 PostgreSQL 사용 시)에서 흔히 볼 수 있는 외부 데이터베이스 커넥션 풀러도 추가했습니다.
테스트 애플리케이션은 무작위 키로 행을 조회하여, JSON 형태로 반환하는 작업을 수행합니다. 전체 소스 코드는 GitHub에서 확인할 수 있습니다.
각 프레임워크의 최적 워커 프로세스 수를 결정하기 위해 다음과 같은 단순한 규칙을 적용했습니다: 단일 워커에서 시작하여 성능이 저하될 때까지 워커 수를 점진적으로 증가시켰습니다. 최적의 워커 수가 동기식과 비동기식 프레임워크 간에 차이를 보이는 이유는 명확합니다. 비동기 프레임워크는 I/O 동시성 덕분에, 단일 워커 프로세스만으로도 CPU를 최대로 활용할 수 있습니다.
반면, 동기식 워커의 경우는 다릅니다. I/O 작업 시 해당 작업이 완료될 때까지 블로킹되므로, 부하 상황에서 모든 CPU 코어를 최대한 활용하기 위해서는 충분한 수의 워커가 필요합니다. 이에 대한 자세한 내용은 gunicorn 문서에서 확인할 수 있습니다.
일반적으로 워커 수는 (2 x CPU 코어 수) + 1을 권장합니다. 엄밀히 과학적인 공식은 아니지만, 이는 각 코어당 한 워커가 소켓 읽기/쓰기를 담당하고 다른 워커가 요청을 처리한다는 가정에 기반합니다.
벤치마크는 Hetzner의 CX31 머신 타입(4 "vCPU"/8GB RAM)에서 실행되었으며, 우분투(Ubuntu) 20.04 환경에서 진행했습니다. 부하 생성기는 별도의 (더 작은) VM(가상머신)*에서 실행했습니다.
*가상머신: 물리적인 컴퓨터 하드웨어를 소프트웨어로 구현한 것
처리량(초당 요청 수) 측면에서 주요 요인은 동기와 비동기의 차이가 아닌, 얼마나 많은 파이썬 코드가 네이티브 코드로 대체되었는가입니다. 간단히 말해, 성능에 민감한 파이썬 코드를 더 많이 대체할수록 더 나은 성능을 보입니다. 이는 오랜 역사를 가진 파이썬 성능 최적화 전략입니다. (numpy가 대표적 사례)
지연 시간 문제는 더 근본적입니다. 부하 상황에서 비동기는 좋지 않은 성능을 보이며, 지연 시간이 전통적인 동기식 배포 환경보다 훨씬 더 크게 증가합니다. 이유가 무엇일까요? 비동기 파이썬에서 멀티스레딩*은 협력적(co-operative)입니다. 이는 커널과 같은 중앙 관리자에 의해 스레드가 강제로 중단되는 것이 아니라, 스레드가 자발적으로 실행 시간을 다른 스레드에 양보해야 함을 의미합니다. asyncio에서는 ‘await’, ‘async for’, ‘async with’라는 세 가지 키워드를 통해 실행 권한이 양도됩니다.
*멀티스레딩: 하나의 프로그램 안에서 둘 이상의 실행 흐름(스레드)을 동시에 처리하는 기술
이는 실행 시간이 ‘공정하게’ 분배되지 않으며, 한 스레드가 작업을 수행하는 동안 다른 스레드가 의도치 않게 CPU 시간을 할당받지 못할 수 있다는 것을 의미합니다. 이것이 지연 시간이 더 불안정한 이유입니다.
반면 UWSGI와 같은 전통적인 동기식 파이썬 웹서버는 커널 스케줄러의 선점적(pre-emptive) 멀티프로세싱*을 사용합니다. 이는 주기적으로 프로세스의 실행을 교체함으로써 공정성을 보장하려 합니다. 따라서 시간이 더 공정하게 분배되어 지연 시간의 변동 폭이 더 작습니다.
*멀티프로세싱: 여러 개의 프로세서(CPU)가 각각 독립된 프로세스를 동시에 실행하는 방식입니다.
대부분의 다른 벤치마크(특히 비동기 프레임워크 제작자들의 벤치마크)는 동기식 프레임워크에 충분한 워커를 설정하지 않습니다. 이로 인해 동기식 프레임워크들이 실제로 사용 가능한 CPU 시간의 대부분을 활용하지 못하게 됩니다.
다음은 Vibora 프로젝트의 벤치마크 사례입니다. (이 프레임워크는 상대적으로 인지도가 낮아 테스트하지 않았습니다.)
Vibora는 플라스크(Flask)보다 500% 높은 처리량을 보인다고 주장합니다. 하지만 그들의 벤치마크 코드를 검토해 보니, Flask를 CPU 당 1개의 워커만 사용하도록 잘못 설정한 것을 발견했습니다. 이를 수정하자 다음과 같은 결과가 나왔습니다.
Webserver | Throughput |
Flask | 11925 req/s |
Vibora | 14148 req/s |
또 다른 문제점은 많은 벤치마크가 지연 시간보다 처리량 결과를 우선시한다는 점입니다. (예를 들어, Vibora의 벤치마크는 지연 시간을 아예 언급하지 않음) 그러나 처리량은 머신을 추가함으로써 개선할 수 있지만, 부하 상황에서의 지연 시간은 머신을 추가한다고 해서 개선되지 않습니다. 처리량 증가는 지연 시간이 허용 가능한 범위 내에 있을 때만 의미가 있습니다.
제가 진행한 벤치마크는 구성 요소 면에서는 꽤 현실적이었지만, 실제 워크로드에 비하면 여전히 단순한 편이었습니다. 모든 요청이 동일한 데이터베이스 쿼리를 수행했거든요. 실제 애플리케이션은 훨씬 더 다양합니다. 느린 작업, 빠른 작업, I/O를 많이 쓰는 작업, CPU를 많이 쓰는 작업이 섞여 있죠. 제 경험상 실제 애플리케이션에서는 지연 시간의 변동 폭이 훨씬 더 크게 나타납니다.
복잡한 환경에서는 비동기 애플리케이션의 성능 저하 문제가 더욱 심화될 것으로 예측됩니다. 여러 현장 사례도 이를 뒷받침합니다.
엣시(Etsy)의 댄 맥킨리(Dan McKinley)는 트위스티드(Twisted) 기반 시스템 운영 경험을 공유했는데요. 그들의 시스템은 지연 시간이 심각하게 불안정했다고 합니다.
트위스티드 전문가들도 인정했습니다. 트위스티드가 전체적인 처리량은 좋지만, 일부 요청에서 심각한 지연이 발생할 수 있다는 것을 말이죠.엣시시스템에서는 PHP 프론트엔드가 이 기능을 한 번의 웹 요청에서 수백, 수천 번씩 호출했기 때문에 큰 문제였습니다.
또한 SQLAlchemy 제작자인 마이크 베이어(Mike Bayer)는 몇 년 전 ‘비동기 파이썬과 데이터베이스’라는 글에서 다른 관점으로 비동기를 분석했는데, 역시 asyncio가 덜 효율적이라는 결과를 얻었습니다.
Rachel by the Bay에서는 ‘파이썬, Gunicorn, Gevent에 관한 고찰’이라는 글로 gevent 기반 설정으로 인한 운영 혼란을 설명했습니다. 저도 프로덕션 환경에서 gevent로 고생한 적이 있죠. (성능 문제는 아니었습니다.)
또 한 가지 언급할 점은 이 벤치마크를 설정하는 과정에서 모든 비동기 구현이 성가신 방식으로 문제를 일으켰다는 겁니다. Uvicorn은 자식 프로세스를 종료하지 않은 채 부모 프로세스가 종료되어서, 8001 포트를 잡고 있는 자식 프로세스들을 일일이 찾아 죽여야 했습니다. AIOHTTP*는 파일 디스크립터 관련 내부 오류를 일으켰는데 종료되지 않았죠. (프로세스 감시자가 재시작할 수 없는 상황으로 치명적인 문제입니다.) Daphne도 로컬에서 문제가 있었는데, 어떻게 해결했는지 기억나지 않네요.
*AIOHTTP: 파이썬의 비동기 기능을 활용한 HTTP 클라이언트/서버 프레임워크
해당 오류들은 SIGKILL*을 통해 일시적으로 해결할 수 있었으나, 프로덕션 환경에서 이러한 라이브러리들에 의존하는 코드를 운영하는 것은 위험 부담이 있습니다. 반면, Gunicorn과 UWSGI의 경우 안정적인 운영이 가능했습니다. 다만 UWSGI의 경우, 애플리케이션 로딩 실패 시 프로세스가 정상적으로 종료되지 않는다는 단점이 있습니다.
*SIGKILL: 프로세스를 즉시 강제 종료하는 시그널(신호)
성능 최적화를 위해서는 일반적인 동기식 파이썬을 활용하되, 가능한 한 많은 부분을 네이티브 코드로 구현하는 것을 추천합니다. 웹서버 구현에 있어 높은 처리량이 요구되는 경우, 플라스크 이외의 프레임워크를 고려해 볼 수 있습니다. 다만 UWSGI 기반의 플라스크 구현 역시 우수한 레이턴시 특성을 보여주고 있어, 충분히 경쟁력 있는 선택이 될 수 있습니다.
<원문>
위 번역글의 원 저작권은 Cal paterson에게 있으며, 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다