<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Readiness on jyukki's Blog</title><link>https://jyukki.com/tags/readiness/</link><description>Recent content in Readiness on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Wed, 06 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/readiness/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Drain-aware 배포 플레이북</title><link>https://jyukki.com/learning/deep-dive/deep-dive-drain-aware-deployment-playbook/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-drain-aware-deployment-playbook/</guid><description>무중단 배포에서 SIGTERM 처리만으로 부족한 이유를 짚고, 로드밸런서·readiness·커넥션 풀·큐 컨슈머를 함께 drain하는 실무 기준을 정리합니다.</description><content:encoded><![CDATA[<p>무중단 배포를 이야기할 때 가장 많이 나오는 단어는 graceful shutdown입니다. 애플리케이션이 <code>SIGTERM</code>을 받으면 새 요청을 받지 않고, 처리 중인 요청을 끝낸 뒤 종료한다는 원칙입니다. 방향은 맞지만, 실무에서는 이것만으로 충분하지 않습니다. 실제 트래픽은 로드밸런서, 서비스 디스커버리, 커넥션 풀, 큐 컨슈머, 배치 워커, 캐시 워밍 상태를 함께 지나가기 때문입니다. 한 레이어만 정상 종료해도 다른 레이어가 아직 이전 인스턴스를 살아 있다고 믿으면 배포 순간에 502, 타임아웃, 중복 처리, 커넥션 리셋이 섞여 나옵니다.</p>
<p>그래서 운영 기준은 단순한 graceful shutdown보다 한 단계 넓은 <strong>drain-aware deployment</strong>가 되어야 합니다. drain-aware란 &ldquo;프로세스를 예쁘게 죽인다&quot;가 아니라, <strong>트래픽과 작업이 안전하게 빠져나가는 순서까지 배포 프로토콜에 포함한다</strong>는 뜻입니다. 이 글은 <a href="/learning/deep-dive/deep-dive-graceful-shutdown/">Graceful Shutdown</a>, <a href="/learning/deep-dive/deep-dive-load-balancer-healthchecks/">로드밸런서 헬스체크</a>, <a href="/learning/deep-dive/deep-dive-deployment-runbook/">배포 런북</a>을 운영 단위로 묶어, 배포 때 실제로 어떤 숫자와 조건을 봐야 하는지 정리합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>무중단 배포 실패가 왜 애플리케이션 종료 코드만의 문제가 아닌지 이해할 수 있습니다.</li>
<li>readiness, load balancer deregistration, keep-alive, 큐 컨슈머 ack를 어떤 순서로 drain해야 하는지 기준을 잡을 수 있습니다.</li>
<li>배포 중 5xx와 타임아웃을 줄이기 위해 팀 런북에 넣을 수 있는 숫자 기준과 체크리스트를 가져갈 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-graceful-shutdown과-draining은-범위가-다르다">1) Graceful shutdown과 draining은 범위가 다르다</h3>
<p>Graceful shutdown은 보통 프로세스 내부 관점입니다. <code>SIGTERM</code>을 받고, 서버 소켓을 닫고, 처리 중인 요청을 기다리고, 일정 시간이 지나면 강제 종료합니다. 반면 draining은 시스템 외부까지 포함합니다.</p>
<ul>
<li>로드밸런서가 해당 인스턴스로 새 요청을 보내지 않는가</li>
<li>서비스 디스커버리나 DNS 캐시가 이전 엔드포인트를 계속 들고 있지 않은가</li>
<li>HTTP keep-alive 연결이 오래 살아 새 요청을 계속 보내지 않는가</li>
<li>큐 컨슈머가 visibility timeout 안에 ack/nack를 안전하게 끝내는가</li>
<li>DB 커넥션 풀과 외부 API 호출이 타임아웃 예산 안에서 닫히는가</li>
</ul>
<p>즉 graceful shutdown은 draining의 한 부분입니다. 팀이 &ldquo;우리 shutdown hook 넣었으니 무중단&quot;이라고 말한다면 절반만 확인한 것입니다.</p>
<h3 id="2-readiness를-먼저-내리고-종료는-나중에-해야-한다">2) readiness를 먼저 내리고, 종료는 나중에 해야 한다</h3>
<p>배포 중 가장 흔한 실수는 종료 신호와 트래픽 차단을 동시에 처리하는 것입니다. 안전한 순서는 보통 아래가 맞습니다.</p>
<ol>
<li>readiness를 false로 전환한다.</li>
<li>로드밸런서와 서비스 디스커버리가 해당 인스턴스를 제외할 시간을 준다.</li>
<li>새 요청 수가 0에 가까워졌는지 확인한다.</li>
<li>처리 중 요청과 작업을 마저 끝낸다.</li>
<li>종료 유예시간 안에 프로세스를 닫는다.</li>
</ol>
<p>숫자 기준은 서비스마다 다르지만, 시작점은 이렇게 잡을 수 있습니다.</p>
<ul>
<li>readiness false 후 최소 대기: <strong>10~30초</strong></li>
<li>LB deregistration delay: API 서버는 <strong>30~120초</strong>, 긴 요청이 있으면 더 길게</li>
<li>종료 유예시간: <code>p99_request_latency * 2 + 외부 호출 timeout</code> 이상</li>
<li>강제 종료 전 inflight request 목표: <strong>0</strong>, 예외적으로 전체 동시 처리의 <strong>1% 이하</strong></li>
</ul>
<p>Kubernetes라면 <code>preStop</code>에서 잠깐 sleep만 넣는 방식이 자주 쓰이지만, sleep은 근본 해결이 아닙니다. readiness 전환, 실제 라우팅 제외, inflight 감소 지표가 같이 있어야 합니다. 헬스체크 설계는 <a href="/learning/deep-dive/deep-dive-load-balancer-healthchecks/">Load Balancer Healthcheck</a>와 함께 봐야 합니다.</p>
<h3 id="3-keep-alive-연결은-배포-중-숨어-있는-새-요청-경로다">3) keep-alive 연결은 배포 중 숨어 있는 새 요청 경로다</h3>
<p>로드밸런서가 새 연결을 막아도 기존 HTTP keep-alive 연결이 남아 있으면 이전 인스턴스로 요청이 계속 들어올 수 있습니다. 특히 게이트웨이, 프록시, SDK 클라이언트가 긴 keep-alive를 유지하는 구조에서는 &ldquo;deregistered인데 요청이 들어온다&quot;는 현상이 생깁니다.</p>
<p>실무 기준은 아래처럼 잡는 편이 안전합니다.</p>
<ul>
<li>drain 시작 후 응답 헤더에 <code>Connection: close</code> 또는 서버별 graceful close 정책 적용</li>
<li>keep-alive idle timeout은 LB timeout보다 짧거나 같게 유지</li>
<li>drain 상태에서는 새 요청을 받더라도 빠르게 503을 내기보다, 가능하면 기존 연결 요청만 제한적으로 처리</li>
<li>장기 스트리밍/WebSocket/SSE는 일반 API와 별도 drain 정책 적용</li>
</ul>
<p>WebSocket이나 SSE는 특히 조심해야 합니다. 일반 API와 같은 30초 유예시간을 적용하면 배포 때마다 연결이 대량으로 끊길 수 있습니다. 이 경우는 <a href="/learning/deep-dive/deep-dive-websocket-sse-patterns/">WebSocket/SSE 패턴</a>처럼 재연결 프로토콜과 세션 복구 기준을 같이 설계해야 합니다.</p>
<h3 id="4-큐-컨슈머는-http-서버보다-더-엄격한-종료-계약이-필요하다">4) 큐 컨슈머는 HTTP 서버보다 더 엄격한 종료 계약이 필요하다</h3>
<p>HTTP 요청은 실패하면 클라이언트가 재시도할 수 있지만, 큐 메시지는 ack 타이밍이 잘못되면 중복 처리나 유실로 이어집니다. 컨슈머 drain은 다음 세 가지를 분리해서 봐야 합니다.</p>
<ol>
<li>새 메시지 poll 중단</li>
<li>이미 받은 메시지 처리 완료</li>
<li>처리 실패/시간 초과 메시지의 재전달 보장</li>
</ol>
<p>예를 들어 visibility timeout이 60초인데 shutdown grace period가 20초라면, 처리 중 메시지가 중간에 죽고 재전달까지 애매한 상태가 됩니다. 최소 기준은 <code>shutdown_grace_period &gt;= message_p99_processing_time + ack_timeout_margin</code>입니다. 배치성 컨슈머라면 현재 batch를 끝내는 데 걸리는 시간도 포함해야 합니다.</p>
<p>운영 추천값은 아래와 같습니다.</p>
<ul>
<li>drain 시작 후 새 poll 즉시 중단</li>
<li>처리 중 메시지 완료 대기: <code>p99_processing_time * 1.5</code> 이상</li>
<li>ack 실패 시 로그와 metric 필수</li>
<li>visibility timeout은 p99 처리시간의 <strong>2~3배</strong>부터 시작</li>
<li>중복 허용이 어렵다면 <a href="/learning/deep-dive/deep-dive-transactional-inbox-idempotent-consumer-playbook/">Idempotent Consumer</a>나 <a href="/learning/deep-dive/deep-dive-queue-visibility-timeout-acknack-playbook/">Queue Visibility Timeout</a> 기준을 먼저 맞춘다</li>
</ul>
<h3 id="5-drain-실패는-배포-문제가-아니라-용량-문제로-번질-수-있다">5) drain 실패는 배포 문제가 아니라 용량 문제로 번질 수 있다</h3>
<p>배포 중 이전 인스턴스가 빠지는 동안 남은 인스턴스가 트래픽을 감당해야 합니다. 만약 평소 CPU 75%, 커넥션 풀 80%로 운영 중이라면 한 대만 빠져도 포화가 시작될 수 있습니다. 그래서 drain-aware 배포는 capacity planning과 연결됩니다.</p>
<p>롤링 배포 기준 예시:</p>
<ul>
<li>배포 중 제거 가능한 인스턴스 수: 전체의 <strong>10~20% 이하</strong>부터 시작</li>
<li>remaining capacity 기준 CPU headroom: <strong>30% 이상</strong> 권장</li>
<li>DB connection pool headroom: <strong>20% 이상</strong></li>
<li>배포 중 p95 latency가 기준 대비 <strong>1.5배</strong> 넘으면 rollout pause</li>
<li>error rate가 <strong>0.5~1%</strong> 이상이면 자동 중단 또는 이전 버전 유지</li>
</ul>
<p>이 숫자는 <a href="/learning/deep-dive/deep-dive-capacity-planning-littles-law-saturation/">Capacity Planning</a>과 <a href="/learning/deep-dive/deep-dive-tail-latency-engineering-playbook/">Tail Latency 엔지니어링</a>의 포화도 기준과 같이 운영해야 합니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-애플리케이션에-drain-상태를-명시적으로-둔다">1) 애플리케이션에 drain 상태를 명시적으로 둔다</h3>
<p>단순히 <code>isShuttingDown=true</code> 같은 플래그 하나만 두지 말고, 상태를 최소 세 단계로 나누는 편이 좋습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>RUNNING -&gt; DRAINING -&gt; TERMINATING
</span></span></code></pre></div><ul>
<li><code>RUNNING</code>: readiness true, 새 요청/작업 수락</li>
<li><code>DRAINING</code>: readiness false, 새 작업 수락 중단, 기존 요청만 처리</li>
<li><code>TERMINATING</code>: 유예시간 종료 또는 inflight 0, 리소스 close</li>
</ul>
<p>이 상태를 로그와 메트릭으로 노출해야 합니다. 배포 사고 때 &ldquo;죽는 중이었는지&rdquo;, &ldquo;라우팅 제외는 됐는지&rdquo;, &ldquo;요청이 남아 있었는지&quot;를 5분 안에 확인할 수 있어야 합니다.</p>
<h3 id="2-관측-지표를-배포-런북에-넣는다">2) 관측 지표를 배포 런북에 넣는다</h3>
<p>drain-aware 배포에서 최소로 봐야 할 지표는 아래입니다.</p>
<ul>
<li><code>inflight_requests</code></li>
<li><code>new_requests_after_drain_started</code></li>
<li><code>lb_target_deregistration_latency</code></li>
<li><code>http_keepalive_active_connections</code></li>
<li><code>queue_inflight_messages</code></li>
<li><code>shutdown_forced_count</code></li>
<li><code>rollout_pause_count</code></li>
</ul>
<p>특히 <code>new_requests_after_drain_started</code>가 0이 아니면 라우팅 경로가 어딘가 남아 있다는 뜻입니다. 이 값이 반복되면 preStop sleep을 늘리기보다 LB, gateway, client keep-alive 설정을 먼저 봐야 합니다.</p>
<h3 id="3-배포-전략별-기준을-다르게-둔다">3) 배포 전략별 기준을 다르게 둔다</h3>
<p>모든 배포에 같은 drain 시간을 쓰면 느리거나 위험합니다.</p>
<ul>
<li>짧은 API 서버: 30~60초 drain부터 시작</li>
<li>외부 API 의존이 많은 서버: 외부 호출 timeout 합산 후 60~120초</li>
<li>WebSocket/SSE 서버: 연결 재배치 또는 세션 복구 포함, 일반 API와 분리</li>
<li>큐 컨슈머: message p99 처리시간과 visibility timeout 기준으로 산정</li>
<li>배치 워커: 현재 chunk 완료 단위로 종료</li>
</ul>
<p>의사결정 우선순위는 <strong>사용자 영향 &gt; 데이터 정합성 &gt; 배포 속도</strong>입니다. 배포가 3분 느려져도 데이터 중복 처리나 502 폭발을 막는 편이 싸게 먹힙니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>Drain 시간을 길게 잡으면 안전해 보이지만, 항상 좋은 것은 아닙니다. 긴 drain은 롤아웃 시간을 늘리고, 취약 버전이 오래 살아 있게 만들며, 긴급 롤백 속도를 늦춥니다. 반대로 짧게 잡으면 배포는 빠르지만 요청 중단과 중복 처리가 늘어납니다.</p>
<p>현실적인 기준은 아래처럼 잡습니다.</p>
<ul>
<li>일반 API는 p99 요청 시간의 <strong>2배</strong>를 시작점으로 둔다.</li>
<li>drain 중 신규 요청이 계속 들어오면 시간을 늘리지 말고 라우팅 제거 경로를 고친다.</li>
<li>강제 종료가 하루 1회라도 발생하면 배포 런북 결함으로 보고 원인 분석한다.</li>
<li>긴 연결 서비스는 일반 API와 같은 deployment group에 묶지 않는다.</li>
<li>재시도 정책이 강한 클라이언트가 많으면 drain 실패가 트래픽 증폭으로 이어질 수 있으므로 <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff</a>를 함께 조정한다.</li>
</ul>
<p>가장 위험한 안티패턴은 &ldquo;배포 중 잠깐 502는 괜찮다&quot;는 태도입니다. 502가 눈에 보이는 정도면 내부에서는 커넥션 리셋, 중복 재시도, 큐 재전달, 캐시 미스가 이미 같이 흔들리고 있을 가능성이 큽니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="배포-전-체크리스트">배포 전 체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> readiness와 liveness가 분리되어 있다.</li>
<li><input disabled="" type="checkbox"> drain 시작 후 readiness false가 즉시 반영된다.</li>
<li><input disabled="" type="checkbox"> LB deregistration delay와 앱 shutdown grace period가 문서화되어 있다.</li>
<li><input disabled="" type="checkbox"> drain 중 새 요청 수와 inflight 요청 수를 메트릭으로 본다.</li>
<li><input disabled="" type="checkbox"> 큐 컨슈머는 새 poll 중단과 처리 중 메시지 완료 대기가 분리되어 있다.</li>
<li><input disabled="" type="checkbox"> 강제 종료 횟수와 종료 원인이 로그로 남는다.</li>
<li><input disabled="" type="checkbox"> 배포 중 p95/p99 latency, error rate 기준으로 rollout pause 조건이 있다.</li>
</ul>
<h3 id="연습">연습</h3>
<p>운영 중인 서비스 하나를 골라 아래 값을 실제로 적어보세요.</p>
<ol>
<li>현재 p99 요청 시간은 몇 ms인가?</li>
<li>readiness false 후 실제로 LB에서 제외되기까지 몇 초 걸리는가?</li>
<li>drain 시작 후에도 들어오는 요청이 있는가?</li>
<li>shutdown grace period는 p99 요청 시간의 몇 배인가?</li>
<li>큐 컨슈머가 있다면 message p99 처리시간과 visibility timeout은 각각 몇 초인가?</li>
</ol>
<p>이 다섯 개를 답하지 못하면 무중단 배포는 아직 감에 의존하고 있는 상태입니다. 우선은 배포 시간을 줄이기보다, drain 경로를 눈에 보이게 만드는 것부터 시작하는 편이 맞습니다.</p>
]]></content:encoded></item></channel></rss>