FastAPI 내에서

BackgroundTasks와 Celery 사용에 대해 알아보자.

 

 

하나의 물리적 서버(또는 컨테이너)에서 실행되는 경우

모든 프로세스는 같은 물리적 메모리를 공유한다.

 

하지만 운영 체제 수준에서 각 프로세스에 할당되는 가상 메모리는 서로 독립적이다.

 

BackgroundTasks

- FastAPI 애플리케이션은 자체 프로세스 메모리를 갖는다.

- BackgroundTasks는 FastAPI worker 프로세스의 메모리를 직접 사용한다.

(FastAPI worker 프로세스 내에서 실행되기 때문에 FastAPI 애플리케이션과 동일한 메모리 공간을 공유)

 

따라서

작업이 많아지거나 무거워지면 FastAPI 애플리케이션이 과부화될 수 있다.

이는 메모리 사용량이 높아져 worker가 다운되는 상황이 발생할 수 있다.

 

간단하고 빠른 작업에 적합하여

메모리 집약적인 작업은 FastAPI worker를 불안정하게 만들 수 있다.

 

ex) 이메일 전송, 간단한 데이터 처리 등

Celery

FastAPI처럼 분리된 서비스라기보다는 백그라운드 작업을 처리하기 위한 도구로,

FastAPI 내부에서 사용할 수 있거나 별도의 작업 처리 서버(worker)로 동작할 수 있다.

컨테이너를 따로 띄워서 운영할 수 있지만, 운영 방식일 뿐

Celery 자체는 Python 애플리케이션에서 동작하는 라이브러리이다.

 

- Celery worker는 별도의 프로세스로 실행된다.

- FastAPI 애플리케이션과 독립적인 메모리 공간을 갖는다.

 

따라서 FastAPI 애플리케이션의 메모리가 가득 차더라도 Celery worker는 영향을 받지 않고 계속 실행 될 수 있다.

다만, 컨테이너의 전체 메모리가 소진되면 컨테이너 자체가 불안정해질 수 있다.

 

- 복잡하고 메모리 집약적인 작업에 적합

 

ex) 대용량 데이터 처리, 머신러닝 모델 훈련 등


Celery를 사용할 때는 보통 Redis나 RabbitMQ와 같은 message broker도 함께 실행해야 한다.

이 broker는 FastAPI와 Celery 사이의 통신을 관리한다.

 

Celery는 작업을 큐에 넣고 worker들이 그 큐에서 작업을 가져가서 처리하는 역할은 한다.

즉, Celery 자체는 작업을 관리하고 처리하는 역할을 하지만

작업이 큐에 쌓이는 것을 조절하는 기능을 제공하지 않는다.

이 때, Lock 통해 요청을 적절히 제한하는 것이 필요하다

 

Lock을 걸어주는 이유는,

이미 Celery worker가 어떤 작업을 수행 중일 때 동일한 작업을 중복해서 실행하지 않도록 방지하기 위함이다.

Redis 등을 통해 특정 작업이 이미 처리 중인지를 확인하고

Lock이 걸려 있다면 새로운 작업을 요청하지 않는 방식으로 구현할 수 있다.

 

주의할 점은

Redis에서 KEYS * 또는 KEYS all 같은 명령어를 사용하면 모든 키를 검색할 수 있다.

Redis는 키-값 저장소로서 매우 빠르지만, 위와 같은 명령어는 성능에 큰 영향을 줄 수 있다.

 

Redis가 단일 스레드로 동작하기 때문에 KEYS 명령어를 실행하는 동안

Redis 서버는 해당 명령을 처리하기 위해 모든 키를 순회하게 되고 

이 과정에서 Redis의 다른 작업들이 일시적으로 멈추거나 느려질 수 있다.

이로 인해 다른 클라이언트의 요청이 지연되거나 차단될 수 있다.


- 단일 프로세스

Celery worker 자체는 단일 프로세스로 동작한다.

 

- 다중 프로세스

하지만 여러 Celery worker를 실행하면 여러 개의 프로세스가 생성되고

각 worker는 개별적으로 작업을 병렬로 처리하므로, 여러 작업을 동시에 처리할 수 있다.

 

- 멀티프로세싱

멀티프로세싱은 하나의 프로세스 안에서 서브 프로세스를 추가로 생성해 병렬로 작업을 처리하는 방식을 말한다.

Celery 자체적으로는 worker 하위에 멀티프로세싱을 지원하지 않는다.

 

하지만 특정 작업에서 멀티프로세싱이 필요하다면,

Celery worker 안에 직접 서브 프로세스를 만들어서 처리하는 방식을 사용할 수 있다.

 

- 코루틴

Celery worker 내에서 코루틴을 사용하여 단일 프로세스 내에서 여러 작업을 동시에 처리할 수 있다.

 

  Multiprocessing Coroutine
작업 처리 방식 다중 프로세스, 병렬 처리 단일 프로세스, 비동기 동시성 처리
CPU 활용 다중 CPU 코어 사용 가능 단일 CPU 코어만 사용
적합한 작업 유형 CPU 바운드 작업 (대규모 계산 등) I/O 바운드 작업 (네트워크, 파일 입출력)
메모리 사용 프로세스마다 메모리 독립 메모리 공유, 상대적으로 적음
성능 CPU 집약적인 작업에서 더 좋음 대규모 I/O 작업에서 효율적
복잡도 프로세스 간 통신 필요, 복잡도가 높음 단일 프로세스에서 작동, 상대적으로 단순

 

작업 유형에 따라 적합한 동시성 처리 방식을 사용하면 된다.


Celery는 다양한 concurrency 옵션을 제공하여 다양한 유형의 작업을 효율적으로 처리할 수 있게 한다.

 

 

- Prefork Pool (기본)

여러 개의 worker 프로세스를 미리 생성

CPU-bound 작업에 적합

ex) celery -A app worker --concurrency=4

- Eventlet

단일 프로세스 내에서 여러 작업을 동시에 처리

그린 스레드 기반의 비동기 처리

I/O bound 작업에 적합

ex) celery -A app worker --pool=eventlet --concurrency=100

- Gevent

Eventlet과 유사한 그린 스레드 기반 처리

I/O bound 작업에 적합

ex) celery -A app worker --pool=gevent --concurrency=100

- Solo Pool

단일 프로세스로 동작

디버깅이나 특정 상황에서 유용

ex) celery -A app worker --pool=solo

 

 

? 그린스레드 (Green Threads) ?

OS 스레드 (운영체제가 관리하는 실제 하드웨어 스레드)와는 다르게,

사용자 레벨에서 관리되는 경량 스레드이다.

OS가 스레드를 스케줄링하는 대신, Python 인터프리터 내에서 스레드 스케줄링이 이루어진다.

 

  Green Threads (Eventlet, Gevent) Coroutine (async/await)
실행 방식 여러 '가상 스레드'가 단일 OS 스레드 내에서 번갈아 실행됨 명시적인 async/await 구문을 통해 작업을 적환
동시성 관리 주체 라이브러리(Eventlet, Gevent) 내부에서 자동 처리 사용자 코드에서 명시적으로 제어
작업 전환 비동기 작업이 발생하면 그린 스레드 간에 자동 전환 await 시 다른 비동기 함수로 전환
작업 대상 주로 I/O 바운드 작업 주로 I/O 바운드 작업
성능 더 많은 컨텍스트 전환을 처리할 수 있으나,
코루틴보다 약간의 오버헤드가 있음
비교적 가볍고 더 명시적임
적용 범위 Eventlet, Gevent가 설치된 환경에서만 동작 표준 Python 3.5+에서 지원

 

그린 스레드는 하나의 OS 스레드 내에서 실행된다.

즉, 실제로는 단일 OS 스레드가 작업을 수행하지만 이 안에서 그린 스레드들이 교대로 실행된다.

여러 그린 스레드가 돌아가는 것처럼 보이지만 실제로는 하나의 OS 스레드가 전환하면서 처리하고 있는 것이다.

 

이 전환은 비동기 작업(예: I/O 대기)시 자동으로 이루어진다.

Python 자체가 각 그린 스레드의 실행을 관리하는 것이고

각 그린 스레드가 I/O 대시 상태에 있을 때 다른 그린 스레드가 CPU를 차지하여 작업을 이어서 수행한다.

 

반면 코루틴은 함수 수준에서 비동기 코드를 작성한다.

여기서 '함수 수준'이라는 말은 코루틴이 특정 함수 단위로 전환될 수 있다는 것을 의미한다.

 

코루틴에서 async 함수는 CPU-bound 작업을 하지 않고,

특정 시점에서 명시적으로 await을 통해 다른 작업으로 전환된다.

프로그래머가 언제, 어떤 작업을 대기 상태로 만들고, 다른 작업을 재개할지 직접 제어하는 것이 코루틴의 핵심이다.

 

 

즉, 코루틴은 작업이 실행될 때 직접 전환을 요청해야 하는 반면,

그린 스레드는 OS 스레드처럼 자동으로 전환된다.

 

코루틴은 함수가 비동기 작업을 만났을 때만 명시적으로 다른 작업으로 넘어가고,

그린 스레드는 I/O 대기 시 자동으로 다른 작업으로 전환되는 방식이다.

 

# Gevent 예시
import gevent
from gevent import monkey; monkey.patch_all()
import time

def task1():
    print("Task 1 start")
    time.sleep(1)  # Gevent가 자동으로 전환
    print("Task 1 end")

def task2():
    print("Task 2 start")
    time.sleep(1)
    print("Task 2 end")

gevent.joinall([gevent.spawn(task1), gevent.spawn(task2)])
# 코루틴 예시
import asyncio

async def task1():
    print("Task 1 start")
    await asyncio.sleep(1)  # 명시적으로 await을 통해 전환
    print("Task 1 end")

async def task2():
    print("Task 2 start")
    await asyncio.sleep(1)
    print("Task 2 end")

async def main():
    await asyncio.gather(task1(), task2())

asyncio.run(main())

'Python > 파이썬 고급' 카테고리의 다른 글

[Python] lock 사용법  (0) 2023.10.04