[DB] Postgres와 Django의 Connection 관리

App과 DB간 통신은 서비스 성능에 많은 영향을 미칩니다. 본 포스트는 Postgres와 Django의 통신에 핵심인 Connection을 어떻게 확인하고 개선할 수 있는지 알아봅니다.

Postgresql에서 Connection 🐘🔌

메모리

Postgresql에서 connection은 상태와 상관없이 일정 메모리를 사용합니다.
(컨넥션당 약 10MB의 메모리를 소비합니다)
Idle 상태는 어플리케이션의 컨넥션을 잡고 있는 상태입니다. Connection pooler 또한 한 개 이상의 idle connection을 유지 및 소비하고 있습니다.

Postgresql system catalog

Postgresql system catalog에는 RDBMS의 핵심 구성요소인 테이블, 컬럼 등의 스키마 메타데이터내부 부기정보(Bookkeeping info)들을 저장하고 있습니다. system catalog는 일반적인 테이블입니다. 따라서 이런 테이블을 지우고 생성하고 데이터도 추가하거나 지울 수도 있습니다. 일반적으로는 이러한 정보를 수정하면 시스템에 문제를 일으킬 수 있기 때문에 가급적 직접 수정하지 말아야 합니다.

catalog중 pg_stat_activity 에서 현재 실행중인 쿼리와 상태에 대한 정보를 제

select count(*), state from pg_catalog.pg_stat_activity group by 2;
count | state
-------+------------------------------
7 | active
32 | idle
22 | idle in transaction
4 | idle in transaction(aborted)
(4 rows)

위 쿼리를 활용하면 시스템의 컨넥션 상태를 확인하고 개선사항을 정하는데 도움을 줍니다.

Connection state

Active — 현재 쿼리가 실행 중인 상태를 의미하며, 한번에 얼마나 많은 컨넥션이 필요한지를 나타냅니다.

Idle — Application과 DB가 연결되었지만 실제 사용하지 않는 상태를 의미합니다. 대부분의 프레임워크는 이러한 연결을 풀로 관리합니다. 만약 Idle connection 관리가 필요하거나 많은 idle connection으로 문제가 될 경우 PgBouncer와 같은 pooler를 통하여 성능을 향상 시킬 수 있습니다. 링크를 통하여 설정에 대한 자세한 내용을 확인할 수 있습니다.

Idle in transaction — idle과 비슷하지만, transaction이 걸려있는 상태(BEGIN)로서 트렌젝션이 종료되기를 기다리고 있고 아무 작업도 하지 않는 상태를 의미합니다.

Idle in transaction (aborted) — 취소가 된 idle in transaction 상태를 의미합니다

Idle in transaction은 좀 더 독특한면이 있습니다. 확인해야 할 점은 얼마나 컨넥션이 오래되었는지 입니다. pg_stat_activity 카달로그를 통하여 이런 쿼리들의 age를 확인할 수 있습니다. 실행된 지 너무 오래된 경우엔 수동으로 연결을 끊어줄 필요가있습니다.

Statement timeout

일부 오래된 트렌젝션이 며칠, 몇시간, 몇분 동안 지속되는 경우 해당 트렌젝션을 종료하도록 기본값을 설정할 수 있습니다. statement_timeout 설정값으로 지정된 시간보다 오랫동안 실행중인(hang인) 모든 명령을 자동적으로 종료시킬 수 있습니다. 설정 범위는 전역 또는 특정 세션에 할당할 수 있습니다. 설정 단위는 millisecond입니다.

전역 설정

show statement_timeout;
# 설정된 명령어 timeout 시간 조회
Alter database [db_name] set statement_timeout = 60000;

세션범위 설정

set statement_timeout = 30000;

Idle in transaction session timeout

PG9.6 버전 이상부터 idle in transaction 상태의 연결만 timeout을 설정할 수 있는 기능이 추가되었습니다.

전역 설정

show idle_in_transaction_session_timeout;alter database [db_name] set idle_in_transaction_session_timeout = 60000;

세션범위 설정

set idle_in_transaction_session_timeout = 60000;

statement_timeoutidle_in_transaction_session_timeout 모두 오랫동안 실행중인 쿼리와 트렌젝션을 취소하는데 도움을 줄 것입니다.

Django에서 Connection

CONN_MAX_AGE

장고는 기본적으로 모든 요청(Request)마다 컨넥션을 맺고 끊습니다. 장고 설정에서 CONN_MAX_AGE값을 통하여 컨넥션을 지정한 시간 동안 재활용할 수 있습니다. 기본값은 0 으로 되어있으며, 따라서 모든 요청마다 컨넥션을 맺고 끊는 것을 의미입니다.

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'db',
'USER': 'root',
'PASSWORD': 'root',
'HOST': '127.0.0.1',
'PORT': '5432',
'OPTIONS': {
'CONN_MAX_AGE': '60',
}
}
}

자세한 설명은 도큐먼트를 참고하세요.

CONN_MAX_AGE 동작 로직

1. 장고에서 DB 트렌젝션이 생성되면 savepoint가 설정되고, 이 함수는 connection함수를 호출하고 컨넥션을 close할 시간값(close_at)을 설정합니다.

## django/db/backends/base/base.py  row 174def connect(self):
"""Connects to the database. Assumes that the connection is
closed."""
# Check for invalid configurations.
self.check_settings()
# In case the previous connection was closed while in an atomic block
self.in_atomic_block = False
self.savepoint_ids = []
self.needs_rollback = False
# Reset parameters defining when to close the connection
max_age = self.settings_dict['CONN_MAX_AGE']
self.close_at = None if max_age is None else time.time() + max_age
self.closed_in_transaction = False

2. 장고는 일을 수행할 때마다 신호(signal)를 줄 것을 설정하고 그 때마다 수행할 동작을 정할 수 있습니다. signal에서 요청(Request) 전과 후에 connection을 종료시키는 callback함수를 이벤트로 등록해놨습니다.

## django/db/__init__.py row 60
# Register an event to reset transaction state and close connections past
# their lifetime.
def close_old_connections(**kwargs):
for conn in connections.all():
conn.close_if_unusable_or_obsolete()
signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)

3. 해당 함수가 실행시 설정된 시간이 지난 connection은 자동적으로 종료됩니다.

## django/db/backends/base/base.py row 502
def close_if_unusable_or_obsolete(self):
"""
Closes the current connection if unrecoverable errors have occurred,
or if it outlived its maximum age.
"""
if self.connection is not None:
# If the application didn't restore the original autocommit setting,
# don't take chances, drop the connection.
if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']:
self.close()
return
# If an exception other than DataError or IntegrityError occurred
# since the last commit / rollback, check if the connection works.
if self.errors_occurred:
if self.is_usable():
self.errors_occurred = False
else:
self.close()
return
if self.close_at is not None and time.time() >= self.close_at:
self.close()
return

유의할 점

단, 유의할 점은 CONN_MAX_AGE요청에 대한 connection close 시간을 설정하는 것입니다. ORM 쿼리를 실행할 때 맺은 connection에는 영향을 주지 못합니다. 따라서 API 호출이 아닌 배치등의 로직으로 맺어진 connection은 관리가 되지 않을 수 있습니다.

하나의 Thread는 하나의 Connection을 생성하기 때문에 만약 멀티스레드를 사용하여 DB를 호출하는 경우 스레드 개수만큼 컨넥션이 생성됩니다. 일반적으로 배치 프로세스가 종료되면(메인 프로세스가 종료되면) 연결 또한 끊깁니다. 하지만 스레드 구현을 잘못했거나 프로세스가 좀비로 남는 등등 여러가지 이유로 connection이 idle 상태로 남아있을 수 있습니다. 따라서 배치로직 마지막에는 명시적으로 모든 connection을 close하는 것이 좋습니다.

from django import db
db.connections.close_all()

참고링크

Blog https://chrisjune.dev Work for www.29cm.co.kr