FastAPI에서 PostgreSQL을 연결할 때 나는 asyncpg의 Connection Pool을 사용했다.
설정은 다음과 같은 형태였다.
pool = await asyncpg.create_pool(
dsn=DATABASE_URL,
min_size=1,
max_size=50
)
처음 이 설정을 볼 때 max_size는 꽤 단순한 숫자로 느껴졌다.
최대 50개의 Connection을 사용할 수 있다는 의미이고, 동시에 DB를 사용하는 요청이 많아진다면 Connection을 더 많이 열 수 있는 쪽이 유리할 것 같았다.
생각의 흐름도 자연스러웠다.
FastAPI는 여러 요청을 비동기로 처리한다. 그런데 요청마다 PostgreSQL을 사용해야 한다면 결국 Connection이 필요하다. Pool에 사용할 수 있는 Connection이 부족하면 요청이 Connection을 기다리게 된다.
그렇다면 Pool을 크게 잡으면 기다리는 요청이 줄어들 것이고, 결국 API도 빨라지지 않을까?
처음에는 나도 그렇게 생각했다.
그런데 too many clients already 같은 PostgreSQL 연결 문제를 겪고 Connection 구조를 다시 보면서 이 생각에는 빠진 부분이 있다는 것을 알게 됐다.
Connection을 빨리 얻는 것과 Query가 빨리 끝나는 것은 같은 문제가 아니었다.
이 차이를 이해하면서 Connection Pool의 크기를 전과는 조금 다르게 보게 됐다.
Pool에서 기다리지 않으면 어디에서도 기다리지 않는 걸까
Connection Pool의 역할을 아주 단순하게 보면 다음과 같다.
FastAPI 요청이 DB 작업을 하려고 한다.
그러면 Pool에서 Connection 하나를 빌린다.
async with pool.acquire() as conn:
rows = await conn.fetch(
"SELECT ..."
)
작업이 끝나면 Connection은 다시 Pool로 돌아간다.
asyncpg의 공식 문서에서도 create_pool()로 Pool을 생성하고 Pool에서 Connection을 빌려 사용하는 구조를 제공한다. max_size는 Pool이 가질 수 있는 최대 Connection 수다.
여기까지 생각하면 Pool 크기를 늘리는 것이 좋아 보인다.
예를 들어 동시에 30개의 요청이 PostgreSQL을 사용하려고 하는데 Pool이 5개라고 생각해보자.
개념적으로는 일부 요청만 즉시 Connection을 얻고 나머지는 기다려야 한다.
요청 1 ─┐
요청 2 │
요청 3 ├─ Connection 사용
요청 4 │
요청 5 ─┘
요청 6 ─┐
요청 7 │
요청 8 ├─ Connection 대기
... │
요청 30 ─┘
이 모습을 보면 답은 간단해 보인다.
Pool을 5에서 30으로 늘리면 30개의 요청이 모두 Connection을 받을 수 있다.
그렇다면 대기가 사라진다.
여기까지만 보면 분명히 좋아졌다.
그런데 다시 생각해보면 한 가지 질문이 남는다.
Connection을 받은 30개의 요청은 그다음 어디로 가는가?
결국 PostgreSQL이다.
Pool에서 기다리던 요청 25개가 사라진 것이 아니라, 30개의 Query가 PostgreSQL 쪽으로 동시에 들어갈 수 있게 된 것이다.
이 차이를 생각하고 나니 Pool 크기를 단순히 “대기시간을 줄이는 숫자”로 보면 부족하다는 생각이 들었다.
Connection을 많이 열어주는 것이 DB의 처리 능력을 늘려주는 것은 아니었다
PostgreSQL 서버가 어떤 순간에 안정적으로 처리할 수 있는 작업량에는 한계가 있다.
CPU도 유한하고 메모리도 유한하며 디스크 I/O 역시 무한하지 않다.
어떤 Query는 매우 빠르게 끝나지만 어떤 Query는 많은 데이터를 읽거나 정렬해야 할 수도 있다. Lock을 기다리는 Query도 있을 수 있다.
따라서 Connection이 10개 있다고 해서 PostgreSQL의 능력이 10이고, Connection을 100개로 늘리면 능력이 100이 되는 것은 아니다.
이 부분을 생각하면서 나는 Connection Pool을 식당의 좌석과 비슷하게 보게 됐다.
주방에서 동시에 제대로 조리할 수 있는 주문이 10개 정도라고 가정해보자.
손님을 밖에서 기다리게 하지 않으려고 테이블을 10개에서 100개로 늘렸다고 해서 주방에서 음식을 만드는 속도가 10배 빨라지지는 않는다.
오히려 주문 100개가 한꺼번에 주방으로 들어갈 수 있다.
테이블 부족
→ 입구에서 기다림
테이블 대폭 증가
→ 모두 착석
→ 주문이 주방으로 한꺼번에 들어감
손님이 기다리는 위치만 달라졌을 수도 있다.
Connection Pool도 비슷한 관점에서 볼 수 있었다.
Pool이 작다면 요청은 애플리케이션에서 Connection을 기다린다.
Pool을 크게 만들면 더 많은 요청이 PostgreSQL까지 내려간다.
그런데 PostgreSQL이 그 작업들을 충분히 빠르게 처리하지 못한다면 이번에는 DB 내부에서 CPU, I/O, Lock 같은 다른 자원을 놓고 경쟁하게 될 수 있다.
즉,
Pool 대기 감소
가 반드시
전체 API 처리시간 감소
를 의미하지는 않는다.
병목이 Pool에 있었을 때만 Pool을 늘리는 것이 직접적인 해결이 될 수 있기 때문이다.
이 부분에서 내가 처음 했던 질문이 조금 잘못됐다고 느꼈다
처음에는 이렇게 물었다.
Pool을 몇 개로 하면 빠를까?
그런데 이 질문에는 현재 어디가 느린지가 빠져 있다.
예를 들어 요청이 느려지는 이유가 정말 Connection을 기다리기 때문이라면 Pool 크기를 검토하는 것이 맞다.
반대로 Connection은 바로 얻고 있는데 실행하는 SQL 자체가 느리다면 Pool을 늘려도 Query 하나의 실행시간은 그대로다.
또 SQL 하나는 빠른데 동시에 너무 많은 Query가 실행되면서 PostgreSQL의 CPU가 포화되는 상황이라면 Pool을 더 크게 만드는 것이 오히려 도움이 되지 않을 수도 있다.
그래서 질문을 다음처럼 바꿔야 했다.
현재 요청은 Connection을 기다리고 있는가, 아니면 Connection을 얻은 뒤 PostgreSQL 작업이 끝나기를 기다리고 있는가?
비슷해 보이지만 완전히 다른 문제다.
첫 번째라면 애플리케이션 Pool의 문제일 가능성을 볼 수 있다.
두 번째라면 SQL이나 PostgreSQL 서버 상태를 먼저 봐야 한다.
그래서 max_size=50이라는 숫자만 보고는 아무것도 판단하기 어려웠다
내 설정에서는 max_size=50을 사용했다.
pool = await asyncpg.create_pool(
dsn=DATABASE_URL,
min_size=1,
max_size=50
)
이 숫자만 떼어놓고 보면 크다고 할 수도 있고 작다고 할 수도 있다.
그런데 사실 둘 다 근거가 부족하다.
API 요청이 하루에 몇 번 들어오지 않는 서비스라면 50개의 최대 Connection이 필요하지 않을 수도 있다.
반대로 많은 요청이 동시에 들어오는 서비스라면 50개가 부족하게 느껴질 수도 있다.
그런데 동시 요청이 많다고 해서 다시 바로 100이나 200으로 올리는 것도 이상하다.
결국 PostgreSQL이 실제로 감당해야 하는 Query도 함께 늘어나기 때문이다.
그래서 Pool 크기는 요청량 하나만 가지고 결정할 수 있는 값이 아니었다.
적어도 다음 세 가지가 서로 연결된다.
FastAPI 동시 요청량
↓
Connection Pool 크기
↓
PostgreSQL이 동시에 처리하게 되는 DB 작업
FastAPI가 비동기로 1,000개의 요청을 받을 수 있다고 해서 PostgreSQL에도 동시에 1,000개의 Query를 보내야 한다는 의미는 아니다.
오히려 Pool이 그 사이에서 얼마나 많은 DB 작업을 동시에 허용할지를 제어하는 장치로 볼 수도 있다.
이렇게 보고 나니 Pool의 의미가 조금 달라졌다.
Connection을 많이 확보해주는 도구이기도 하지만, 반대로 PostgreSQL로 들어가는 동시 작업량을 제한하는 역할도 할 수 있는 것이다.
처음에는 Pool에서 기다리는 것이 무조건 나쁜 것이라고 생각했다
Connection을 기다리는 요청이 있다는 말을 들으면 자연스럽게 문제가 있다고 생각하기 쉽다.
사용자가 요청을 보냈는데 Pool에 Connection이 없어서 기다린다.
당연히 좋지 않아 보인다.
나도 처음에는 이 기다림을 없애는 것이 성능 개선이라고 생각했다.
그런데 DB 전체를 놓고 보면 일정한 대기는 오히려 시스템을 보호하는 역할을 할 수도 있다는 생각이 들었다.
예를 들어 PostgreSQL이 안정적으로 처리할 수 있는 동시 Query 수보다 훨씬 많은 작업을 애플리케이션이 한꺼번에 밀어 넣는다고 해보자.
Pool이 그것을 제한하지 않는다면 DB는 동시에 훨씬 많은 일을 처리하려고 한다.
그 결과 각 Query의 처리시간이 길어질 수도 있고, 서버 자원 사용량도 올라갈 수 있다.
이때는 모든 요청이 Connection을 바로 얻었다는 사실 자체가 좋은 성능을 의미하지 않는다.
요청들이 Pool 앞에서 조금 기다리는 대신 PostgreSQL에는 일정한 수의 Query만 보내는 구조가 전체적으로 더 안정적일 수도 있다.
이것은 Pool의 대기열이 무조건 좋다는 뜻은 아니다.
대기가 너무 길다면 당연히 응답시간 문제가 된다.
내가 중요하게 느낀 것은:
대기가 있다 = Pool이 무조건 작다.
라고 바로 결론 내리면 안 된다는 점이다.
그 대기를 없앴을 때 실제 병목이 사라지는지, 아니면 PostgreSQL 쪽으로 이동하는지를 확인해야 한다.
PostgreSQL의 max_connections와 Pool의 max_size도 같은 숫자가 아니었다
Connection 문제를 볼 때 자주 같이 등장하는 것이 PostgreSQL의 max_connections다.
PostgreSQL의 현재 공식 문서에서 max_connections는 서버가 허용하는 최대 동시 연결 수를 결정하며, 이 값을 높이면 PostgreSQL이 일부 자원을 더 크게 할당하게 된다고 설명한다.
그래서 처음에는 다음 두 숫자를 비슷하게 보기 쉽다.
asyncpg max_size
PostgreSQL max_connections
하지만 역할은 다르다.
asyncpg max_size는 특정 애플리케이션 Pool이 얼마나 많은 Connection을 보유할 수 있는지 정한다.
max_connections는 PostgreSQL 서버 전체가 받아들일 수 있는 Connection 수의 상한이다.
그리고 PostgreSQL은 내 FastAPI 하나만 사용하는 것이 아닐 수도 있다.
예를 들어 같은 DB에:
검색 API
관리 API
배치 프로그램
운영자가 사용하는 DB 클라이언트
모니터링 프로그램
이 함께 접속할 수도 있다.
그러면 FastAPI Pool 하나가 PostgreSQL 전체 Connection을 독점하면 안 된다.
이런 구조까지 생각하고 나면 max_size=50이라는 숫자는 단독으로 존재하는 설정이 아니었다.
PostgreSQL 전체 Connection 예산 중 이 애플리케이션에 얼마를 허용할 것인지에 가까워진다.
이 생각 때문에 max_connections를 무작정 크게 올리는 것도 다시 보게 됐다
too many clients already 같은 오류를 보면 가장 직접적인 해결책은 PostgreSQL의 max_connections를 올리는 것이다.
실제로 더 많은 정상 연결이 필요한 시스템이라면 조정이 필요할 수 있다.
하지만 연결 문제가 생겼다는 이유만으로 상한을 크게 올리는 것은 조금 다른 문제다.
PostgreSQL 공식 문서에서도 max_connections를 높이면 관련 서버 자원 할당이 증가한다고 설명하고 있고, PostgreSQL 자체가 메모리 부족의 원인인 상황에서는 너무 많은 DB 연결을 허용하기보다 Connection Pooling을 사용하는 것이 나을 수 있다고 안내한다.
이 문서를 다시 보면 Connection Pool의 목적을 이해하기가 더 쉬워진다.
Pool은 Connection 생성 비용을 줄이는 용도도 있지만 DB 서버로 들어갈 수 있는 Connection 수를 애플리케이션 쪽에서 관리하는 역할도 한다.
그런데 애플리케이션 Pool을 무작정 크게 만들고 PostgreSQL의 max_connections도 계속 크게 만들면 두 제한 장치를 모두 느슨하게 만드는 셈이다.
그러면 당장의 Connection 부족은 사라질 수 있어도:
왜 이렇게 많은 Connection이 필요해졌는가?
라는 질문은 해결되지 않는다.
그래서 지금 다시 같은 상황을 본다면 숫자를 올리기 전에 실제 Connection 상태부터 확인하려고 할 것 같다.
여기서 pg_stat_activity가 필요한 이유가 생겼다
Pool 설정만 보고 있으면 애플리케이션이 최대 몇 개까지 사용할 수 있는지는 알 수 있다.
하지만 실제로 지금 몇 개를 사용하고 있는지는 알 수 없다.
예를 들어:
max_size=50
이라고 되어 있어도 실제 서비스가 평소 3개만 사용하고 있을 수 있다.
반대로 트래픽이 몰릴 때 대부분의 Connection을 계속 사용하고 있을 수도 있다.
그래서 PostgreSQL에서 현재 연결 상태를 확인해야 한다.
PostgreSQL의 pg_stat_activity는 서버 프로세스별 현재 활동을 보여주며 state, 현재 Query 등의 정보를 확인할 수 있다.
예를 들어 상태별로 Connection을 확인해볼 수 있다.
SELECT
state,
COUNT(*)
FROM pg_stat_activity
GROUP BY state;
처음에는 이 결과에서 idle이 많으면 나쁜 것이라고 생각하기 쉽다.
그런데 Connection Pool을 사용하고 있다면 사용이 끝난 Connection을 바로 끊지 않고 다시 사용하기 위해 유지할 수 있다.
따라서 idle이 존재한다는 사실만 가지고 Connection 누수라고 판단하면 안 된다.
내가 정말 알고 싶은 것은 조금 다르다.
현재 보이는 Connection 수가 내가 구성한 Pool과 서비스 구조로 설명 가능한가?
예를 들어 Pool은 하나라고 생각했는데 예상보다 훨씬 많은 동일 애플리케이션 Connection이 보인다면 그때는 Pool 생성 위치나 worker, 컨테이너 수를 다시 확인할 이유가 생긴다.
즉 pg_stat_activity는 단순히 숫자를 세는 도구라기보다 내가 생각한 시스템 구조와 실제 DB 상태가 일치하는지를 비교하는 도구가 된다.
Pool이 부족한지 확인하려면 DB Connection 수만 봐서도 부족하다
여기서 다시 한 단계 더 생각해야 했다.
PostgreSQL에서 Connection을 10개 사용하고 있다는 사실만으로 Pool이 충분한지 부족한지를 결정할 수도 없다.
예를 들어 Pool 최대값이 10이고 항상 10개가 사용 중이라고 하자.
그럼 Pool을 20으로 늘려야 할까?
직감적으로는 그렇다.
그런데 10개의 Connection이 왜 모두 사용 중인지 알아야 한다.
각 Query가 10ms 만에 끝나는데 요청량이 너무 많아서 계속 10개가 사용 중인 것인지,
아니면 Query 하나가 5초씩 걸려서 Connection이 오랫동안 반환되지 않는 것인지에 따라 대응이 완전히 다르다.
두 번째 상황에서 Pool을 20으로 늘리면 느린 Query를 동시에 20개 실행할 수 있게 될 뿐이다.
근본 원인은 여전히 Query가 5초 걸린다는 것이다.
이 지점에서 나는 Pool 포화가 원인일 수도 있지만 결과일 수도 있다고 생각하게 됐다.
느린 Query 때문에 Connection이 오랫동안 반환되지 않는다.
그 결과 Pool이 가득 찬다.
새 요청은 Pool을 기다린다.
겉으로 보면:
Pool 대기가 길다
이지만 시작점은:
느린 Query
였던 것이다.
따라서 Connection Pool만 측정해서도 충분하지 않다.
그래서 DB 문제를 보면 Query 시간도 같이 봐야 했다
PostgreSQL에서는 pg_stat_activity로 현재 실행 중인 활동을 볼 수 있고, pg_stat_statements를 사용하면 SQL 문의 planning과 execution 통계를 추적할 수 있다. 현재 PostgreSQL 문서에서도 pg_stat_statements를 SQL 실행 통계를 추적하는 모듈로 제공하고 있다.
이 기능들이 의미 있는 이유가 여기 있다.
내가 알고 싶은 것은 단순히:
Connection이 몇 개인가?
가 아니다.
가능하면:
그 Connection들이 무엇을 하고 있는가?
까지 알아야 한다.
예를 들어 Pool이 계속 가득 차는데 특정 Query 실행시간이 매우 길다면 Pool 크기를 늘리기 전에 그 Query를 보는 것이 맞을 수 있다.
반대로 Query는 충분히 빠르고 PostgreSQL 자원도 여유가 있는데 요청이 Connection을 기다리는 시간이 크다면 그때는 Pool 크기를 조정할 근거가 조금 더 생긴다.
즉 Pool 크기를 결정하려면 Connection 대기와 Query 실행시간을 분리해서 봐야 한다.
여기서부터 성능 측정을 어떻게 해야 할지도 조금 명확해졌다
처음에는 Pool 크기를 테스트한다면 다음처럼 하면 될 것 같았다.
max_size=5
→ 응답시간 측정
max_size=10
→ 응답시간 측정
max_size=20
→ 응답시간 측정
max_size=50
→ 응답시간 측정
그리고 가장 빠른 숫자를 선택한다.
이 방식도 의미는 있다.
하지만 결과만 가지고는 왜 빨라졌거나 느려졌는지 설명할 수 없다.
예를 들어 max_size=20에서 가장 빠르게 나왔다고 하자.
왜 20일까?
10에서는 Connection 대기가 있었기 때문인가?
50에서는 PostgreSQL 동시 Query가 늘어 서버가 더 바빠졌기 때문인가?
우연히 테스트 당시 트래픽이 달랐던 것인가?
원인을 알 수 없다.
그래서 지금 다시 테스트한다면 Pool 크기와 함께 최소한 세 종류의 시간을 나눠 보고 싶다.
1. Pool에서 Connection을 기다린 시간
2. Connection을 얻은 뒤 Query가 끝나는 시간
3. 전체 API 응답시간
이렇게 하면 훨씬 많은 것을 설명할 수 있다.
예를 들어 실제 측정에서:
Pool 5
→ Connection 대기 큼
→ Query 시간 안정적
Pool 20
→ Connection 대기 감소
→ Query 시간 안정적
Pool 50
→ Connection 대기 거의 없음
→ Query 시간이 크게 증가
라는 패턴이 나온다면 어떤 일이 일어나고 있는지 추론할 수 있다.
위 숫자와 결과는 설명을 위한 예시일 뿐 실제 측정 결과는 아니다.
하지만 이런 식으로 측정해야 “20이 빨랐다”가 아니라 “왜 20에서 빨랐는가”를 설명할 수 있다.
바로 이런 데이터가 내가 앞으로 글에 남기고 싶은 부분이다.
평균 응답시간만 봐도 또 놓치는 것이 생긴다
Pool 설정을 바꾼 뒤 평균 API 응답시간만 비교하는 것도 부족할 수 있다.
예를 들어 대부분 요청은 빠른데 일부 요청만 Connection을 오래 기다린다고 하자.
평균값에서는 그 현상이 약하게 보일 수 있다.
그래서 실제 부하 테스트를 한다면 평균뿐 아니라 p50, p95 같은 percentile도 같이 보고 싶다.
특히 p95가 크게 증가한다면 일부 사용자가 꽤 긴 대기를 경험하고 있다는 의미로 해석할 수 있다.
여기에 동시 요청량도 고정해야 한다.
동시 요청 10
동시 요청 30
동시 요청 50
을 섞어서 실행하면 Pool 크기 때문인지 요청량 차이 때문인지 알기 어렵기 때문이다.
결국 실험의 핵심은 가능한 한 다른 조건을 고정하고 Pool 크기 하나만 바꾸는 것이다.
그리고 FastAPI worker가 여러 개라면 다시 계산이 달라진다
이전에 Connection 문제를 보면서 가장 헷갈리기 쉬웠던 부분이 이것이었다.
코드에는 하나의 설정만 보인다.
max_size=50
그래서 애플리케이션이 최대 50개의 Connection을 사용하는 것처럼 느껴진다.
하지만 여러 프로세스에서 각각 Pool을 생성하는 배포 구조라면 각 프로세스에 별도의 Pool이 존재할 수 있다.
asyncpg Pool은 Python 애플리케이션에서 생성하는 객체이므로 배포 프로세스가 여러 개라면 Pool의 총 규모를 프로세스 구조와 함께 봐야 한다. asyncpg 공식 문서에서 Pool 자체가 애플리케이션이 생성하고 Connection을 빌리는 객체라는 점을 확인할 수 있다.
따라서 Pool 크기를 테스트할 때도:
max_size
만 기록하면 부족하다.
프로세스 수
컨테이너 수
동일 DB를 사용하는 서비스 수
까지 당시 조건을 기록해야 나중에 그 결과를 다시 해석할 수 있다.
그렇지 않으면 몇 달 뒤:
Pool 20이 가장 좋았다.
라는 기록만 남고, 당시 worker가 몇 개였는지 몰라 같은 설정을 재현하기 어려워질 수 있다.
지금 생각하면 Pool 크기는 “성능 설정”보다 “동시성 예산”에 가까웠다
처음에는 max_size를 성능 설정으로 생각했다.
숫자를 높이면 더 많은 요청을 처리하고, 숫자를 낮추면 느려질 것이라고 생각하기 쉬웠다.
그런데 FastAPI와 PostgreSQL 사이의 전체 흐름을 보면서 조금 다른 표현이 더 적절하다고 느꼈다.
Pool 크기는 애플리케이션이 PostgreSQL에 동시에 몇 개의 작업을 맡길 수 있도록 허용할 것인지 정하는 예산에 가깝다.
예를 들어 Pool이 20이라면 동시에 100개의 API 요청이 들어오더라도 DB Connection이 필요한 모든 요청이 동시에 PostgreSQL로 내려가는 것을 어느 정도 제한할 수 있다.
FastAPI 동시 요청 100
↓
Connection Pool 20
↓
PostgreSQL 동시 DB 작업 제한
물론 Query 사용 방식과 Connection을 잡고 있는 시간에 따라 실제 모습은 훨씬 복잡하다.
하지만 이 관점으로 바꾸고 나니 왜 Pool을 무조건 크게 만들면 안 되는지가 이해됐다.
Pool은 요청을 빨리 보내기 위한 통로이면서 동시에 DB를 보호하는 경계가 될 수도 있다.
그렇다면 적절한 Pool 크기는 얼마일까
여기까지 읽으면 결국 이 질문이 남는다.
그래서 5가 좋은가, 10이 좋은가, 20이 좋은가, 50이 좋은가?
이 글에서 특정 숫자를 정답처럼 말하고 싶지는 않다.
내가 실제로 사용한 값은 min_size=1, max_size=50이었지만, 이 값이 모든 서비스에서 최적이라는 의미는 전혀 없다.
오히려 지금 다시 결정한다면 처음부터 50을 정하고 시작하기보다 작은 값에서 부하를 점차 늘리면서 어디에서 병목이 발생하는지를 볼 것 같다.
Pool 크기를 늘릴 때마다 다음 질문을 한다.
Connection 대기가 실제로 줄었는가?
그다음:
그 결과 전체 응답시간도 줄었는가?
다시:
PostgreSQL Query 시간이나 CPU/I/O는 악화되지 않았는가?
그리고:
Pool을 더 늘렸을 때 얻는 개선이 여전히 있는가?
이런 식으로 보면 어느 순간부터 Pool을 늘려도 개선 폭이 거의 없어지는 지점이 생길 수 있다.
그 이후는 더 많은 Connection이 실질적인 성능 향상을 만들지 않는 구간일 가능성을 생각할 수 있다.
결국 찾고 싶은 숫자는 가장 큰 Pool이 아니다.
현재 트래픽에서 PostgreSQL을 과도하게 밀어붙이지 않으면서 Connection 대기도 받아들일 수 있는 수준으로 유지하는 Pool 크기다.
그래서 실제 실험을 한다면 이런 글을 다시 쓰고 싶다
현재 글에서는 과거의 max_size=50이라는 실제 설정과, 그 설정을 다시 보면서 알게 된 구조를 중심으로 정리했다.
하지만 여기에서 한 단계 더 가려면 실제 측정이 필요하다.
같은 API를 대상으로:
max_size=5
max_size=10
max_size=20
max_size=50
을 순서대로 적용한다.
동시 요청 수도 고정한다.
그리고 각 조건에서:
Pool acquire 대기시간
SQL 실행시간
전체 API 응답시간
p50
p95
PostgreSQL Connection 수
PostgreSQL CPU 사용량
을 함께 남긴다.
그 결과가 나온다면 그때는 정말 다음과 같은 제목의 글을 쓸 수 있다.
FastAPI asyncpg Pool을 5·10·20·50으로 바꿔 직접 측정해봤다.
그리고 그 글에서는:
20이 좋았다.
라고만 쓰지 않을 것이다.
왜 20까지는 좋아졌고, 왜 그 이후에는 개선되지 않았는지를 데이터로 설명할 수 있어야 한다.
그때부터 비로소 Connection Pool 튜닝이라는 말을 쓸 수 있다고 생각한다.
마무리
FastAPI에서 DB Connection Pool을 처음 설정할 때 나는 max_size를 비교적 단순하게 봤다.
동시 요청이 많다면 Connection도 많을수록 유리할 것이라고 생각하기 쉬웠다.
실제로 Pool이 너무 작아서 요청이 Connection을 기다리고 있다면 크기를 늘리는 것이 성능 개선으로 이어질 수도 있다.
하지만 그 다음 단계를 생각하지 않으면 설명이 부족하다.
Pool에서 Connection을 얻은 요청은 결국 PostgreSQL로 간다.
PostgreSQL의 CPU와 메모리, I/O 처리 능력이 Pool 크기를 늘린다고 같이 커지는 것은 아니다.
따라서 Pool에서의 대기를 없애면서 DB 쪽 경쟁을 늘리는 상황도 생각해야 한다.
이 관점에서 보면:
Connection을 빨리 얻었다
와
요청이 빨리 끝났다
는 서로 다른 결과다.
그리고:
Pool이 꽉 찼다
라는 현상도 반드시:
Pool이 너무 작다
를 의미하지 않는다.
느린 Query가 Connection을 오래 점유한 결과일 수도 있다.
그래서 지금은 Pool 문제가 의심되면 숫자를 바로 올리는 것보다 먼저 묻는다.
요청이 실제로 어디에서 기다리고 있는가?
Pool에서 기다리고 있는가.
PostgreSQL Query가 끝나기를 기다리고 있는가.
Lock을 기다리고 있는가.
외부 시스템을 기다리고 있는가.
이 위치를 알지 못하면 Pool 크기를 변경해도 무엇을 개선한 것인지 설명하기 어렵다.
결국 max_size는 많이 열어두기 위한 숫자가 아니라, 애플리케이션과 PostgreSQL 사이에서 동시에 허용할 DB 작업량을 조절하는 값으로 보는 편이 내게는 더 이해하기 쉬웠다.
그리고 이 관점으로 바꾸고 나서야 max_size=50이라는 숫자를 보고 단순히 크다거나 작다고 말하는 대신,
왜 50이어야 하는가?
라는 질문을 먼저 하게 됐다.
그 질문에 실제 측정값으로 답할 수 있어야 그 설정이 비로소 내 서비스에 맞는 설정이라고 말할 수 있다고 생각한다.