새로 뜬 인스턴스가 먼저 죽는다 — 워밍업과 배포 직후 DB 부하

부하가 올라 서버를 늘렸는데 새로 뜬 인스턴스만 유독 느리고, 늘릴수록 상황이 나빠져요. 같은 이미지인데도요. 새 인스턴스에 없는 게 무엇인지, 미리 데워 두는 방법을 담았어요.

부하가 올라 인스턴스를 늘렸어요. 그런데 새로 뜬 인스턴스가 기존 것들보다 훨씬 느려요. 응답 시간이 몇 배씩 나오고, 어떤 건 헬스체크에 걸려 다시 제외돼요.

그러면 남은 인스턴스로 부하가 더 몰리고, 또 새로 띄우고, 새 인스턴스가 또 죽어요. 늘릴수록 나빠지는 상태예요.

같은 이미지, 같은 설정, 같은 코드인데 왜 새것만 느린지가 이 편의 질문이에요.

답은 기존 인스턴스가 오래 살면서 축적한 것들에 있어요.

그게 뭔지 알면 새 인스턴스에도 미리 만들어 줄 수 있어요. 그리고 그걸 안 하면 늘릴수록 나빠지는 상태에서 못 빠져나옵니다.

왜 그런가

오래 산 인스턴스는 여러 가지를 축적해요.

애플리케이션 캐시가 채워져 있어요. 자주 쓰는 데이터가 메모리에 있으니 DB 를 안 가도 되죠. 새 인스턴스는 전부 DB 로 갑니다.

커넥션 풀이 채워져 있어요. 기존 인스턴스는 이미 맺어진 연결을 쓰지만, 새 인스턴스는 요청마다 새로 맺어야 해요. 연결 수립은 생각보다 비싸고, 여러 인스턴스가 동시에 뜨면 그것만으로 DB 에 부하가 됩니다.

런타임 최적화가 되어 있어요. 여러 런타임이 자주 실행되는 코드를 실행 중에 최적화해요. 시간이 지나야 그 최적화가 적용되니, 갓 뜬 프로세스는 같은 코드도 느리게 실행합니다.

그리고 여기서 되먹임이 생겨요. 새 인스턴스가 느리면 헬스체크나 로드 밸런서가 문제로 판단해요. 제외되면 남은 곳에 부하가 몰리고, 그래서 또 늘려야 하고, 새로 뜬 것이 또 느려요.

그리고 트래픽 분배 방식이 이 문제를 키워요. 라운드 로빈으로 균등하게 나누면, 아직 준비 안 된 새 인스턴스에도 처음부터 같은 양이 갑니다.

코드로 보면

준비될 때까지 부하를 조금씩:

코드
· 1) 준비 검사에 실제 준비를 넣는다
 캐시 적재 · 커넥션 풀 최소치 확보 후에 통과
 포트만 열렸다고 준비된 게 아니다

· 2) 트래픽을 점진적으로 올린다
 새 인스턴스의 가중치를 0 에서 서서히 100 으로
 워밍업 기간을 두는 로드 밸런서 설정

· 3) 기동 시 예열한다
 자주 쓰는 코드 경로를 미리 몇 번 실행
 커넥션 풀을 미리 채운다 (단, 흩어서)

· 4) 기동 유예를 넉넉히
 준비 안 된 인스턴스를 제외하면 상황이 더 나빠진다
 기동 직후 일정 시간은 헬스체크 실패를 봐준다

✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.

실전에서는

대응의 핵심은 준비될 때까지 부하를 적게 주는 것이에요.

준비 검사에 실제 준비를 넣는 게 첫 단계예요. 포트가 열렸다는 것만으로 통과시키면 안 돼요. 커넥션 풀이 최소치를 확보했는지, 필수 캐시가 채워졌는지를 확인하고 통과시켜요. 기동은 느려지지만 첫 요청부터 정상 속도가 나옵니다.

트래픽을 점진적으로 올리는 것도 효과적이에요. 로드 밸런서가 새 인스턴스의 가중치를 0 에서 서서히 올리면, 그 인스턴스가 적은 부하로 몸을 풀 시간을 얻어요. 많은 로드 밸런서가 이 기능을 갖고 있는데 기본으로 꺼져 있는 경우가 많습니다.

예열도 방법이에요. 기동할 때 자주 쓰는 경로를 몇 번 실행해 두면 런타임 최적화가 미리 적용돼요. 그리고 커넥션 풀을 미리 채우되, 여러 인스턴스가 동시에 뜰 때를 생각해 흩어야 해요.

기동 유예를 넉넉히 주는 것도 중요해요. 준비 안 된 인스턴스를 헬스체크로 제외하면, 앞서 본 되먹임이 시작돼요. 기동 직후 일정 시간은 실패를 봐주는 설정이 필요합니다.

여기서 근본적인 관점이 하나 있어요. 급할 때 늘리는 건 늦다. 앞 편에서 본 대로 오토스케일 자체가 늦은데, 새 인스턴스가 느리기까지 하면 더 늦어요. 그래서 여유 용량을 미리 두는 게 결국 가장 확실한 대응이에요.

그리고 이 문제는 배포와도 겹쳐요. 배포는 모든 인스턴스를 새것으로 바꾸는 일이니, 이 편의 문제가 전면적으로 일어나요. 순차 배포로 기존 인스턴스를 남겨 두는 게 그래서 중요합니다.

처방

  1. 준비 검사에 실제 준비 여부를 넣으세요. 포트가 열린 것과 일할 수 있는 것은 다릅니다.
  2. 트래픽 가중치를 점진적으로 올리세요. 로드 밸런서에 기능이 있는데 꺼져 있는 경우가 많습니다.
  3. 기동 시 예열하세요. 자주 쓰는 경로를 미리 실행하고 커넥션 풀을 채우세요.
  4. 커넥션 수립을 흩으세요. 여러 인스턴스가 동시에 뜨면 그것만으로 부하가 됩니다.
  5. 기동 유예를 넉넉히 주세요. 준비 안 된 인스턴스를 제외하면 되먹임이 시작됩니다.
  6. 여유 용량을 미리 두세요. 급할 때 늘리는 건 구조적으로 늦습니다.
  7. 순차 배포로 기존 인스턴스를 남기세요. 배포는 이 문제가 전면적으로 일어나는 순간입니다.
  8. 새 인스턴스의 초기 응답 시간을 따로 재세요. 평균에 묻히면 안 보입니다.
  9. 원인을 캐시로 단정하기 전에 가용 영역·인스턴스 유형·이웃 부하도 확인하세요. 새것만 느린 이유가 다른 데 있을 수 있습니다.

지금 확인해 보세요

새 인스턴스만 느린가:

코드
· 인스턴스 나이별 응답 시간
 기동 후 경과 시간으로 나눠 p99 를 본다
 새것만 느리면 이 편의 문제다

· 준비 검사가 실제 준비를 보는가
 포트만 열려도 통과한다면 그게 원인이다

· 기동에서 준비까지 걸리는 시간
 이 값이 곧 위험 구간의 길이다

· 새 인스턴스가 만드는 부하
 기동 시 DB 쿼리 수 · 커넥션 수립 수
 여러 대가 동시에 뜰 때를 곱해서 본다

· 원인이 정말 캐시인지 다른 요인인지
 가용 영역 · 인스턴스 유형 · 이웃 부하도 확인한다

핵심 한 줄새 인스턴스는 준비되지 않았다. 준비될 때까지 부하를 적게 줘라.

배포 직후 DB 가 맞는다

배포가 끝나자마자 DB 부하가 치솟아요. 몇 분간 응답이 느려지다가 서서히 회복돼요. 코드에는 무거운 쿼리를 추가한 적이 없어요.

배포를 자주 하는 팀일수록 이 현상을 자주 겪어요. 그리고 "배포하면 원래 좀 느려진다"로 받아들이게 돼요.

그런데 원래 그런 게 아니에요. 배포 순간에만 일어나는 일들이 있고, 그게 겹쳐서 부하를 만드는 거예요.

원인이 하나가 아니라는 게 이 문제의 특징이에요. 캐시, 커넥션, 계획, 기동 작업, 밀린 요청이 각각 다른 이유로 같은 시점에 몰려요.

각각은 감당할 만한데 다섯이 같은 몇 분에 겹치면 넘칩니다. 그래서 원인을 하나로 짚으려 하면 계속 틀려요.

왜 그런가

배포 직후에 동시에 일어나는 일들을 나열해 볼게요.

첫째, 캐시가 비어 있어요. 애플리케이션 안의 캐시는 프로세스와 함께 사라져요. 새 프로세스는 아무것도 모르는 상태로 시작하니, 캐시가 채워질 때까지 모든 요청이 DB 로 갑니다. 평소 90% 를 캐시가 받아 주고 있었다면, 그동안 DB 부하가 열 배가 돼요.

둘째, 커넥션을 새로 맺어요. 커넥션 수립은 생각보다 비싸요. 여러 파드가 동시에 뜨면서 각자 풀을 채우면, 짧은 시간에 수백 개의 연결 요청이 몰립니다.

셋째, 계획 캐시가 비어 있어요. 앞서 본 프리페어드 계획이 커넥션에 붙어 있으니, 새 커넥션은 계획을 처음부터 세워요. 계획 수립 비용이 잠시 늘어납니다.

넷째, 기동 시 작업이 몰려요. 설정 로딩, 초기 데이터 조회, 스키마 검증 같은 것들이 모든 인스턴스에서 동시에 일어나요.

다섯째, 밀린 요청이 한꺼번에 들어와요. 배포 중 잠시 처리량이 줄면 큐에 쌓이고, 새 버전이 뜨면 그게 한꺼번에 밀려듭니다.

이 다섯이 같은 몇 분에 겹쳐요. 각각은 감당할 만한데 합치면 넘칩니다.

코드로 보면

한꺼번에 몰리지 않게 흩는다:

코드
· 1) 순차 배포 — 한 번에 전부 교체하지 않는다
 maxSurge: 25%   maxUnavailable: 0
 캐시가 살아 있는 인스턴스가 항상 남아 있게

· 2) 캐시를 미리 채운 뒤 트래픽을 받는다
 준비됨 검사에 '주요 캐시 적재 완료' 를 포함

· 3) 커넥션 풀을 미리 열되 한꺼번에 열지 않는다
 최소 유휴 커넥션을 두고 점진적으로 늘린다
 인스턴스마다 무작위 지연을 넣어 흩는다

· 4) 기동 시 조회를 줄인다
 설정·기준 데이터는 지연 로딩하거나 캐시 계층에서

· 5) 밀린 요청이 몰릴 때를 대비
 동시 실행 제한 + 큐 상한 (넘치면 흘려보낸다)

✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.

실전에서는

가장 큰 원인은 대개 캐시예요. 그래서 여기부터 봅니다.

캐시가 프로세스 안에만 있으면 배포마다 통째로 사라져요. 공유 캐시를 쓰면 이 문제가 크게 줄어요. 새 프로세스가 떠도 캐시는 살아 있으니까요. 다만 원격 캐시는 왕복 비용이 있어서, 아주 뜨거운 데이터는 여전히 프로세스 안에 두게 됩니다. 두 겹으로 쓰는 구조가 흔한 이유예요.

순차 배포도 중요해요. 한 번에 전부 교체하면 캐시가 있는 인스턴스가 하나도 안 남아요. 조금씩 교체하면 기존 인스턴스가 부하를 나눠 받아 주면서 새 인스턴스의 캐시가 채워질 시간을 벌어 줍니다.

준비됨 판정에 캐시 적재를 넣는 방법도 있어요. 기동할 때 자주 쓰는 데이터를 미리 불러오고, 그게 끝나야 트래픽을 받는 거죠. 기동은 느려지지만 첫 요청부터 정상 속도가 나와요. 앞 편에서 본 "준비되기 전에 트래픽을 받으면 다시 나빠진다"의 해법이기도 해요.

커넥션 쪽은 흩는 게 답이에요. 인스턴스마다 무작위 지연을 조금씩 주면 동시 연결 요청이 분산돼요. 그리고 풀의 최소 크기를 너무 크게 잡지 않는 것도 도움이 됩니다.

그리고 이 문제는 앞 편들과 연결돼요. 밀린 요청이 몰리는 건 급증 문제고, 새 인스턴스가 느려서 제외되는 건 헬스체크 문제고, 그때 재시도가 늘면 증폭 문제예요. 배포 직후는 여러 취약점이 동시에 시험받는 시점입니다.

그래서 배포를 자주 하는 게 오히려 안전해요. 한 번에 큰 변화를 넣으면 이 부하도 크고 원인 파악도 어려우니까요.

처방

  1. 캐시를 프로세스 밖에 두세요. 공유 캐시가 있으면 배포마다 비워지지 않습니다.
  2. 순차 배포로 조금씩 교체하세요. 캐시가 살아 있는 인스턴스가 항상 남아 있게요.
  3. 준비됨 판정에 캐시 적재를 포함하세요. 기동은 느려져도 첫 요청부터 정상입니다.
  4. 커넥션 수립을 흩으세요. 인스턴스마다 무작위 지연을 주면 동시 요청이 분산됩니다.
  5. 기동 시 DB 조회를 줄이세요. 설정과 기준 데이터를 매번 전부 읽을 필요가 있는지 확인하세요.
  6. 밀린 요청에 대비해 동시 실행 제한과 큐 상한을 두세요. 넘치면 흘려보내는 게 다 같이 죽는 것보다 낫습니다.
  7. 배포 직후 몇 분간의 지표를 따로 보세요. 평상시 지표에 묻혀서 안 보입니다.
  8. 배포를 자주 하세요. 변화가 작을수록 이 부하도 작습니다.
  9. 원인을 캐시로 단정하기 전에 배포 전후의 쿼리 수를 비교하세요. 코드 변경으로 N+1 이 새로 생겼을 수도 있습니다.

지금 확인해 보세요

배포 직후 몇 분을 따로 본다:

코드
· 배포 시각을 지표에 표시하고 그 구간을 확대해서 본다
 DB 쿼리 수 · 커넥션 수 · 캐시 적중률 · 응답 시간

· 캐시 적중률이 회복되는 데 걸리는 시간
 이 시간이 곧 위험 구간의 길이다

· 배포 전후 쿼리 수 비교 — 코드 변경이 원인일 수도
SELECT calls, left(query,60) FROM pg_stat_statements
 ORDER BY calls DESC LIMIT 20;
 배포 전에 초기화해 두고 배포 후에 다시 본다

· 동시에 열린 커넥션 수의 봉우리
SELECT count(*) FROM pg_stat_activity;

· 새 인스턴스가 준비되기 전에 트래픽을 받는지
 기동 직후 응답 시간이 튀면 준비됨 판정이 이르다

핵심 한 줄배포 직후는 여러 취약점이 동시에 시험받는 시점이다. 몰리지 않게 흩어라.

브루니가 적어요

색을 바꾸며 숨는 카멜레온이에요. 운영에서 실제로 터지는 문제를 한 편씩 적어요. 같은 실수를 두 번 하지 않으려고 쓰는 기록이에요.

배포·관측 글 16편 모두 보기 →