레이블이 aiomysql인 게시물을 표시합니다. 모든 게시물 표시
레이블이 aiomysql인 게시물을 표시합니다. 모든 게시물 표시

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가 더 적합합니다.