커넥션은 공짜가 아니다

English

커넥션을 "DB로 가는 통로" 정도로 생각하기 쉽습니다. 통로라면 많을수록 좋겠죠. 그런데 실제로는 반대입니다. 커넥션을 늘리면 DB가 느려지는 구간이 옵니다.

커넥션은 통로가 아니라 자원이기 때문입니다.

커넥션 하나에 붙는 것들

MySQL을 기준으로 보면, 커넥션 하나마다 이런 게 따라옵니다.

커넥션이 수백 수천 개가 되면 이 비용이 합쳐집니다. DB가 쿼리를 처리하는 대신 커넥션을 관리하는 데 메모리와 CPU를 씁니다. 컨텍스트 스위칭이 늘고 락 경합도 같이 늘어납니다.

그래서 풀 크기에는 적정값이 있습니다. 키운다고 처리량이 따라 늘지 않고, 어느 지점을 넘으면 오히려 떨어집니다. HikariCP 문서가 코어 수 기준으로 작은 값을 권하는 이유가 이겁니다.

앱 커넥션 풀만으로는 부족합니다

앱에는 이미 커넥션 풀이 있습니다. 문제는 그 풀이 인스턴스마다 따로라는 점입니다.

한 인스턴스가 풀 100개를 잡고 있으면 실제 DB 커넥션 100개를 쥐고 있는 겁니다. 인스턴스가 여섯 대로 늘면 600개가 됩니다. DB의 max_connections가 500이면 넘는 순간 거절됩니다. 대기가 아니라 하드 실패입니다.

1 · 풀 1002 · 풀 1003 · 풀 1004 · 풀 1005 · 풀 1006 · 풀 100MySQL max=500600개 요청 → 100개 즉시 거절
앱마다 자기 풀을 쥐고 있어서, 합치면 DB 한계를 넘습니다

앱 풀은 자기 인스턴스 안에서만 대기를 관리합니다. 인스턴스들이 합쳐서 DB 한계를 넘는 건 앱 풀이 알 방법도, 막을 방법도 없습니다. 공용 대기열이 없습니다.

RDS Proxy가 하는 일

앱과 DB 사이에 프록시를 두면 이 구조가 바뀝니다. 앱은 프록시에 넉넉하게 연결하고, 프록시는 진짜 DB 커넥션을 몇 개만 붙잡고 여러 요청에 돌려씁니다. 이게 커넥션 멀티플렉싱입니다.

1234RDS Proxy 공용 50실제 커넥션 50MySQL넘치면 대기열
프록시가 진짜 커넥션 몇 개를 붙잡고 돌려씁니다

앱 쪽 커넥션이 1000개여도 실제 DB 커넥션은 50개로 유지할 수 있습니다. 그리고 커넥션이 다 찼을 때 거절하는 대신 대기열에 세웁니다. 반납되면 순서대로 내주고, 너무 오래 기다리면 타임아웃입니다. 직접 연결에서 한계를 넘으면 즉시 거절되는 것과 대비됩니다.

"그냥 더 기다리게 하는 것 아닌가"

대기열만 보면 그렇게 보입니다. 실제 이득은 다른 데 있습니다.

DB를 건강한 구간에 묶어둡니다. 이게 제일 큽니다. 커넥션 수에 상한이 걸리니 DB는 항상 효율적으로 도는 범위에 머뭅니다. 커넥션이 폭주해서 DB 전체가 느려지거나 죽는 상황을 막습니다. 그러면 대기하게 된 요청뿐 아니라, 멀쩡히 돌고 있던 다른 쿼리들까지 같이 지켜집니다. 기다리지 않는 쪽이 얻는 이득입니다.

커넥션을 따뜻하게 유지합니다. 매 요청마다 TCP 연결과 TLS 핸드셰이크, 인증을 새로 하면 수십 밀리초가 그냥 나갑니다. 재사용하면 그게 사라집니다.

페일오버가 부드러워집니다. DB가 교체될 때 프록시가 앱 쪽 커넥션을 붙잡고 뒤에서 재연결합니다. 앱에 그대로 전달되는 에러가 줄어듭니다. AWS는 이걸로 페일오버 시간이 최대 66%까지 줄어든다고 말합니다.

앱 대수와 DB 커넥션 수가 분리됩니다. 오토스케일로 인스턴스가 늘어도 DB가 보는 커넥션 수는 그대로입니다.

서버리스에서 특히

람다는 요청이 몰리면 순식간에 수백에서 수천 개의 실행 환경으로 늘어납니다. 각 환경은 격리되어 있고 수명이 짧습니다. 커넥션 풀을 공유할 방법이 없습니다.

전통적인 DB에 그대로 붙이면 커넥션이 폭발하고, 매번 새로 연결하느라 핸드셰이크 비용을 반복해서 냅니다. RDS Proxy가 정확히 이 문제를 겨냥해서 나왔고, AWS가 대표 사례로 드는 것도 이 조합입니다.

안 되는 것

쿼리 속도 문제는 못 풉니다. 프록시는 커넥션 개수 문제를 다룹니다. 쿼리가 느려서 커넥션을 오래 잡고 있으면 대기열만 길어지고 결국 타임아웃입니다. 인덱스와 쿼리 최적화는 별개로 해야 합니다.

pinning에 주의해야 합니다. 트랜잭션을 열거나 세션 상태를 쓰면 — 세션 변수, 임시 테이블 같은 것들 — 그 커넥션이 해당 요청에 묶입니다. 묶인 동안은 다른 요청에 못 돌려주므로 멀티플렉싱 효율이 떨어집니다. 프록시를 붙였는데 기대만큼 안 줄어든다면 여기를 봐야 합니다.

ProxySQL은 한 발 더 갑니다

RDS Proxy는 커넥션 흡수에 집중한 매니지드 서비스입니다. ProxySQL은 오픈소스고 범위가 더 넓습니다.

쿼리를 읽어서 읽기와 쓰기를 자동으로 라우팅합니다. SELECT는 리플리카로, 쓰기는 프라이머리로 보냅니다. 애플리케이션 코드에서 손으로 나누던 걸 프록시가 대신하는 겁니다. 쿼리 캐싱과 리라이팅, 로드밸런싱도 합니다.

대신 직접 운영해야 합니다. 관리 부담과 기능을 맞바꾸는 선택입니다.

언제 쓰고 언제 안 쓰나

쓸 만한 상황. 커넥션이 실제 병목일 때입니다. 서버리스, 공격적인 오토스케일, 스파이크성 트래픽, 페일오버가 잦은 환경.

굳이 필요 없는 상황. 인스턴스 수가 고정된 상시 서버에서 풀이 잘 튜닝돼 있다면 층을 하나 더 두는 값어치가 없습니다.

판단 기준은 하나입니다. 지금 아픈 게 커넥션 개수인가, 쿼리 속도인가. 후자라면 프록시를 붙여도 달라지지 않습니다.


밤 10시만 되면 서비스가 느려졌다에서 겪은 게 정확히 이 주제입니다. 다만 그때 커넥션 풀 고갈의 원인은 인덱스 누락으로 인한 느린 쿼리였습니다. 프록시로 풀 수 있는 문제가 아니었고, 실제로 쓰지도 않았습니다.