<?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>MySQL Hands-on 操作路線 on Tarragon</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/</link><description>Recent content in MySQL Hands-on 操作路線 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/backend/01-database/vendors/mysql/hands-on/index.xml" rel="self" type="application/rss+xml"/><item><title>MySQL Backup Restore Drill</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/backup-restore-drill/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/backup-restore-drill/</guid><description>&lt;p>MySQL backup restore drill 的核心責任是證明資料可以從 backup 回到可用狀態。這篇承接 &lt;a href="../../pitr-backup/">PITR / Backup&lt;/a>，用 logical dump 建立最小演練框架，並保留 physical backup / binlog PITR 的 evidence 欄位。&lt;/p>
&lt;p>本文的驗收標準是：你能產出 dump、記錄 binlog position、還原到隔離 database、跑 validation query，並寫下 RPO / RTO note。&lt;/p>
&lt;h2 id="create-backup">Create Backup&lt;/h2>
&lt;p>Create backup 的核心責任是建立可還原 artifact。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir -p /tmp/mysql-backup-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mysqldump -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --single-transaction --routines --triggers appdb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &amp;gt; /tmp/mysql-backup-lab/appdb.sql&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>記錄 binlog 狀態：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw -e &lt;span class="s2">&amp;#34;SHOW BINARY LOG STATUS;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--single-transaction&lt;/code> 適合 InnoDB consistent dump。大型 production 要評估 physical backup、backup lock、replication lag 與 binlog retention。&lt;/p>
&lt;h2 id="mutate-source">Mutate Source&lt;/h2>
&lt;p>Mutate source 的核心責任是讓 restore 時間點具體化。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw appdb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 777, &amp;#39;after-backup-write&amp;#39;);&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Source 現在比 backup 多一筆。這能用來討論 RPO 與 binlog PITR。&lt;/p>
&lt;h2 id="restore-isolated-database">Restore Isolated Database&lt;/h2>
&lt;p>Restore isolated database 的核心責任是避免覆蓋 source。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="s2">&amp;#34;DROP DATABASE IF EXISTS appdb_restore; CREATE DATABASE appdb_restore;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw appdb_restore &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &amp;lt; /tmp/mysql-backup-lab/appdb.sql&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Validation：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u root -proot_pw appdb_restore &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM accounts;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM ledger_entries;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">FROM accounts a JOIN ledger_entries l ON l.account_id = a.id
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">GROUP BY a.owner_name;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Validation query 要和 application smoke test 對齊。正式 drill 還要啟動 app 指向 restore database。&lt;/p>
&lt;h2 id="rpo--rto-note">RPO / RTO Note&lt;/h2>
&lt;p>RPO / RTO note 的核心責任是把演練結果轉成服務承諾。&lt;/p></description><content:encoded><![CDATA[<p>MySQL backup restore drill 的核心責任是證明資料可以從 backup 回到可用狀態。這篇承接 <a href="../../pitr-backup/">PITR / Backup</a>，用 logical dump 建立最小演練框架，並保留 physical backup / binlog PITR 的 evidence 欄位。</p>
<p>本文的驗收標準是：你能產出 dump、記錄 binlog position、還原到隔離 database、跑 validation query，並寫下 RPO / RTO note。</p>
<h2 id="create-backup">Create Backup</h2>
<p>Create backup 的核心責任是建立可還原 artifact。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir -p /tmp/mysql-backup-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysqldump -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --single-transaction --routines --triggers appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  &gt; /tmp/mysql-backup-lab/appdb.sql</span></span></code></pre></div><p>記錄 binlog 狀態：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw -e <span class="s2">&#34;SHOW BINARY LOG STATUS;&#34;</span></span></span></code></pre></div><p><code>--single-transaction</code> 適合 InnoDB consistent dump。大型 production 要評估 physical backup、backup lock、replication lag 與 binlog retention。</p>
<h2 id="mutate-source">Mutate Source</h2>
<p>Mutate source 的核心責任是讓 restore 時間點具體化。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -e <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 777, &#39;after-backup-write&#39;);&#34;</span></span></span></code></pre></div><p>Source 現在比 backup 多一筆。這能用來討論 RPO 與 binlog PITR。</p>
<h2 id="restore-isolated-database">Restore Isolated Database</h2>
<p>Restore isolated database 的核心責任是避免覆蓋 source。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -e <span class="s2">&#34;DROP DATABASE IF EXISTS appdb_restore; CREATE DATABASE appdb_restore;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw appdb_restore <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  &lt; /tmp/mysql-backup-lab/appdb.sql</span></span></code></pre></div><p>Validation：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u root -proot_pw appdb_restore <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT COUNT(*) FROM accounts;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SELECT COUNT(*) FROM ledger_entries;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">FROM accounts a JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">GROUP BY a.owner_name;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Validation query 要和 application smoke test 對齊。正式 drill 還要啟動 app 指向 restore database。</p>
<h2 id="rpo--rto-note">RPO / RTO Note</h2>
<p>RPO / RTO note 的核心責任是把演練結果轉成服務承諾。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>記錄內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup time</td>
          <td>dump start / finish</td>
      </tr>
      <tr>
          <td>Binlog position</td>
          <td>file、position 或 GTID set</td>
      </tr>
      <tr>
          <td>Restore time</td>
          <td>開始 restore 到 validation 成功</td>
      </tr>
      <tr>
          <td>Data gap</td>
          <td>backup 後需要 binlog 補回的寫入</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>application workflow</td>
      </tr>
  </tbody>
</table>
<p>完成本篇後，binlog CDC 讀 <a href="../../binlog-cdc/">Binlog CDC</a>；PITR 策略讀 <a href="../../pitr-backup/">PITR / Backup</a>。</p>
]]></content:encoded></item><item><title>MySQL Local Lab Quickstart</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/local-lab-quickstart/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/local-lab-quickstart/</guid><description>&lt;p>MySQL local lab quickstart 的核心責任是建立後續 ProxySQL、OSC、replication、backup 與 Vitess sandbox 共用的本地環境。這個 lab 提供可重建 MySQL instance、baseline schema、seed data 與 basic evidence。&lt;/p>
&lt;p>本文的驗收標準是：你能啟動 MySQL、套用 schema、跑 sample workload、取得 processlist / InnoDB status / table count，並能 teardown 重建。&lt;/p>
&lt;h2 id="docker-compose">Docker Compose&lt;/h2>
&lt;p>Docker Compose 的核心責任是讓 lab 環境可重建。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">services&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"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mysql&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="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql:8.4&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="nt">environment&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="nt">MYSQL_ROOT_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">root_pw&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="nt">MYSQL_DATABASE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">appdb&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="nt">MYSQL_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app_user&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app_pw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&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">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;33069:3306&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&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">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--performance-schema=ON&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--log-bin=mysql-bin&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;--server-id=1&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">MYSQL_PWD&lt;/span>&lt;span class="o">=&lt;/span>app_pw
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user appdb -e &lt;span class="s2">&amp;#34;SELECT VERSION();&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="baseline-schema">Baseline Schema&lt;/h2>
&lt;p>Baseline schema 的核心責任是建立可測 transaction、index、binlog 與 OSC 的模型。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user appdb &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE accounts (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s"> id BIGINT PRIMARY KEY AUTO_INCREMENT,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s"> tenant_id CHAR(36) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s"> owner_name VARCHAR(128) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s"> status ENUM(&amp;#39;active&amp;#39;, &amp;#39;closed&amp;#39;) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> KEY idx_accounts_tenant (tenant_id)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s">) ENGINE=InnoDB;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">CREATE TABLE ledger_entries (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="s"> id BIGINT PRIMARY KEY AUTO_INCREMENT,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s"> account_id BIGINT NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s"> amount_cents BIGINT NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s"> idempotency_key VARCHAR(128) NOT NULL,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s"> created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s"> UNIQUE KEY uk_ledger_idempotency (idempotency_key),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s"> KEY idx_ledger_account_created (account_id, created_at),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s"> CONSTRAINT fk_ledger_account FOREIGN KEY (account_id) REFERENCES accounts(id)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="s">) ENGINE=InnoDB;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="seed-and-evidence">Seed and Evidence&lt;/h2>
&lt;p>Seed and evidence 的核心責任是產生可重跑資料與 baseline。&lt;/p></description><content:encoded><![CDATA[<p>MySQL local lab quickstart 的核心責任是建立後續 ProxySQL、OSC、replication、backup 與 Vitess sandbox 共用的本地環境。這個 lab 提供可重建 MySQL instance、baseline schema、seed data 與 basic evidence。</p>
<p>本文的驗收標準是：你能啟動 MySQL、套用 schema、跑 sample workload、取得 processlist / InnoDB status / table count，並能 teardown 重建。</p>
<h2 id="docker-compose">Docker Compose</h2>
<p>Docker Compose 的核心責任是讓 lab 環境可重建。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">services</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="nt">mysql</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="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mysql:8.4</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">environment</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="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">root_pw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">appdb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_USER</span><span class="p">:</span><span class="w"> </span><span class="l">app_user</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">app_pw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">ports</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 class="s2">&#34;33069:3306&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">command</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="s2">&#34;--performance-schema=ON&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--log-bin=mysql-bin&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;--server-id=1&#34;</span></span></span></code></pre></div><p>啟動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose up -d
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">MYSQL_PWD</span><span class="o">=</span>app_pw
</span></span><span class="line"><span class="ln">3</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SELECT VERSION();&#34;</span></span></span></code></pre></div><h2 id="baseline-schema">Baseline Schema</h2>
<p>Baseline schema 的核心責任是建立可測 transaction、index、binlog 與 OSC 的模型。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">CREATE TABLE accounts (
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">  id BIGINT PRIMARY KEY AUTO_INCREMENT,
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  tenant_id CHAR(36) NOT NULL,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">  owner_name VARCHAR(128) NOT NULL,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  status ENUM(&#39;active&#39;, &#39;closed&#39;) NOT NULL,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">  KEY idx_accounts_tenant (tenant_id)
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">) ENGINE=InnoDB;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">CREATE TABLE ledger_entries (
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  id BIGINT PRIMARY KEY AUTO_INCREMENT,
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  account_id BIGINT NOT NULL,
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">  amount_cents BIGINT NOT NULL,
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">  idempotency_key VARCHAR(128) NOT NULL,
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s">  UNIQUE KEY uk_ledger_idempotency (idempotency_key),
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">  KEY idx_ledger_account_created (account_id, created_at),
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">  CONSTRAINT fk_ledger_account FOREIGN KEY (account_id) REFERENCES accounts(id)
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s">) ENGINE=InnoDB;
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><h2 id="seed-and-evidence">Seed and Evidence</h2>
<p>Seed and evidence 的核心責任是產生可重跑資料與 baseline。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">INSERT INTO accounts(tenant_id, owner_name, status)
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">VALUES (&#39;tenant-a&#39;, &#39;Ada&#39;, &#39;active&#39;), (&#39;tenant-b&#39;, &#39;Lin&#39;, &#39;active&#39;);
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key)
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">VALUES (1, 1000, &#39;seed-ada-1&#39;), (1, -200, &#39;seed-ada-2&#39;), (2, 500, &#39;seed-lin-1&#39;);
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">SELECT a.owner_name, SUM(l.amount_cents) AS balance_cents
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">FROM accounts a JOIN ledger_entries l ON l.account_id = a.id
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">GROUP BY a.owner_name;
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Basic evidence：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SHOW FULL PROCESSLIST;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SHOW TABLE STATUS;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user appdb -e <span class="s2">&#34;SHOW ENGINE INNODB STATUS\\G&#34;</span></span></span></code></pre></div><h2 id="teardown">Teardown</h2>
<p>Teardown 的核心責任是讓 lab 可重跑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose down -v</span></span></code></pre></div><p>完成本篇後，backup 進入 <a href="../backup-restore-drill/">Backup Restore Drill</a>；schema change 進入 <a href="../online-schema-change-lab/">Online Schema Change Lab</a>；routing 進入 <a href="../proxysql-routing-lab/">ProxySQL Routing Lab</a>。</p>
]]></content:encoded></item><item><title>MySQL Online Schema Change Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/online-schema-change-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/online-schema-change-lab/</guid><description>&lt;p>MySQL online schema change lab 的核心責任是讓讀者看到 schema change 的 metadata lock、algorithm、copy / cutover 與 validation evidence。這篇承接 &lt;a href="../../online-schema-change-tools/">Online Schema Change Tools&lt;/a> 與 &lt;a href="../../metadata-lock-deep-dive/">Metadata Lock Deep Dive&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能跑一個低風險 ALTER、觀察 metadata lock、記錄 validation query，並理解 gh-ost / pt-osc 的 cutover evidence。&lt;/p>
&lt;h2 id="direct-alter-baseline">Direct ALTER Baseline&lt;/h2>
&lt;p>Direct ALTER baseline 的核心責任是先看 MySQL 原生 DDL 的行為。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw appdb &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">ALTER TABLE accounts ADD COLUMN email VARCHAR(255) NULL;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SHOW CREATE TABLE accounts\G
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>記錄 ALTER duration、algorithm、lock impact 與 table size。不同 MySQL 版本與 DDL 類型會有不同行為，production 要在 staging dry run。&lt;/p>
&lt;h2 id="metadata-lock-observation">Metadata Lock Observation&lt;/h2>
&lt;p>Metadata lock observation 的核心責任是看到 blocker。&lt;/p>
&lt;p>開 Session A：&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">START&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TRANSACTION&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">2&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="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">accounts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>保持 transaction 開啟。Session B 執行：&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">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">accounts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COLUMN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">note&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">VARCHAR&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">255&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Session C 查：&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">OBJECT_SCHEMA&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_NAME&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOCK_TYPE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOCK_STATUS&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OWNER_THREAD_ID&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">performance_schema&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">metadata_locks&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">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OBJECT_SCHEMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;appdb&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>完成觀察後，Session A &lt;code>COMMIT&lt;/code>。這段 lab 展示 long transaction 如何讓 DDL 等待。&lt;/p>
&lt;h2 id="osc-frame">OSC Frame&lt;/h2>
&lt;p>OSC frame 的核心責任是理解 gh-ost / pt-online-schema-change 的證據，而非要求每個 lab 都安裝工具。&lt;/p>
&lt;p>OSC runbook 要記錄：&lt;/p>
&lt;ol>
&lt;li>Source table、ghost table、migration statement。&lt;/li>
&lt;li>Copy progress、chunk size、throttle condition。&lt;/li>
&lt;li>Replication lag / load threshold。&lt;/li>
&lt;li>Cutover pre-check：long transaction、metadata lock、traffic。&lt;/li>
&lt;li>Cutover duration 與 validation query。&lt;/li>
&lt;li>Rollback / drop ghost table policy。&lt;/li>
&lt;/ol>
&lt;p>Cutover 前最重要的是 metadata lock pre-check。工具能降低大部分 copy 風險，但最後 rename / swap 仍需要短暫鎖。&lt;/p>
&lt;h2 id="validation">Validation&lt;/h2>
&lt;p>Validation 的核心責任是證明 schema change 後資料與 query 仍正確。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">33069&lt;/span> -u app_user -papp_pw appdb &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM accounts;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT COUNT(*) FROM ledger_entries;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">EXPLAIN SELECT * FROM accounts WHERE tenant_id = &amp;#39;tenant-a&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>正式 migration 要補 row checksum、null rate、index usage、replication lag 與 application smoke test。&lt;/p></description><content:encoded><![CDATA[<p>MySQL online schema change lab 的核心責任是讓讀者看到 schema change 的 metadata lock、algorithm、copy / cutover 與 validation evidence。這篇承接 <a href="../../online-schema-change-tools/">Online Schema Change Tools</a> 與 <a href="../../metadata-lock-deep-dive/">Metadata Lock Deep Dive</a>。</p>
<p>本文的驗收標準是：你能跑一個低風險 ALTER、觀察 metadata lock、記錄 validation query，並理解 gh-ost / pt-osc 的 cutover evidence。</p>
<h2 id="direct-alter-baseline">Direct ALTER Baseline</h2>
<p>Direct ALTER baseline 的核心責任是先看 MySQL 原生 DDL 的行為。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">ALTER TABLE accounts ADD COLUMN email VARCHAR(255) NULL;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SHOW CREATE TABLE accounts\G
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>記錄 ALTER duration、algorithm、lock impact 與 table size。不同 MySQL 版本與 DDL 類型會有不同行為，production 要在 staging dry run。</p>
<h2 id="metadata-lock-observation">Metadata Lock Observation</h2>
<p>Metadata lock observation 的核心責任是看到 blocker。</p>
<p>開 Session A：</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">START</span><span class="w"> </span><span class="k">TRANSACTION</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">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">accounts</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></span></code></pre></div><p>保持 transaction 開啟。Session B 執行：</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">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">note</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span></span></span></code></pre></div><p>Session C 查：</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">OBJECT_SCHEMA</span><span class="p">,</span><span class="w"> </span><span class="n">OBJECT_NAME</span><span class="p">,</span><span class="w"> </span><span class="n">LOCK_TYPE</span><span class="p">,</span><span class="w"> </span><span class="n">LOCK_STATUS</span><span class="p">,</span><span class="w"> </span><span class="n">OWNER_THREAD_ID</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">performance_schema</span><span class="p">.</span><span class="n">metadata_locks</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">WHERE</span><span class="w"> </span><span class="n">OBJECT_SCHEMA</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;appdb&#39;</span><span class="p">;</span></span></span></code></pre></div><p>完成觀察後，Session A <code>COMMIT</code>。這段 lab 展示 long transaction 如何讓 DDL 等待。</p>
<h2 id="osc-frame">OSC Frame</h2>
<p>OSC frame 的核心責任是理解 gh-ost / pt-online-schema-change 的證據，而非要求每個 lab 都安裝工具。</p>
<p>OSC runbook 要記錄：</p>
<ol>
<li>Source table、ghost table、migration statement。</li>
<li>Copy progress、chunk size、throttle condition。</li>
<li>Replication lag / load threshold。</li>
<li>Cutover pre-check：long transaction、metadata lock、traffic。</li>
<li>Cutover duration 與 validation query。</li>
<li>Rollback / drop ghost table policy。</li>
</ol>
<p>Cutover 前最重要的是 metadata lock pre-check。工具能降低大部分 copy 風險，但最後 rename / swap 仍需要短暫鎖。</p>
<h2 id="validation">Validation</h2>
<p>Validation 的核心責任是證明 schema change 後資料與 query 仍正確。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">33069</span> -u app_user -papp_pw appdb <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT COUNT(*) FROM accounts;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">SELECT COUNT(*) FROM ledger_entries;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">EXPLAIN SELECT * FROM accounts WHERE tenant_id = &#39;tenant-a&#39;;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>正式 migration 要補 row checksum、null rate、index usage、replication lag 與 application smoke test。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的核心責任是形成交付 artifact。</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">Migration:
</span></span><span class="line"><span class="ln">2</span><span class="cl">DDL / OSC command:
</span></span><span class="line"><span class="ln">3</span><span class="cl">Table size:
</span></span><span class="line"><span class="ln">4</span><span class="cl">MDL pre-check:
</span></span><span class="line"><span class="ln">5</span><span class="cl">Duration:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Validation:
</span></span><span class="line"><span class="ln">7</span><span class="cl">Rollback:
</span></span><span class="line"><span class="ln">8</span><span class="cl">Owner:</span></span></code></pre></div><p>完成本篇後，MDL 事故讀 <a href="../../metadata-lock-deep-dive/">Metadata Lock Deep Dive</a>；工具選型讀 <a href="../../online-schema-change-tools/">Online Schema Change Tools</a>。</p>
]]></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><item><title>MySQL Replication Failover Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/replication-failover-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/replication-failover-lab/</guid><description>&lt;p>MySQL replication failover lab 的核心責任是讓讀者觀察 source / replica 拓撲在 promotion 時的資料與 client route。這篇承接 &lt;a href="../../replication-topology/">Replication Topology&lt;/a> 與 &lt;a href="../../orchestrator-failover/">Orchestrator Failover&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能記錄 replication status、lag、promotion timeline、client error sample、validation query 與 incident decision log。&lt;/p>
&lt;h2 id="baseline-replication">Baseline Replication&lt;/h2>
&lt;p>Baseline replication 的核心責任是先保存 source / replica 狀態。實際建立 replication 依 GTID、binlog file position、Docker topology 或 managed service 而異；本文聚焦演練 evidence。&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">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REPLICA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STATUS&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="k">G&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">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BINARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STATUS&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Baseline 要記錄：&lt;/p>
&lt;ol>
&lt;li>Source host / replica host。&lt;/li>
&lt;li>GTID executed / retrieved。&lt;/li>
&lt;li>IO thread / SQL thread。&lt;/li>
&lt;li>Seconds behind source。&lt;/li>
&lt;li>Read endpoint / write endpoint。&lt;/li>
&lt;/ol>
&lt;h2 id="client-workload">Client Workload&lt;/h2>
&lt;p>Client workload 的核心責任是讓 failover 對 application 可見。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">while&lt;/span> true&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> mysql -h &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$MYSQL_WRITE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -u app_user -papp_pw appdb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 1, UUID());&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> sleep &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 synthetic workload 產生成功、timeout、duplicate、read-only 或 connection error。正式演練要避免碰 production side effect。&lt;/p>
&lt;h2 id="promotion-frame">Promotion Frame&lt;/h2>
&lt;p>Promotion frame 的核心責任是把 failover action 寫成可審查步驟。&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">failover_start:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">old_source:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">candidate_replica:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">lag_before:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">promotion_method:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">accepted_data_loss:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">operator:&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Managed service、Orchestrator 或手動 promotion 都要留下同樣欄位。工具不同，決策證據一致。&lt;/p>
&lt;h2 id="validation">Validation&lt;/h2>
&lt;p>Validation 的核心責任是確認 promoted instance 可讀寫且資料符合預期。&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="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&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">2&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="k">MAX&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&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">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARIABLES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;read_only&amp;#39;&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">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARIABLES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;super_read_only&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>若使用 GTID，還要比較 source / replica 的 GTID set。若有 external side effect，要用 idempotency key 做 reconciliation。&lt;/p>
&lt;h2 id="client-route">Client Route&lt;/h2>
&lt;p>Client route 的核心責任是確認 application、ProxySQL、DNS 或 secret 已指向新 writer。&lt;/p>
&lt;p>檢查項目：&lt;/p>
&lt;ol>
&lt;li>Write endpoint 是否更新。&lt;/li>
&lt;li>ProxySQL writer hostgroup 是否切換。&lt;/li>
&lt;li>Application pool 是否清掉舊連線。&lt;/li>
&lt;li>Retry 是否有 backoff。&lt;/li>
&lt;li>Read replica 是否重新掛到新 source。&lt;/li>
&lt;/ol>
&lt;p>Failover 完成標準包含資料庫 promotion 與 client route 穩定。只 promote 成功，application 仍可能寫到舊 endpoint。&lt;/p></description><content:encoded><![CDATA[<p>MySQL replication failover lab 的核心責任是讓讀者觀察 source / replica 拓撲在 promotion 時的資料與 client route。這篇承接 <a href="../../replication-topology/">Replication Topology</a> 與 <a href="../../orchestrator-failover/">Orchestrator Failover</a>。</p>
<p>本文的驗收標準是：你能記錄 replication status、lag、promotion timeline、client error sample、validation query 與 incident decision log。</p>
<h2 id="baseline-replication">Baseline Replication</h2>
<p>Baseline replication 的核心責任是先保存 source / replica 狀態。實際建立 replication 依 GTID、binlog file position、Docker topology 或 managed service 而異；本文聚焦演練 evidence。</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">SHOW</span><span class="w"> </span><span class="n">REPLICA</span><span class="w"> </span><span class="n">STATUS</span><span class="err">\</span><span class="k">G</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">SHOW</span><span class="w"> </span><span class="nb">BINARY</span><span class="w"> </span><span class="n">LOG</span><span class="w"> </span><span class="n">STATUS</span><span class="p">;</span></span></span></code></pre></div><p>Baseline 要記錄：</p>
<ol>
<li>Source host / replica host。</li>
<li>GTID executed / retrieved。</li>
<li>IO thread / SQL thread。</li>
<li>Seconds behind source。</li>
<li>Read endpoint / write endpoint。</li>
</ol>
<h2 id="client-workload">Client Workload</h2>
<p>Client workload 的核心責任是讓 failover 對 application 可見。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  mysql -h <span class="s2">&#34;</span><span class="nv">$MYSQL_WRITE_HOST</span><span class="s2">&#34;</span> -u app_user -papp_pw appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    -e <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 1, UUID());&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  sleep <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這個 synthetic workload 產生成功、timeout、duplicate、read-only 或 connection error。正式演練要避免碰 production side effect。</p>
<h2 id="promotion-frame">Promotion Frame</h2>
<p>Promotion frame 的核心責任是把 failover action 寫成可審查步驟。</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">failover_start:
</span></span><span class="line"><span class="ln">2</span><span class="cl">old_source:
</span></span><span class="line"><span class="ln">3</span><span class="cl">candidate_replica:
</span></span><span class="line"><span class="ln">4</span><span class="cl">lag_before:
</span></span><span class="line"><span class="ln">5</span><span class="cl">promotion_method:
</span></span><span class="line"><span class="ln">6</span><span class="cl">accepted_data_loss:
</span></span><span class="line"><span class="ln">7</span><span class="cl">operator:</span></span></code></pre></div><p>Managed service、Orchestrator 或手動 promotion 都要留下同樣欄位。工具不同，決策證據一致。</p>
<h2 id="validation">Validation</h2>
<p>Validation 的核心責任是確認 promoted instance 可讀寫且資料符合預期。</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="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ledger_entries</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">SELECT</span><span class="w"> </span><span class="k">MAX</span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ledger_entries</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">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;read_only&#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 class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;super_read_only&#39;</span><span class="p">;</span></span></span></code></pre></div><p>若使用 GTID，還要比較 source / replica 的 GTID set。若有 external side effect，要用 idempotency key 做 reconciliation。</p>
<h2 id="client-route">Client Route</h2>
<p>Client route 的核心責任是確認 application、ProxySQL、DNS 或 secret 已指向新 writer。</p>
<p>檢查項目：</p>
<ol>
<li>Write endpoint 是否更新。</li>
<li>ProxySQL writer hostgroup 是否切換。</li>
<li>Application pool 是否清掉舊連線。</li>
<li>Retry 是否有 backoff。</li>
<li>Read replica 是否重新掛到新 source。</li>
</ol>
<p>Failover 完成標準包含資料庫 promotion 與 client route 穩定。只 promote 成功，application 仍可能寫到舊 endpoint。</p>
<h2 id="incident-log">Incident Log</h2>
<p>Incident log 的核心責任是把演練結果保存。</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">Drill id:
</span></span><span class="line"><span class="ln">2</span><span class="cl">RTO observed:
</span></span><span class="line"><span class="ln">3</span><span class="cl">RPO / accepted data loss:
</span></span><span class="line"><span class="ln">4</span><span class="cl">Client errors:
</span></span><span class="line"><span class="ln">5</span><span class="cl">Validation:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Follow-up:</span></span></code></pre></div><p>完成本篇後，拓撲設計讀 <a href="../../replication-topology/">Replication Topology</a>；自動化工具讀 <a href="../../orchestrator-failover/">Orchestrator Failover</a>；routing 讀 <a href="../proxysql-routing-lab/">ProxySQL Routing Lab</a>。</p>
]]></content:encoded></item><item><title>MySQL Vitess Sandbox Route</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/vitess-sandbox-route/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/vitess-sandbox-route/</guid><description>&lt;p>MySQL Vitess sandbox route 的核心責任是讓讀者用 sandbox 理解 Vitess 如何把 MySQL 拓展成 sharded database platform。這篇承接 &lt;a href="../../vitess-sharding/">Vitess Sharding&lt;/a> 與 &lt;a href="../../migrate-to-planetscale/">MySQL to PlanetScale&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能建立 sandbox、辨識 keyspace / shard / tablet / vtgate、跑基本 query，並記錄 resharding preview 的 evidence。&lt;/p>
&lt;p>官方文件路由的核心責任是固定 sandbox 指令。實作前先查 &lt;a href="https://vitess.io/docs/21.0/get-started/local/">Vitess local install docs&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="concept-map">Concept Map&lt;/h2>
&lt;p>Concept map 的核心責任是先建立 Vitess vocabulary。&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>Keyspace&lt;/td>
 &lt;td>logical database / routing boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shard&lt;/td>
 &lt;td>keyrange 分片&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tablet&lt;/td>
 &lt;td>MySQL instance + Vitess sidecar role&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>vtgate&lt;/td>
 &lt;td>application query routing endpoint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VSchema&lt;/td>
 &lt;td>routing、vindex、sharding metadata&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VReplication&lt;/td>
 &lt;td>resharding / materialize workflow&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Vitess 的重點是 routing 與 resharding。Application 看到的是 vtgate；底下是多個 MySQL tablet 與 topology service。&lt;/p>
&lt;h2 id="sandbox-setup">Sandbox Setup&lt;/h2>
&lt;p>Sandbox setup 的核心責任是使用官方 sandbox 建立可丟棄環境。實際命令依 Vitess 版本調整，正式操作以 Vitess 官方文件為準。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Conceptual route. Use the current Vitess examples for exact commands.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git clone https://github.com/vitessio/vitess.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> vitess/examples/local
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">./101_initial_cluster.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動後要記錄：&lt;/p>
&lt;ol>
&lt;li>Vitess version。&lt;/li>
&lt;li>Keyspace name。&lt;/li>
&lt;li>Shard count。&lt;/li>
&lt;li>vtgate host / port。&lt;/li>
&lt;li>Tablet roles。&lt;/li>
&lt;/ol>
&lt;h2 id="query-through-vtgate">Query Through vtgate&lt;/h2>
&lt;p>Query through vtgate 的核心責任是確認 application 走 routing layer。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mysql -h 127.0.0.1 -P &lt;span class="m">15306&lt;/span> -u user &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="s">SHOW DATABASES;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s">USE commerce;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s">SHOW TABLES;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="s">SELECT * FROM product LIMIT 5;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="s">SQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Evidence 要包含 query result、target keyspace、vtgate endpoint 與 tablet health。Production migration 要確認 ORM / driver 對 vtgate 的相容性。&lt;/p>
&lt;h2 id="vschema-review">VSchema Review&lt;/h2>
&lt;p>VSchema review 的核心責任是理解 shard key 與 routing。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Conceptual command; exact path depends on sandbox.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">cat vschema_commerce_initial.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>審查問題：&lt;/p>
&lt;ol>
&lt;li>哪些 table 是 sharded。&lt;/li>
&lt;li>shard key / vindex 是什麼。&lt;/li>
&lt;li>lookup vindex 是否需要維護。&lt;/li>
&lt;li>cross-shard query 是否存在。&lt;/li>
&lt;li>sequence / id generation 如何處理。&lt;/li>
&lt;/ol>
&lt;p>VSchema 是 Vitess migration 的核心設計文件。選錯 shard key 會讓跨 shard transaction、hot shard 與 resharding 成本升高。&lt;/p></description><content:encoded><![CDATA[<p>MySQL Vitess sandbox route 的核心責任是讓讀者用 sandbox 理解 Vitess 如何把 MySQL 拓展成 sharded database platform。這篇承接 <a href="../../vitess-sharding/">Vitess Sharding</a> 與 <a href="../../migrate-to-planetscale/">MySQL to PlanetScale</a>。</p>
<p>本文的驗收標準是：你能建立 sandbox、辨識 keyspace / shard / tablet / vtgate、跑基本 query，並記錄 resharding preview 的 evidence。</p>
<p>官方文件路由的核心責任是固定 sandbox 指令。實作前先查 <a href="https://vitess.io/docs/21.0/get-started/local/">Vitess local install docs</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="concept-map">Concept Map</h2>
<p>Concept map 的核心責任是先建立 Vitess vocabulary。</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyspace</td>
          <td>logical database / routing boundary</td>
      </tr>
      <tr>
          <td>Shard</td>
          <td>keyrange 分片</td>
      </tr>
      <tr>
          <td>Tablet</td>
          <td>MySQL instance + Vitess sidecar role</td>
      </tr>
      <tr>
          <td>vtgate</td>
          <td>application query routing endpoint</td>
      </tr>
      <tr>
          <td>VSchema</td>
          <td>routing、vindex、sharding metadata</td>
      </tr>
      <tr>
          <td>VReplication</td>
          <td>resharding / materialize workflow</td>
      </tr>
  </tbody>
</table>
<p>Vitess 的重點是 routing 與 resharding。Application 看到的是 vtgate；底下是多個 MySQL tablet 與 topology service。</p>
<h2 id="sandbox-setup">Sandbox Setup</h2>
<p>Sandbox setup 的核心責任是使用官方 sandbox 建立可丟棄環境。實際命令依 Vitess 版本調整，正式操作以 Vitess 官方文件為準。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Conceptual route. Use the current Vitess examples for exact commands.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git clone https://github.com/vitessio/vitess.git
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">cd</span> vitess/examples/local
</span></span><span class="line"><span class="ln">4</span><span class="cl">./101_initial_cluster.sh</span></span></code></pre></div><p>啟動後要記錄：</p>
<ol>
<li>Vitess version。</li>
<li>Keyspace name。</li>
<li>Shard count。</li>
<li>vtgate host / port。</li>
<li>Tablet roles。</li>
</ol>
<h2 id="query-through-vtgate">Query Through vtgate</h2>
<p>Query through vtgate 的核心責任是確認 application 走 routing layer。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mysql -h 127.0.0.1 -P <span class="m">15306</span> -u user <span class="s">&lt;&lt;&#39;SQL&#39;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SHOW DATABASES;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">USE commerce;
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">SHOW TABLES;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">SELECT * FROM product LIMIT 5;
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">SQL</span></span></span></code></pre></div><p>Evidence 要包含 query result、target keyspace、vtgate endpoint 與 tablet health。Production migration 要確認 ORM / driver 對 vtgate 的相容性。</p>
<h2 id="vschema-review">VSchema Review</h2>
<p>VSchema review 的核心責任是理解 shard key 與 routing。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Conceptual command; exact path depends on sandbox.</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">cat vschema_commerce_initial.json</span></span></code></pre></div><p>審查問題：</p>
<ol>
<li>哪些 table 是 sharded。</li>
<li>shard key / vindex 是什麼。</li>
<li>lookup vindex 是否需要維護。</li>
<li>cross-shard query 是否存在。</li>
<li>sequence / id generation 如何處理。</li>
</ol>
<p>VSchema 是 Vitess migration 的核心設計文件。選錯 shard key 會讓跨 shard transaction、hot shard 與 resharding 成本升高。</p>
<h2 id="resharding-preview">Resharding Preview</h2>
<p>Resharding preview 的核心責任是看見 Vitess 的主要價值與操作成本。</p>
<p>Resharding evidence 欄位：</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">source shard:
</span></span><span class="line"><span class="ln">2</span><span class="cl">target shards:
</span></span><span class="line"><span class="ln">3</span><span class="cl">workflow name:
</span></span><span class="line"><span class="ln">4</span><span class="cl">copy phase duration:
</span></span><span class="line"><span class="ln">5</span><span class="cl">replication lag:
</span></span><span class="line"><span class="ln">6</span><span class="cl">cutover time:
</span></span><span class="line"><span class="ln">7</span><span class="cl">validation query:
</span></span><span class="line"><span class="ln">8</span><span class="cl">rollback:</span></span></code></pre></div><p>Resharding 是 production operation，不只是一次 migration。Runbook 要包含 throttling、lag、tablet health、cutover 與 application query validation。</p>
<h2 id="migration-decision">Migration Decision</h2>
<p>Migration decision 的核心責任是判斷何時從 MySQL 走向 Vitess / PlanetScale 類路線。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 MySQL writer 到頂</td>
          <td>需要 horizontal write scaling</td>
      </tr>
      <tr>
          <td>tenant shard boundary 清楚</td>
          <td>Vitess keyspace / shard 有機會匹配</td>
      </tr>
      <tr>
          <td>online resharding 是核心需求</td>
          <td>Vitess value 高</td>
      </tr>
      <tr>
          <td>app 缺少 routing 語意改造空間</td>
          <td>先重構 repository / query</td>
      </tr>
  </tbody>
</table>
<p>完成本篇後，設計細節讀 <a href="../../vitess-sharding/">Vitess Sharding</a>；managed route 讀 <a href="../../migrate-to-planetscale/">MySQL to PlanetScale</a>。</p>
]]></content:encoded></item></channel></rss>