<?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>Locality on Tarragon</title><link>https://tarrragon.github.io/blog/tags/locality/</link><description>Recent content in Locality on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 27 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/locality/index.xml" rel="self" type="application/rss+xml"/><item><title>CockroachDB Locality-Aware Schema：跨州合規 + 邏輯一個 cluster 的 region placement 策略</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/locality-aware-schema/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 multi-region 能力、本文聚焦 &lt;em>locality 配置怎麼解合規地理邊界 + 跨 boundary 業務邏輯需求&lt;/em> — 用 Hard Rock Digital 跨 8 州單一邏輯 cluster 作為 concrete framing。Replica placement 機制屬前置、見 &lt;a href="../hlc-raft-consensus/">HLC + Raft consensus&lt;/a>、survival goal 互動見 &lt;a href="../survival-goals/">survival goals&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境hard-rock-的跨州-sportsbook-拓樸創新">問題情境：Hard Rock 的跨州 sportsbook 拓樸創新&lt;/h2>
&lt;p>美國 sportsbook 受 &lt;em>Wire Act&lt;/em> 規範、betting data 必須在下注州內處理 → 每個營運州都要有州內運算資源。傳統路徑是「每州一個獨立 silo、each silo 一個獨立 DB cluster」、合規上沒問題、但撞牆於三個業務需求：&lt;/p>
&lt;ul>
&lt;li>&lt;em>跨州統一帳戶&lt;/em>：玩家在 NJ 跟 FL 兩州都有帳戶、登入要看到統一 portfolio&lt;/li>
&lt;li>&lt;em>跨州 reporting&lt;/em>：總公司 BI / 財務 reporting 要橫跨所有州、不能 query N 個 cluster 後再合&lt;/li>
&lt;li>&lt;em>跨州欺詐偵測&lt;/em>：同一張身分證在不同州 IP 同時下注 → 風控引擎要看 &lt;em>cross-state aggregated&lt;/em> 資料&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a> 跨 8 州（AZ / IN / TN / FL / OH / IL / NJ / VA）用 AWS Outposts 把運算放進州內、但邏輯上仍是 &lt;em>一個&lt;/em> CockroachDB cluster — region placement 配置決定哪些 range 釘在哪個 Outpost / AWS region。case 觀察段直接揭露「跨所有 region 一個 logical database」這個拓樸 fact。&lt;/p>
&lt;p>讀者常問：&lt;/p>
&lt;ul>
&lt;li>合規逼我每州一 cluster、但跨州帳戶 / 風控 / 欺詐偵測撞牆怎麼辦？&lt;/li>
&lt;li>&lt;code>REGIONAL BY ROW&lt;/code> 跟 &lt;code>REGIONAL BY TABLE&lt;/code> 怎麼選、&lt;code>GLOBAL&lt;/code> 又在什麼場景？&lt;/li>
&lt;li>&lt;code>GLOBAL&lt;/code> table 為什麼讀快但寫慢、預設為什麼不全部用？&lt;/li>
&lt;li>AWS Outposts 是 latency 工具還是合規工具？&lt;/li>
&lt;/ul>
&lt;p>對照 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a>：60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region、locality 配置直接影響 cluster 規模治理。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 multi-region 能力、本文聚焦 <em>locality 配置怎麼解合規地理邊界 + 跨 boundary 業務邏輯需求</em> — 用 Hard Rock Digital 跨 8 州單一邏輯 cluster 作為 concrete framing。Replica placement 機制屬前置、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>、survival goal 互動見 <a href="../survival-goals/">survival goals</a>。</p></blockquote>
<hr>
<h2 id="問題情境hard-rock-的跨州-sportsbook-拓樸創新">問題情境：Hard Rock 的跨州 sportsbook 拓樸創新</h2>
<p>美國 sportsbook 受 <em>Wire Act</em> 規範、betting data 必須在下注州內處理 → 每個營運州都要有州內運算資源。傳統路徑是「每州一個獨立 silo、each silo 一個獨立 DB cluster」、合規上沒問題、但撞牆於三個業務需求：</p>
<ul>
<li><em>跨州統一帳戶</em>：玩家在 NJ 跟 FL 兩州都有帳戶、登入要看到統一 portfolio</li>
<li><em>跨州 reporting</em>：總公司 BI / 財務 reporting 要橫跨所有州、不能 query N 個 cluster 後再合</li>
<li><em>跨州欺詐偵測</em>：同一張身分證在不同州 IP 同時下注 → 風控引擎要看 <em>cross-state aggregated</em> 資料</li>
</ul>
<p><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a> 跨 8 州（AZ / IN / TN / FL / OH / IL / NJ / VA）用 AWS Outposts 把運算放進州內、但邏輯上仍是 <em>一個</em> CockroachDB cluster — region placement 配置決定哪些 range 釘在哪個 Outpost / AWS region。case 觀察段直接揭露「跨所有 region 一個 logical database」這個拓樸 fact。</p>
<p>讀者常問：</p>
<ul>
<li>合規逼我每州一 cluster、但跨州帳戶 / 風控 / 欺詐偵測撞牆怎麼辦？</li>
<li><code>REGIONAL BY ROW</code> 跟 <code>REGIONAL BY TABLE</code> 怎麼選、<code>GLOBAL</code> 又在什麼場景？</li>
<li><code>GLOBAL</code> table 為什麼讀快但寫慢、預設為什麼不全部用？</li>
<li>AWS Outposts 是 latency 工具還是合規工具？</li>
</ul>
<p>對照 <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>：60+ multi-region cluster、最大 Gaming cluster 48-node 跨 4 region、locality 配置直接影響 cluster 規模治理。</p>
<p>對照 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> Aurora 7 cluster fleet：銀行業跨國合規邊界、走的是「每市場獨立 Aurora cluster」路徑 — 跟 Hard Rock 邏輯一個 cluster 的拓樸完全不同。兩條路徑沒有對錯、trigger 條件不同（合規顆粒 × 跨 boundary 業務邏輯需求）。</p>
<h2 id="核心機制三種-table-locality--row-level-region-標記">核心機制：三種 table locality + row-level region 標記</h2>
<h3 id="三種-locality-模式">三種 locality 模式</h3>
<p>CockroachDB 用 <a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">Range Sharding</a> 把 multi-region table 抽象成三種 locality、配合 <a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> 合規邊界決定 row 落在哪個 region：</p>
<table>
  <thead>
      <tr>
          <th>Locality</th>
          <th>Read 行為</th>
          <th>Write 行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>REGIONAL BY TABLE</code></td>
          <td>本 region 快、其他 region 走 follower read</td>
          <td>本 region 快、其他 region 慢</td>
          <td>整 table 服務單一 region（如：us-orders）</td>
      </tr>
      <tr>
          <td><code>REGIONAL BY ROW</code></td>
          <td>該 row 所在 region 快、其他 follower</td>
          <td>該 row 所在 region 快、其他慢</td>
          <td>用戶資料跟地理綁定（玩家 / 訂單 / 帳戶）</td>
      </tr>
      <tr>
          <td><code>GLOBAL</code></td>
          <td>每 region local（快）</td>
          <td>跨 region quorum（慢）</td>
          <td>reference data（國碼、貨幣、規則表）</td>
      </tr>
  </tbody>
</table>
<h3 id="regional-by-row每-row-帶-crdb_region-隱含欄位">REGIONAL BY ROW：每 row 帶 <code>crdb_region</code> 隱含欄位</h3>
<p><code>REGIONAL BY ROW</code> 是 Hard Rock 場景的主要選擇。每 row 自動帶一個 <code>crdb_region</code> 隱含欄位、根據這個欄位把 row 對應的 range 釘在指定 region：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-az&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-nj&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-fl&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 寫入時指定 row 屬哪個 region
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">crdb_region</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="s1">&#39;us-east1-nj&#39;</span><span class="p">);</span></span></span></code></pre></div><p>CockroachDB planner 自動感知 <code>crdb_region</code>、把 read / write 路由到 row 所在 region 的 leaseholder。application 不用手動配 shard key、不用 application 端路由邏輯 — 這是 distributed SQL 的「宣告式 locality」優勢。</p>
<h3 id="global每-region-local-read跨-region-sync-write">GLOBAL：每 region local read、跨 region sync write</h3>
<p><code>GLOBAL</code> table 適合 <em>reference data</em> — 變更少、read 頻繁、需要全球 local read latency：</p>
<ul>
<li>read：每 region 都有 leaseholder、本地 read p99 跟 single-region 一樣</li>
<li>write：跨 region quorum、p99 100ms+</li>
</ul>
<p>實務上 <code>GLOBAL</code> 只放國家代碼、貨幣表、規則 lookup 等 <em>變更頻率低</em> 的 reference data。把 high-write workload 設成 <code>GLOBAL</code> 是典型錯配（見失敗模式段）。</p>
<h3 id="follower-readnon-voting-replica-提供本地-read">Follower read：non-voting replica 提供本地 read</h3>
<p>CockroachDB 區分 voting 跟 non-voting replica：</p>
<ul>
<li>voting replica 參與 Raft majority、決定 commit</li>
<li>non-voting replica 不參與 commit、只 serve <a href="/blog/backend/knowledge-cards/follower-read/" data-link-title="Follower Read" data-link-desc="分散式 SQL 從 non-voting replica 讀 closed timestamp 之前的資料、不參與 Raft commit、低 latency 但 read-after-write 場景仍可能 stale">Follower Read</a></li>
</ul>
<p><code>REGIONAL BY ROW</code> + <code>SURVIVE REGION FAILURE</code> 配合時：row 所在 region 是 voting + <a href="/blog/backend/knowledge-cards/leaseholder/" data-link-title="Leaseholder" data-link-desc="分散式 SQL 每個 range 在任一時間點的 read / write entry point、通常等於 Raft leader、承擔該 range 的 coordination">Leaseholder</a>、其他 region 有 voting replica（survival 需要）+ non-voting replica（本地 follower read）。</p>
<p>Follower read 讀到的是 <em>closed timestamp</em> 之前的資料 — strong consistency 場景不能用（read-after-write 會 stale）、但 dashboard / reporting / 風控分析等 <em>容忍 stale</em> 場景大幅降低 cross-region latency。</p>
<h3 id="配置語法跟驗證">配置語法跟驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- 設 database 的 region
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">mydb</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;europe-west1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="c1">-- 設 table locality
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">country_codes</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">GLOBAL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders_us</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="s2">&#34;us-east1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- 驗證
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">RANGES</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 看 replica 分佈
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 看 query plan 是否 local</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a>、<a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">table partitioning 卡</a> 的具體機制實現。</p>
<h2 id="操作流程從合規-boundary-到-schema-配置">操作流程：從合規 boundary 到 schema 配置</h2>
<h3 id="配置-multi-region-database">配置 multi-region database</h3>
<p>第一步是把所有 region 加入 database：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 假設 cluster 已跨 8 個州（透過 AWS Outposts 在每州內）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-virginia&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-nj&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-fl&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">sportsbook</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="n">REGION</span><span class="w"> </span><span class="s2">&#34;us-east1-az&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">-- ...其他州</span></span></span></code></pre></div><p>每個「region」對應一個 Outpost / AWS region 的 locality tag、CockroachDB Raft 根據 locality 自動分佈 replica。</p>
<h3 id="table-level-locality-配置">Table-level locality 配置</h3>
<p>bet placement / settlement table 走 <code>REGIONAL BY ROW</code>（資料跟玩家所在州綁定）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">settlements</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">ROW</span><span class="p">;</span></span></span></code></pre></div><p>account / user profile 跨州統一帳戶 — 玩家可能在多州下注、但 <em>主檔</em> 留 single region：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="n">REGIONAL</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="s2">&#34;us-east1-virginia&#34;</span><span class="p">;</span></span></span></code></pre></div><p>reference data（運動類別、賽事 metadata）— 全球變更少、每州都要快速 read：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sports_metadata</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">LOCALITY</span><span class="w"> </span><span class="k">GLOBAL</span><span class="p">;</span></span></span></code></pre></div><h3 id="application-端寫入">Application 端寫入</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 顯式指定 row 所在 region（推薦、明確）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">crdb_region</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="s1">&#39;NJ&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;us-east1-nj&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 或用 gateway_region() default（依 application 連到的 region）
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">bets</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">user_id</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(...,</span><span class="w"> </span><span class="p">...,</span><span class="w"> </span><span class="s1">&#39;NJ&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="p">);</span><span class="w">  </span><span class="c1">-- crdb_region 自動填 gateway 端</span></span></span></code></pre></div><p><code>gateway_region()</code> 是便利但有風險的 default — 如果 application server 在 us-east1-fl 但 user 在 NJ 下注、row 會被放到 FL 而不是 NJ、違反 Wire Act 合規。Hard Rock 場景下顯式指定 <code>crdb_region</code> 是更安全的做法。</p>
<h3 id="rollback-邊界">Rollback 邊界</h3>
<p>locality 變更即時生效、Raft 自動 rebalance — 無不可逆動作。但 rebalance 期間 cross-region traffic 暴增、p99 短期 spike。production 環境改 locality 應該選低流量時段、並監控 rebalance queue。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="拆獨立-cluster-解合規但破壞業務邏輯反模式hard-rock-對比-standard-charteredf410">「拆獨立 cluster 解合規但破壞業務邏輯」反模式（Hard Rock 對比 Standard Chartered、F4.10）</h3>
<p>直覺路徑是「合規要求資料留某地理邊界 → 每邊界開一個獨立 cluster」、合規上沒問題。但獨立 cluster 之間：</p>
<ul>
<li>玩家統一帳戶撞牆 — 每 cluster 各自有 user table、跨 cluster query 麻煩</li>
<li>跨州 reporting 要 N 個 cluster + ETL pipeline</li>
<li>欺詐偵測要 <em>cross-state aggregated view</em> — 獨立 cluster 拼不出</li>
</ul>
<p>Hard Rock 選擇 <em>邏輯一個 cluster + 物理跨州 Outpost placement</em> — 合規 boundary 用 region placement 表達、不是 cluster fragmentation。對比 Standard Chartered：</p>
<ul>
<li><strong>Standard Chartered Aurora 7 cluster fleet</strong>：銀行業跨國合規邊界、<em>跨 cluster 業務邏輯需求弱</em>（每市場用戶獨立、跨境統一帳戶不是核心 driver）→ 用 fleet 拓樸吸收合規可行</li>
<li><strong>Hard Rock Wire Act 跨州</strong>：跨州統一帳戶 + 跨州 reporting + 欺詐偵測是 <em>核心業務需求</em> → 必須邏輯一個 cluster、用 locality + placement 吸收合規</li>
</ul>
<p>兩條路徑沒有對錯、trigger 條件不同。判讀軸線：</p>
<ul>
<li>合規顆粒（跨國 vs 跨州 vs 跨 AZ）</li>
<li>跨 boundary 業務邏輯需求強度（強 → CockroachDB locality / 弱 → 拆獨立 cluster 可行）</li>
<li>團隊運維能力（CockroachDB 邏輯一個 cluster vs Aurora 多 cluster fleet 的人月成本）</li>
</ul>
<h3 id="outposts-是-latency-工具動機誤判f413case-反直覺判讀">「Outposts 是 latency 工具」動機誤判（F4.13、case 反直覺判讀）</h3>
<p>AWS Outposts 主要為「資料留某地理邊界」存在、latency 改善是 <em>副作用</em>。Hard Rock 策略段 2 明確警告：「決策時先看合規驅動力、latency 改善列為 bonus」。</p>
<p>若把 Outposts 當跨州 latency 改善工具、會在沒合規驅動的場景過度投資 — Outposts 硬體成本 + 維運複雜度遠高於純 AWS region 部署。實務判讀：</p>
<ul>
<li>有合規驅動（Wire Act / GDPR / 各州博彩牌照）→ Outposts 是合理投資</li>
<li>純 latency 優化 → 用 AWS Local Zones、用 CDN、用 edge cache、不要碰 Outposts</li>
<li>兩者並存 → Outposts 投資按 <em>合規</em> 計算、latency 改善是 ROI 加分項</li>
</ul>
<h3 id="global-table-write-太慢"><code>GLOBAL</code> table write 太慢</h3>
<p><code>GLOBAL</code> table 每次 write 跨 region quorum、p99 100ms+。用在 high-write workload 是典型錯配 — 該用在 reference data（國家代碼、貨幣表、規則 lookup）。</p>
<p>判讀：</p>
<ul>
<li>write QPS &lt; 10 + read QPS 跨 region 高 → <code>GLOBAL</code> 合理</li>
<li>write QPS &gt; 100 → 不要用 <code>GLOBAL</code>、改 <code>REGIONAL BY ROW</code> + 接受 cross-region read 偶爾走 follower</li>
</ul>
<h3 id="regional-by-row-但-row-沒設-crdb_region"><code>REGIONAL BY ROW</code> 但 row 沒設 <code>crdb_region</code></h3>
<p>application 寫入時忘了設 <code>crdb_region</code>、default 走 <code>gateway_region()</code> — application server 所在 region 變成 row 的 region。常見後果：</p>
<ul>
<li>application server 集中部署 → 所有 row 跑同一 region、locality 失效</li>
<li>application server 跟 user 不同 region → 合規 violation（Wire Act 場景）</li>
</ul>
<p>修法：顯式指定 <code>crdb_region</code>、把 user 的合規區域當業務欄位明確管理。</p>
<h3 id="cross-region-join-跑爆-latency">Cross-region join 跑爆 latency</h3>
<p>兩個 <code>REGIONAL BY ROW</code> table join、planner 要跨 region 拉資料、p99 暴漲。</p>
<p>修法：</p>
<ul>
<li>兩個 table partition by <em>同樣</em> 的 key（如：user_id）、保證 join 對應 row 在同 region</li>
<li>不能保證 co-location 時、考慮用 follower read 接受 stale 資料</li>
<li>query 重寫成多步：先在各 region 算 local 結果、application 端 merge</li>
</ul>
<h3 id="follower-read-假設-strong-consistency">Follower read 假設 strong consistency</h3>
<p>non-voting replica 是 <em>closed timestamp</em> 之前的資料、read-after-write 場景仍會 stale。</p>
<p>修法：</p>
<ul>
<li>read-after-write critical（如：剛下注立刻顯示「下注成功」）→ 不能走 follower、要走 leaseholder</li>
<li>dashboard / 分析 / reporting 容忍 stale → follower read 安全、大幅降 latency</li>
</ul>
<h3 id="data-residency-違規">Data residency 違規</h3>
<p>受監管州 / 國資料應留 boundary 內、但 application 從別 region 寫入 row、沒設 <code>crdb_region</code>、資料跑出 boundary、合規 violation（Wire Act / GDPR / 各州博彩牌照都有類似條款）。</p>
<p>修法（schema-level + application-level 雙保險）：</p>
<ul>
<li>schema：<code>REGIONAL BY ROW</code> + <code>crdb_region</code> 是 NOT NULL + CHECK constraint 限制可選值</li>
<li>application：寫入前明確驗證 <code>crdb_region</code> 對應 user 所在合規區</li>
<li>監控：定期跑 <code>SELECT crdb_region, count(*) FROM bets GROUP BY crdb_region</code> 確認分佈符合預期</li>
</ul>
<h3 id="hard-rock-場景的組合配置9c41">Hard Rock 場景的組合配置（9.C41）</h3>
<p>bet placement / settlement / account management 都需要跨州資料存取 + 州內合規 placement。Hard Rock 案例揭露的具體組合：</p>
<ul>
<li><code>REGIONAL BY ROW</code> + <code>crdb_region</code> 標州別 + region placement pin Outpost</li>
<li>account 跨州統一 → <code>REGIONAL BY TABLE</code> IN primary region、其他州走 follower read</li>
<li>sports metadata → <code>GLOBAL</code>、reference data 全州 local read</li>
</ul>
<p>這是滿足 Wire Act + 跨州業務邏輯的組合、不是唯一解、但揭露了 schema 設計的 <em>判讀軸</em> — 不是「locality 越強越好」、是「locality 對應業務 + 合規邊界」。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Range locality distribution</code>：range 分佈跟 locality 配置是否一致</li>
<li><code>Cross-region query count</code>：cross-region query 數量、locality 失效訊號</li>
<li><code>Follower read rate</code>：follower read 命中率、降 latency 效果</li>
<li><code>Leaseholder distribution by region</code>：leaseholder 在 region 間是否均勻</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<ul>
<li>cross-region traffic = <code>GLOBAL</code> table write QPS × region count</li>
<li><code>REGIONAL BY ROW</code> 跨 region read = follower read rate × QPS</li>
<li>storage 用量 = base storage × replication factor × (voting + non-voting replica count)</li>
</ul>
<h3 id="容量上限">容量上限</h3>
<ul>
<li>region count：建議 ≤ 5（多 region 增加 quorum latency + 維運複雜度）</li>
<li><code>GLOBAL</code> table 數量：建議只放 reference data、總 row 數 &lt; 10 萬</li>
<li>single range 寫 throughput ~1000 QPS（通用估算、見 <a href="../hlc-raft-consensus/">HLC + Raft consensus</a>）</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 cross-region-bound vs CPU-bound</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游合規 / latency 取捨</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../survival-goals/">survival goals</a>：locality + survival goal 一起決定 replica placement</li>
<li><a href="../transaction-retry-pattern/">transaction retry pattern</a>：partition 降低 hot row contention 的 schema 路徑</li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：leaseholder 跟 locality 的關係</li>
</ul>
<h3 id="跟-aurora-global-database-對照">跟 Aurora Global Database 對照</h3>
<p>Aurora 不支援 row-level locality — 跨 region 只能 cluster-per-region + async replication。CockroachDB 在一個 cluster 內可以 fine-grained locality、application 不需要管 cross-cluster 路由。Aurora Global Database 適合 <em>async DR</em> 場景、不適合 <em>跨 region 強一致 + row-level locality</em> 需求。</p>
<h3 id="跟-spanner-interleaved-tables-對照">跟 Spanner interleaved tables 對照</h3>
<p>Spanner 的 <a href="/blog/backend/knowledge-cards/interleaved-table/" data-link-title="Interleaved Table" data-link-desc="Spanner 把 parent / child table row 物理交錯儲存、parent &#43; child JOIN 不跨 split">Interleaved Table</a> 跟 CockroachDB 的 <code>REGIONAL BY ROW</code> 概念類似（parent-child row co-location）、語法不同。Spanner 在 GCP region 內 placement、無 Outposts 等效 — Hard Rock 場景下 Spanner 不能直接套用。</p>
<h3 id="aurora-dsql--spanner-對比">Aurora DSQL / Spanner 對比</h3>
<p>完整三家 distributed SQL 在 locality / multi-region placement 的取捨、見 <a href="../aurora-dsql-spanner-decision-tree/">aurora-dsql-spanner-decision-tree</a>。</p>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 上游</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">table partitioning 卡</a></li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>single-region 部署、無 data residency 需求 → 用 default locality 即可</li>
<li>合規邊界 <em>禁止</em> 跨境 replica（如 Standard Chartered 模式）→ 拆 cluster-per-市場、不走本文 locality 路徑</li>
<li>純 latency 優化、無合規驅動 → 用 CDN / cache / Local Zones、不必動 schema</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>（concrete framing — 跨 8 州 + Outposts + 邏輯一個 cluster）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a>（多 region locality 規模治理）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a>（fleet 拓樸對照、不同合規邊界）</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read 卡</a> / <a href="/blog/backend/knowledge-cards/table-partitioning/" data-link-title="Table Partitioning" data-link-desc="說明單一資料庫內如何把大表拆成多個分區，並由查詢規劃器只掃相關片段">table partitioning 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview.html">CockroachDB Multi-Region Capabilities</a> / <a href="https://www.cockroachlabs.com/docs/stable/table-localities.html">Table Localities</a> / <a href="https://www.cockroachlabs.com/docs/stable/follower-reads.html">Follower Reads</a></li>
</ul>
]]></content:encoded></item></channel></rss>