20250104

asyncio와 MySQL 라이브러리의 동시성 처리

 Python에서 asyncio를 사용하여 동시성 코드를 작성할 때, MySQL과 같은 데이터베이스 작업을 처리하는 경우가 많습니다. 하지만 일부 MySQL 드라이버(예: mysqlclient)는 블로킹 I/O 방식으로 동작하기 때문에, asyncio의 이벤트 루프에서 제대로 동작하지 않을 수 있습니다. 이 글에서는 이 문제를 해결하기 위한 방법과 다양한 테스트 결과를 공유합니다.

1. 문제 상황: 블로킹 I/O와 asyncio

1.1 블로킹 I/O란?

블로킹 I/O는 특정 작업(예: 데이터베이스 쿼리)이 완료될 때까지 호출한 프로그램이 멈춰 있는 방식입니다. 예를 들어, mysqlclient는 데이터베이스 작업을 수행할 때 호출한 스레드가 작업이 끝날 때까지 멈춥니다.

1.2 asyncio와의 충돌

asyncio는 비동기 이벤트 루프를 기반으로 동작하며, 코루틴(coroutine)을 통해 논블로킹 방식으로 작업을 처리합니다. 하지만 블로킹 I/O 라이브러리를 사용하면 asyncio의 이벤트 루프가 멈추고, 다른 코루틴이 실행되지 못하는 문제가 발생합니다.

2. 테스트 환경 및 목표

테스트 환경

  • Python 3.7.9
  • 라이브러리:
    • aiomysql (비동기 MySQL 드라이버)
    • mysqlclient (블로킹 MySQL 드라이버)

테스트 목표

  • 동일한 작업(3개의 SELECT SLEEP(X) 쿼리)을 각각의 방식으로 실행하여 성능과 동작 방식을 비교합니다.
  • 블로킹 문제를 해결하기 위해 run_in_executor() 및 기타 방법을 사용합니다.

3. 테스트 코드 및 결과

3.1 aiomysql + asyncio

코드

python
import asyncio import logging import aiomysql logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') async def sleep_async(delay): logging.info(f'{delay}-start') async with aiomysql.connect() as dbconn: async with dbconn.cursor() as cursor: await cursor.execute(f'SELECT SLEEP({delay})') logging.info(f'{delay}-end') loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(sleep_async(2), sleep_async(4), sleep_async(6)))

결과

text
2020-11-04 12:46:04 [INFO] 2-start 2020-11-04 12:46:04 [INFO] 4-start 2020-11-04 12:46:04 [INFO] 6-start 2020-11-04 12:46:06 [INFO] 2-end 2020-11-04 12:46:08 [INFO] 4-end 2020-11-04 12:46:10 [INFO] 6-end

분석

  • 총 실행 시간은 약 6초.
  • 비동기 방식으로 잘 작동하며, 모든 쿼리가 동시에 실행됩니다.

3.2 mysqlclient + asyncio

코드

python
import asyncio from contextlib import closing import logging import MySQLdb as mysql logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') def sleep(delay): logging.info(f'{delay}-start') with closing(mysql.connect()) as dbconn: with dbconn.cursor() as cursor: cursor.execute(f'SELECT SLEEP({delay})') logging.info(f'{delay}-end') async def sleep_async(delay): return sleep(delay) loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(sleep_async(2), sleep_async(4), sleep_async(6)))

결과

text
2020-11-04 12:47:30 [INFO] 2-start 2020-11-04 12:47:32 [INFO] 2-end 2020-11-04 12:47:32 [INFO] 4-start 2020-11-04 12:47:36 [INFO] 4-end 2020-11-04 12:47:36 [INFO] 6-start 2020-11-04 12:47:42 [INFO] 6-end

분석

  • 총 실행 시간은 약 12초.
  • 각 쿼리가 순차적으로 실행되며, 블로킹 문제가 발생했습니다.

3.3 mysqlclient + Thread

코드

python
from threading import Thread from contextlib import closing import logging import MySQLdb as mysql logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') def sleep(delay): logging.info(f'{delay}-start') with closing(mysql.connect()) as dbconn: with dbconn.cursor() as cursor: cursor.execute(f'SELECT SLEEP({delay})') logging.info(f'{delay}-end') threads = [Thread(target=sleep, args=(i,)) for i in range(2, 7, 2)] for thread in threads: thread.start() for thread in threads: thread.join()

결과

text
2020-11-04 12:49:02 [INFO] 2-start 2020-11-04 12:49:02 [INFO] 4-start 2020-11-04 12:49:02 [INFO] 6-start 2020-11-04 12:49:04 [INFO] 2-end 2020-11-04 12:49:06 [INFO] 4-end 2020-11-04 12:49:08 [INFO] 6-end

분석

총 실행 시간은 약 6초이며, 스레드를 사용하여 병렬 처리가 잘 이루어졌습니다.

3.4 mysqlclient + asyncio (run_in_executor)

코드 (기본 실행기 사용)

python
import asyncio from contextlib import closing import logging import MySQLdb as mysql logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') def sleep(delay): logging.info(f'{delay}-start') with closing(mysql.connect()) as dbconn: with dbconn.cursor() as cursor: cursor.execute(f'SELECT SLEEP({delay})') logging.info(f'{delay}-end') loop = asyncio.get_event_loop() futures = [loop.run_in_executor(None, sleep, i) for i in range(2, 7, 2)] loop.run_until_complete(asyncio.gather(*futures))

결과 (기본 실행기 사용)

text
2020-11-04 13:00:05 [INFO] 2-start 2020-11-04 13:00:05 [INFO] 4-start 2020-11-04 13:00:05 [INFO] 6-start 2020-11-04 13:00:07 [INFO] 2-end 2020-11-04 13:00:09 [INFO] 4-end 2020-11-04 13:00:11 [INFO] 6-end

분석 (기본 실행기 사용)

총 실행 시간은 약 6초이며, 블로킹 문제 없이 병렬 처리가 이루어졌습니다.

결론 및 권장 사항

  1. 최적의 선택
    • 가능한 경우 비동기 라이브러리인 aiomysql 또는 asyncmy를 사용하는 것이 가장 효율적입니다.
    • 하지만 성능이 중요한 경우 mysqlclient가 더 빠른 선택일 수 있습니다.
  2. 블로킹 라이브러리 처리
    • run_in_executor() 또는 Python >=3.9의 asyncio.to_thread()를 사용하여 블로킹 코드를 비동기로 전환할 수 있습니다.
    • 스레드풀 크기를 조정해 성능을 최적화할 수 있습니다.
  3. 드라이버 성능
    • 여러 벤치마크에 따르면 mysqlclient는 가장 빠른 MySQL 드라이버입니다.
    • 하지만 비동기 환경에서는 aiomysql이나 asyncmy가 더 적합합니다.

댓글 없음: