새로 뜬 인스턴스가 먼저 죽는다 — 워밍업과 배포 직후 DB 부하
부하가 올라 서버를 늘렸는데 새로 뜬 인스턴스만 유독 느리고, 늘릴수록 상황이 나빠져요. 같은 이미지인데도요. 새 인스턴스에 없는 게 무엇인지, 미리 데워 두는 방법을 담았어요.
부하가 올라 인스턴스를 늘렸어요. 그런데 새로 뜬 인스턴스가 기존 것들보다 훨씬 느려요. 응답 시간이 몇 배씩 나오고, 어떤 건 헬스체크에 걸려 다시 제외돼요.
그러면 남은 인스턴스로 부하가 더 몰리고, 또 새로 띄우고, 새 인스턴스가 또 죽어요. 늘릴수록 나빠지는 상태예요.
같은 이미지, 같은 설정, 같은 코드인데 왜 새것만 느린지가 이 편의 질문이에요.
답은 기존 인스턴스가 오래 살면서 축적한 것들에 있어요.
그게 뭔지 알면 새 인스턴스에도 미리 만들어 줄 수 있어요. 그리고 그걸 안 하면 늘릴수록 나빠지는 상태에서 못 빠져나옵니다.
왜 그런가
오래 산 인스턴스는 여러 가지를 축적해요.
애플리케이션 캐시가 채워져 있어요. 자주 쓰는 데이터가 메모리에 있으니 DB 를 안 가도 되죠. 새 인스턴스는 전부 DB 로 갑니다.
커넥션 풀이 채워져 있어요. 기존 인스턴스는 이미 맺어진 연결을 쓰지만, 새 인스턴스는 요청마다 새로 맺어야 해요. 연결 수립은 생각보다 비싸고, 여러 인스턴스가 동시에 뜨면 그것만으로 DB 에 부하가 됩니다.
런타임 최적화가 되어 있어요. 여러 런타임이 자주 실행되는 코드를 실행 중에 최적화해요. 시간이 지나야 그 최적화가 적용되니, 갓 뜬 프로세스는 같은 코드도 느리게 실행합니다.
그리고 여기서 되먹임이 생겨요. 새 인스턴스가 느리면 헬스체크나 로드 밸런서가 문제로 판단해요. 제외되면 남은 곳에 부하가 몰리고, 그래서 또 늘려야 하고, 새로 뜬 것이 또 느려요.
그리고 트래픽 분배 방식이 이 문제를 키워요. 라운드 로빈으로 균등하게 나누면, 아직 준비 안 된 새 인스턴스에도 처음부터 같은 양이 갑니다.
코드로 보면
준비될 때까지 부하를 조금씩:
· 1) 준비 검사에 실제 준비를 넣는다
캐시 적재 · 커넥션 풀 최소치 확보 후에 통과
포트만 열렸다고 준비된 게 아니다
· 2) 트래픽을 점진적으로 올린다
새 인스턴스의 가중치를 0 에서 서서히 100 으로
워밍업 기간을 두는 로드 밸런서 설정
· 3) 기동 시 예열한다
자주 쓰는 코드 경로를 미리 몇 번 실행
커넥션 풀을 미리 채운다 (단, 흩어서)
· 4) 기동 유예를 넉넉히
준비 안 된 인스턴스를 제외하면 상황이 더 나빠진다
기동 직후 일정 시간은 헬스체크 실패를 봐준다✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.
실전에서는
대응의 핵심은 준비될 때까지 부하를 적게 주는 것이에요.
준비 검사에 실제 준비를 넣는 게 첫 단계예요. 포트가 열렸다는 것만으로 통과시키면 안 돼요. 커넥션 풀이 최소치를 확보했는지, 필수 캐시가 채워졌는지를 확인하고 통과시켜요. 기동은 느려지지만 첫 요청부터 정상 속도가 나옵니다.
트래픽을 점진적으로 올리는 것도 효과적이에요. 로드 밸런서가 새 인스턴스의 가중치를 0 에서 서서히 올리면, 그 인스턴스가 적은 부하로 몸을 풀 시간을 얻어요. 많은 로드 밸런서가 이 기능을 갖고 있는데 기본으로 꺼져 있는 경우가 많습니다.
예열도 방법이에요. 기동할 때 자주 쓰는 경로를 몇 번 실행해 두면 런타임 최적화가 미리 적용돼요. 그리고 커넥션 풀을 미리 채우되, 여러 인스턴스가 동시에 뜰 때를 생각해 흩어야 해요.
기동 유예를 넉넉히 주는 것도 중요해요. 준비 안 된 인스턴스를 헬스체크로 제외하면, 앞서 본 되먹임이 시작돼요. 기동 직후 일정 시간은 실패를 봐주는 설정이 필요합니다.
여기서 근본적인 관점이 하나 있어요. 급할 때 늘리는 건 늦다. 앞 편에서 본 대로 오토스케일 자체가 늦은데, 새 인스턴스가 느리기까지 하면 더 늦어요. 그래서 여유 용량을 미리 두는 게 결국 가장 확실한 대응이에요.
그리고 이 문제는 배포와도 겹쳐요. 배포는 모든 인스턴스를 새것으로 바꾸는 일이니, 이 편의 문제가 전면적으로 일어나요. 순차 배포로 기존 인스턴스를 남겨 두는 게 그래서 중요합니다.
처방
- 준비 검사에 실제 준비 여부를 넣으세요. 포트가 열린 것과 일할 수 있는 것은 다릅니다.
- 트래픽 가중치를 점진적으로 올리세요. 로드 밸런서에 기능이 있는데 꺼져 있는 경우가 많습니다.
- 기동 시 예열하세요. 자주 쓰는 경로를 미리 실행하고 커넥션 풀을 채우세요.
- 커넥션 수립을 흩으세요. 여러 인스턴스가 동시에 뜨면 그것만으로 부하가 됩니다.
- 기동 유예를 넉넉히 주세요. 준비 안 된 인스턴스를 제외하면 되먹임이 시작됩니다.
- 여유 용량을 미리 두세요. 급할 때 늘리는 건 구조적으로 늦습니다.
- 순차 배포로 기존 인스턴스를 남기세요. 배포는 이 문제가 전면적으로 일어나는 순간입니다.
- 새 인스턴스의 초기 응답 시간을 따로 재세요. 평균에 묻히면 안 보입니다.
- 원인을 캐시로 단정하기 전에 가용 영역·인스턴스 유형·이웃 부하도 확인하세요. 새것만 느린 이유가 다른 데 있을 수 있습니다.
지금 확인해 보세요
새 인스턴스만 느린가:
· 인스턴스 나이별 응답 시간
기동 후 경과 시간으로 나눠 p99 를 본다
새것만 느리면 이 편의 문제다
· 준비 검사가 실제 준비를 보는가
포트만 열려도 통과한다면 그게 원인이다
· 기동에서 준비까지 걸리는 시간
이 값이 곧 위험 구간의 길이다
· 새 인스턴스가 만드는 부하
기동 시 DB 쿼리 수 · 커넥션 수립 수
여러 대가 동시에 뜰 때를 곱해서 본다
· 원인이 정말 캐시인지 다른 요인인지
가용 영역 · 인스턴스 유형 · 이웃 부하도 확인한다핵심 한 줄새 인스턴스는 준비되지 않았다. 준비될 때까지 부하를 적게 줘라.
배포 직후 DB 가 맞는다
배포가 끝나자마자 DB 부하가 치솟아요. 몇 분간 응답이 느려지다가 서서히 회복돼요. 코드에는 무거운 쿼리를 추가한 적이 없어요.
배포를 자주 하는 팀일수록 이 현상을 자주 겪어요. 그리고 "배포하면 원래 좀 느려진다"로 받아들이게 돼요.
그런데 원래 그런 게 아니에요. 배포 순간에만 일어나는 일들이 있고, 그게 겹쳐서 부하를 만드는 거예요.
원인이 하나가 아니라는 게 이 문제의 특징이에요. 캐시, 커넥션, 계획, 기동 작업, 밀린 요청이 각각 다른 이유로 같은 시점에 몰려요.
각각은 감당할 만한데 다섯이 같은 몇 분에 겹치면 넘칩니다. 그래서 원인을 하나로 짚으려 하면 계속 틀려요.
왜 그런가
배포 직후에 동시에 일어나는 일들을 나열해 볼게요.
첫째, 캐시가 비어 있어요. 애플리케이션 안의 캐시는 프로세스와 함께 사라져요. 새 프로세스는 아무것도 모르는 상태로 시작하니, 캐시가 채워질 때까지 모든 요청이 DB 로 갑니다. 평소 90% 를 캐시가 받아 주고 있었다면, 그동안 DB 부하가 열 배가 돼요.
둘째, 커넥션을 새로 맺어요. 커넥션 수립은 생각보다 비싸요. 여러 파드가 동시에 뜨면서 각자 풀을 채우면, 짧은 시간에 수백 개의 연결 요청이 몰립니다.
셋째, 계획 캐시가 비어 있어요. 앞서 본 프리페어드 계획이 커넥션에 붙어 있으니, 새 커넥션은 계획을 처음부터 세워요. 계획 수립 비용이 잠시 늘어납니다.
넷째, 기동 시 작업이 몰려요. 설정 로딩, 초기 데이터 조회, 스키마 검증 같은 것들이 모든 인스턴스에서 동시에 일어나요.
다섯째, 밀린 요청이 한꺼번에 들어와요. 배포 중 잠시 처리량이 줄면 큐에 쌓이고, 새 버전이 뜨면 그게 한꺼번에 밀려듭니다.
이 다섯이 같은 몇 분에 겹쳐요. 각각은 감당할 만한데 합치면 넘칩니다.
코드로 보면
한꺼번에 몰리지 않게 흩는다:
· 1) 순차 배포 — 한 번에 전부 교체하지 않는다
maxSurge: 25% maxUnavailable: 0
캐시가 살아 있는 인스턴스가 항상 남아 있게
· 2) 캐시를 미리 채운 뒤 트래픽을 받는다
준비됨 검사에 '주요 캐시 적재 완료' 를 포함
· 3) 커넥션 풀을 미리 열되 한꺼번에 열지 않는다
최소 유휴 커넥션을 두고 점진적으로 늘린다
인스턴스마다 무작위 지연을 넣어 흩는다
· 4) 기동 시 조회를 줄인다
설정·기준 데이터는 지연 로딩하거나 캐시 계층에서
· 5) 밀린 요청이 몰릴 때를 대비
동시 실행 제한 + 큐 상한 (넘치면 흘려보낸다)✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.
실전에서는
가장 큰 원인은 대개 캐시예요. 그래서 여기부터 봅니다.
캐시가 프로세스 안에만 있으면 배포마다 통째로 사라져요. 공유 캐시를 쓰면 이 문제가 크게 줄어요. 새 프로세스가 떠도 캐시는 살아 있으니까요. 다만 원격 캐시는 왕복 비용이 있어서, 아주 뜨거운 데이터는 여전히 프로세스 안에 두게 됩니다. 두 겹으로 쓰는 구조가 흔한 이유예요.
순차 배포도 중요해요. 한 번에 전부 교체하면 캐시가 있는 인스턴스가 하나도 안 남아요. 조금씩 교체하면 기존 인스턴스가 부하를 나눠 받아 주면서 새 인스턴스의 캐시가 채워질 시간을 벌어 줍니다.
준비됨 판정에 캐시 적재를 넣는 방법도 있어요. 기동할 때 자주 쓰는 데이터를 미리 불러오고, 그게 끝나야 트래픽을 받는 거죠. 기동은 느려지지만 첫 요청부터 정상 속도가 나와요. 앞 편에서 본 "준비되기 전에 트래픽을 받으면 다시 나빠진다"의 해법이기도 해요.
커넥션 쪽은 흩는 게 답이에요. 인스턴스마다 무작위 지연을 조금씩 주면 동시 연결 요청이 분산돼요. 그리고 풀의 최소 크기를 너무 크게 잡지 않는 것도 도움이 됩니다.
그리고 이 문제는 앞 편들과 연결돼요. 밀린 요청이 몰리는 건 급증 문제고, 새 인스턴스가 느려서 제외되는 건 헬스체크 문제고, 그때 재시도가 늘면 증폭 문제예요. 배포 직후는 여러 취약점이 동시에 시험받는 시점입니다.
그래서 배포를 자주 하는 게 오히려 안전해요. 한 번에 큰 변화를 넣으면 이 부하도 크고 원인 파악도 어려우니까요.
처방
- 캐시를 프로세스 밖에 두세요. 공유 캐시가 있으면 배포마다 비워지지 않습니다.
- 순차 배포로 조금씩 교체하세요. 캐시가 살아 있는 인스턴스가 항상 남아 있게요.
- 준비됨 판정에 캐시 적재를 포함하세요. 기동은 느려져도 첫 요청부터 정상입니다.
- 커넥션 수립을 흩으세요. 인스턴스마다 무작위 지연을 주면 동시 요청이 분산됩니다.
- 기동 시 DB 조회를 줄이세요. 설정과 기준 데이터를 매번 전부 읽을 필요가 있는지 확인하세요.
- 밀린 요청에 대비해 동시 실행 제한과 큐 상한을 두세요. 넘치면 흘려보내는 게 다 같이 죽는 것보다 낫습니다.
- 배포 직후 몇 분간의 지표를 따로 보세요. 평상시 지표에 묻혀서 안 보입니다.
- 배포를 자주 하세요. 변화가 작을수록 이 부하도 작습니다.
- 원인을 캐시로 단정하기 전에 배포 전후의 쿼리 수를 비교하세요. 코드 변경으로 N+1 이 새로 생겼을 수도 있습니다.
지금 확인해 보세요
배포 직후 몇 분을 따로 본다:
· 배포 시각을 지표에 표시하고 그 구간을 확대해서 본다
DB 쿼리 수 · 커넥션 수 · 캐시 적중률 · 응답 시간
· 캐시 적중률이 회복되는 데 걸리는 시간
이 시간이 곧 위험 구간의 길이다
· 배포 전후 쿼리 수 비교 — 코드 변경이 원인일 수도
SELECT calls, left(query,60) FROM pg_stat_statements
ORDER BY calls DESC LIMIT 20;
배포 전에 초기화해 두고 배포 후에 다시 본다
· 동시에 열린 커넥션 수의 봉우리
SELECT count(*) FROM pg_stat_activity;
· 새 인스턴스가 준비되기 전에 트래픽을 받는지
기동 직후 응답 시간이 튀면 준비됨 판정이 이르다핵심 한 줄배포 직후는 여러 취약점이 동시에 시험받는 시점이다. 몰리지 않게 흩어라.