Celery prefork 모델에서 데이터베이스 연결 풀 문제 발생, 해결 과정 공개
Celery worker의 fork() 동작 방식과 데이터베이스 연결 풀(Connection Pool)의 상호 작용으로 인해 PoolTimeout 오류 발생
Django signal listener 등록을 위한 설정 변경이 문제의 원인으로, AppConfig.ready()에서 데이터베이스 연결 풀이 초기화됨
fork() 이후 자원 공유 문제로 인해 TCP 소켓(TCP Socket), 락(Lock), 스케줄러(Scheduler)가 예상치 못한 방식으로 동작
해결책으로 fork() 전에 데이터베이스 연결을 닫고, 각 자식 프로세스에서 새로운 연결 풀을 생성하는 worker_before_create_process 및 worker_process_init 훅(hook) 사용
fork() 시스템 콜의 동작 원리
본문에서는 fork() 시스템 콜(System Call)이 자식 프로세스를 생성할 때, 메모리를 copy-on-write(COW) 방식으로 복사한다고 설명한다. 즉, fork() 시점에는 메모리를 즉시 복제하지 않고, 쓰기 시점에 복사본을 생성한다. 하지만, 파일 디스크립터(File Descriptor)는 공유되며, TCP 소켓(TCP Socket)과 같은 커널 리소스는 복사되지 않고 공유된다. 이러한 특성 때문에, 데이터베이스 연결 풀(Connection Pool)과 같은 자원을 fork() 이후에 공유하면 예기치 않은 문제가 발생할 수 있다. 특히, TCP 소켓 공유는 데이터베이스 프로토콜을 손상시키고, 락(Lock)과 스케줄러(Scheduler)는 자식 프로세스에서 제대로 동작하지 않아 PoolTimeout 오류를 발생시킨다.
데이터베이스 연결 풀(Connection Pool)의 문제점
문제의 핵심은 fork() 이후 데이터베이스 연결 풀(Connection Pool)이 자식 프로세스에서 제대로 초기화되지 않는다는 점이다. 부모 프로세스에서 생성된 연결 풀은 fork()를 통해 자식 프로세스에 상속되지만, TCP 소켓(TCP Socket), 락(Lock), 스케줄러(Scheduler)와 같은 자원들은 자식 프로세스에서 예상대로 동작하지 않는다. 특히, futex 기반의 락은 copy-on-write로 인해 부모와 자식 간의 키가 달라져 데드락(Deadlock)을 유발하고, 스케줄러는 자식 프로세스에서 존재하지 않아 PoolTimeout 오류를 발생시킨다. 이러한 문제로 인해, 각 Celery worker는 데이터베이스 연결을 획득하지 못하고, 작업 처리에 실패하게 된다.
문제 해결을 위한 훅(Hook) 사용
본문에서는 Celery의 worker_before_create_process 및 worker_process_init 훅(hook)을 사용하여 문제를 해결한다. worker_before_create_process 훅은 fork() 전에 데이터베이스 연결을 닫고, 연결 풀을 파괴하여 자식 프로세스에 불필요한 자원이 상속되는 것을 방지한다. worker_process_init 훅은 각 자식 프로세스에서 새로운 연결 풀을 생성하여, 각 프로세스가 독립적인 데이터베이스 연결을 갖도록 보장한다. 이러한 방식으로, fork() 이후에도 각 worker가 안전하게 데이터베이스에 접근할 수 있도록 데이터 격리 아키텍처(Data Isolation Architecture)를 구현한다.
코드 변경의 영향과 테스트의 중요성
본문에서는 코드 변경이 의도치 않은 결과를 초래할 수 있음을 강조하며, 테스트의 중요성을 역설한다. 특히, Django signal listener 등록을 위한 설정 변경은 기능적으로는 문제가 없었지만, Celery의 prefork 모델과 상호 작용하여 예상치 못한 문제를 발생시켰다. 이러한 사례는, 개별적으로는 문제가 없는 코드 변경이 시스템 전체에 영향을 미칠 수 있음을 보여준다. 따라서, 시스템의 복잡성을 고려하여 회귀 테스트(Regression Test)를 철저히 수행하고, 잠재적인 문제점을 미리 파악하는 것이 중요하다.