MySQL 主从复制交互实验手册 - Docker 在线实战

MySQL 主从复制交互实验手册 - Docker 在线实战
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MySQL 主从复制交互实验手册 - Docker 在线实战</title>
  <meta name="keywords" content="MySQL, 主从复制, Replication, Docker Compose, binlog, 边学边练, 数据库运维">
  <meta name="description" content="基于 Docker 的 MySQL 8.0 主从复制交互实验手册,在云端终端逐任务实操:编排部署、位点复制、数据一致性验证、离线追赶演练、ROW 格式过滤真相与 1146 故障修复全流程。">
  <meta name="author" content="边学边练">
  <style>
    :root, [data-theme="light"] {
      --bg-base: #f6f8fa;
      --bg-card: #ffffff;
      --bg-subtle: #f6f8fa;
      --border-color: #d0d7de;
      --border-muted: #e2e8f0;
      --text-primary: #1f2328;
      --text-secondary: #656d76;
      --text-muted: #8c959f;
      --k8s-blue: #326ce5;
      --mysql-teal: #00758f;
      --mysql-orange: #e48e00;
      --primary: #0969da;
      --primary-hover: #0550ae;
      --primary-bg: #ddf4ff;
      --success: #1a7f37;
      --success-bg: #dafbe1;
      --danger: #cf222e;
      --danger-hover: #a40e26;
      --danger-bg: #ffebe9;
      --code-bg: #161b22;
      --code-text: #e6edf3;
      --code-prompt: #7d8590;
      --output-bg: #f6f8fa;
      --output-text: #24292f;
      --output-border: #d0d7de;
      --btn-bg: #ffffff;
      --btn-border: #d0d7de;
      --btn-text: #24292f;
      --btn-hover: #f3f4f6;
      --card-radius: 6px;
      --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
      --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    }

    [data-theme="dark"] {
      --bg-base: #0d1117;
      --bg-card: #161b22;
      --bg-subtle: #21262d;
      --border-color: #30363d;
      --border-muted: #21262d;
      --text-primary: #e6edf3;
      --text-secondary: #7d8590;
      --text-muted: #6e7681;
      --k8s-blue: #388bfd;
      --mysql-teal: #23a2c8;
      --mysql-orange: #f0a84a;
      --primary: #2f81f7;
      --primary-hover: #58a6ff;
      --primary-bg: #033d8b;
      --success: #3fb950;
      --success-bg: #033a16;
      --danger: #f85149;
      --danger-hover: #ff7b72;
      --danger-bg: #490202;
      --code-bg: #0d1117;
      --code-text: #e6edf3;
      --code-prompt: #7d8590;
      --output-bg: #161b22;
      --output-text: #e6edf3;
      --output-border: #30363d;
      --btn-bg: #21262d;
      --btn-border: #363b42;
      --btn-text: #c9d1d9;
      --btn-hover: #30363d;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      background-color: var(--bg-base);
      color: var(--text-primary);
      font-family: var(--font-sans);
      font-size: 13px;
      line-height: 1.5;
      padding: 16px;
      -webkit-font-smoothing: antialiased;
    }

    .lab-container {
      max-width: 960px;
      margin: 0 auto;
    }

    /* 顶部导航与元信息 */
    .lab-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-bottom: 14px;
      margin-bottom: 16px;
      border-bottom: 1px solid var(--border-color);
      flex-wrap: wrap;
      gap: 10px;
    }

    .header-left {
      display: flex;
      align-items: center;
      gap: 12px;
    }

    .mysql-logo {
      width: 28px;
      height: 28px;
      flex-shrink: 0;
    }

    .lab-title-group h1 {
      font-size: 16px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .meta-badges {
      display: flex;
      align-items: center;
      gap: 6px;
      margin-top: 4px;
      flex-wrap: wrap;
    }

    .badge {
      font-size: 11px;
      font-family: var(--font-mono);
      padding: 1px 6px;
      border-radius: 4px;
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      color: var(--text-secondary);
    }

    .badge-primary {
      color: var(--primary);
      border-color: var(--border-color);
    }

    .badge-verified {
      color: var(--success);
      background: var(--success-bg);
      border-color: var(--success);
    }

    .header-right {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .theme-toggle {
      background: var(--btn-bg);
      border: 1px solid var(--btn-border);
      color: var(--btn-text);
      padding: 4px 8px;
      border-radius: var(--card-radius);
      font-size: 12px;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      transition: background 0.15s ease;
    }

    .theme-toggle:hover {
      background: var(--btn-hover);
    }

    /* 进度条 */
    .progress-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: var(--card-radius);
      padding: 10px 14px;
      margin-bottom: 16px;
      display: flex;
      align-items: center;
      gap: 12px;
      flex-wrap: wrap;
    }

    .progress-label {
      font-size: 12px;
      color: var(--text-secondary);
      font-family: var(--font-mono);
      white-space: nowrap;
    }

    .progress-track {
      flex: 1;
      min-width: 140px;
      height: 6px;
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      border-radius: 999px;
      overflow: hidden;
    }

    .progress-fill {
      height: 100%;
      width: 0%;
      background: var(--success);
      transition: width 0.25s ease;
    }

    .btn-mini {
      background: var(--btn-bg);
      border: 1px solid var(--btn-border);
      color: var(--btn-text);
      padding: 2px 8px;
      border-radius: 3px;
      font-size: 11px;
      font-family: var(--font-mono);
      cursor: pointer;
      transition: all 0.15s ease;
    }

    .btn-mini:hover {
      background: var(--btn-hover);
    }

    /* 架构概览区 */
    .arch-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: var(--card-radius);
      padding: 14px 16px;
      margin-bottom: 16px;
    }

    .arch-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 10px;
    }

    .arch-title {
      font-size: 13px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .arch-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      gap: 12px;
    }

    .node-col {
      background: var(--bg-subtle);
      border: 1px solid var(--border-muted);
      border-radius: 4px;
      padding: 10px 12px;
    }

    .node-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 8px;
      padding-bottom: 6px;
      border-bottom: 1px solid var(--border-color);
    }

    .node-name {
      font-family: var(--font-mono);
      font-weight: 600;
      font-size: 12px;
      color: var(--text-primary);
    }

    .node-tag {
      font-size: 10px;
      font-family: var(--font-mono);
      padding: 1px 5px;
      border-radius: 3px;
      background: var(--success-bg);
      color: var(--success);
      border: 1px solid currentColor;
    }

    .node-tag-slave {
      background: transparent;
      color: var(--mysql-teal);
      border: 1px solid currentColor;
    }

    .comp-list {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .comp-item {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--text-secondary);
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .comp-port {
      color: var(--text-muted);
      font-size: 10px;
    }

    .arch-note {
      font-size: 11px;
      color: var(--text-secondary);
      margin-top: 10px;
      padding-top: 8px;
      border-top: 1px dashed var(--border-color);
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 6px;
    }

    /* 任务卡片体系 */
    .task-card {
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      border-radius: var(--card-radius);
      margin-bottom: 16px;
      overflow: hidden;
    }

    .task-card.done {
      border-color: var(--success);
    }

    .task-header {
      padding: 10px 14px;
      background: var(--bg-subtle);
      border-bottom: 1px solid var(--border-color);
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      flex-wrap: wrap;
    }

    .task-title {
      font-size: 13px;
      font-weight: 600;
      color: var(--text-primary);
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .task-id {
      font-family: var(--font-mono);
      font-size: 11px;
      padding: 1px 6px;
      border-radius: 3px;
      background: var(--primary);
      color: #ffffff;
      font-weight: normal;
    }

    .task-check {
      background: var(--btn-bg);
      border: 1px solid var(--btn-border);
      color: var(--btn-text);
      font-family: var(--font-mono);
      font-size: 11px;
      padding: 2px 8px;
      border-radius: 3px;
      cursor: pointer;
      white-space: nowrap;
      transition: all 0.15s ease;
    }

    .task-check:hover {
      background: var(--btn-hover);
    }

    .task-check.checked {
      background: var(--success-bg);
      border-color: var(--success);
      color: var(--success);
    }

    .task-body {
      padding: 14px;
    }

    .task-desc {
      font-size: 12px;
      color: var(--text-secondary);
      margin-bottom: 12px;
      line-height: 1.6;
    }

    .task-desc code {
      font-family: var(--font-mono);
      font-size: 11px;
      padding: 1px 4px;
      border-radius: 3px;
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
    }

    /* 代码块与操作区 */
    .cmd-block {
      border: 1px solid var(--border-color);
      border-radius: 4px;
      margin-bottom: 10px;
      overflow: hidden;
      background: var(--code-bg);
    }

    .cmd-bar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: rgba(255, 255, 255, 0.04);
      padding: 6px 10px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
      gap: 8px;
      flex-wrap: wrap;
    }

    .cmd-label {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--code-prompt);
    }

    .cmd-actions {
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .btn-action {
      background: #21262d;
      border: 1px solid #363b42;
      color: #c9d1d9;
      padding: 2px 8px;
      font-size: 11px;
      font-family: var(--font-mono);
      border-radius: 3px;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      transition: all 0.15s ease;
    }

    .btn-action:hover {
      background: #30363d;
      border-color: #8b949e;
      color: #ffffff;
    }

    .btn-action-primary {
      background: #1f6feb;
      border-color: #388bfd;
      color: #ffffff;
    }

    .btn-action-primary:hover {
      background: #388bfd;
    }

    .btn-action-danger {
      background: #21262d;
      border-color: var(--danger);
      color: var(--danger);
    }

    .btn-action-danger:hover {
      background: var(--danger);
      color: #ffffff;
    }

    .cmd-content {
      padding: 8px 12px;
      font-family: var(--font-mono);
      font-size: 12px;
      color: var(--code-text);
      overflow-x: auto;
      white-space: pre-wrap;
      word-break: break-all;
      cursor: pointer;
    }

    .cmd-prompt {
      color: var(--code-prompt);
      user-select: none;
      margin-right: 6px;
    }

    /* 预期输出块 */
    .output-box {
      background: var(--output-bg);
      border: 1px solid var(--output-border);
      border-radius: 4px;
      padding: 8px 12px;
      margin-top: 8px;
      margin-bottom: 10px;
    }

    .output-title {
      font-family: var(--font-mono);
      font-size: 10px;
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      margin-bottom: 4px;
      user-select: none;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .output-toggle {
      font-size: 9px;
      color: var(--text-muted);
    }

    .output-content {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--output-text);
      line-height: 1.4;
      white-space: pre;
      overflow-x: auto;
    }

    .output-content.collapsed {
      display: none;
    }

    /* 多行预格式文本 */
    .multiline-pre {
      padding: 10px 12px;
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--code-text);
      max-height: 240px;
      overflow-y: auto;
      white-space: pre;
      line-height: 1.45;
    }

    /* 提示条 */
    .callout {
      padding: 10px 12px;
      border-radius: 4px;
      margin-bottom: 12px;
      font-size: 12px;
      color: var(--text-primary);
      line-height: 1.6;
    }

    .callout-warn {
      background: rgba(227, 179, 65, 0.15);
      border-left: 4px solid #d29922;
    }

    .callout-info {
      background: rgba(56, 139, 253, 0.1);
      border-left: 4px solid var(--primary);
    }

    .callout-danger {
      background: rgba(207, 34, 46, 0.08);
      border-left: 4px solid var(--danger);
    }

    .callout-ok {
      background: rgba(26, 127, 55, 0.1);
      border-left: 4px solid var(--success);
    }

    .callout code {
      font-family: var(--font-mono);
      font-size: 11px;
      padding: 1px 4px;
      border-radius: 3px;
      background: var(--bg-subtle);
      border: 1px solid var(--border-color);
    }

    /* 底部轻量提示 */
    .toast {
      position: fixed;
      bottom: 16px;
      right: 16px;
      background: var(--bg-card);
      border: 1px solid var(--border-color);
      color: var(--text-primary);
      padding: 6px 12px;
      border-radius: 4px;
      font-size: 12px;
      font-family: var(--font-mono);
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      display: none;
      align-items: center;
      gap: 6px;
      z-index: 1000;
    }

    @media (max-width: 600px) {
      body {
        padding: 10px;
      }
      .arch-grid {
        grid-template-columns: 1fr;
      }
      .header-right {
        width: 100%;
        justify-content: flex-end;
      }
    }
  </style>
</head>
<body>
  <div class="lab-container">
    <!-- 头部规范 -->
    <header class="lab-header">
      <div class="header-left">
        <svg class="mysql-logo" viewBox="0 0 32 32" fill="none">
          <circle cx="16" cy="16" r="15" fill="#00758f"/>
          <path d="M9 21c1.2 1.4 3 2.2 5 2.2 2.2 0 3.4-1 3.4-2.4 0-1.4-1-2-2.8-2.5l-1.3-.4c-1.5-.4-2.3-1-2.3-2 0-1.2 1-2 2.7-2 1.7 0 3 .7 4 1.9" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>
          <ellipse cx="22.5" cy="15" rx="5.5" ry="2.2" stroke="#e48e00" stroke-width="1.6" fill="none"/>
        </svg>
        <div class="lab-title-group">
          <h1>MySQL 8.0 主从复制交互实验手册</h1>
          <div class="meta-badges">
            <span class="badge">Docker Compose</span>
            <span class="badge">mysql:8.0</span>
            <span class="badge">binlog + Position</span>
            <span class="badge">ROW</span>
            <span class="badge badge-verified">全任务已实机验证</span>
          </div>
        </div>
      </div>
      <div class="header-right">
        <span class="badge" id="progress-text">0 / 18</span>
        <button class="theme-toggle" onclick="toggleTheme()" id="theme-toggle-btn">
          <span id="theme-text">亮色模式</span>
        </button>
      </div>
    </header>

    <!-- 进度条 -->
    <div class="progress-card">
      <span class="progress-label">实验进度</span>
      <div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div>
      <button class="btn-mini" onclick="resetProgress()">重置进度</button>
      <button class="btn-mini" onclick="scrollPrev()">▲ 上一个未做</button>
    </div>

    <!-- 架构概览 -->
    <article class="arch-card">
      <div class="arch-header">
        <div class="arch-title">🔷 主从拓扑与实例标识 (Replication Topology)</div>
        <span class="badge badge-primary">node1 / Ubuntu 24.04</span>
      </div>
      <div class="arch-grid">
        <div class="node-col">
          <div class="node-head">
            <span class="node-name">mysql-master</span>
            <span class="node-tag">source</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>server-id</span><span class="comp-port">1</span></div>
            <div class="comp-item"><span>log-bin</span><span class="comp-port">mysql-bin</span></div>
            <div class="comp-item"><span>binlog_format</span><span class="comp-port">ROW</span></div>
            <div class="comp-item"><span>sync_binlog</span><span class="comp-port">1</span></div>
            <div class="comp-item"><span>Dump Thread</span><span class="comp-port">3306</span></div>
          </div>
        </div>
        <div class="node-col">
          <div class="node-head">
            <span class="node-name">mysql-slave</span>
            <span class="node-tag node-tag-slave">replica</span>
          </div>
          <div class="comp-list">
            <div class="comp-item"><span>server-id</span><span class="comp-port">2</span></div>
            <div class="comp-item"><span>relay-log</span><span class="comp-port">mysqld-relay-bin</span></div>
            <div class="comp-item"><span>replicate-do-db</span><span class="comp-port">cmdb01</span></div>
            <div class="comp-item"><span>relay_log_recovery</span><span class="comp-port">ON</span></div>
            <div class="comp-item"><span>IO / SQL Thread</span><span class="comp-port">3307</span></div>
          </div>
        </div>
      </div>
      <div class="arch-note">
        <span>宿主端口:<code>3306</code> 主库 · <code>3307</code> 从库(容器内仍为 3306)</span>
        <span>root 密码 <code>srebro@2024</code> · 复制账号 <code>repluser / srebro@test</code></span>
        <span>专用网络 <code>mysql-ms-net</code></span>
      </div>
    </article>

    <!-- Task 0 -->
    <article class="task-card" id="task-0">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 0</span><span>环境准备:创建实验工作目录</span></div>
        <button class="task-check" onclick="toggleDone(0)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">所有物料集中在 <code>/root/mysql-ms</code>,两节点配置分目录存放,便于挂载区分。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="mkdir -p /root/mysql-ms/master /root/mysql-ms/slave &amp;&amp; cd /root/mysql-ms &amp;&amp; ls -d master slave">执行</button>
              <button class="btn-action" onclick="copyCommand('mkdir -p /root/mysql-ms/master /root/mysql-ms/slave &amp;&amp; cd /root/mysql-ms &amp;&amp; ls -d master slave')">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>mkdir -p /root/mysql-ms/master /root/mysql-ms/slave &amp;&amp; cd /root/mysql-ms &amp;&amp; ls -d master slave</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">master
slave</div>
        </div>
      </div>
    </article>

    <!-- Task 1 -->
    <article class="task-card" id="task-1">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 1</span><span>编写编排文件 docker-compose.yml</span></div>
        <button class="task-check" onclick="toggleDone(1)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-info">💡 <strong>设计要点:</strong><code>--server-id</code> / <code>--relay-log</code> / <code>--replicate-do-db</code> 通过 <code>command</code> 下发而非写进 my.cnf,一份配置文件可双节点复用。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">docker-compose.yml (heredoc)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-compose')">整段下发</button>
              <button class="btn-action" onclick="copyBlock('blk-compose')">复制</button>
            </div>
          </div>
          <pre class="multiline-pre" id="blk-compose"><code>cd /root/mysql-ms &amp;&amp; cat &gt; docker-compose.yml &lt;&lt;'EOF'
services:
  mysql-master:
    image: mysql:8.0
    container_name: mysql-master
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "srebro@2024"
      TZ: "Asia/Shanghai"
    ports:
      - "3306:3306"
    volumes:
      - ./master/my.cnf:/etc/mysql/conf.d/my.cnf:ro
      - master-data:/var/lib/mysql
    command:
      - --server-id=1
      - --log-bin=mysql-bin
      - --binlog-format=ROW
      - --sync-binlog=1
      - --innodb-flush-log-at-trx-commit=1
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_general_ci
      - --default-time-zone=+8:00
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-uroot", "-psrebro@2024"]
      interval: 5s
      timeout: 5s
      retries: 30
    networks: [mysql-ms]

  mysql-slave:
    image: mysql:8.0
    container_name: mysql-slave
    restart: unless-stopped
    depends_on:
      mysql-master:
        condition: service_healthy
    environment:
      MYSQL_ROOT_PASSWORD: "srebro@2024"
      TZ: "Asia/Shanghai"
    ports:
      - "3307:3306"
    volumes:
      - ./slave/my.cnf:/etc/mysql/conf.d/my.cnf:ro
      - slave-data:/var/lib/mysql
    command:
      - --server-id=2
      - --relay-log=mysqld-relay-bin
      - --replicate-do-db=cmdb01
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_general_ci
      - --default-time-zone=+8:00
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-uroot", "-psrebro@2024"]
      interval: 5s
      timeout: 5s
      retries: 30
    networks: [mysql-ms]

volumes:
  master-data: {}
  slave-data: {}

networks:
  mysql-ms:
    name: mysql-ms-net
EOF
echo WROTE &amp;&amp; wc -l docker-compose.yml</code></pre>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">WROTE
65 docker-compose.yml</div>
        </div>
      </div>
    </article>

    <!-- Task 2 -->
    <article class="task-card" id="task-2">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 2</span><span>编写两节点配置文件 my.cnf</span></div>
        <button class="task-check" onclick="toggleDone(2)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-warn">⚠️ <strong>MySQL 8.0 适配:</strong>参考模板中的 <code>sql_mode=...NO_AUTO_CREATE_USER</code> 在 8.0 已被移除,照抄会导致实例无法启动;<code>skip_ssl</code> 亦已废弃。以下为剔除后的可用版本。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">master/my.cnf + slave/my.cnf (heredoc)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-mycnf')">整段下发</button>
              <button class="btn-action" onclick="copyBlock('blk-mycnf')">复制</button>
            </div>
          </div>
          <pre class="multiline-pre" id="blk-mycnf"><code>cd /root/mysql-ms &amp;&amp; cat &gt; master/my.cnf &lt;&lt;'EOF'
[mysqld]
max_connections=1000
default-storage-engine=INNODB
character-set-server=utf8mb4
collation_server=utf8mb4_general_ci
skip-name-resolve=1
lower_case_table_names=1
log_timestamps=SYSTEM
default-time-zone=+8:00
max_allowed_packet=512M
binlog_expire_logs_seconds=604800

[mysql]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4
EOF
cat &gt; slave/my.cnf &lt;&lt;'EOF'
[mysqld]
max_connections=1000
default-storage-engine=INNODB
character-set-server=utf8mb4
collation_server=utf8mb4_general_ci
skip-name-resolve=1
lower_case_table_names=1
log_timestamps=SYSTEM
default-time-zone=+8:00
max_allowed_packet=512M
relay_log_recovery=ON
replicate-ignore-db=mysql
replicate-ignore-db=information_schema
replicate-ignore-db=sys
replicate-ignore-db=performance_schema

[mysql]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4
EOF
echo OK &amp;&amp; wc -l master/my.cnf slave/my.cnf</code></pre>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">OK
 17 master/my.cnf
 21 slave/my.cnf
 38 total</div>
        </div>
      </div>
    </article>

    <!-- Task 3 -->
    <article class="task-card" id="task-3">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 3</span><span>拉起集群 (Compose Up)</span></div>
        <button class="task-check" onclick="toggleDone(3)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">首次执行需拉取镜像,耗时约 30 秒至数分钟。<code>depends_on</code> 使用 <code>condition: service_healthy</code>,Compose 会等主库真正可服务后才创建从库。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="cd /root/mysql-ms &amp;&amp; docker compose up -d">执行</button>
              <button class="btn-action" onclick="copyCommand('cd /root/mysql-ms &amp;&amp; docker compose up -d')">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>cd /root/mysql-ms &amp;&amp; docker compose up -d</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">[+] up 5/5
 ✔ Network mysql-ms-net        Created     0.1s
 ✔ Volume mysql-ms_master-data Created     0.0s
 ✔ Volume mysql-ms_slave-data  Created     0.0s
 ✔ Container mysql-master      Healthy    27.6s
 ✔ Container mysql-slave       Created     0.0s</div>
        </div>
        <div class="callout callout-info">💡 若命令返回超时但容器仍在启动,<strong>不要重复提交</strong>;用下一个 Task 的命令轮询真实状态即可。</div>
      </div>
    </article>

    <!-- Task 4 -->
    <article class="task-card" id="task-4">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 4</span><span>查看集群状态与端口映射 (Compose PS)</span></div>
        <button class="task-check" onclick="toggleDone(4)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker compose -f /root/mysql-ms/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker compose -f /root/mysql-ms/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker compose -f /root/mysql-ms/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">NAME           SERVICE        STATUS                   PORTS
mysql-master   mysql-master   Up 45 seconds (healthy)  0.0.0.0:3306-&gt;3306/tcp, 33060/tcp
mysql-slave    mysql-slave    Up 17 seconds (healthy)  33060/tcp, 0.0.0.0:3307-&gt;3306/tcp</div>
        </div>
      </div>
    </article>

    <!-- Task 5 -->
    <article class="task-card" id="task-5">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 5</span><span>主库创建复制账号 (Create Repl User)</span></div>
        <button class="task-check" onclick="toggleDone(5)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-danger">🔥 <strong>MySQL 8.0 语法变更(实测报错):</strong>旧笔记里的 <code>grant replication slave on *.* to repluser@'%' identified by '...'</code> 在 8.0 已失效,实测返回 <code>ERROR 1064</code>。8.0 起 <code>GRANT</code> 不再隐式创建用户,必须先 <code>CREATE USER</code>。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">旧语法对照(会报错,勿用于 8.0)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e &quot;grant replication slave on *.* to repluser2@'%' identified by 'x';&quot; 2&gt;&amp;1 | tail -2">复现报错</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e "grant replication slave on *.* to repluser2@'%' identified by 'x';" 2&gt;&amp;1 | tail -2</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the right syntax to use near 'identified by 'x'' at line 1</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">✅ 8.0 正确写法</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e &quot;CREATE USER IF NOT EXISTS 'repluser'@'%' IDENTIFIED WITH mysql_native_password BY 'srebro@test'; GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%'; FLUSH PRIVILEGES;&quot; 2&gt;/dev/null &amp;&amp; echo OK">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -e \&quot;CREATE USER IF NOT EXISTS 'repluser'@'%' IDENTIFIED WITH mysql_native_password BY 'srebro@test'; GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%'; FLUSH PRIVILEGES;\&quot; 2&gt;/dev/null &amp;&amp; echo OK&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e "CREATE USER IF NOT EXISTS 'repluser'@'%' IDENTIFIED WITH mysql_native_password BY 'srebro@test'; GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%'; FLUSH PRIVILEGES;" 2&gt;/dev/null &amp;&amp; echo OK</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">核验账号与认证插件</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e &quot;SELECT CONCAT(user,'@',host,'  plugin=',plugin) FROM mysql.user WHERE user='repluser';&quot; 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e \&quot;SELECT CONCAT(user,'@',host,'  plugin=',plugin) FROM mysql.user WHERE user='repluser';\&quot; 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e "SELECT CONCAT(user,'@',host,'  plugin=',plugin) FROM mysql.user WHERE user='repluser';" 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">OK
repluser@%  plugin=mysql_native_password</div>
        </div>
      </div>
    </article>

    <!-- Task 6 -->
    <article class="task-card" id="task-6">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 6</span><span>获取主库 binlog 位点 (Show Master Status)</span></div>
        <button class="task-check" onclick="toggleDone(6)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">位点复制必须拿到当前 binlog 文件名与偏移量,从库据此确定从哪个位置开始拉取。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e 'SHOW MASTER STATUS;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e 'SHOW MASTER STATUS;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e 'SHOW MASTER STATUS;' 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">mysql-bin.000003   849</div>
        </div>
      </div>
    </article>

    <!-- Task 7 -->
    <article class="task-card" id="task-7">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 7</span><span>从库建链并启动复制 (Change Master &amp; Start Slave)</span></div>
        <button class="task-check" onclick="toggleDone(7)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">脚本自动读取主库当前位点并完成 <code>CHANGE MASTER TO</code>,避免人工抄写位点出错,可重复执行。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">setup-slave.sh (heredoc)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" onclick="runBlock('blk-setup')">整段下发</button>
              <button class="btn-action" onclick="copyBlock('blk-setup')">复制</button>
            </div>
          </div>
          <pre class="multiline-pre" id="blk-setup"><code>cd /root/mysql-ms &amp;&amp; cat &gt; setup-slave.sh &lt;&lt;'EOF'
#!/usr/bin/env bash
set -euo pipefail
PW='srebro@2024'
read -r MFILE MPOS REST &lt; &lt;(docker exec mysql-master mysql -uroot -p"$PW" -N -B -e 'SHOW MASTER STATUS' 2&gt;/dev/null)
echo "MASTER_FILE=$MFILE MASTER_POS=$MPOS"
docker exec mysql-slave mysql -uroot -p"$PW" -e 'STOP SLAVE;' 2&gt;/dev/null
docker exec mysql-slave mysql -uroot -p"$PW" -e "CHANGE MASTER TO MASTER_HOST='mysql-master', MASTER_USER='repluser', MASTER_PASSWORD='srebro@test', MASTER_PORT=3306, MASTER_LOG_FILE='$MFILE', MASTER_LOG_POS=$MPOS;" 2&gt;/dev/null
docker exec mysql-slave mysql -uroot -p"$PW" -e 'START SLAVE;' 2>/dev/null
echo SLAVE_SETUP_DONE
EOF
bash setup-slave.sh</code></pre>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">MASTER_FILE=mysql-bin.000003 MASTER_POS=849
SLAVE_SETUP_DONE</div>
        </div>
        <div class="callout callout-info">💡 <code>MASTER_POS</code> 是执行瞬间的 binlog 偏移量,随主库写入推进(首次干净部署实测为 <code>849</code>)。重复执行时这里会变成当前最新位点,属正常现象。</div>
      </div>
    </article>

    <!-- Task 8 -->
    <article class="task-card" id="task-8">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 8</span><span>复制链路健康检查 (Health Check)</span></div>
        <button class="task-check" onclick="toggleDone(8)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-ok">✅ <strong>健康判据:</strong>只看两个 <code>Yes</code> 不够。必须同时满足 <code>Last_IO_Errno=0</code>、<code>Last_SQL_Errno=0</code>、<code>Seconds_Behind_Master</code> 收敛为 <code>0</code>。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">一键体检</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2&gt;/dev/null | grep -E 'Slave_IO_Running:|Slave_SQL_Running:|Master_Log_File|Read_Master_Log_Pos|Seconds_Behind_Master|Last_IO_Errno|Last_SQL_Errno|Master_Server_Id'">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2&gt;/dev/null | grep -E 'Slave_IO_Running:|Slave_SQL_Running:|Master_Log_File|Read_Master_Log_Pos|Seconds_Behind_Master|Last_IO_Errno|Last_SQL_Errno|Master_Server_Id'&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2&gt;/dev/null | grep -E 'Slave_IO_Running:|Slave_SQL_Running:|Master_Log_File|Read_Master_Log_Pos|Seconds_Behind_Master|Last_IO_Errno|Last_SQL_Errno|Master_Server_Id'</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 849
        Relay_Master_Log_File: mysql-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 0
                Last_IO_Errno: 0
               Last_SQL_Errno: 0
             Master_Server_Id: 1</div>
        </div>
      </div>
    </article>

    <!-- Task 9 -->
    <article class="task-card" id="task-9">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 9</span><span>主库写入测试数据 (Write &amp; Verify)</span></div>
        <button class="task-check" onclick="toggleDone(9)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-warn">⚠️ <strong>顺序陷阱(实测报错):</strong>不能用 <code>-D cmdb01</code> 来「顺便建库」。库不存在时客户端直接失败:<code>ERROR 1049 (42000): Unknown database 'cmdb01'</code>。必须<strong>先不带 -D 建库</strong>。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">① 先建库(不带 -D)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE DATABASE IF NOT EXISTS cmdb01 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS cmdb02 DEFAULT CHARACTER SET utf8mb4; SHOW DATABASES LIKE &quot;cmdb%&quot;;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE DATABASE IF NOT EXISTS cmdb01 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS cmdb02 DEFAULT CHARACTER SET utf8mb4; SHOW DATABASES LIKE \&quot;cmdb%\&quot;;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE DATABASE IF NOT EXISTS cmdb01 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS cmdb02 DEFAULT CHARACTER SET utf8mb4; SHOW DATABASES LIKE "cmdb%";' 2&gt;/dev/null</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">② 建表并写入(带 -D 指定默认库)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS host_info(id INT PRIMARY KEY AUTO_INCREMENT, hostname VARCHAR(64) NOT NULL, ip VARCHAR(32), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); INSERT INTO host_info(hostname,ip) VALUES(&quot;node-a&quot;,&quot;10.0.0.11&quot;),(&quot;node-b&quot;,&quot;10.0.0.12&quot;),(&quot;node-c&quot;,&quot;10.0.0.13&quot;); SELECT COUNT(*) AS master_rows FROM host_info;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS host_info(id INT PRIMARY KEY AUTO_INCREMENT, hostname VARCHAR(64) NOT NULL, ip VARCHAR(32), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); INSERT INTO host_info(hostname,ip) VALUES(\&quot;node-a\&quot;,\&quot;10.0.0.11\&quot;),(\&quot;node-b\&quot;,\&quot;10.0.0.12\&quot;),(\&quot;node-c\&quot;,\&quot;10.0.0.13\&quot;); SELECT COUNT(*) AS master_rows FROM host_info;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS host_info(...); INSERT INTO host_info(hostname,ip) VALUES("node-a","10.0.0.11"),("node-b","10.0.0.12"),("node-c","10.0.0.13"); SELECT COUNT(*) AS master_rows FROM host_info;' 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">Database (cmdb%)
cmdb01
cmdb02

master_rows
3</div>
        </div>
      </div>
    </article>

    <!-- Task 10 -->
    <article class="task-card" id="task-10">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 10</span><span>从库读取验证数据一致性 (Consistency Check)</span></div>
        <button class="task-check" onclick="toggleDone(10)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">Shell Command</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'SELECT id,hostname,ip,created_at FROM host_info ORDER BY id;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'SELECT id,hostname,ip,created_at FROM host_info ORDER BY id;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'SELECT id,hostname,ip,created_at FROM host_info ORDER BY id;' 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">id  hostname  ip          created_at
1   node-a    10.0.0.11   2026-10-30 15:50:54
2   node-b    10.0.0.12   2026-10-30 15:50:54
3   node-c    10.0.0.13   2026-10-30 15:50:54</div>
        </div>
      </div>
    </article>

    <!-- Task 11 -->
    <article class="task-card" id="task-11">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 11</span><span>验证 replicate-do-db 过滤规则</span></div>
        <button class="task-check" onclick="toggleDone(11)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">从库只配了 <code>--replicate-do-db=cmdb01</code>,因此写入白名单外的 <code>cmdb02</code> 不应出现在从库。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">① 主库写 cmdb02</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb02 -e 'CREATE TABLE IF NOT EXISTS filtered_check(id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(64)); INSERT INTO filtered_check(note) VALUES(&quot;should-not-replicate&quot;); SELECT COUNT(*) AS master_cmdb02_rows FROM filtered_check;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb02 -e 'CREATE TABLE IF NOT EXISTS filtered_check(id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(64)); INSERT INTO filtered_check(note) VALUES(\&quot;should-not-replicate\&quot;); SELECT COUNT(*) AS master_cmdb02_rows FROM filtered_check;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb02 -e 'CREATE TABLE IF NOT EXISTS filtered_check(...); INSERT INTO filtered_check(note) VALUES("should-not-replicate"); SELECT COUNT(*) AS master_cmdb02_rows FROM filtered_check;' 2&gt;/dev/null</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">② 从库确认 cmdb02 不存在</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e &quot;SHOW DATABASES LIKE 'cmdb%';&quot; 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -e \&quot;SHOW DATABASES LIKE 'cmdb%';\&quot; 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e "SHOW DATABASES LIKE 'cmdb%';" 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">master_cmdb02_rows
1

Database (cmdb%)
cmdb01</div>
        </div>
      </div>
    </article>

    <!-- Task 12 -->
    <article class="task-card" id="task-12">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 12</span><span>离线追赶演练 (Catch-up After Outage)</span></div>
        <button class="task-check" onclick="toggleDone(12)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">制造从库断链,主库持续写入,再恢复复制验证断点续传。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">① 停复制 + 主库继续写</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'STOP SLAVE;' 2&gt;/dev/null; docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'INSERT INTO host_info(hostname,ip) VALUES(&quot;node-d-offline&quot;,&quot;10.0.0.14&quot;),(&quot;node-e-offline&quot;,&quot;10.0.0.15&quot;); SELECT COUNT(*) AS master_rows FROM host_info;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'STOP SLAVE;' 2&gt;/dev/null; docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'INSERT INTO host_info(hostname,ip) VALUES(\&quot;node-d-offline\&quot;,\&quot;10.0.0.14\&quot;),(\&quot;node-e-offline\&quot;,\&quot;10.0.0.15\&quot;); SELECT COUNT(*) AS master_rows FROM host_info;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'STOP SLAVE;' 2&gt;/dev/null; docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'INSERT INTO host_info(hostname,ip) VALUES("node-d-offline","10.0.0.14"),("node-e-offline","10.0.0.15"); SELECT COUNT(*) AS master_rows FROM host_info;' 2&gt;/dev/null</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">② 断链期间从库行数(应当冻结)</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_during_outage FROM cmdb01.host_info;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_during_outage FROM cmdb01.host_info;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_during_outage FROM cmdb01.host_info;' 2&gt;/dev/null</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">③ 恢复复制并复查</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_after_recover FROM cmdb01.host_info;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_after_recover FROM cmdb01.host_info;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_after_recover FROM cmdb01.host_info;' 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">master_rows
5

slave_rows_during_outage
3

slave_rows_after_recover
5</div>
        </div>
        <div class="callout callout-ok">✅ I/O 线程凭持久化的位点向主库续请求,SQL 线程顺次回放未执行事件,约 3 秒内追平。这是复制具备实用价值的核心能力。</div>
      </div>
    </article>

    <!-- Task 13 -->
    <article class="task-card" id="task-13">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 13</span><span>ROW 格式下的过滤真相:DDL 与 DML 规则不同</span></div>
        <button class="task-check" onclick="toggleDone(13)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-danger">🔥 <strong>最反直觉的一条(本手册实测结论):</strong>在 <code>binlog_format=ROW</code> 下,<code>--replicate-do-db</code> 对 <strong>DML</strong> 按「行所属表所在的库」过滤,对 <strong>DDL</strong> 仍按「会话默认库」过滤。二者规则不同,混用会直接造成从库少表。</div>
        <table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:12px">
          <tr style="background:var(--bg-subtle)"><th style="border:1px solid var(--border-color);padding:6px">语句类型</th><th style="border:1px solid var(--border-color);padding:6px">无默认库(不用 -D)</th><th style="border:1px solid var(--border-color);padding:6px">有默认库(-D cmdb01)</th></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px">DML 增删改</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--success)">✅ 会复制(按表属库)</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--success)">✅ 会复制</td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px">DDL 建表等</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--danger)">❌ 丢弃</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--success)">✅ 会复制</td></tr>
        </table>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">① 不带默认库建表(DDL 被丢弃)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE TABLE cmdb01.drift_test(id INT PRIMARY KEY);' 2&gt;/dev/null; sleep 2; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e &quot;SHOW TABLES IN cmdb01 LIKE 'drift%';&quot; 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE TABLE cmdb01.drift_test(id INT PRIMARY KEY);' 2&gt;/dev/null; sleep 2; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e \&quot;SHOW TABLES IN cmdb01 LIKE 'drift%';\&quot; 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE TABLE cmdb01.drift_test(id INT PRIMARY KEY);' 2&gt;/dev/null; sleep 2; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e "SHOW TABLES IN cmdb01 LIKE 'drift%';" 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出(空 = DDL 未复制)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">(无输出 —— 从库没有 drift_test 表)</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">② 不带默认库插数据(DML 却会复制 → 触发 1146)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'INSERT INTO cmdb01.drift_test VALUES(1);' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2&gt;/dev/null | grep -E 'Slave_SQL_Running:|Last_SQL_Errno'">危险:制造故障</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'INSERT INTO cmdb01.drift_test VALUES(1);' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2&gt;/dev/null | grep -E 'Slave_SQL_Running:|Last_SQL_Errno'&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'INSERT INTO cmdb01.drift_test VALUES(1);' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2&gt;/dev/null | grep -E 'Slave_SQL_Running:|Last_SQL_Errno'</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出:SQL 线程已中止</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">            Slave_SQL_Running: No
               Last_SQL_Errno: 1146</div>
        </div>
      </div>
    </article>

    <!-- Task 14 -->
    <article class="task-card" id="task-14">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 14</span><span>故障定位与修复 (1146 Recovery)</span></div>
        <button class="task-check" onclick="toggleDone(14)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="task-desc">多级并行复制下,主状态只给概要。<code>performance_schema.replication_applier_status_by_worker</code> 才有精确到 SQL 的报错。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">① 精确定位报错</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -N -B -e 'SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER&lt;&gt;0;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -N -B -e 'SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER&lt;&gt;0;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -N -B -e 'SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER&lt;&gt;0;' 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">Worker 1 failed executing transaction 'ANONYMOUS' at source log mysql-bin.000003,
end_log_pos 6534; Error executing row event: 'Table 'cmdb01.drift_test' doesn't exist',
Error_code: MY-001146</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">② 容器日志交叉印证</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="docker logs --tail 8 mysql-slave 2&gt;&amp;1">执行</button>
              <button class="btn-action" onclick="copyCommand('docker logs --tail 8 mysql-slave 2&gt;&amp;1')">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker logs --tail 8 mysql-slave 2&gt;&amp;1</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">[Repl] Replica SQL for channel '': Worker 1 failed executing transaction 'ANONYMOUS'
at source log mysql-bin.000003, end_log_pos 6534; Error executing row event:
'Table 'cmdb01.drift_test' doesn't exist', Error_code: MY-001146
[Repl] ... The replica coordinator and worker threads are stopped, possibly leaving
data in inconsistent state. Error_code: MY-001756</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">③ 补齐缺失表并恢复</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS drift_test(id INT PRIMARY KEY);' 2&gt;/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_drift_rows FROM cmdb01.drift_test;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS drift_test(id INT PRIMARY KEY);' 2&gt;/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_drift_rows FROM cmdb01.drift_test;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS drift_test(id INT PRIMARY KEY);' 2&gt;/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2&gt;/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_drift_rows FROM cmdb01.drift_test;' 2&gt;/dev/null</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出:自动补数并恢复</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">slave_drift_rows
1

             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 0
               Last_SQL_Errno: 0</div>
        </div>
        <div class="callout callout-info">💡 因为 DDL 从未进入从库,这里手工建表不会与主库事件冲突(不会 1050)。若冲突则用 <code>STOP SLAVE; SET GLOBAL sql_slave_skip_counter=1; START SLAVE;</code> 跳过该事件。</div>
      </div>
    </article>

    <!-- Task 15 -->
    <article class="task-card" id="task-15">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 15</span><span>主从一致性总核验 (Final Consistency)</span></div>
        <button class="task-check" onclick="toggleDone(15)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">两侧行数对照</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2&gt;/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2&gt;/dev/null">执行</button>
              <button class="btn-action" onclick="copyCommand(&quot;docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2&gt;/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2&gt;/dev/null&quot;)">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master ... ; docker exec mysql-slave ...   # 两侧对照 host_info / drift_test 行数</div>
        </div>
        <div class="output-box">
          <div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
          <div class="output-content">=== MASTER ===
host_info  drift_test
7          1

=== SLAVE ===
host_info  drift_test
7          1

              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 6073
          Exec_Master_Log_Pos: 6073
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 0</div>
        </div>
        <div class="callout callout-ok">✅ <code>Read_Master_Log_Pos</code> 等于 <code>Exec_Master_Log_Pos</code>,说明中继日志已全部回放完毕,两侧行数一致。</div>
      </div>
    </article>

    <!-- Task 16 -->
    <article class="task-card" id="task-16">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 16</span><span>排障速查表 (Troubleshooting Cheatsheet)</span></div>
        <button class="task-check" onclick="toggleDone(16)">标记完成</button>
      </div>
      <div class="task-body">
        <table style="width:100%;border-collapse:collapse;font-size:11px">
          <tr style="background:var(--bg-subtle)"><th style="border:1px solid var(--border-color);padding:6px">现象</th><th style="border:1px solid var(--border-color);padding:6px">原因</th><th style="border:1px solid var(--border-color);padding:6px">处置</th></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px"><code>Slave_IO_Running: Connecting</code></td><td style="border:1px solid var(--border-color);padding:6px">网络不通 / 账号密码错 / 主库端口</td><td style="border:1px solid var(--border-color);padding:6px">在从库容器 <code>mysql -h mysql-master -u repluser -p</code> 验证连通性</td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px"><code>Last_SQL_Errno: 1146</code></td><td style="border:1px solid var(--border-color);padding:6px">从库缺表(DDL 未按预期复制)</td><td style="border:1px solid var(--border-color);padding:6px">补建表后 <code>START SLAVE</code></td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px"><code>Last_SQL_Errno: 1050</code></td><td style="border:1px solid var(--border-color);padding:6px">从库表已存在,重复执行 DDL</td><td style="border:1px solid var(--border-color);padding:6px"><code>SET GLOBAL sql_slave_skip_counter=1</code> 后重启</td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px"><code>ERROR 1064</code> 建账号</td><td style="border:1px solid var(--border-color);padding:6px">8.0 不支持 GRANT 隐式建用户</td><td style="border:1px solid var(--border-color);padding:6px">先 <code>CREATE USER</code> 再 <code>GRANT</code></td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px"><code>ERROR 1049</code> Unknown database</td><td style="border:1px solid var(--border-color);padding:6px">用 <code>-D</code> 指定了尚不存在的库</td><td style="border:1px solid var(--border-color);padding:6px">先不带 <code>-D</code> 建库</td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px">server_id 冲突</td><td style="border:1px solid var(--border-color);padding:6px">主从 <code>server-id</code> 相同</td><td style="border:1px solid var(--border-color);padding:6px">保证全局唯一</td></tr>
          <tr><td style="border:1px solid var(--border-color);padding:6px">位点被 purge</td><td style="border:1px solid var(--border-color);padding:6px"><code>binlog_expire_logs_seconds</code> 过期清理</td><td style="border:1px solid var(--border-color);padding:6px">只能重做从库</td></tr>
        </table>
      </div>
    </article>

    <!-- Task 17 -->
    <article class="task-card" id="task-17">
      <div class="task-header">
        <div class="task-title"><span class="task-id">Task 17</span><span>环境清理 (Teardown)</span></div>
        <button class="task-check" onclick="toggleDone(17)">标记完成</button>
      </div>
      <div class="task-body">
        <div class="callout callout-warn">⚠️ 加 <code>-v</code> 会连数据卷一起删除,下次起 \"从零重建\"。仅停止容器则用不带 <code>-v</code> 的版本。</div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">停止并保留数据卷</span>
            <div class="cmd-actions">
              <button class="btn-action" data-cmd="cd /root/mysql-ms &amp;&amp; docker compose down">执行</button>
              <button class="btn-action" onclick="copyCommand('cd /root/mysql-ms &amp;&amp; docker compose down')">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>cd /root/mysql-ms &amp;&amp; docker compose down</div>
        </div>
        <div class="cmd-block">
          <div class="cmd-bar">
            <span class="cmd-label">彻底清理(含数据卷)</span>
            <div class="cmd-actions">
              <button class="btn-action btn-action-danger" data-cmd="cd /root/mysql-ms &amp;&amp; docker compose down -v">执行</button>
              <button class="btn-action" onclick="copyCommand('cd /root/mysql-ms &amp;&amp; docker compose down -v')">复制</button>
            </div>
          </div>
          <div class="cmd-content"><span class="cmd-prompt">$</span>cd /root/mysql-ms &amp;&amp; docker compose down -v</div>
        </div>
        <div class="callout callout-info">💡 <strong>生产化待办:</strong>本手册为演练环境,尚未覆盖 ① 从库 <code>read_only=ON</code> + <code>super_read_only</code> ② 半同步 <code>rpl_semi_sync</code> ③ GTID 模式(免位点管理)④ 从库初始化一致性校验(xtrabackup / mysqldump <code>--single-transaction --master-data=2</code>)。</div>
      </div>
    </article>

    <footer class="arch-note" style="border-top:1px solid var(--border-color);margin-top:20px;padding-top:12px">
      <span>MySQL 8.0 主从复制交互实验手册 · 全部命令与输出均在云端 node1 的 0 号终端实机验证 · 2026-10-30</span>
      <span class="badge badge-verified">18 / 18 任务已验证</span>
    </footer>
  </div>

  <div class="toast" id="toast"></div>

  <script>
    // 统一下发:本手册所有命令都跑在宿主原生终端(node1 的 0 号终端)
    function dispatchCommand(cmd) {
      if (window.sendTerminalCommand) {
        window.sendTerminalCommand(cmd, "terminal");
        return;
      }
      if (window.parent && window.parent !== window) {
        window.parent.postMessage({
          type: "LAE_TERMINAL_COMMAND",
          command: cmd,
          target: "terminal",
          autoExecute: true
        }, "*");
      }
    }

    // 监听 data-cmd 委托并下发到终端
    document.addEventListener("click", function(e) {
      if (window.__LAE_SDK_ATTACHED__ || window.__TERMINAL_BRIDGE_LOADED__) {
        return;
      }
      const trigger = e.target.closest("[data-cmd]");
      if (trigger) {
        const cmd = trigger.getAttribute("data-cmd");
        if (cmd) {
          if (trigger.classList.contains("btn-copy")) {
            copyCommand(cmd);
            return;
          }
          dispatchCommand(cmd);
          showToast("命令已发送至 0 号终端");
        }
      }
    });

    // 下发多行命令块(读 pre/code 的纯文本)
    function blockText(id) {
      const el = document.getElementById(id);
      return el ? el.textContent : "";
    }

    function runBlock(id) {
      const cmd = blockText(id);
      if (cmd) {
        dispatchCommand(cmd);
        showToast("脚本已发送至 0 号终端");
      }
    }

    function copyBlock(id) {
      const cmd = blockText(id);
      if (cmd) {
        copyCommand(cmd);
      }
    }

    function copyCommand(text) {
      if (navigator.clipboard) {
        navigator.clipboard.writeText(text).then(() => {
          showToast("已复制");
        }).catch(() => {
          fallbackCopy(text);
        });
      } else {
        fallbackCopy(text);
      }
    }

    function fallbackCopy(text) {
      const input = document.createElement("textarea");
      input.value = text;
      document.body.appendChild(input);
      input.select();
      document.execCommand("copy");
      document.body.removeChild(input);
      showToast("已复制");
    }

    // 预期输出折叠
    function toggleOutput(titleEl) {
      const box = titleEl.parentElement;
      const content = box.querySelector(".output-content");
      const toggle = titleEl.querySelector(".output-toggle");
      const collapsed = content.classList.toggle("collapsed");
      if (toggle) {
        toggle.textContent = collapsed ? "展开 ▼" : "收起 ▲";
      }
    }

    // 进度管理
    const STORE_KEY = "mysql_ms_lab_progress";
    const TOTAL = 18;

    function loadDone() {
      try {
        const raw = localStorage.getItem(STORE_KEY);
        return raw ? JSON.parse(raw) : [];
      } catch (e) {
        return [];
      }
    }

    function saveDone(list) {
      try {
        localStorage.setItem(STORE_KEY, JSON.stringify(list));
      } catch (e) {}
    }

    function renderProgress() {
      const done = loadDone();
      const fill = document.getElementById("progress-fill");
      const text = document.getElementById("progress-text");
      if (fill) fill.style.width = Math.round((done.length / TOTAL) * 100) + "%";
      if (text) text.textContent = done.length + " / " + TOTAL;
      for (let i = 0; i < TOTAL; i++) {
        const card = document.getElementById("task-" + i);
        if (!card) continue;
        const btn = card.querySelector(".task-check");
        const isDone = done.indexOf(i) !== -1;
        card.classList.toggle("done", isDone);
        if (btn) {
          btn.classList.toggle("checked", isDone);
          btn.textContent = isDone ? "✓ 已完成" : "标记完成";
        }
      }
    }

    function toggleDone(id) {
      const done = loadDone();
      const idx = done.indexOf(id);
      if (idx === -1) {
        done.push(id);
      } else {
        done.splice(idx, 1);
      }
      saveDone(done);
      renderProgress();
    }

    function resetProgress() {
      try {
        localStorage.removeItem(STORE_KEY);
      } catch (e) {}
      renderProgress();
      showToast("进度已重置");
    }

    function scrollPrev() {
      const done = loadDone();
      for (let i = 0; i < TOTAL; i++) {
        if (done.indexOf(i) === -1) {
          const card = document.getElementById("task-" + i);
          if (card) {
            card.scrollIntoView({ behavior: "smooth", block: "start" });
            showToast("跳转到 Task " + i);
            return;
          }
        }
      }
      showToast("全部任务已完成");
    }

    let toastTimer = null;
    function showToast(msg) {
      const toast = document.getElementById("toast");
      if (toast) {
        toast.textContent = msg;
        toast.style.display = "inline-flex";
        if (toastTimer) clearTimeout(toastTimer);
        toastTimer = setTimeout(() => {
          toast.style.display = "none";
        }, 1500);
      }
    }

    // 主题切换管理
    function initTheme() {
      const urlParams = new URLSearchParams(window.location.search);
      const urlTheme = urlParams.get("theme");
      const savedTheme = localStorage.getItem("mysql_ms_theme");
      setTheme(urlTheme || savedTheme || "light", false);

      window.addEventListener("message", (event) => {
        if (event.data && (event.data.type === "THEME_CHANGE" || event.data.type === "SET_THEME")) {
          const t = event.data.theme;
          if (t === "dark" || t === "light") {
            setTheme(t, true);
          }
        }
      });
    }

    function setTheme(theme, save = true) {
      document.documentElement.setAttribute("data-theme", theme);
      const text = document.getElementById("theme-text");
      if (text) {
        text.textContent = theme === "dark" ? "暗色模式" : "亮色模式";
      }
      if (save) {
        localStorage.setItem("mysql_ms_theme", theme);
      }
    }

    function toggleTheme() {
      const curr = document.documentElement.getAttribute("data-theme") || "light";
      setTheme(curr === "dark" ? "light" : "dark", true);
    }

    initTheme();
    renderProgress();
  </script>
</body>
</html>

💻 配套实训环境与动手练习

本文涉及的相关命令操作、开发环境与容器编排已内置在我们的在线脱机环境中,无需安装依赖,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有
  • 上一篇:
  • 下一篇: