<?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>Cancellation on jyukki's Blog</title><link>https://jyukki.com/tags/cancellation/</link><description>Recent content in Cancellation on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Wed, 01 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/cancellation/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: 종단간 Deadline Budget과 Cancellation Propagation 운영 플레이북</title><link>https://jyukki.com/learning/deep-dive/deep-dive-end-to-end-deadline-cancellation-playbook/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-end-to-end-deadline-cancellation-playbook/</guid><description>클라이언트가 이미 포기한 요청을 백엔드가 계속 처리하는 낭비를 줄이기 위해, 홉별 deadline 배분과 취소 전파를 숫자 기준으로 설계하는 방법을 정리합니다.</description><content:encoded><![CDATA[<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>타임아웃을 단순 상수로 두는 방식에서 벗어나, <strong>클라이언트→게이트웨이→서비스→DB</strong>로 이어지는 종단간 deadline budget을 설계하는 기준을 잡을 수 있습니다.</li>
<li>&ldquo;사용자는 이미 떠났는데 서버는 계속 일하는&rdquo; 상태를 줄이기 위해, **cancellation propagation(취소 전파)**를 어디까지 강제해야 하는지 판단할 수 있습니다.</li>
<li>팀에서 바로 적용할 수 있는 **실무 의사결정 기준(숫자·조건·우선순위)**과 점검 체크리스트를 가져갈 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-장애는-느린-응답보다-낭비되는-작업에서-커진다">1) 장애는 느린 응답보다 &ldquo;낭비되는 작업&quot;에서 커진다</h3>
<p>많은 팀이 타임아웃 이슈를 &ldquo;응답이 늦다&rdquo; 문제로만 다룹니다. 그런데 운영에서 더 비싼 문제는 따로 있습니다.</p>
<ol>
<li>클라이언트는 2초에서 포기(HTTP 499/timeout)</li>
<li>서버는 8~15초 동안 DB/외부 API를 계속 호출</li>
<li>워커/커넥션 풀이 의미 없는 요청으로 점유</li>
<li>정상 요청이 대기열에서 밀리며 p95/p99가 급격히 악화</li>
</ol>
<p>즉 핵심은 단순 지연이 아니라 <strong>불필요한 작업의 잔존 시간</strong>입니다. 이 문제는 <a href="/learning/deep-dive/deep-dive-tail-latency-engineering-playbook/">Tail Latency 엔지니어링 플레이북</a>, <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff 설계</a>, <a href="/learning/deep-dive/deep-dive-admission-control-concurrency-limits/">Admission Control &amp; 동시성 제한</a>과 함께 보면 더 명확해집니다.</p>
<h3 id="2-timeout과-deadline은-다르다-절대시간-예산으로-통일해야-한다">2) Timeout과 Deadline은 다르다: 절대시간 예산으로 통일해야 한다</h3>
<ul>
<li><strong>Timeout</strong>: &ldquo;지금부터 N초&rdquo;</li>
<li><strong>Deadline</strong>: &ldquo;절대 시각 T까지&rdquo;</li>
</ul>
<p>마이크로서비스 홉이 늘어나는 환경에서는 timeout만으로는 예산 합이 쉽게 무너집니다. 서비스 A가 800ms, B가 800ms, C가 800ms를 각각 갖고 있으면 상위 요청은 이미 2.4초를 넘어설 수 있습니다.</p>
<p>그래서 운영 기준은 timeout 체인이 아니라, <strong>상위 요청의 절대 deadline을 하위 홉에 전달</strong>하는 방식으로 맞춰야 합니다.</p>
<ul>
<li>HTTP: <code>X-Request-Deadline</code>(epoch ms) 또는 <code>grpc-timeout</code> 변환</li>
<li>gRPC: 클라이언트 deadline을 컨텍스트로 전달</li>
<li>내부 비동기 작업: 원요청 deadline이 지나면 enqueue 자체를 차단</li>
</ul>
<p>핵심은 &ldquo;각 팀이 알아서 타임아웃&quot;이 아니라, <strong>요청 단위 예산의 일관성</strong>입니다.</p>
<h3 id="3-cancellation-propagation이-빠지면-deadline은-절반짜리다">3) Cancellation Propagation이 빠지면 deadline은 절반짜리다</h3>
<p>deadline을 두어도 취소 전파가 없으면 의미가 약합니다. 실제로는 아래 경계에서 누락이 자주 발생합니다.</p>
<ul>
<li>API 서버는 취소됐지만 DB 쿼리는 계속 실행</li>
<li>상위 서비스는 취소됐지만 하위 RPC는 그대로 진행</li>
<li>워커 큐에 이미 들어간 작업이 소비되어 후속 부하를 계속 생성</li>
</ul>
<p>취소 전파는 &ldquo;코드 스타일&rdquo; 문제가 아니라 <strong>자원 보호 정책</strong>입니다. 최소 기준은 아래와 같습니다.</p>
<ul>
<li>DB 레이어: 쿼리 타임아웃 + cancel signal 지원 드라이버 사용</li>
<li>외부 호출: context cancellation 감지 시 즉시 중단</li>
<li>비동기 파이프라인: deadline 초과 메시지는 소비 전 드롭/지연 큐 이동</li>
<li>배치/스트림: 사용자 요청 기원 작업은 취소 가능성과 재실행 전략을 분리</li>
</ul>
<h3 id="4-budget-분해는-균등-분배가-아니라-실패-확률-기반으로-해야-한다">4) Budget 분해는 균등 분배가 아니라 실패 확률 기반으로 해야 한다</h3>
<p>홉별로 똑같이 200ms를 나누면 단순하지만 실제로는 비효율적입니다. I/O 편차가 큰 구간(DB, 외부 결제, 검색)에는 더 넓은 예산이 필요하고, CPU 중심 구간은 상대적으로 짧게 둬야 합니다.</p>
<p>실무에서 많이 쓰는 시작점은 아래입니다.</p>
<ul>
<li>전체 API SLO p95 목표: 1,200ms</li>
<li>게이트웨이/인증/직렬화: 150ms</li>
<li>핵심 비즈니스 서비스: 300ms</li>
<li>DB + 캐시 + 외부 API: 650ms</li>
<li>여유 버퍼(재시도 방지용): 100ms</li>
</ul>
<p>이후 2주 단위로 &ldquo;예산 초과 홉&quot;을 좁히는 방식이 운영 비용 대비 효과가 좋습니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-의사결정-기준숫자조건우선순위">1) 의사결정 기준(숫자·조건·우선순위)</h3>
<p>우선순위는 보통 <strong>시스템 생존 &gt; 사용자 체감 지연 &gt; 개별 요청 완결성</strong> 순으로 두는 편이 안전합니다.</p>
<p>권장 기준 예시:</p>
<ol>
<li><strong>요청 잔존 작업 비율</strong>(client canceled 이후 1초 넘게 실행된 작업 비율)
<ul>
<li>5분 이동평균 <strong>3% 초과</strong> 시 P1 개선 항목 등록</li>
</ul>
</li>
<li><strong>deadline 초과 하위 호출 비율</strong>
<ul>
<li>서비스별 <strong>1% 초과</strong> 시 해당 구간 타임아웃 재배분</li>
</ul>
</li>
<li><strong>취소 누락 쿼리 비율</strong>(cancel 이후 DB 실행 지속)
<ul>
<li><strong>0.5% 초과</strong> 시 드라이버/ORM 설정 점검을 배포 게이트로 승격</li>
</ul>
</li>
<li><strong>보호 모드 진입 조건</strong>
<ul>
<li>CPU 80% 5분 지속 + 잔존 작업 비율 3% 초과 시</li>
<li>신규 비핵심 요청에 대해 강제 짧은 deadline(예: 500ms) 적용</li>
</ul>
</li>
</ol>
<h3 id="2-구현-원칙-전파관측차단을-한-세트로-묶는다">2) 구현 원칙: 전파·관측·차단을 한 세트로 묶는다</h3>
<h4 id="a-전파">(a) 전파</h4>
<ul>
<li>ingress에서 요청 시작 시각과 absolute deadline 계산</li>
<li>모든 내부 호출에 deadline metadata 주입</li>
<li>background job enqueue 시 <code>deadline_at</code> 필드 저장</li>
</ul>
<h4 id="b-관측">(b) 관측</h4>
<ul>
<li><code>request_deadline_ms</code>, <code>remaining_budget_ms</code> 로그 필드 고정</li>
<li>취소 원인(<code>client_cancel</code>, <code>server_timeout</code>, <code>circuit_open</code>) 태그 분리</li>
<li>&ldquo;완료&quot;와 &ldquo;취소 후 종료&quot;를 서로 다른 성공 기준으로 기록</li>
</ul>
<h4 id="c-차단">(c) 차단</h4>
<ul>
<li><code>remaining_budget_ms &lt; 0</code>이면 하위 호출 금지</li>
<li><code>remaining_budget_ms &lt; 100ms</code>면 DB fan-out 쿼리 대신 degraded path 사용</li>
<li>동일 요청 내 재시도는 예산 내에서만 허용(예: 최대 1회)</li>
</ul>
<p>이 구조는 <a href="/learning/deep-dive/deep-dive-slo-sli-error-budget/">SLO/SLI/Error Budget</a>, <a href="/learning/deep-dive/deep-dive-observability-alarms/">알람 전략</a>, <a href="/learning/deep-dive/deep-dive-connection-pool/">Connection Pool 운영</a>과 붙여야 실제 효과가 납니다.</p>
<h3 id="3-4주-도입-플랜">3) 4주 도입 플랜</h3>
<p><strong>1주차: 측정 고정</strong></p>
<ul>
<li>취소 이후 잔존 작업 비율, 하위 호출 deadline 초과율 대시보드 생성</li>
<li>상위 5개 API의 홉 맵 정리</li>
</ul>
<p><strong>2주차: 전파 통일</strong></p>
<ul>
<li>HTTP/gRPC 공통 deadline 헤더/컨텍스트 규약 도입</li>
<li>신규 API는 deadline 필수 검증(없으면 400 또는 기본값)</li>
</ul>
<p><strong>3주차: 취소 강제</strong></p>
<ul>
<li>DB/외부 API 클라이언트에 cancel timeout 강제 설정</li>
<li>비동기 큐 소비 시 <code>deadline_at</code> 검사 추가</li>
</ul>
<p><strong>4주차: 보호 모드 자동화</strong></p>
<ul>
<li>잔존 작업 비율/CPU 결합 규칙으로 보호 모드 자동 진입</li>
<li>주간 리뷰로 budget 재배분</li>
</ul>
<h3 id="4-간단한-의사코드-예시">4) 간단한 의사코드 예시</h3>
<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-go" data-lang="go"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">func</span> <span style="color:#50fa7b">Handle</span>(ctx context.Context, req Request) (Response, <span style="color:#8be9fd">error</span>) {
</span></span><span style="display:flex;"><span>    deadline <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">extractOrDefaultDeadline</span>(req)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> time.<span style="color:#50fa7b">Until</span>(deadline) <span style="color:#ff79c6">&lt;=</span> <span style="color:#bd93f9">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span> ErrTimeoutFast
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    ctx, cancel <span style="color:#ff79c6">:=</span> context.<span style="color:#50fa7b">WithDeadline</span>(ctx, deadline)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">defer</span> <span style="color:#50fa7b">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> <span style="color:#50fa7b">remaining</span>(ctx) &lt; <span style="color:#bd93f9">100</span><span style="color:#ff79c6">*</span>time.Millisecond {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span> <span style="color:#50fa7b">degradedResponse</span>(), <span style="color:#ff79c6">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    out, err <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">callDownstream</span>(ctx, req)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> errors.<span style="color:#50fa7b">Is</span>(err, context.DeadlineExceeded) <span style="color:#ff79c6">||</span> errors.<span style="color:#50fa7b">Is</span>(err, context.Canceled) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span> ErrTimeoutMapped
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> out, err
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>핵심은 &ldquo;실패를 피하려는 재시도&quot;보다 &ldquo;예산 안에서 멈추는 규율&quot;을 우선하는 것입니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<ol>
<li>
<p><strong>deadline을 짧게 잡으면 비용은 줄지만 기능 완결성이 떨어질 수 있다</strong><br>
특히 조회 집계, 복합 검색처럼 fan-out이 큰 API에서 사용자 체감 누락이 늘어날 수 있습니다.</p>
</li>
<li>
<p><strong>취소 전파를 강하게 걸수록 레거시 연동 이슈가 드러난다</strong><br>
오래된 드라이버나 외부 API SDK가 cancel을 무시하면, 오히려 오류율이 상승한 것처럼 보일 수 있습니다.</p>
</li>
<li>
<p><strong>예산 분배를 한번 정하고 고정하면 금방 현실과 어긋난다</strong><br>
트래픽 패턴, 릴리즈, 인프라 상태가 변하므로 월 1회 이상 재조정이 필요합니다.</p>
</li>
<li>
<p><strong>재시도 정책과 deadline 정책이 충돌하기 쉽다</strong><br>
재시도 횟수를 늘리면 성공률이 좋아 보이지만, 전체 tail latency와 잔존 작업을 악화시킬 수 있습니다.</p>
</li>
</ol>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 상위 요청의 absolute deadline이 모든 하위 호출로 전달된다.</li>
<li><input disabled="" type="checkbox"> 취소 이후 1초 이상 실행되는 잔존 작업 비율을 지표로 본다.</li>
<li><input disabled="" type="checkbox"> DB/외부 API/큐 소비자에 cancel 처리 기준이 문서화돼 있다.</li>
<li><input disabled="" type="checkbox"> 남은 예산이 임계치 이하일 때 degraded path가 준비돼 있다.</li>
<li><input disabled="" type="checkbox"> 재시도 정책이 deadline budget을 넘지 않도록 제한돼 있다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>최근 14일간 <code>client_cancel</code> 로그를 수집해, 취소 후 1초 이상 실행된 작업의 비율을 계산해 보세요.</li>
<li>핵심 API 1개를 선택해 홉별 예산표(게이트웨이/서비스/DB/외부 API)를 작성하고, 현재 p95와 비교해 과대·과소 구간을 표시해 보세요.</li>
<li><code>remaining_budget_ms &lt; 100</code>일 때 degraded path로 전환하는 기능 플래그를 넣고, 오류율·p95·CPU 변화를 1주간 측정해 보세요.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-tail-latency-engineering-playbook/">Tail Latency 엔지니어링 플레이북</a></li>
<li><a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff 설계</a></li>
<li><a href="/learning/deep-dive/deep-dive-admission-control-concurrency-limits/">Admission Control &amp; 동시성 제한</a></li>
<li><a href="/learning/deep-dive/deep-dive-slo-sli-error-budget/">SLO/SLI/Error Budget 운영</a></li>
<li><a href="/learning/deep-dive/deep-dive-connection-pool/">Connection Pool 튜닝 가이드</a></li>
</ul>
]]></content:encoded></item></channel></rss>