<?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>Proxysql on Tarragon</title><link>https://tarrragon.github.io/blog/tags/proxysql/</link><description>Recent content in Proxysql on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 22 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/proxysql/index.xml" rel="self" type="application/rss+xml"/><item><title>MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/proxysql-config/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/proxysql-config/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>ProxySQL 配置&lt;/em> — connection pool + query routing 的 4 段 lifecycle 跟 rule chain 設計。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="proxysql-lifecycle每個-query-走-4-段">ProxySQL Lifecycle：每個 query 走 4 段&lt;/h2>
&lt;p>從 application 連 ProxySQL 到拿到 response、每個 query 都走完整 4 段：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">1. Connection 接入 → application connect 到 ProxySQL（不是 MySQL）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. Query parse + rule match → ProxySQL 解析 query、match query rule chain
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. Backend route → 決定走哪個 hostgroup（primary / replica）+ 哪個 server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. Response 返回 → 將 result set 回 application、connection 可被 reuse&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每段都有獨立配置 + failure mode + 觀測 metric。ProxySQL 不是 &lt;em>簡單的 connection pool&lt;/em>、是 &lt;em>query-aware proxy&lt;/em> — 看得到 SQL 內容才能做 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">read/write split&lt;/a>、replica lag-aware routing、query mirroring。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &amp;#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PostgreSQL pgBouncer&lt;/a> 比、pgBouncer 是 &lt;em>transaction-level pool&lt;/em>（只看連線、不看 SQL）、ProxySQL 是 &lt;em>query-level proxy&lt;/em>（看 SQL、做 routing decision）。能力不同、target use case 不同。&lt;/p>
&lt;h2 id="stage-1connection-接入--hostgroup--server--user-三層-schema">Stage 1：Connection 接入 — Hostgroup / Server / User 三層 schema&lt;/h2>
&lt;p>ProxySQL 不直接 expose backend MySQL、用 &lt;em>hostgroup&lt;/em> 作為 routing 抽象。Application 不知道有幾個 backend、只知道 ProxySQL。&lt;/p>
&lt;p>&lt;strong>核心 table（在 &lt;code>main&lt;/code> database）&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Table&lt;/th>
 &lt;th>角色&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>mysql_servers&lt;/code>&lt;/td>
 &lt;td>列每個 backend MySQL server、屬於哪個 hostgroup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mysql_replication_hostgroups&lt;/code>&lt;/td>
 &lt;td>定義 writer hostgroup ↔ reader hostgroup 配對、自動偵測 primary 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mysql_users&lt;/code>&lt;/td>
 &lt;td>列允許連 ProxySQL 的 application user、預設 hostgroup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>mysql_query_rules&lt;/code>&lt;/td>
 &lt;td>Query rule chain、決定哪個 query 走哪個 hostgroup&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>典型部署&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>ProxySQL 配置</em> — connection pool + query routing 的 4 段 lifecycle 跟 rule chain 設計。</p></blockquote>
<hr>
<h2 id="proxysql-lifecycle每個-query-走-4-段">ProxySQL Lifecycle：每個 query 走 4 段</h2>
<p>從 application 連 ProxySQL 到拿到 response、每個 query 都走完整 4 段：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. Connection 接入        →  application connect 到 ProxySQL（不是 MySQL）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Query parse + rule match  → ProxySQL 解析 query、match query rule chain
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. Backend route          →  決定走哪個 hostgroup（primary / replica）+ 哪個 server
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Response 返回          →  將 result set 回 application、connection 可被 reuse</span></span></code></pre></div><p>每段都有獨立配置 + failure mode + 觀測 metric。ProxySQL 不是 <em>簡單的 connection pool</em>、是 <em>query-aware proxy</em> — 看得到 SQL 內容才能做 <a href="/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">read/write split</a>、replica lag-aware routing、query mirroring。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PostgreSQL pgBouncer</a> 比、pgBouncer 是 <em>transaction-level pool</em>（只看連線、不看 SQL）、ProxySQL 是 <em>query-level proxy</em>（看 SQL、做 routing decision）。能力不同、target use case 不同。</p>
<h2 id="stage-1connection-接入--hostgroup--server--user-三層-schema">Stage 1：Connection 接入 — Hostgroup / Server / User 三層 schema</h2>
<p>ProxySQL 不直接 expose backend MySQL、用 <em>hostgroup</em> 作為 routing 抽象。Application 不知道有幾個 backend、只知道 ProxySQL。</p>
<p><strong>核心 table（在 <code>main</code> database）</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Table</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>mysql_servers</code></td>
          <td>列每個 backend MySQL server、屬於哪個 hostgroup</td>
      </tr>
      <tr>
          <td><code>mysql_replication_hostgroups</code></td>
          <td>定義 writer hostgroup ↔ reader hostgroup 配對、自動偵測 primary 切換</td>
      </tr>
      <tr>
          <td><code>mysql_users</code></td>
          <td>列允許連 ProxySQL 的 application user、預設 hostgroup</td>
      </tr>
      <tr>
          <td><code>mysql_query_rules</code></td>
          <td>Query rule chain、決定哪個 query 走哪個 hostgroup</td>
      </tr>
  </tbody>
</table>
<p><strong>典型部署</strong>：</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">-- 進 ProxySQL admin (6032 port)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">mysql</span><span class="w"> </span><span class="o">-</span><span class="n">uadmin</span><span class="w"> </span><span class="o">-</span><span class="n">padmin</span><span class="w"> </span><span class="o">-</span><span class="n">h127</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="n">P6032</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 設 2 個 hostgroup：10=writer、20=reader
</span></span></span><span class="line"><span class="ln"> 5</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">mysql_servers</span><span class="p">(</span><span class="n">hostgroup_id</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">weight</span><span class="p">,</span><span class="w"> </span><span class="n">max_connections</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="k">VALUES</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;primary.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">3306</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="mi">200</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="p">(</span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;replica1.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">3306</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="mi">100</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="p">(</span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;replica2.example.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">3306</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="c1">-- 自動偵測 primary（用 read_only flag）
</span></span></span><span class="line"><span class="ln">12</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">mysql_replication_hostgroups</span><span class="p">(</span><span class="n">writer_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">reader_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="k">comment</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;production cluster&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="c1">-- 設 application user、預設走 reader（保守）
</span></span></span><span class="line"><span class="ln">16</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">mysql_users</span><span class="p">(</span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="p">,</span><span class="w"> </span><span class="n">default_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">max_connections</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;app&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;app_password&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="c1">-- 套用設定到 runtime
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">SERVERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">USERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span><span class="c1">-- 持久化到 disk（重啟保留）
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">SERVERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">USERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span></span></span></code></pre></div><p>注意 ProxySQL 的 <em>三層 state</em>：<code>disk</code>（持久化）→ <code>memory</code>（編輯區）→ <code>runtime</code>（實際運作）。每次改完要 <code>LOAD ... TO RUNTIME</code> 才生效、<code>SAVE ... TO DISK</code> 才能 reboot 保留。沒 <code>SAVE</code> 重啟後 config 消失是新手最常踩的雷。</p>
<h2 id="stage-2query-parse--rule-match--query-rule-engine">Stage 2：Query Parse + Rule Match — query rule engine</h2>
<p>ProxySQL 不只 forward connection、看 <em>SQL 內容</em> 決定怎麼 route。Query rule 是 <em>ordered chain</em>、match 第一個符合的 rule。</p>
<p><strong>Query rule 核心欄位</strong>：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>rule_id</code></td>
          <td>排序（越小越先 match）</td>
      </tr>
      <tr>
          <td><code>match_pattern</code></td>
          <td>regex 比對 SQL（支援 <code>^SELECT</code> / <code>FOR UPDATE</code> 等）</td>
      </tr>
      <tr>
          <td><code>destination_hostgroup</code></td>
          <td>match 後送哪個 hostgroup</td>
      </tr>
      <tr>
          <td><code>apply</code></td>
          <td>match 後是否停 chain（1=stop、0=繼續看後面 rule）</td>
      </tr>
      <tr>
          <td><code>cache_ttl</code></td>
          <td>result cache TTL（毫秒）— ProxySQL 內建 query cache</td>
      </tr>
      <tr>
          <td><code>mirror_hostgroup</code></td>
          <td>query 鏡像送到第二個 hostgroup（不等 response、用於 shadow test）</td>
      </tr>
  </tbody>
</table>
<p><strong>典型讀寫分離 rule</strong>：</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">-- Rule 100: SELECT ... FOR UPDATE 必須走 primary
</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">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</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="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^SELECT.*FOR UPDATE$&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</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">-- Rule 200: 一般 SELECT 走 replica（reader）
</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">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</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="mi">200</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^SELECT&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1</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></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c1">-- Rule 300: BEGIN / START TRANSACTION 走 primary
</span></span></span><span class="line"><span class="ln">10</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">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">300</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^(BEGIN|START TRANSACTION)&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</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></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="c1">-- 其他（INSERT / UPDATE / DELETE）預設走 default_hostgroup（user 設的）
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- application user default 設 10 (writer)、所以寫入自動走 primary
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span></span></span></code></pre></div><p><strong>Rule 順序很重要</strong>：<code>rule_id</code> 100 先 match、200 再 match、依此類推。Rule 200 比 100 寬鬆（任何 SELECT）、所以 <code>FOR UPDATE</code> 必須先 match rule 100 才不會誤送 replica。</p>
<h2 id="stage-3backend-route--replica-lag-aware--circuit-breaker">Stage 3：Backend Route — replica lag-aware + circuit breaker</h2>
<p>Rule match 後 ProxySQL 從 hostgroup 內挑一個 server。Backend selection 不是 pure round-robin、考慮：</p>
<ul>
<li><em>Weight</em>：每個 server <code>weight</code> 比例分配（典型用於 replica capacity 不同）</li>
<li><em>Replica lag</em>：若 hostgroup 設 <code>max_replication_lag</code>、lag 超過 threshold 的 replica 自動暫時退出</li>
<li><em>Connection count</em>：避免某個 server connection 滿</li>
<li><em>Server status</em>：<code>mysql_servers.status</code> (ONLINE / SHUNNED / OFFLINE_SOFT / OFFLINE_HARD) 決定是否可用</li>
</ul>
<p><strong>Replica lag-aware routing 配置</strong>：</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">-- 給整個 reader hostgroup 設 lag threshold
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">mysql_servers</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">SET</span><span class="w"> </span><span class="n">max_replication_lag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">  </span><span class="c1">-- 秒
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">hostgroup_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">20</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></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">SERVERS</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</span><span class="p">;</span></span></span></code></pre></div><p>ProxySQL 內部用 <em>monitor module</em> 定期跑 <code>SHOW SLAVE STATUS</code>、lag 超過 5 秒 → 該 replica 暫時退出 reader hostgroup。讀 query 自動避開 lagging replica。</p>
<p><strong>Circuit breaker（自動 shun）</strong>：server 連續失敗 → ProxySQL 自動 <code>SHUNNED</code>、避免持續打 broken server。但 <em>application 層仍要處理 retry</em>、ProxySQL 不保證 query 100% 成功。</p>
<h2 id="stage-4response-返回--connection-multiplexing">Stage 4：Response 返回 — connection multiplexing</h2>
<p>ProxySQL 對 application connection 跟 backend connection 是 <em>N:M 多工</em>：</p>
<ul>
<li>Application connection 跟 ProxySQL 1:1</li>
<li>ProxySQL 跟 backend MySQL connection 共用 pool（multiplexing）</li>
</ul>
<p><strong>Multiplexing 條件</strong>：</p>
<ul>
<li>Transaction 內：connection 綁定特定 backend（保 transaction atomicity）</li>
<li>跨 transaction：connection 可以換 backend</li>
<li><code>SET</code> statement 改 session variable：connection 黏死 backend（防 session state leak）</li>
<li>User variable（<code>@var</code>）：connection 黏死 backend</li>
</ul>
<p><strong>結果</strong>：application 看到的是「自己有 1000 個 connection」、ProxySQL 後端可能只有 100 connection 到 MySQL。對 connection-bound MySQL（max_connections 限制）是關鍵 cost saving。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-query-rule-順序錯亂--for-update-被-select-route-到-replica">1. Query rule 順序錯亂 — <code>FOR UPDATE</code> 被 SELECT route 到 replica</h3>
<p>Rule 200（<code>^SELECT</code>）寫在 rule 100（<code>^SELECT.*FOR UPDATE$</code>）之前、ProxySQL match 第一個 rule（rule 200）就停、<code>SELECT ... FOR UPDATE</code> 被送 replica、replica 沒 lock、application 假設有 lock 跑 race condition。</p>
<p>修法：</p>
<ul>
<li><code>rule_id</code> 排序：精確 rule（多條件 regex）放小、寬鬆 rule 放大</li>
<li>用 <code>apply=1</code> 強制停 chain、不要讓 query 繼續往下 match</li>
<li>跑 ProxySQL <code>SHOW PROCESSLIST</code> + audit log 確認 routing 正確</li>
</ul>
<h3 id="2-connection-漂移--multiplexing-把-session-variable-弄丟">2. Connection 漂移 — Multiplexing 把 session variable 弄丟</h3>
<p>Application 跑 <code>SET sql_mode=...</code>、ProxySQL 把這 connection 暫時黏死 backend 1。下個 query ProxySQL forget、把 connection unstick、實際 forward 到 backend 2（沒 <code>SET sql_mode</code>）、SQL 解析行為不同、application bug。</p>
<p>修法：</p>
<ul>
<li>用 <code>mysql-multiplexing=false</code> 全 disable（最簡單但浪費 connection pool 效率）</li>
<li>或在 application init 連線後跑的 <code>SET</code> 全列在 <code>mysql_users.connect_init</code>（每個 connection ProxySQL 自動跑、不會漂移）</li>
<li>避免 application 中途改 session variable、改成全部走 ProxySQL connect_init</li>
</ul>
<h3 id="3-write-不小心-route-到-replica--default_hostgroup-設錯">3. Write 不小心 route 到 replica — <code>default_hostgroup</code> 設錯</h3>
<p>Application user <code>default_hostgroup</code> 設 20 (reader)、INSERT / UPDATE / DELETE 沒 match 到任何 rule（沒寫 catch-all write rule）、走 default → 送 replica → replica 是 read-only → error。或更糟：replica 不是 read-only mode、寫入 <em>寫到 replica 上</em>、replication 反向不同步、data corruption。</p>
<p>修法：</p>
<ul>
<li>Application user <code>default_hostgroup</code> 設 10 (writer) — 寫入預設走 primary</li>
<li>Replica MySQL 一定要 <code>read_only=1</code>（防 stale write 寫到 replica）</li>
<li>監控 <code>mysql_query_rules</code> match 率、寫入 query 應該大部分透過 default_hostgroup 路由、不是個別 rule</li>
</ul>
<h3 id="4-runtime--disk-schema-drift--改了-runtime-沒-save重啟-config-消失">4. Runtime / disk schema drift — 改了 runtime 沒 save、重啟 config 消失</h3>
<p><code>LOAD ... TO RUNTIME</code> 跟 <code>SAVE ... TO DISK</code> 是兩個獨立操作。On-call 在事故中改 ProxySQL 配置（add server、調 query rule）、<code>LOAD</code> 套到 runtime 但忘記 <code>SAVE</code>、隔天 ProxySQL 重啟（OS update / crash）、config 回到 disk 版本、半夜 alert。</p>
<p>修法：</p>
<ul>
<li>每次 <code>LOAD ... TO RUNTIME</code> 後立刻 <code>SAVE ... TO DISK</code>（變成 habit）</li>
<li>用 IaC（Terraform / Ansible）管 ProxySQL config、不要手動改 admin</li>
<li>監控：對比 <code>runtime_mysql_servers</code> 跟 <code>mysql_servers</code>（disk）、有 diff 即告警</li>
</ul>
<h3 id="5-mirror-traffic-副作用--insert-鏡像到-staging-寫了兩次">5. Mirror traffic 副作用 — INSERT 鏡像到 staging 寫了兩次</h3>
<p><code>mirror_hostgroup</code> 把 query 鏡像送到第二個 hostgroup（不等 response、用於 shadow test 新 schema）。但 <em>鏡像是真實執行</em>、不是 dry-run。鏡像 INSERT 到 staging hostgroup → staging 真的多了 row。如果 staging hostgroup 接到 production 表（誤接）、production 寫入 doubled。</p>
<p>修法：</p>
<ul>
<li>Mirror 只用於 <em>獨立 staging cluster</em>、不混用 production schema</li>
<li>Mirror 設定要 review（規則 <code>match_pattern</code> 跟 <code>mirror_hostgroup</code> 配對）</li>
<li>開 mirror 前在 staging 跑 dry-run、確認 schema 跟 production isolated</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p>對 100 application instance × 50 connection / instance = 5000 application connection 場景：</p>
<table>
  <thead>
      <tr>
          <th>配置</th>
          <th>ProxySQL 設定</th>
          <th>MySQL backend 配置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application → ProxySQL</td>
          <td><code>mysql-max_connections=10000</code></td>
          <td>不影響</td>
      </tr>
      <tr>
          <td>ProxySQL → MySQL primary</td>
          <td><code>max_connections=200</code>（per server）</td>
          <td>MySQL <code>max_connections=300</code>（多 100 buffer for admin）</td>
      </tr>
      <tr>
          <td>ProxySQL → MySQL replica</td>
          <td><code>max_connections=200</code>（per server）</td>
          <td>同上</td>
      </tr>
      <tr>
          <td>ProxySQL 數量（HA）</td>
          <td>至少 2 instance（HAProxy / VIP）</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Memory per ProxySQL</td>
          <td>2-4 GB（query rule cache + connection pool）</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>ProxySQL 本身需要 HA：放兩個 instance 後面接 VIP（keepalived）或 HAProxy。Application 連 VIP / HAProxy、不直接連 ProxySQL hostname（單點失效）。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>ProxySQL 透過 <em>monitor module</em> 自動偵測 primary（檢查 <code>read_only</code> flag）+ replica lag（檢查 <code>Seconds_Behind_Master</code>）。這個 monitor 依賴 MySQL replication 已配好（GTID + binlog ROW format）。詳見 <a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>。</p>
<h3 id="跟-orchestrator-ha">跟 Orchestrator HA</h3>
<p>Orchestrator 自動 failover 後新 primary 的 <code>read_only</code> flag 變 0、舊 primary 變 1。ProxySQL monitor 偵測到、自動把 hostgroup 10（writer）的 server 切換、application 不必改 connection string。</p>
<p>詳見 <em>Orchestrator failover 設計</em> 篇（待寫）。</p>
<h3 id="跟-osc-toolgh-ost--pt-osc">跟 OSC tool（gh-ost / pt-osc）</h3>
<p>ProxySQL 可以 <em>暫時 throttle</em> application 對某張表的寫入（query rule <code>delay</code> 欄位）、配合 OSC tool cut-over 時段降低 metadata lock 衝突。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">Online Schema Change Tools</a>。</p>
<h3 id="跟-aurora-mysql--rds-proxy">跟 Aurora MySQL / RDS Proxy</h3>
<p>Aurora MySQL 推 <em>RDS Proxy</em>（AWS managed proxy）取代 ProxySQL — 跟 IAM 整合、failover &lt; 30 秒。但 RDS Proxy <em>沒有 query routing rule engine</em>（只做 connection pool）、不能讀寫分離。Aurora user 仍可能用 ProxySQL 在前面、再用 RDS Proxy 作 backend connection pool。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor page</a>。</p>
<h3 id="跟-postgresql-pgbouncer-對比">跟 PostgreSQL pgBouncer 對比</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>ProxySQL（MySQL）</th>
          <th>pgBouncer（PostgreSQL）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>抽象層</td>
          <td>Query-level proxy</td>
          <td>Transaction-level pool</td>
      </tr>
      <tr>
          <td>Query routing</td>
          <td>內建（rule engine）</td>
          <td>無（不看 SQL）</td>
      </tr>
      <tr>
          <td>Connection pool</td>
          <td>內建</td>
          <td>核心功能</td>
      </tr>
      <tr>
          <td>Read/write split</td>
          <td>內建（自動 + rule）</td>
          <td>要 application 層或 HAProxy 配</td>
      </tr>
      <tr>
          <td>Replica lag-aware</td>
          <td>內建</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Query cache</td>
          <td>內建</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>ProxySQL 是 <em>query 層中介</em>、pgBouncer 是 <em>connection 層中介</em>。詳見 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（read replica routing 前提）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/online-schema-change-tools/" data-link-title="MySQL Online Schema Change：gh-ost 跟 pt-online-schema-change 兩條完全不同的 ghost table 路徑" data-link-desc="MySQL ALTER TABLE 可能鎖整張表，production 需要 online schema change 流程。gh-ost（GitHub）跟 pt-online-schema-change（Percona）都用 ghost table 解決、但底層機制完全不同：pt-osc 用 trigger 同步、gh-ost 用 binlog stream 同步。本文走兩工具機制對照表 → trigger vs binlog 各自取捨 → 配置 step-by-step → 5 production 踩雷（trigger overhead / binlog 延遲 / FK constraint / hot trigger lock / 切換瞬間 deadlock）→ 何時用哪一個">MySQL Online Schema Change Tools</a>（OSC + ProxySQL throttle 整合）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PostgreSQL pgBouncer</a>（PG sibling、不同抽象層）</li>
<li><a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor page</a>（RDS Proxy + ProxySQL 取捨）</li>
<li><a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">Connection Pool 卡片</a></li>
<li>官方：<a href="https://proxysql.com/documentation/">ProxySQL Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL ProxySQL Routing Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/proxysql-routing-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/proxysql-routing-lab/</guid><description>&lt;p>MySQL ProxySQL routing lab 的核心責任是讓讀者看到 database proxy 如何把 application query 導向不同 hostgroup。這篇承接 &lt;a href="../../proxysql-config/">ProxySQL Config&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能定義 writer / reader hostgroup、建立 query rule、觀察 routing stats，並寫下 stale read 與 failover 風險。&lt;/p>
&lt;h2 id="hostgroup-model">Hostgroup Model&lt;/h2>
&lt;p>Hostgroup model 的核心責任是把 backend 分成 writer 與 reader。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">hostgroup 10: writer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">hostgroup 20: reader&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在單節點 lab 中，writer / reader 可以先指向同一 MySQL；正式環境應用 replica 作 reader，並搭配 replication lag guard。&lt;/p>
&lt;h2 id="query-rule">Query Rule&lt;/h2>
&lt;p>Query rule 的核心責任是示範 routing policy。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- Conceptual ProxySQL admin commands. Adjust host / credential for your lab.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">mysql_query_rules&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rule_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">active&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">match_pattern&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">destination_hostgroup&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;^SELECT&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;.*&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LOAD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MYSQL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">QUERY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RULES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RUNTIME&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">SAVE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MYSQL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">QUERY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RULES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DISK&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個規則把 &lt;code>SELECT&lt;/code> 導向 reader，其餘導向 writer。Production 要排除 &lt;code>SELECT ... FOR UPDATE&lt;/code>、transaction、read-after-write 與 session state。&lt;/p>
&lt;h2 id="routing-evidence">Routing Evidence&lt;/h2>
&lt;p>Routing evidence 的核心責任是確認 query 真的走到預期 hostgroup。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hostgroup&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">srv_host&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Queries&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">stats_mysql_connection_pool&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rule_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hits&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">stats_mysql_query_rules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rule_id&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Evidence 要和 application log 對齊。若某個 workflow 寫後立刻讀，routing rule 要保證它走 writer 或具備 freshness policy。&lt;/p>
&lt;h2 id="failure-note">Failure Note&lt;/h2>
&lt;p>Failure note 的核心責任是記錄 proxy 常見風險。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>風險&lt;/th>
 &lt;th>控制方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Stale read&lt;/td>
 &lt;td>lag guard、read-after-write to writer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction split&lt;/td>
 &lt;td>transaction pinning、query rule review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bad regex&lt;/td>
 &lt;td>query digest / allowlist&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backend unhealthy&lt;/td>
 &lt;td>health check、hostgroup failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Credential drift&lt;/td>
 &lt;td>ProxySQL user sync / secret rotation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>完成本篇後，完整設定讀 &lt;a href="../../proxysql-config/">ProxySQL Config&lt;/a>；replica 與 failover 讀 &lt;a href="../replication-failover-lab/">Replication Failover Lab&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>MySQL ProxySQL routing lab 的核心責任是讓讀者看到 database proxy 如何把 application query 導向不同 hostgroup。這篇承接 <a href="../../proxysql-config/">ProxySQL Config</a>。</p>
<p>本文的驗收標準是：你能定義 writer / reader hostgroup、建立 query rule、觀察 routing stats，並寫下 stale read 與 failover 風險。</p>
<h2 id="hostgroup-model">Hostgroup Model</h2>
<p>Hostgroup model 的核心責任是把 backend 分成 writer 與 reader。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">hostgroup 10: writer
</span></span><span class="line"><span class="ln">2</span><span class="cl">hostgroup 20: reader</span></span></code></pre></div><p>在單節點 lab 中，writer / reader 可以先指向同一 MySQL；正式環境應用 replica 作 reader，並搭配 replication lag guard。</p>
<h2 id="query-rule">Query Rule</h2>
<p>Query rule 的核心責任是示範 routing policy。</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">-- Conceptual ProxySQL admin commands. Adjust host / credential for your lab.
</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">mysql_query_rules</span><span class="p">(</span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">active</span><span class="p">,</span><span class="w"> </span><span class="n">match_pattern</span><span class="p">,</span><span class="w"> </span><span class="n">destination_hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">apply</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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;^SELECT&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1</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="p">(</span><span class="mi">20</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;.*&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">1</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="k">LOAD</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">RUNTIME</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="n">SAVE</span><span class="w"> </span><span class="n">MYSQL</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">RULES</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">DISK</span><span class="p">;</span></span></span></code></pre></div><p>這個規則把 <code>SELECT</code> 導向 reader，其餘導向 writer。Production 要排除 <code>SELECT ... FOR UPDATE</code>、transaction、read-after-write 與 session state。</p>
<h2 id="routing-evidence">Routing Evidence</h2>
<p>Routing evidence 的核心責任是確認 query 真的走到預期 hostgroup。</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">SELECT</span><span class="w"> </span><span class="n">hostgroup</span><span class="p">,</span><span class="w"> </span><span class="n">srv_host</span><span class="p">,</span><span class="w"> </span><span class="n">Queries</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">FROM</span><span class="w"> </span><span class="n">stats_mysql_connection_pool</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></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">rule_id</span><span class="p">,</span><span class="w"> </span><span class="n">hits</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">FROM</span><span class="w"> </span><span class="n">stats_mysql_query_rules</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">rule_id</span><span class="p">;</span></span></span></code></pre></div><p>Evidence 要和 application log 對齊。若某個 workflow 寫後立刻讀，routing rule 要保證它走 writer 或具備 freshness policy。</p>
<h2 id="failure-note">Failure Note</h2>
<p>Failure note 的核心責任是記錄 proxy 常見風險。</p>
<table>
  <thead>
      <tr>
          <th>風險</th>
          <th>控制方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stale read</td>
          <td>lag guard、read-after-write to writer</td>
      </tr>
      <tr>
          <td>Transaction split</td>
          <td>transaction pinning、query rule review</td>
      </tr>
      <tr>
          <td>Bad regex</td>
          <td>query digest / allowlist</td>
      </tr>
      <tr>
          <td>Backend unhealthy</td>
          <td>health check、hostgroup failover</td>
      </tr>
      <tr>
          <td>Credential drift</td>
          <td>ProxySQL user sync / secret rotation</td>
      </tr>
  </tbody>
</table>
<p>完成本篇後，完整設定讀 <a href="../../proxysql-config/">ProxySQL Config</a>；replica 與 failover 讀 <a href="../replication-failover-lab/">Replication Failover Lab</a>。</p>
]]></content:encoded></item></channel></rss>