<?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>Read Routing on jyukki's Blog</title><link>https://jyukki.com/tags/read-routing/</link><description>Recent content in Read Routing on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Wed, 29 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/read-routing/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Lag-Aware Read Routing과 Follower Read 운영 플레이북</title><link>https://jyukki.com/learning/deep-dive/deep-dive-lag-aware-read-routing-follower-reads-playbook/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-lag-aware-read-routing-follower-reads-playbook/</guid><description>읽기/쓰기 분리 다음 단계로, replica lag를 숫자로 보고 follower read를 허용하거나 차단하는 기준을 실무 관점에서 정리합니다.</description><content:encoded><![CDATA[<p>읽기/쓰기 분리를 처음 도입할 때는 보통 &ldquo;조회는 replica로 보내면 된다&quot;는 수준에서 출발합니다. 그런데 운영을 조금만 해 보면 금방 문제가 드러납니다. 방금 저장한 주문이 안 보이고, 결제 직후 상태 조회가 뒤로 밀리고, 관리자 화면은 오래된 설정을 보여 줍니다. 반대로 모든 중요한 조회를 다시 primary로 몰리게 하면 replica를 둔 이점이 빠르게 사라집니다.</p>
<p>그래서 다음 단계에서 필요한 것이 <strong>lag-aware read routing</strong>입니다. 핵심은 단순히 replica를 붙이는 것이 아니라, <strong>이 조회는 몇 밀리초, 몇 초까지 오래돼도 되는가</strong>를 제품 계약으로 먼저 정하고, 그 계약 안에서만 follower read를 허용하는 것입니다. 이 글은 <a href="/learning/deep-dive/deep-dive-db-replication-read-write-splitting/">DB 복제와 읽기/쓰기 분리</a>, <a href="/learning/deep-dive/deep-dive-bounded-staleness-read-your-writes-playbook/">Bounded Staleness와 Read-Your-Writes</a>, <a href="/learning/deep-dive/deep-dive-postgresql-wal-checkpoint-replication-lag/">PostgreSQL WAL과 Replication Lag 운영 기준</a>, <a href="/learning/deep-dive/deep-dive-service-discovery-health-aware-routing/">Service Discovery와 Health-Aware Routing</a>에서 다룬 내용을 한 단계 더 실무적으로 묶어 봅니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>follower read를 단순 성능 기법이 아니라 <strong>일관성 예산(staleness budget)</strong> 계약으로 설계하는 기준을 잡을 수 있습니다.</li>
<li>어떤 조회는 replica로 보내고, 어떤 조회는 primary 또는 sticky read로 남겨야 하는지 <strong>엔드포인트 단위 판단 기준</strong>을 세울 수 있습니다.</li>
<li>lag 측정, 라우팅 차단, fallback, failover 직후 보호 규칙까지 포함한 <strong>운영 기준선 숫자</strong>를 정리할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-follower-read는-db-기능보다-제품-계약에-가깝다">1) follower read는 DB 기능보다 제품 계약에 가깝다</h3>
<p>같은 &ldquo;조회&quot;라도 요구 일관성은 전부 다릅니다. 예를 들어 아래 세 종류는 같은 정책으로 묶으면 안 됩니다.</p>
<ol>
<li><strong>자기 변경 직후 확인 화면</strong>
<ul>
<li>주문 생성 직후 주문 상세, 비밀번호 변경 직후 보안 설정 화면</li>
<li>사실상 Read-Your-Writes가 필요합니다.</li>
</ul>
</li>
<li><strong>약간 늦어도 되는 탐색형 화면</strong>
<ul>
<li>상품 목록, 검색 결과, 통계 카드</li>
<li>500ms에서 3초 정도 stale해도 사용자가 크게 문제를 느끼지 않는 경우가 많습니다.</li>
</ul>
</li>
<li><strong>완전히 느려도 되는 분석/백오피스 조회</strong>
<ul>
<li>정산 리포트, 집계 배치 결과, 운영 대시보드</li>
<li>수 초에서 수 분 lag를 허용할 수 있습니다.</li>
</ul>
</li>
</ol>
<p>실무에서 흔한 실패는 이 셋을 모두 &ldquo;read API&rdquo; 하나로 묶는 것입니다. follower read는 결국 <strong>이 API가 감당 가능한 stale window가 얼마인가</strong>를 정하는 일입니다. 이 분류가 없으면 replica를 붙여도 곧 primary fallback 예외 규칙만 늘어납니다.</p>
<h3 id="2-lag는-한-숫자가-아니라-읽기-안전성을-설명하는-신호-묶음이다">2) lag는 한 숫자가 아니라, 읽기 안전성을 설명하는 신호 묶음이다</h3>
<p><code>replication_lag_seconds</code> 하나만 보면 자주 오판합니다. 실제로는 최소 아래를 같이 봐야 합니다.</p>
<ul>
<li><strong>transport lag</strong>: 로그가 replica까지 도착하는 지연</li>
<li><strong>apply lag</strong>: 로그를 받았지만 replica가 아직 반영하지 못한 지연</li>
<li><strong>oldest replay age</strong>: 가장 오래된 미적용 변경의 나이</li>
<li><strong>replica query latency p95</strong>: 읽기 자체가 느린지 여부</li>
<li><strong>replay pause / recovery conflict</strong>: replica가 사실상 안전하지 않은 상태인지 여부</li>
</ul>
<p>사용자 API 기준으로는 apply lag가 더 중요합니다. 로그가 네트워크로 금방 도착해도 replay가 밀리면 사용자는 여전히 오래된 데이터를 봅니다. 제가 권하는 초기 기준은 이렇습니다.</p>
<ul>
<li>인터랙티브 사용자 조회: replica apply lag p95 <strong>500ms 이하</strong>에서만 허용</li>
<li>일반 목록/탐색 화면: p95 <strong>2초 이하</strong></li>
<li>백오피스/운영 통계: p95 <strong>10초 이하</strong></li>
<li>lag p99가 임계치의 <strong>2배</strong>를 넘으면 해당 replica는 즉시 read pool에서 제외</li>
</ul>
<p>즉 평균 lag가 아니라 <strong>p95, p99, oldest age</strong>를 함께 봐야 라우팅이 안전해집니다.</p>
<h3 id="3-핵심은-모든-요청을-어디로-보낼까가-아니라-누가-freshness를-증명하나다">3) 핵심은 &ldquo;모든 요청을 어디로 보낼까&quot;가 아니라 &ldquo;누가 freshness를 증명하나&quot;다</h3>
<p>Read-Your-Writes가 필요한 요청은 보통 세 방식 중 하나로 다룹니다.</p>
<ol>
<li><strong>짧은 시간 primary stickiness</strong>
<ul>
<li>마지막 쓰기 후 3초, 5초, 10초 동안은 해당 사용자 세션 조회를 primary로 강제</li>
<li>구현은 쉽지만 primary 부담이 늘고, 시간값을 크게 잡으면 이득이 줄어듭니다.</li>
</ul>
</li>
<li><strong>LSN/GTID 토큰 기반 follower read</strong>
<ul>
<li>쓰기 응답 시 &ldquo;최소 이 지점까지 반영된 replica에서만 읽어라&quot;는 토큰을 돌려줌</li>
<li>더 정교하지만 앱, 게이트웨이, DB 메타데이터가 함께 필요합니다.</li>
</ul>
</li>
<li><strong>도메인별 fallback 규칙</strong>
<ul>
<li>예: 주문 상태는 primary, 상품 목록은 replica, 추천 위젯은 cache 우선</li>
<li>구현이 단순하지만 API별 예외가 커질 수 있습니다.</li>
</ul>
</li>
</ol>
<p>작은 팀이라면 보통 1번에서 시작해도 충분합니다. 다만 자기 데이터 확인 화면이 많고 primary 부하가 빠르게 오르면 2번을 검토할 가치가 큽니다. 특히 <a href="/learning/deep-dive/deep-dive-bounded-staleness-read-your-writes-playbook/">Bounded Staleness와 Read-Your-Writes</a>를 이미 설계했다면, follower read 허용 여부를 세션 토큰이나 버전 토큰과 함께 묶는 편이 훨씬 깔끔합니다.</p>
<h3 id="4-lag-aware-routing은-service-discovery와-같은-수준의-health-판단이어야-한다">4) lag-aware routing은 service discovery와 같은 수준의 health 판단이어야 한다</h3>
<p>많은 팀이 read replica를 단순 로드밸런서 뒤에 두고 round-robin으로 뿌립니다. 그런데 replica는 &ldquo;살아 있다&quot;와 &ldquo;지금 읽어도 안전하다&quot;가 다릅니다. 따라서 health check도 단순 TCP 성공 여부가 아니라 아래 신호를 포함해야 합니다.</p>
<ul>
<li>lag p95가 허용 임계치 이내인가</li>
<li>replay가 멈췄거나 pause 상태는 아닌가</li>
<li>replica CPU, IO 포화 때문에 query latency가 급등하지 않았는가</li>
<li>failover 직후 아직 warmup이 끝나지 않았는가</li>
</ul>
<p>권장 규칙 예시는 아래와 같습니다.</p>
<ul>
<li>lag p95 &gt; 500ms, 사용자 인터랙티브 pool에서 제외</li>
<li>lag p95 &gt; 2초, 일반 read pool에서 제외</li>
<li>replica query latency p95 &gt; primary 대비 1.5배, 우선순위 낮춤</li>
<li>failover 후 첫 60초, follower read 전면 차단 또는 whitelist만 허용</li>
</ul>
<p>이건 결국 <a href="/learning/deep-dive/deep-dive-service-discovery-health-aware-routing/">Service Discovery와 Health-Aware Routing</a>의 replica 버전입니다. endpoint health 대신 <strong>freshness health</strong>를 본다고 생각하면 됩니다.</p>
<h3 id="5-follower-read의-진짜-비용은-장애-시-fallback-storm다">5) follower read의 진짜 비용은 장애 시 fallback storm다</h3>
<p>정상 시에는 replica offload가 잘 보이지만, 장애 때 더 중요한 문제는 fallback입니다. lag가 커지는 순간 모든 요청이 primary로 돌아오면 primary가 감당 못 하고 전체 서비스가 흔들릴 수 있습니다. 그래서 fallback도 무제한이면 안 됩니다.</p>
<p>실무에서는 아래 세 가지를 같이 둡니다.</p>
<ul>
<li><strong>요청 클래스별 fallback 우선순위</strong>: 주문 상세는 primary fallback 허용, 추천 피드는 stale cache 우선</li>
<li><strong>primary 보호 한도</strong>: fallback 유입이 primary read QPS의 **20~30%**를 넘으면 비핵심 조회는 stale 허용 또는 brownout</li>
<li><strong>fallback budget</strong>: 특정 화면이 5분 창에서 primary fallback 비율 <strong>15% 초과</strong>면 replica 문제를 사용자 API 문제로 승격</li>
</ul>
<p>즉 follower read는 단순 최적화가 아니라, <strong>평소에는 비용 절감, 사고 때는 blast radius 제어</strong>까지 같이 설계해야 안전합니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-엔드포인트를-freshness-등급으로-먼저-자른다">1) 엔드포인트를 freshness 등급으로 먼저 자른다</h3>
<p>처음부터 LSN 기반 정밀 제어를 만들기보다, 아래처럼 API를 세 등급으로 나누는 것이 효과적입니다.</p>
<table>
  <thead>
      <tr>
          <th>등급</th>
          <th>예시</th>
          <th>허용 stale budget</th>
          <th>기본 라우팅</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>F0</td>
          <td>결제 직후 상태, 내 주문 상세, 권한/설정 확인</td>
          <td>0~100ms 수준, 사실상 RYW</td>
          <td>primary 또는 token-verified replica</td>
      </tr>
      <tr>
          <td>F1</td>
          <td>상품 목록, 검색, 콘텐츠 피드</td>
          <td>500ms~2초</td>
          <td>lag-aware replica 우선</td>
      </tr>
      <tr>
          <td>F2</td>
          <td>통계, 리포트, 백오피스 조회</td>
          <td>5초~60초</td>
          <td>replica 우선, 필요 시 분석 저장소</td>
      </tr>
  </tbody>
</table>
<p>이 표만 있어도 &ldquo;왜 어떤 조회는 replica로 못 보내는가&quot;를 제품 팀과 훨씬 쉽게 합의할 수 있습니다.</p>
<h3 id="2-최소-라우터-규칙-예시">2) 최소 라우터 규칙 예시</h3>
<ol>
<li>요청에 최근 쓰기 토큰이 있으면 F0로 승격</li>
<li>선택한 replica의 apply lag p95가 임계치 이하면 그대로 라우팅</li>
<li>임계치 초과 시, F1은 primary fallback 여부를 예산 기준으로 판단</li>
<li>F2는 primary fallback보다 stale 허용 또는 응답 지연을 우선</li>
<li>failover 직후 60초, F1 이하만 제한적으로 replica 재개</li>
</ol>
<p>출발 임계치는 다음 정도가 무난합니다.</p>
<ul>
<li><code>interactive_replica_lag_p95 &lt;= 500ms</code></li>
<li><code>general_replica_lag_p95 &lt;= 2s</code></li>
<li><code>backoffice_replica_lag_p95 &lt;= 10s</code></li>
<li><code>primary_fallback_ratio_5m &lt; 15%</code></li>
<li><code>replica_pool_exclusion_recovery_window = 60~180s</code></li>
</ul>
<h3 id="3-운영-대시보드에서-꼭-따로-봐야-할-지표">3) 운영 대시보드에서 꼭 따로 봐야 할 지표</h3>
<ul>
<li><code>replica_apply_lag_p95</code>, <code>p99</code></li>
<li><code>oldest_replay_age_seconds</code></li>
<li><code>follower_read_qps_ratio</code></li>
<li><code>primary_fallback_ratio</code></li>
<li><code>read_your_writes_violation_count</code></li>
<li><code>replica_query_latency_p95</code></li>
<li><code>replica_pool_excluded_count</code></li>
</ul>
<p>특히 <code>read_your_writes_violation_count</code>는 단순 DB 지표보다 훨씬 강한 품질 신호입니다. 방금 저장한 데이터가 안 보였다는 사용자 체감 문제를 직접 잡아내기 때문입니다.</p>
<h3 id="4-도입-순서">4) 도입 순서</h3>
<ul>
<li><strong>1주차</strong>: 주요 조회 API를 F0, F1, F2로 분류하고, 현재 primary read 비중과 replica lag p95를 측정</li>
<li><strong>2주차</strong>: F1만 lag-aware replica 우선으로 전환, primary fallback 비율 관찰</li>
<li><strong>3주차</strong>: 최근 쓰기 3~5초 stickiness 또는 version token 도입</li>
<li><strong>4주차</strong>: failover 직후 보호 규칙, replica exclusion 자동화, brownout 정책 추가</li>
</ul>
<p>핵심 우선순위는 항상 같습니다. <strong>일관성 계약 명시 → 측정 → 제한적 라우팅 → 자동화</strong> 순서로 가야 합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<ol>
<li><strong>primary stickiness는 쉽지만 남용하면 replica 이점이 사라집니다.</strong> 기본값을 30초처럼 크게 잡으면 거의 전부 primary로 돌아옵니다.</li>
<li><strong>lag 평균값만 보면 사고를 놓칩니다.</strong> 짧은 스파이크라도 p99와 oldest replay age가 크면 사용자 체감은 급격히 나빠집니다.</li>
<li><strong>cross-region follower read는 훨씬 보수적으로 봐야 합니다.</strong> 네트워크 RTT와 장애 도메인이 커져, 같은 500ms lag라도 체감 리스크가 더 큽니다.</li>
<li><strong>fallback은 구원 장치이면서 증폭기이기도 합니다.</strong> 비핵심 트래픽까지 한꺼번에 primary로 돌리면, replica 문제가 전체 장애로 번질 수 있습니다.</li>
</ol>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 조회 API를 freshness 등급(F0/F1/F2 등)으로 분류했다.</li>
<li><input disabled="" type="checkbox"> replica lag는 평균이 아니라 p95, p99, oldest replay age로 본다.</li>
<li><input disabled="" type="checkbox"> Read-Your-Writes가 필요한 API는 primary stickiness 또는 토큰 검증 규칙이 있다.</li>
<li><input disabled="" type="checkbox"> primary fallback 비율에 상한을 두고, 비핵심 조회의 stale 허용 정책을 문서화했다.</li>
<li><input disabled="" type="checkbox"> failover 직후 follower read 재개 조건을 health check와 함께 고정했다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>현재 서비스의 조회 API 10개를 골라 F0, F1, F2로 분류해 보세요.</li>
<li>각 API에 허용 stale budget을 숫자로 적고, 그 기준이 제품적으로 왜 괜찮은지 한 줄씩 써 보세요.</li>
<li>replica lag p95가 3초로 튄 상황에서, 어떤 API를 primary fallback하고 어떤 API는 stale 허용할지 표로 정리해 보세요.</li>
<li>장애 훈련용으로 &ldquo;lag 급증 10분&rdquo; 시나리오를 만들고, primary 보호 규칙이 실제로 동작하는지 점검해 보세요.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-db-replication-read-write-splitting/">DB 복제와 읽기/쓰기 분리</a></li>
<li><a href="/learning/deep-dive/deep-dive-bounded-staleness-read-your-writes-playbook/">Bounded Staleness와 Read-Your-Writes 보장 설계</a></li>
<li><a href="/learning/deep-dive/deep-dive-postgresql-wal-checkpoint-replication-lag/">PostgreSQL WAL, Checkpoint, Replication Lag 운영 기준</a></li>
<li><a href="/learning/deep-dive/deep-dive-service-discovery-health-aware-routing/">Service Discovery와 Health-Aware Routing</a></li>
<li><a href="/learning/deep-dive/deep-dive-graceful-degradation-brownout-playbook/">Graceful Degradation 플레이북</a></li>
</ul>
]]></content:encoded></item></channel></rss>