본문으로 건너뛰기

[Open WebUI] asyncio.to_thread로 heartbeat DB 쓰기 이벤트 루프 블로킹 해소

PR 링크: open-webui/open-webui#22980 상태: Merged | 변경: +1 / -1

들어가며

async 웹 서버에서 가장 흔한 성능 실수 중 하나는 async 핸들러 안에서 동기 I/O를 호출하는 것입니다. Open WebUI의 WebSocket heartbeat 핸들러에서도 이 문제가 있었습니다. 접속 중인 사용자마다 30초마다 실행되는 heartbeat에서 동기 DB 호출이 이벤트 루프를 블로킹하고 있었습니다.

핵심 코드 분석

Before (socket/main.py):

@sio.on('heartbeat')
async def heartbeat(sid, data):
    user = SESSION_POOL.get(sid)
    if user:
        SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())}
        Users.update_last_active_by_id(user['id'])

After:

@sio.on('heartbeat')
async def heartbeat(sid, data):
    user = SESSION_POOL.get(sid)
    if user:
        SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())}
        await asyncio.to_thread(Users.update_last_active_by_id, user['id'])

왜 이게 좋은가

  • Users.update_last_active_by_id는 SQLAlchemy를 통한 동기 DB 쓰기 작업입니다
  • async 핸들러 내에서 동기적으로 호출하면 해당 DB 쿼리가 완료될 때까지 이벤트 루프 전체가 멈춥니다
  • 동시 접속자가 100명이면 매 30초마다 100번의 이벤트 루프 블로킹이 발생합니다
  • asyncio.to_thread는 별도 스레드풀에서 동기 함수를 실행하므로 이벤트 루프를 블로킹하지 않습니다
  • 반환값을 사용하지 않는 호출이므로 기능적 변경은 전혀 없습니다
  • 같은 코드베이스의 get_event_emitter에서 Chats.* 호출에 이미 사용하고 있는 패턴과 일관성을 맞춘 변경입니다

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글