
지난 주에 스테이지에서 결국 롤백했다. 프로덕션 붙이기 전에 걸러진 게 다행이라면 다행인데, 나흘을 매달렸으니 후련한 기분은 아니다.
발단은 흔한 이유였다. 라이더팀 서비스가 시간대별로 커넥션이 폭주하면서 too many connections 로그를 하루에 몇 십건씩 뱉기 시작했다. Aurora PostgreSQL 15, max_connections 200에 라이더 API 파드가 오토스케일링으로 4개에서 20개까지 오가는 상황. HikariCP maximumPoolSize를 낮춰봐도 임계가 애매하게 겹칠 때는 여전히 튕겼다. AWS 문서에서 "RDS Proxy는 이럴 때 붙이면 된다"라고 아주 자신있게 쓰여있길래, 시니어분과 얘기해서 도입 PoC를 시작했다.
붙인 순간, 왜인지 느려졌다
붙이고 나서 첫 번째 부하 테스트를 돌렸다. 결과 요약이 이랬다.
- 직접 연결 P99: 8ms
- RDS Proxy 경유 P99: 24ms
세 배. 처음엔 warm-up 문제인가 싶어서 30분 돌려도 마찬가지. 오히려 스파이크가 나면서 60ms를 찍는 순간도 있었다. 문서에는 "1~3ms 정도만 늘어난다"고 되어있는데 이건 그게 아니었다.
pg_stat_activity를 보니 이상한 게 하나 보였다. application_name이 죄다 PostgreSQL JDBC Driver인데, 각 백엔드가 트랜잭션 안에서 꽤 오래 잡혀 있는 상태였다. Proxy가 커넥션을 재사용하려면 트랜잭션 밖에 나와야 하는데, 우리 앱은 트랜잭션이 상당히 길다.
진짜 원인은 pinning 이었다
RDS Proxy 문서를 다시 정독하고 알게 된 게 pinning이다. 특정 조건이 걸리면 프록시가 그 클라이언트 세션을 특정 백엔드에 "고정"시켜버린다. 이러면 사실상 커넥션 풀링이 안 되고, 그냥 프록시 한 홉이 더 낀 직접 연결이 된다.
RDS Proxy CloudWatch 지표에서 DatabaseConnectionsCurrentlyBorrowed, DatabaseConnectionsCurrentlySessionPinned을 확인해 봤더니 pinned가 70%를 넘고 있었다. 완전히 망한 상태.
pinning이 걸리는 조건은 문서에 나열돼 있는데, 우리 케이스에서 트리거한 건 두 가지였다.
첫째, SET 명령. HikariCP의 connectionInitSql로 SET statement_timeout = '30s' 같은 걸 넣어놨다. 이거 하나 있으면 세션 전체가 pinned. 이건 커넥션 풀에 맞물려 파라미터를 세팅하는 아주 흔한 관행인데, 프록시에서는 독약이다.
둘째, prepared statement. Spring Data JPA + Hibernate 조합이 자동으로 서버 사이드 prepared statement를 만든다. prepareThreshold를 조절해 client-side로 강제할 수도 있지만, 우리는 그렇게 안 해놨다. Aurora PostgreSQL에서 RDS Proxy가 extended query protocol을 프록싱은 하지만, 우리처럼 안 튜닝한 채로 붙으면 마찬가지로 pinning 유발한다.
사이드 이슈: DNS TTL 5초의 함정
pinning을 원인으로 확정하고 튜닝 방향을 잡는 와중에 하나 더 걸린 게 있었다. 스테이지 프록시로 트래픽을 옮기고 반나절 지났는데, 서비스 파드 중 한 두 개가 계속 원래 RDS writer 엔드포인트로 붙고 있었다. netstat 찍어보니 IP가 프록시 대역이 아니었다.
원인은 JVM DNS 캐시. 기본값이 30초인데, 우리는 java.security에서 networkaddress.cache.ttl=-1(무한)로 하드코딩한 잔재가 있었다. 몇 년 전 어떤 이슈 우회하려고 넣어둔 게 그대로 남아있던 것. RDS Proxy 엔드포인트는 내부적으로 A 레코드가 여러 개고 TTL이 짧다. JVM이 처음 resolve한 IP만 붙들고 있으면 프록시 뒤에서 노드가 롤링되든 말든 트래픽이 한 쪽으로 쏠리는데, 어느 정도 지나면 그 IP가 사라지고 커넥션이 죽는다.
networkaddress.cache.ttl=30 정도로 바꿔서 배포하고 나서야 그 부분은 잠잠해졌다. 이건 사실 프록시 문제가 아니라 예전 코드의 문제였는데, 프록시 도입 없이는 절대 안 튀어나올 이슈였다. 몇 년 묵은 하드코딩을 청소한 건 소득이라고 치자.
// java.security 오버라이드 파일에서 발견한 문제
networkaddress.cache.ttl=-1
networkaddress.cache.negative.ttl=10
그래서 결론은
일단 스테이지에서 프록시를 떼고 원래대로 되돌렸다. 튜닝 없이 그냥 붙인 우리 잘못이 크지만, 결과적으로 이 세팅 조합으로는 프록시가 오히려 손해였다.
앞으로 계획은 대략 이렇다.
- 파라미터 세팅을 세션 레벨이 아니라 파라미터 그룹으로 옮기기.
statement_timeout같은 건 대부분 DB 파라미터 그룹에서 default로 잡을 수 있다. prepareThreshold=0또는preferQueryMode=simple로 prepared statement 회피. 근데 이건 성능 상 손해가 어느 정도 있어서 벤치를 다시 돌려야 한다.- 그러고 나서 다시 프록시 붙이고 pinning 지표를 20% 이하로 유지되는지 확인.
- 이 모든 걸 다 했는데도 P99가 직접 연결보다 낫다는 근거가 안 나오면, 그냥 프록시 없이 커넥션 풀 크기 재설계 방향으로 간다.
솔직히 4번으로 갈 확률이 지금 시점에서는 반반이라고 본다. Fargate처럼 파드 라이프사이클이 짧고 예측 불가능한 워크로드가 아닌 이상, HikariCP 잘 튜닝하고 앱 파드 수를 안정화시키는 게 프록시보다 예측 가능한 경우가 많다.
한 줄 교훈
RDS Proxy 붙이기 전에 앱이 세션에 뭘 설정하는지, prepared statement를 어떻게 쓰는지부터 확인하자. SessionPinned 지표는 도입 첫날부터 대시보드 상단에 걸어두는 게 낫다. 안 그러면 나처럼 나흘 태우고 롤백한다.
다음에는 프록시 없이 HikariCP + PgBouncer sidecar 조합으로 가는 실험을 해볼 생각이다. 결과 나오면 그것도 정리하겠다.
'IT > AWS' 카테고리의 다른 글
| EKS Pod Identity로 IRSA 마이그레이션 가이드 — 한 워크로드씩 옮기기 (0) | 2026.06.29 |
|---|---|
| NLB cross-zone off + 단일 AZ 노드 소실, 50% 트래픽 블랙홀이 된 새벽 인시던트 (0) | 2026.06.24 |
| Karpenter consolidation 너무 믿었다가 새벽 3시에 호출받은 이야기 (0) | 2026.06.23 |
| EKS Pod Identity vs IRSA, 2년 굴려보고 우리 팀이 정리한 것 (0) | 2026.06.20 |
| Karpenter NodePool weight, 함정이 하나 있다 (0) | 2026.06.18 |