스포카 / 조회수 : 2230
안녕하세요. 스포카 프로그래머 김재석입니다.
SQLAlchemy는 파이썬 데이터베이스 툴킷으로는 가장 독보적인 수준으로 우아한 기능을 제공하고 있어 많은 사람이 애용하고 있습니다. 스포카에서도 파이썬 프로젝트인데 데이터베이스에 접근해야 한다면 필수로 이용하고 있죠.
오늘은 SQLAlchemy의 연결 풀에 대한 기본 개념과 실전에서 연결 풀링과 관하여 알면 좋을 여러 이슈에 대해 다뤄보고자 합니다.
연결 풀링은 차후에 발생할 데이터베이스 요청에 대비하여 데이터베이스 연결을 캐싱하는 기법입니다. 빈번한 데이터베이스 요청이 여러 사용자에 의해 발생할 때, 매번 연결을 생성하고 닫는 과정을 반복하면 이에 대한 비용이 크기 때문에 이 기법을 사용하여 연결 생성 과정을 줄일 수 있습니다. 짧은 요청이 빈번하게 발생하는 웹 서비스와 같은 형태가 연결 풀과 궁합이 잘 맞습니다.
SQLAlchemy 역시 연결 풀을 기본적으로 채택하고 있는데, 그중 기본으로 제공하는 것은 큐 풀(QueuePool
)입니다. 큐 풀은 설정된 pool_size
와 max_overflow
를 바탕으로 복수의 연결 풀을 구성해서 운용합니다. SQLite를 제외한1 모든 데이터베이스에서 기본값으로 이용하므로, 이 글에서는 큐 풀의 관리 방법을 주로 다루도록 하겠습니다.
pool_size
까지는 더 연결이 필요하지 않은 상황이라도 연결을 종료하지 않습니다.pool_size
까지 다 찼다 할지라도 유효 연결이 없으면 초과하여 하나 생성합니다.pool_size
에 총연결 수를 맞춥니다.QueuePool
이 관리하는 연결이 pool_size
+ max_overflow
까지 다 찬 상황에서 요청이 들어오면, 일단 기다리게 합니다. 기본값으로는 30
초를 기다립니다.TimeoutError
예외를 발생시킵니다.서비스가 작을 때는 기본값이면 충분하지만, 서비스 사용량이 많아지고 규모 문제가 발생하게 된다면 설정을 현재 상황에 맞춰 바꿔주는 게 좋습니다. 보통 QueuePool
관련 위 언급한 2가지 값(pool_size
, max_overflow
)을 바꿔주는 게 좋은데 기본값은 5
, 10
입니다.
pool_size
: 현재 구성에서 연결 생성 부담을 최소화할 수 있는 가장 작은 값이 되어야 합니다.max_overflow
: 현재 구성에서 데이터베이스, 웹 인스턴스가 물리적으로 버틸 수 있는 최댓값이 되어야 합니다.pool_size
가 과하게 설정되어있으면 데이터베이스 입장에서 너무 많은 연결을 점유하고 있으니 비효율적입니다. 그렇다고, 너무 적게 설정한다면 오버플로가 자주 발생하여 풀링으로 얻을 수 있는 효율을 누리지 못합니다. 즉, 파이썬 측에서 비효율적입니다.
max_overflow
가 데이터베이스나 웹 인스턴스의 한계치보다 너무 빡빡하게 잡혀있으면 조금만 사용자 유입이 늘어도 TimeoutError
를 쉽게 만나거나 서비스 속도 저하를 자주 경험하게 됩니다. 그렇다고 무한으로 두면 사용량 폭증시 이해할 수 없는 에러 파티를 경험하게 될 것입니다. (데이터베이스나 파이썬 앱, 혹은 둘 다 드러눕습니다.)
결국 서비스마다 그만의 퍼포먼스와 장비 한계치가 있으니만큼 내부에서 스트레스 테스트를 통한 벤치마킹으로 적정 값을 뽑아내는 것을 추천합니다.
TimeoutError
예외를 발생하며 응답을 안 합니다.SQLAlchemy 쓰는 서비스를 만들어서, 개발 잘 하고 배포했는데 프로덕션에서 잠깐 잘 돌더니 TimeoutError
를 내뱉으며 픽픽 죽어버리는 경험을 많이 하는 것 같습니다. 이 에러 자체는 Session
이 큐 풀에 연결을 받기 위해 기다리다가 못 참고 TimeoutError
를 내는 것인데요. 위의 생애주기 기준, 7번에 해당하는 상황이죠. 큐 풀의 timeout
기본값은 30이니까 30초 동안 풀의 모든 연결이 점유된 상태에서 아무것도 받지 못한 상태가 된 것이라고 보시면 됩니다.
위와 같은 경험이라면 서비스 사용량이 폭증하는 쪽보다는 십중팔구 기존에 점유한 Session
에서 제대로 연결을 반환해주지 않아서 발생하는 문제입니다. 특히 웹서비스라면 Flask 등에서 요청 시마다 Session
이 연결을 불러다 써놓고 Pool에 돌려주는 일을 빼먹는 실수가 잦은데, Flask를 쓰고 계신다면 Flask-SQLAlchemy 등을 쓰셔서 생애주기 관리 자체를 타 라이브러리에 위임하시거나, 현재 구조상에서 요청이 끝나는 시점에 맞춰 session.close()
를 적절히 호출해주시면 됩니다. (사실 Flask-SQLAlchemy가 해주는 것도 딱 이 수준입니다.)
역시 웹서비스 개발하다보면 발생하는 이슈입니다. SQLAlchemy를 쓰면 Session
활용을 암시적으로 하게 될 때가 많습니다. Session
이 실제로 요청을 보내는 시점에서야 연결을 시도하기 때문에, 예상치 못한 기능 변경으로 연결 폭증을 겪는 것인데요. 제가 자주 본 것은 Flask의 생애주기중 before_request
구현에서 데이터베이스에 접근하는 것입니다.
본래 데이터베이스 연결이 필요한 엔드포인트에서만 접속이 발생하던 것이, before_request
에 붙으면서 모든 엔드포인트가 데이터베이스 연결을 하게 되면 사용량이 폭증하기 쉽게 되는데요. 이처럼 전역적인 영역에서 DB 접근을 하는 시나리오를 최소화하는 정책으로 실수를 완화할 수 있습니다.
SQLAlchemy의 연결 풀의 동작 방식을 이해하면 상용 서비스를 운영할 때 발생하는 데이터베이스 부하 문제를 진단하고 해결하는 데 많은 도움이 됩니다. pool_size
와 max_overflow
의 적정값은 서비스에 따라, 인프라의 사양에 따라 다르므로 이를 잘 파악하여 효율적으로 연결 풀이 운영될 수 있도록 세팅하는 것을 추천합니다.
연결 풀을 관리하는 방법으로는 SQLAlchemy내의 기본 큐 풀을 쓰는 것 외에 Pgpool-II과 같은 미들웨어를 연결하는 안도 있습니다. 추후 이에 대해서도 다루어보도록 하겠습니다.
SQLAlchemy 0.7부터 SQLite 같은 파일 기반 데이터베이스에서는 기본적으로 NullPool
을 채택합니다. 파일 기반 데이터베이스에는 네트워크 연결이 일어나지 않기 때문에, 연결 비용이 적기 때문입니다. NullPool
은 이름에서 알 수 있듯이 연결 풀을 유지하지 않고2 풀에 연결이 들어오는 즉시 폐기합니다. ↩
큐 풀의 pool_size
를 0으로 하는 것과 같다고 착각할 수 있으나, 큐 풀은 pool_size
가 0일 때 pool_size
가 무한대인 것으로 인식합니다. 따라서 풀을 만들지 않으려면 NullPool
을 쓰는 것이 적절합니다. ↩
#스포카 #개발팀 #개발자 #인사이트 #업무일지 #후기
관련 스택