<?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>Posts on ZYF.IM BLOG</title>
    <link>https://zyf.im/posts/</link>
    <description>Recent content in Posts on ZYF.IM BLOG</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <lastBuildDate>Thu, 23 Apr 2026 15:06:07 +0000</lastBuildDate>
    <atom:link href="https://zyf.im/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>从 1Password 迁移到 Vaultwarden</title>
      <link>https://zyf.im/2026/04/23/migrate-from-1password-to-vaultwarden/</link>
      <pubDate>Thu, 23 Apr 2026 15:06:07 +0000</pubDate>
      <guid>https://zyf.im/2026/04/23/migrate-from-1password-to-vaultwarden/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;
&lt;p&gt;1Password 不再有跨区订阅优惠，Family Annual ¥498/年 还是有些肉疼。同事们都推荐 Vaultwarden，正好有空闲的 VPS，干脆自建。&lt;/p&gt;
&lt;p&gt;Vaultwarden 是 Bitwarden 服务端的 Rust 重写实现，协议完全兼容官方客户端（桌面 / 移动端 / 浏览器扩展），但资源占用极低，1C/1G 的小机器跑得很轻松。&lt;/p&gt;
&lt;h2 id=&#34;前置准备&#34;&gt;前置准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一台 Debian 12 VPS（1C/1G 起步即可，家用绰绰有余）&lt;/li&gt;
&lt;li&gt;一个域名，例如 &lt;code&gt;vault.example.com&lt;/code&gt;，在 DNS 服务商把 A 记录指向 VPS 公网 IP&lt;/li&gt;
&lt;li&gt;一个 SMTP 邮箱（用于邀请家人、找回密码），推荐 &lt;a href=&#34;https://resend.com&#34;&gt;Resend&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;环境初始化&#34;&gt;环境初始化&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apt update &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade -y &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt install -y curl ca-certificates
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;timedatectl set-timezone Asia/Shanghai
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;adduser evan
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;usermod -aG sudo evan
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -fsSL https://get.docker.com &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sh
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl &lt;span class=&#34;nb&#34;&gt;enable&lt;/span&gt; --now docker
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;usermod -aG docker evan
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;部署-vaultwarden--caddy&#34;&gt;部署 Vaultwarden + Caddy&lt;/h2&gt;
&lt;p&gt;切到普通用户，建立工作目录：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>1Password 不再有跨区订阅优惠，Family Annual ¥498/年 还是有些肉疼。同事们都推荐 Vaultwarden，正好有空闲的 VPS，干脆自建。</p>
<p>Vaultwarden 是 Bitwarden 服务端的 Rust 重写实现，协议完全兼容官方客户端（桌面 / 移动端 / 浏览器扩展），但资源占用极低，1C/1G 的小机器跑得很轻松。</p>
<h2 id="前置准备">前置准备</h2>
<ul>
<li>一台 Debian 12 VPS（1C/1G 起步即可，家用绰绰有余）</li>
<li>一个域名，例如 <code>vault.example.com</code>，在 DNS 服务商把 A 记录指向 VPS 公网 IP</li>
<li>一个 SMTP 邮箱（用于邀请家人、找回密码），推荐 <a href="https://resend.com">Resend</a></li>
</ul>
<h2 id="环境初始化">环境初始化</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt update <span class="o">&amp;&amp;</span> apt upgrade -y <span class="o">&amp;&amp;</span> apt install -y curl ca-certificates
</span></span><span class="line"><span class="cl">timedatectl set-timezone Asia/Shanghai
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">adduser evan
</span></span><span class="line"><span class="cl">usermod -aG sudo evan
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">curl -fsSL https://get.docker.com <span class="p">|</span> sh
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> --now docker
</span></span><span class="line"><span class="cl">usermod -aG docker evan
</span></span></code></pre></div><h2 id="部署-vaultwarden--caddy">部署 Vaultwarden + Caddy</h2>
<p>切到普通用户，建立工作目录：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -iu evan
</span></span><span class="line"><span class="cl">mkdir -p ~/vaultwarden/<span class="o">{</span>vw-data,caddy-data,caddy-config<span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/vaultwarden
</span></span></code></pre></div><p>写 <code>docker-compose.yml</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vaultwarden</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vaultwarden/server:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">vaultwarden</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">      </span><span class="nt">DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://${VAULT_HOST}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ADMIN_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${ADMIN_TOKEN}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SIGNUPS_ALLOWED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;smtp.resend.com&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_FROM</span><span class="p">:</span><span class="w"> </span><span class="l">${SMTP_FROM}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;465&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_SECURITY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;force_tls&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;resend&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${SMTP_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cpus</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;0.5&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">512M</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./vw-data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">internal]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">caddy:2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">caddy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">      </span><span class="nt">VAULT_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">${VAULT_HOST}</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">      </span>- <span class="s2">&#34;80:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;443:443&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./Caddyfile:/etc/caddy/Caddyfile:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./caddy-data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./caddy-config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">internal]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">internal</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">bridge</span><span class="w">
</span></span></span></code></pre></div><p>写 <code>.env</code>，凭据全部走环境变量，别直接写进 compose。<code>VAULT_HOST</code> 是单一来源，Vaultwarden 的 <code>DOMAIN</code> 由 compose 拼上 <code>https://</code> 前缀，Caddyfile 通过 <code>{$VAULT_HOST}</code> 读到纯 hostname：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vim .env
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">VAULT_HOST</span><span class="o">=</span><span class="s2">&#34;vault.example.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SMTP_FROM</span><span class="o">=</span><span class="s2">&#34;vault@notify.example.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SMTP_PASSWORD</span><span class="o">=</span><span class="s2">&#34;re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">ADMIN_TOKEN</span><span class="o">=</span><span class="s1">&#39;$argon2id$v=19$m=65540,t=3,p=4$&lt;salt&gt;$&lt;hash&gt;&#39;</span>
</span></span></code></pre></div><p><code>ADMIN_TOKEN</code> 是后台管理页 <code>/admin</code> 的访问密钥，跟普通用户登录是两套体系。在 <code>SIGNUPS_ALLOWED: false</code> 的私有部署下，邀请家人、改运行时配置、管理 Organization 都要靠它。两点务必照做：</p>
<p><strong>1. 填 argon2id hash，别填明文。</strong> 明文会被 <code>docker inspect</code> / debug 模式下的 <code>docker logs</code> / 某些重启场景下的 <code>ps -ef</code> 捕获；哈希后即使 <code>.env</code> 泄漏，攻击者也无法直接登录。生成命令：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -it vaultwarden/server /vaultwarden <span class="nb">hash</span>
</span></span></code></pre></div><p>交互式输入两次 token，输出 <code>$argon2id$...</code> 整段粘进 <code>.env</code>。</p>
<blockquote>
<p>同一个 token 每次跑出的 hash 都不一样，这是 argon2id 故意的——salt 是现场随机生成并嵌在 hash 串里的。任挑一个写进 <code>.env</code> 都行，不必纠结哪个&quot;对&quot;。验证时 Vaultwarden 会从存储 hash 里提取 salt 复现比对。</p>
</blockquote>
<p>注意：<strong>登录 <code>/admin</code> 时输入的是原文 token，不是 hash</strong>，原文请自己用密码管理器存好。</p>
<p><strong>2. 配置稳定后关掉后台。</strong> 家人都加完、SMTP 调通后，把 <code>.env</code> 里 <code>ADMIN_TOKEN=</code> 留空（或注释整行），重启容器后 <code>/admin</code> 直接 404，攻击面归零。后续要管理时再临时打开重启即可。</p>
<p>更多细节参考 <a href="https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page">Vaultwarden Wiki: Enabling admin page</a>。</p>
<p>写 <code>Caddyfile</code>，域名从 compose 注入的环境变量读取：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddy" data-lang="caddy"><span class="line"><span class="cl"><span class="se">{$VAULT_HOST}</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">encode</span> <span class="s">zstd</span> <span class="s">gzip</span>
</span></span><span class="line"><span class="cl">    <span class="k">reverse_proxy</span> <span class="n">vaultwarden</span><span class="p">:</span><span class="mi">80</span>
</span></span><span class="line"><span class="cl"><span class="p">}</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="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs -f
</span></span></code></pre></div><p>容器跑起来后，访问 <a href="https://vault.example.com/admin">https://vault.example.com/admin</a>，用 <code>ADMIN_TOKEN</code> 原文登录后台，就可以邀请家人了。</p>
<h2 id="数据备份方案">数据备份方案</h2>
<p>Vaultwarden 所有状态都在 <code>vw-data/</code> 下：SQLite 数据库 <code>db.sqlite3</code>、附件 <code>attachments/</code>、Sends <code>sends/</code>、RSA 私钥 <code>rsa_key.*</code>、运行时配置 <code>config.json</code>。<code>icon_cache/</code> 只是网页 favicon 缓存，可忽略。</p>
<p>但要&quot;一键复原&quot;光 <code>vw-data/</code> 不够：<code>docker-compose.yml</code> / <code>Caddyfile</code> / <code>.env</code> 决定怎么把数据跑起来；<code>caddy-data/</code> 存了 Let&rsquo;s Encrypt 账户密钥和已签发证书，丢了重签会触发频率限制。所以备份范围放到整个 <code>~/vaultwarden/</code> 父目录。</p>
<p>VPS 故障、误删条目、或者哪天 docker volume 玄学损坏，没备份就是全家几百条密码一锅端。三条原则：</p>
<ol>
<li><strong>每日异地备份</strong>（脱离 VPS）</li>
<li><strong>加密</strong>（云端泄漏不直接暴露 vault）</li>
<li><strong>季度演练恢复</strong>（没真解密验证过的备份等于没备份）</li>
</ol>
<h3 id="restic--backblaze-b2">restic + Backblaze B2</h3>
<p><a href="https://restic.net/">restic</a> 自带去重、增量、AES-256 加密，单二进制，跟 B2 / R2 / S3 都能直连。下面以 <a href="https://secure.backblaze.com/">B2</a> 为例（10GB 内免费，对密码库绰绰有余）。</p>
<p>宿主机一次性初始化：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install -y restic
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat &gt; /root/.restic-env <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">B2_ACCOUNT_ID=&lt;key-id&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">B2_ACCOUNT_KEY=&lt;application-key&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">RESTIC_REPOSITORY=b2:my-vaultwarden-backup:/
</span></span></span><span class="line"><span class="cl"><span class="s">RESTIC_PASSWORD_FILE=/root/.restic-password
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> /root/.restic-env
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;&lt;a-strong-passphrase&gt;&#39;</span> &gt; /root/.restic-password
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> /root/.restic-password
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -a<span class="p">;</span> <span class="nb">source</span> /root/.restic-env<span class="p">;</span> <span class="nb">set</span> +a
</span></span><span class="line"><span class="cl">restic init
</span></span></code></pre></div><ul>
<li><code>set -a</code> 让随后赋值的变量自动 export，<code>source</code> 才能把 <code>.restic-env</code> 喂给 restic 子进程；<code>set +a</code> 收尾。<code>set</code> 内置命令的符号约定跟普通 CLI 反过来 —— <code>-</code> 启用、<code>+</code> 关闭（<code>-e</code> / <code>-u</code> / <code>-o pipefail</code> 这些脚本安全开关同理）。新开 shell 手动跑 restic 时要重新 source 一次。</li>
<li><code>RESTIC_PASSWORD</code> 是恢复备份的<strong>唯一凭据</strong>，丢了就什么都救不回来。它又不能只存在 Vaultwarden 里（鸡生蛋问题），可以打印一份纸质放抽屉。</li>
</ul>
<h3 id="备份脚本">备份脚本</h3>
<p><code>/root/vaultwarden-backup.sh</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -a<span class="p">;</span> <span class="nb">source</span> /root/.restic-env<span class="p">;</span> <span class="nb">set</span> +a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 任何退出路径都保证容器恢复运行</span>
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;docker start vaultwarden &gt;/dev/null 2&gt;&amp;1 || true&#39;</span> EXIT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 短暂停机（~5s）拿到一致性快照</span>
</span></span><span class="line"><span class="cl">docker stop vaultwarden &gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 备份整个部署目录：vw-data + Caddyfile + docker-compose.yml + .env + caddy-data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 用相对路径，snapshot 里只记 vaultwarden/，恢复时落点干净</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /home/evan <span class="o">&amp;&amp;</span> restic backup vaultwarden <span class="se">\
</span></span></span><span class="line"><span class="cl">  --tag vaultwarden <span class="se">\
</span></span></span><span class="line"><span class="cl">  --exclude<span class="o">=</span><span class="s1">&#39;vaultwarden/vw-data/icon_cache&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 滚动保留：日 7 / 周 4 / 月 12</span>
</span></span><span class="line"><span class="cl">restic forget --prune <span class="se">\
</span></span></span><span class="line"><span class="cl">  --keep-daily <span class="m">7</span> --keep-weekly <span class="m">4</span> --keep-monthly <span class="m">12</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x /root/vaultwarden-backup.sh
</span></span></code></pre></div><blockquote>
<p>不在线备份（不用 <code>sqlite3 .backup</code>）的原因：vaultwarden 官方镜像不一定带 sqlite3 CLI，且 WAL 模式下直接 rsync 容易拿到不一致快照。stop 5 秒最稳。家用场景凌晨 3 点没人在用，这点停机完全无感。
用相对路径是因为 restic 按 snapshot 路径串做增量匹配，绝对 / 相对混用会让下次 backup 当全新数据重传。新仓库一开始就定下来。</p>
</blockquote>
<h3 id="每日跑">每日跑</h3>
<p>cron 一行：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo crontab -e
</span></span></code></pre></div><pre tabindex="0"><code class="language-cron" data-lang="cron">30 3 * * * /root/vaultwarden-backup.sh &gt;&gt; /var/log/vaultwarden-backup.log 2&gt;&amp;1
</code></pre><h3 id="演练恢复">演练恢复</h3>
<p><strong>每季度做一次</strong>，且必须真解密一条记录验证 —— 文件存在不等于能解密：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 拉一份最新快照到临时目录</span>
</span></span><span class="line"><span class="cl">restic snapshots
</span></span><span class="line"><span class="cl">restic restore latest --target /tmp/vw-restore
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># /tmp/vw-restore/vaultwarden/ 是完整部署，cd 进去 docker compose up -d 就能起</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 用 master password 登录 → 打开任意条目 → 字段能看到明文 = 成功</span>
</span></span></code></pre></div><p>SQLite 文件损坏或 RSA 密钥不匹配时，文件都在但密码全部解不开。<strong>演练才是备份的最后一公里</strong>。</p>
<h2 id="ssh-agent-设置">SSH Agent 设置</h2>
<p>Bitwarden 桌面端从 2024 年起内置了 SSH Agent，可以替代 1Password 的 SSH 体验。Vaultwarden 服务端不参与，全部在客户端完成。</p>
<p>启用步骤：</p>
<ol>
<li>
<p>打开 Bitwarden Desktop → Settings，勾选 <strong>Use Bitwarden SSH Agent</strong></p>
</li>
<li>
<p>客户端会在系统目录里建一个 socket，先确认路径：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">lsof -U <span class="p">|</span> grep bitwarden-ssh-agent
</span></span></code></pre></div><p>macOS 上典型路径：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">~/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock
</span></span></code></pre></div></li>
<li>
<p>把它 symlink 到一个稳定的位置，方便 shell 引用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ln -sf <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">       <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.bitwarden-ssh-agent.sock&#34;</span>
</span></span></code></pre></div></li>
<li>
<p>在 <code>~/.zshrc</code> / <code>~/.bashrc</code> 里导出：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">SSH_AUTH_SOCK</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.bitwarden-ssh-agent.sock&#34;</span>
</span></span></code></pre></div></li>
<li>
<p>验证：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-add -l
</span></span></code></pre></div><p>能列出 Bitwarden 里标记为 SSH Key 的条目就成功了。</p>
</li>
</ol>
<h2 id="迁移注意点">迁移注意点</h2>
<ul>
<li><strong>用桌面客户端导出 / 导入，不要用手机端的两端同步迁移</strong>，手机端那个流程会丢失部分自定义字段。</li>
<li><strong>1Password 的 Vault 分组信息会丢失</strong>，迁移后需要在 Vaultwarden 里手工建文件夹重新归类。</li>
<li><strong>Passkey 无法迁移</strong>，Passkey 是绑定在验证器 app 内部的，必须在新平台上对每个站点重新注册。</li>
<li><strong>附件无法直接迁移</strong>，但 1Password 导出的 <code>.1pux</code> 文件本质是 zip，把后缀改成 <code>.zip</code> 解压后就能拿到原始附件，再手工挂回 Vaultwarden 对应条目。</li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>自建代理订阅管理</title>
      <link>https://zyf.im/2025/12/23/self-hosted-proxy-subscription-management/</link>
      <pubDate>Tue, 23 Dec 2025 10:12:12 +0000</pubDate>
      <guid>https://zyf.im/2025/12/23/self-hosted-proxy-subscription-management/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;
&lt;p&gt;挂着两三家机场 + 一台自建节点，每家命名规则不一样、节点重复、规则集要在每个客户端各贴一份。机场换链接、加节点、改分流，所有设备都要重订阅一轮。&lt;/p&gt;
&lt;p&gt;要的是：&lt;strong&gt;所有原始订阅塞进一个聚合器，自动去重 / 改名 / 分组 / 套规则，输出一条稳定 URL，客户端只订阅这条&lt;/strong&gt;。本文用 &lt;a href=&#34;https://github.com/sub-store-org/Sub-Store&#34;&gt;Sub-Store&lt;/a&gt; + &lt;a href=&#34;https://github.com/tindy2013/subconverter&#34;&gt;subconverter&lt;/a&gt; + Docker Compose 实现。&lt;/p&gt;
&lt;h2 id=&#34;架构与暴露策略&#34;&gt;架构与暴露策略&lt;/h2&gt;
&lt;p&gt;三层各暴露一个稳定 URL，上下游互不影响。链路单向：客户端 → subconverter 公网 URL → 容器内拉 Sub-Store → Sub-Store 拉机场。中间任一层挂掉只影响下次订阅刷新，已下载的配置继续可用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;机场订阅 · 自建节点
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Sub-Store · 合并 / 去重 / 改名 / 过滤 / 分组
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      ↓  纯节点订阅
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;subconverter (+ sub-web GUI) · 套 ACL4SSR · 转目标格式
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;客户端 · Mihomo / Surge / sing-box
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只有 subconverter 必须公网（客户端要拉订阅），其余关进 SSH 隧道：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>挂着两三家机场 + 一台自建节点，每家命名规则不一样、节点重复、规则集要在每个客户端各贴一份。机场换链接、加节点、改分流，所有设备都要重订阅一轮。</p>
<p>要的是：<strong>所有原始订阅塞进一个聚合器，自动去重 / 改名 / 分组 / 套规则，输出一条稳定 URL，客户端只订阅这条</strong>。本文用 <a href="https://github.com/sub-store-org/Sub-Store">Sub-Store</a> + <a href="https://github.com/tindy2013/subconverter">subconverter</a> + Docker Compose 实现。</p>
<h2 id="架构与暴露策略">架构与暴露策略</h2>
<p>三层各暴露一个稳定 URL，上下游互不影响。链路单向：客户端 → subconverter 公网 URL → 容器内拉 Sub-Store → Sub-Store 拉机场。中间任一层挂掉只影响下次订阅刷新，已下载的配置继续可用。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">机场订阅 · 自建节点
</span></span><span class="line"><span class="cl">      ↓
</span></span><span class="line"><span class="cl">Sub-Store · 合并 / 去重 / 改名 / 过滤 / 分组
</span></span><span class="line"><span class="cl">      ↓  纯节点订阅
</span></span><span class="line"><span class="cl">subconverter (+ sub-web GUI) · 套 ACL4SSR · 转目标格式
</span></span><span class="line"><span class="cl">      ↓
</span></span><span class="line"><span class="cl">客户端 · Mihomo / Surge / sing-box
</span></span></code></pre></div><p>只有 subconverter 必须公网（客户端要拉订阅），其余关进 SSH 隧道：</p>
<table>
	<thead>
			<tr>
					<th>层</th>
					<th>服务</th>
					<th>容器端口</th>
					<th>宿主机绑定</th>
					<th>对外</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2</td>
					<td>sub-store</td>
					<td>3001</td>
					<td><code>127.0.0.1:3001</code></td>
					<td>仅 SSH 隧道</td>
			</tr>
			<tr>
					<td>3</td>
					<td>subconverter</td>
					<td>25500</td>
					<td><code>127.0.0.1:25500</code></td>
					<td><strong>公网域名 + Caddy</strong></td>
			</tr>
			<tr>
					<td>3</td>
					<td>sub-web</td>
					<td>80</td>
					<td><code>127.0.0.1:25580</code></td>
					<td>仅 SSH 隧道</td>
			</tr>
	</tbody>
</table>
<p>subconverter 公网入口叠两层防御：<strong>Caddy UA 白名单</strong>（藏 banner，挡裸扫描器）+ <strong><code>api_access_token</code></strong>（每条 URL 必带 token，真访问控制）。</p>
<h2 id="前置准备">前置准备</h2>
<ul>
<li>VPS 一台（1C/512M 起步），Debian 12 / Ubuntu 22.04+</li>
<li>域名 <code>sub.example.com</code>，A 记录指向 VPS（只 subconverter 用）</li>
<li>本机 SSH 公钥已加进 VPS</li>
</ul>
<h2 id="环境初始化">环境初始化</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt update <span class="o">&amp;&amp;</span> apt upgrade -y <span class="o">&amp;&amp;</span> apt install -y curl ca-certificates
</span></span><span class="line"><span class="cl">timedatectl set-timezone Asia/Shanghai
</span></span><span class="line"><span class="cl">adduser evan <span class="o">&amp;&amp;</span> usermod -aG sudo evan
</span></span><span class="line"><span class="cl">curl -fsSL https://get.docker.com <span class="p">|</span> sh
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> --now docker
</span></span><span class="line"><span class="cl">usermod -aG docker evan
</span></span></code></pre></div><h2 id="部署核心栈">部署核心栈</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -iu evan
</span></span><span class="line"><span class="cl">mkdir -p ~/sherpa/<span class="o">{</span>sub-store-data,subconverter/<span class="o">{</span>snippets,rules<span class="o">}}</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/sherpa
</span></span></code></pre></div><h3 id="docker-composeyml">docker-compose.yml</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">x-logging</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;log</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">json-file</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;10m&#34;</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#34;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sub-store</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">xream/sub-store:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">sub-store</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;127.0.0.1:3001:3001&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">./sub-store-data:/opt/app/data]</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">      </span>- <span class="s2">&#34;SUB_STORE_FRONTEND_BACKEND_PATH=${SUB_STORE_FRONTEND_BACKEND_PATH:?missing in .env}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">SUB_STORE_FRONTEND_HOST=0.0.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">SUB_STORE_FRONTEND_PORT=3001</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">SUB_STORE_DATA_BASE_PATH=/opt/app/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">sherpa-net]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w"> </span><span class="cp">*log</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># MetaCubeX fork 跟进 Hysteria2 / TUIC / Reality;tindy2013 原版更新慢</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">subconverter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/metacubex/subconverter:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">subconverter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;127.0.0.1:25500:25500&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./subconverter/pref.toml:/base/pref.toml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./subconverter/snippets:/base/snippets:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./subconverter/rules:/base/rules:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">sherpa-net]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w"> </span><span class="cp">*log</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sub-web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">careywong/subweb:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">sub-web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">API_URL=https://${SUB_DOMAIN}]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;127.0.0.1:25580:80&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">subconverter]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">sherpa-net]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w"> </span><span class="cp">*log</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># 显式 name,Caddy 子项目 external 引用;容器之间用服务名通信，不走宿主机 loopback</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sherpa-net</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">bridge</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sherpa-net</span><span class="w">
</span></span></span></code></pre></div><h3 id="env">.env</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; .env <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">SUB_DOMAIN=sub.example.com
</span></span></span><span class="line"><span class="cl"><span class="s">SUB_STORE_FRONTEND_BACKEND_PATH=/api-$(openssl rand -hex 16)
</span></span></span><span class="line"><span class="cl"><span class="s">SSH_HOST=evan@&lt;vps-ip&gt;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> .env
</span></span></code></pre></div><p><code>SUB_STORE_FRONTEND_BACKEND_PATH</code> 是 Sub-Store「路径即密钥」的访问控制 —— 后端 API 挂到 <code>/&lt;random&gt;/api/...</code>，没前缀直接 404。即便不暴露公网也必填（前端代码写死要带）。</p>
<h3 id="subconverterpreftoml">subconverter/pref.toml</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; subconverter/pref.toml <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">[common]
</span></span></span><span class="line"><span class="cl"><span class="s">api_mode = true
</span></span></span><span class="line"><span class="cl"><span class="s">api_access_token = &#34;$(openssl rand -hex 32)&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">[node_pref]
</span></span></span><span class="line"><span class="cl"><span class="s">udp_flag = true
</span></span></span><span class="line"><span class="cl"><span class="s">filter_deprecated_nodes = true
</span></span></span><span class="line"><span class="cl"><span class="s">append_sub_userinfo = true
</span></span></span><span class="line"><span class="cl"><span class="s">clash_use_new_field_name = true
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">[managed_config]
</span></span></span><span class="line"><span class="cl"><span class="s">write_managed_config = true
</span></span></span><span class="line"><span class="cl"><span class="s"># 必须真实公网域名,客户端按这个 URL 自动刷新;写 localhost 客户端更新不了
</span></span></span><span class="line"><span class="cl"><span class="s">managed_config_prefix = &#34;https://sub.example.com&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">[emojis]
</span></span></span><span class="line"><span class="cl"><span class="s">add_emoji = true
</span></span></span><span class="line"><span class="cl"><span class="s">remove_old_emoji = true
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">[ruleset]
</span></span></span><span class="line"><span class="cl"><span class="s">enabled = true
</span></span></span><span class="line"><span class="cl"><span class="s">overwrite_original_rules = true
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> subconverter/pref.toml
</span></span></code></pre></div><h3 id="启动--自检">启动 + 自检</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">curl -s http://127.0.0.1:25500/version       <span class="c1"># subconverter vx.x.x backend</span>
</span></span><span class="line"><span class="cl">curl -sI http://127.0.0.1:3001/ <span class="p">|</span> head -1    <span class="c1"># HTTP/1.1 200 OK</span>
</span></span></code></pre></div><h2 id="公网入口caddy-反代">公网入口：Caddy 反代</h2>
<p>宿主机已有 Caddy，<code>/etc/caddy/Caddyfile</code> 加一段，<code>systemctl reload caddy</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddy" data-lang="caddy"><span class="line"><span class="cl"><span class="gh">sub.example.com</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">encode</span> <span class="s">gzip</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@blocked</span> <span class="k">not</span> <span class="k">header</span> <span class="s">User-Agent</span> <span class="s">*Clash*</span> <span class="s">*Quantumult*</span> <span class="s">*Surge*</span> <span class="s">*Shadowrocket*</span> <span class="s">*Stash*</span> <span class="s">*Loon*</span> <span class="s">*Mihomo*</span> <span class="s">*sing-box*</span> <span class="s">*curl*</span> <span class="s">*wget*</span>
</span></span><span class="line"><span class="cl">    <span class="k">respond</span> <span class="nd">@blocked</span> <span class="mi">403</span>
</span></span><span class="line"><span class="cl">    <span class="k">reverse_proxy</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">25500</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>没 Caddy 就跑容器版子项目 <code>caddy/</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># caddy/docker-compose.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">caddy:2-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;80:80&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;443:443&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./Caddyfile:/etc/caddy/Caddyfile:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">caddy_data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">caddy_config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">caddy-net, sherpa-net]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy-net</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="nt">bridge, name</span><span class="p">:</span><span class="w"> </span><span class="l">caddy-net }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sherpa-net</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="nt">true, name</span><span class="p">:</span><span class="w"> </span><span class="l">sherpa-net }  </span><span class="w"> </span><span class="c"># 引用主项目同名网络</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">caddy_data</span><span class="p">:</span><span class="w"> </span>{<span class="nt">}, caddy_config</span><span class="p">:</span><span class="w"> </span>{}<span class="w"> </span>}<span class="w">
</span></span></span></code></pre></div><p><code>caddy/Caddyfile</code> 跟上面一样，把 <code>reverse_proxy</code> 改成走容器网络的服务名：<code>reverse_proxy subconverter:25500</code>。</p>
<p>自检：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -i https://sub.example.com/version            <span class="c1"># 403,UA 被挡</span>
</span></span><span class="line"><span class="cl">curl -iA <span class="s2">&#34;Mihomo&#34;</span> https://sub.example.com/version  <span class="c1"># 200</span>
</span></span></code></pre></div><blockquote>
<p>UA 白名单不是安全机制（改 UA 太容易），作用是把 zgrab / nmap 扫描里的 banner 干掉。真正的访问控制是 <code>api_access_token</code>。</p>
</blockquote>
<h2 id="本机访问ssh-隧道">本机访问：SSH 隧道</h2>
<p>Sub-Store / sub-web 只绑 loopback，靠 SSH 隧道把端口转回本机。配 <code>~/.ssh/config</code>：</p>
<pre tabindex="0"><code class="language-ssh-config" data-lang="ssh-config">Host sherpa
    HostName &lt;vps-ip&gt;
    User evan
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 3001  127.0.0.1:3001
    LocalForward 25580 127.0.0.1:25580
    ServerAliveInterval 60
    ExitOnForwardFailure yes
</code></pre><p>简单粗暴：<code>ssh -fN sherpa</code>。坏处是要关只能 pgrep + kill。幂等管理用 ControlMaster，开 / 关 / 查对着同一个 socket：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">SOCK</span><span class="o">=</span>/tmp/sherpa-tunnel-<span class="k">$(</span>id -u<span class="k">)</span>.sock
</span></span><span class="line"><span class="cl">ssh -S <span class="s2">&#34;</span><span class="nv">$SOCK</span><span class="s2">&#34;</span> -O check sherpa 2&gt;/dev/null <span class="o">||</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  ssh -fN -M -S <span class="s2">&#34;</span><span class="nv">$SOCK</span><span class="s2">&#34;</span> sherpa     <span class="c1"># 启动(已有则跳过)</span>
</span></span><span class="line"><span class="cl">ssh -S <span class="s2">&#34;</span><span class="nv">$SOCK</span><span class="s2">&#34;</span> -O check sherpa     <span class="c1"># 查</span>
</span></span><span class="line"><span class="cl">ssh -S <span class="s2">&#34;</span><span class="nv">$SOCK</span><span class="s2">&#34;</span> -O <span class="nb">exit</span>  sherpa     <span class="c1"># 关</span>
</span></span></code></pre></div><p>长期保活：<code>autossh -M 0 -fN sherpa</code>（<code>-M 0</code> 关掉 autossh 自带 echo 探测，完全依赖 SSH 的 <code>ServerAliveInterval</code>）。</p>
<p>隧道起来后：</p>
<ul>
<li><strong>Sub-Store 后台</strong> <code>http://localhost:3001/?api=http://localhost:3001/api-xxxxxxxx</code>（<code>api=</code> 填 <code>.env</code> 的 <code>SUB_STORE_FRONTEND_BACKEND_PATH</code>，浏览器记 localStorage）</li>
<li><strong>sub-web GUI</strong> <code>http://localhost:25580</code>，「后端地址」填 <code>https://sub.example.com</code></li>
</ul>
<blockquote>
<p>手机端 SSH 隧道不友好，VPS 和手机都加 <a href="https://tailscale.com/">Tailscale</a> tailnet，手机直连 <code>http://&lt;tailscale-ip&gt;:3001/...</code>。这部分单独写。</p>
</blockquote>
<h2 id="第-1-层节点源">第 1 层：节点源</h2>
<ul>
<li><strong>机场订阅</strong>：复制后台的 <code>https://airport.example.com/api/v1/client/subscribe?token=xxx</code> URL，多家就准备多条</li>
<li><strong>自建节点（可选）</strong>：另一台 VPS 上跑 <a href="https://github.com/mack-a/v2ray-agent">mack-a/v2ray-agent</a> 一键脚本，协议选 VLESS-Reality（抗封锁最强，免域名证书）。生成的 <code>vless://...</code> 当一条「单节点订阅」喂给 Sub-Store。<strong>独立 VPS</strong>，管理面挂了不影响出口流量</li>
</ul>
<h2 id="第-2-层sub-store-编排">第 2 层：Sub-Store 编排</h2>
<p>两个抽象：<strong>订阅</strong>（单源）+ <strong>组合订阅</strong>（多源合并 + 节点操作流水线）。典型用法：每家机场 / 每条自建节点建一条单订阅 → 全部加进主组合 <code>all</code> → 客户端只订阅主组合。</p>
<p>「组合订阅 → all」加节点操作流水线，顺序重要：</p>
<table>
	<thead>
			<tr>
					<th>#</th>
					<th>操作</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>类型过滤</td>
					<td>砍掉客户端不支持的协议</td>
			</tr>
			<tr>
					<td>2</td>
					<td>正则过滤</td>
					<td>关键字 <code>剩余|到期|官网|订阅|流量|套餐</code>，砍伪节点</td>
			</tr>
			<tr>
					<td>3</td>
					<td>倍率过滤（脚本）</td>
					<td>砍倍率 ≥ 2 的高耗节点</td>
			</tr>
			<tr>
					<td>4</td>
					<td>正则改名</td>
					<td><code>🇭🇰 香港 01</code> / <code>HK-01</code> / <code>Hong Kong-01</code> 统一格式</td>
			</tr>
			<tr>
					<td>5</td>
					<td>正则排序</td>
					<td>按国家分组</td>
			</tr>
			<tr>
					<td>6</td>
					<td>去重</td>
					<td>按 <code>server + port + uuid</code></td>
			</tr>
	</tbody>
</table>
<p><code>all</code> 最终拿到一个 URL，<strong>视场景换 host</strong>：</p>
<table>
	<thead>
			<tr>
					<th>视角</th>
					<th>URL</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>容器内</td>
					<td><code>http://sub-store:3001/api-xxx/download/collection/all?target=ClashMeta</code></td>
					<td>✅ 填进 sub-web</td>
			</tr>
			<tr>
					<td>隧道</td>
					<td><code>http://localhost:3001/api-xxx/download/collection/all?target=ClashMeta</code></td>
					<td>本地调试看节点</td>
			</tr>
	</tbody>
</table>
<p>容器内用服务名 <code>sub-store</code>（同 <code>sherpa-net</code>，Docker DNS 解析）。这条 URL 会出现在客户端订阅链接的 <code>url=</code> 参数里，但 hostname <code>sub-store</code> 只在 docker 网络内可解析，公网拿到也访问不了。</p>
<h2 id="第-3-层subconverter-串接">第 3 层：subconverter 串接</h2>
<p>subconverter URL 模板：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">https://sub.example.com/sub
</span></span><span class="line"><span class="cl">  ?target=clash
</span></span><span class="line"><span class="cl">  &amp;url=&lt;URL-encoded 第 2 层 URL&gt;
</span></span><span class="line"><span class="cl">  &amp;config=&lt;URL-encoded 远程 config&gt;
</span></span><span class="line"><span class="cl">  &amp;access_token=&lt;pref.toml 的 api_access_token&gt;
</span></span><span class="line"><span class="cl">  &amp;emoji=true&amp;list=false&amp;udp=true
</span></span></code></pre></div><p>手拼太累，用 sub-web GUI（<code>http://localhost:25580</code>）填表：</p>
<table>
	<thead>
			<tr>
					<th>字段</th>
					<th>填法</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>后端地址</td>
					<td><code>https://sub.example.com</code></td>
			</tr>
			<tr>
					<td>订阅链接</td>
					<td><strong>容器内视角</strong> <code>http://sub-store:3001/api-xxx/download/collection/all</code></td>
			</tr>
			<tr>
					<td>客户端</td>
					<td><code>Clash</code></td>
			</tr>
			<tr>
					<td>远程配置</td>
					<td><a href="https://github.com/ACL4SSR/ACL4SSR">ACL4SSR</a> 模板（完整 / 精简 / 含广告拦截，按需选）</td>
			</tr>
			<tr>
					<td>高级 → access_token</td>
					<td><code>pref.toml</code> 里的那个</td>
			</tr>
	</tbody>
</table>
<p>远程 config 默认走 GitHub 原始链接，怕限流就 clone 到本地：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone --depth<span class="o">=</span><span class="m">1</span> https://github.com/ACL4SSR/ACL4SSR.git ~/sherpa/subconverter/rules/ACL4SSR
</span></span><span class="line"><span class="cl"><span class="c1"># config 改成 config/ACL4SSR/Clash/config/ACL4SSR_Online_Full.ini</span>
</span></span></code></pre></div><h3 id="上下游联动">上下游联动</h3>
<p>三层架构最舒服的地方：改一处不影响其他。</p>
<table>
	<thead>
			<tr>
					<th>想做</th>
					<th>改哪层</th>
					<th>客户端 URL</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>机场换链接</td>
					<td>第 2 层单订阅</td>
					<td>❌ 不变</td>
			</tr>
			<tr>
					<td>加自建节点</td>
					<td>第 2 层加单订阅 + 进组合</td>
					<td>❌ 不变</td>
			</tr>
			<tr>
					<td>砍某地区节点</td>
					<td>第 2 层组合过滤规则</td>
					<td>❌ 不变</td>
			</tr>
			<tr>
					<td>换分流规则</td>
					<td>第 3 层 <code>config</code> / <code>pref.toml</code></td>
					<td>✅ 换新 URL</td>
			</tr>
			<tr>
					<td>加 sing-box 客户端</td>
					<td>第 3 层 <code>target=singbox</code></td>
					<td>✅ 给 sing-box 新 URL</td>
			</tr>
	</tbody>
</table>
<p>下一节的短链方案能把所有 ✅ 都变 ❌。</p>
<h2 id="短链固定订阅-url">短链固定订阅 URL</h2>
<p>上一节的 subconverter URL 又长又会变：换 token / 调规则 / 加 target 都要重订一遍。家里多设备 / 共享家人就翻车。</p>
<p>套一层短链：客户端订阅短链，短链指真实 URL。改后端时只改短链目标，客户端无感。推荐 <a href="https://short.io/">short.io</a> —— 免费版支持自定义域名 + API，不挂广告中转页（bit.ly / tinyurl 免费版会跳广告，部分客户端识别为非订阅响应直接报错）。</p>
<p>注册 → 加自己的域名 <code>s.example.com</code>（按界面提示加 CNAME）→ 每种格式 / 每个家人各建一条：</p>
<table>
	<thead>
			<tr>
					<th>短链</th>
					<th>目标</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>s.example.com/clash</code></td>
					<td>clash 格式</td>
			</tr>
			<tr>
					<td><code>s.example.com/sing</code></td>
					<td>sing-box 格式</td>
			</tr>
			<tr>
					<td><code>s.example.com/mom</code></td>
					<td>给妈用的（可叠 token / 限节点）</td>
			</tr>
	</tbody>
</table>
<p>注意：</p>
<ul>
<li><strong><code>access_token</code> 暴露给 short.io</strong> —— 介意就自托管 <a href="https://shlink.io/">Shlink</a> / <a href="https://yourls.org/">YOURLS</a> 跟主栈放一起</li>
<li><strong>每人独立短链</strong> —— 想踢人 / 限速只动一条目标 URL</li>
<li><strong>免费版每月 1k 跳转</strong> —— 6 小时刷一次，五口之家月 120 次够用</li>
</ul>
<h2 id="客户端">客户端</h2>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>推荐</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>macOS / Linux</td>
					<td><a href="https://github.com/mihomo-party-org/mihomo-party">Mihomo Party</a> · <a href="https://github.com/clash-verge-rev/clash-verge-rev">Clash Verge Rev</a> · <a href="https://nssurge.com/">Surge</a></td>
			</tr>
			<tr>
					<td>iOS</td>
					<td><a href="https://apps.apple.com/app/shadowrocket/id932747118">Shadowrocket</a> · <a href="https://apps.apple.com/app/stash/id1596063349">Stash</a></td>
			</tr>
			<tr>
					<td>Android</td>
					<td><a href="https://github.com/MetaCubeX/ClashMetaForAndroid">Clash Meta for Android</a></td>
			</tr>
	</tbody>
</table>
<p>桌面优先 <a href="https://github.com/MetaCubeX/mihomo">Mihomo</a> 内核（规则兼容性最好）。<a href="https://github.com/SagerNet/sing-box">sing-box</a> 用 <code>target=singbox</code>，配置精简但规则生态没 Mihomo 完整。</p>
<h2 id="节点诊断">节点诊断</h2>
<table>
	<thead>
			<tr>
					<th>工具</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><a href="https://www.whatismyip.com/">https://www.whatismyip.com/</a></td>
					<td>出口 IP + ISP 归属</td>
			</tr>
			<tr>
					<td><a href="https://ping.pe/8.8.8.8">https://ping.pe/8.8.8.8</a></td>
					<td>全球多点 ping 延迟 / 丢包</td>
			</tr>
			<tr>
					<td><a href="https://lg.dmit.sh/">https://lg.dmit.sh/</a></td>
					<td>多家机房 Looking Glass，反测 VPS 网络质量</td>
			</tr>
			<tr>
					<td><a href="https://apimobile.meituan.com/locate/v2/ip/loc?rgeo=true&amp;ip=8.8.8.8">https://apimobile.meituan.com/locate/v2/ip/loc?rgeo=true&ip=8.8.8.8</a></td>
					<td>美团 IP 库定位，判断 IP 在国内 App 眼里属哪个城市</td>
			</tr>
			<tr>
					<td><a href="https://apimobile.meituan.com/group/v1/city/latlng/21.7,110.9?tag=0">https://apimobile.meituan.com/group/v1/city/latlng/21.7,110.9?tag=0</a></td>
					<td>经纬度反查城市，配合上一条交叉验证</td>
			</tr>
			<tr>
					<td><a href="https://ip.skk.moe/">https://ip.skk.moe/</a></td>
					<td>IP 信息 + 流媒体解锁 + IP 类型（家宽 / 机房 / 移动）一站式</td>
			</tr>
	</tbody>
</table>
<p>挑节点优先看「IP 类型 = 家宽 (Residential)」，机房 IP 越来越多被 Netflix / ChatGPT 风控。</p>
<h2 id="维护">维护</h2>
<ul>
<li><strong>数据</strong>：业务数据在 <code>~/sherpa/sub-store-data/</code> + <code>~/sherpa/subconverter/</code> + <code>.env</code>，整个 <code>~/sherpa/</code> 打 tar 异地。备份策略复用 [[migrate-from-1password-to-vaultwarden|Vaultwarden 那篇]] 的 restic + B2，差异是可以把 restic 也跑 Docker（crontab 写进容器）跟主栈解耦</li>
<li><strong>升级</strong>：<code>docker compose pull &amp;&amp; docker compose up -d</code>，三个 <code>latest</code> 都滚动，介意稳定性锁 tag</li>
<li><strong>日志</strong>：<code>docker compose logs -f sub-store / subconverter / sub-web</code></li>
<li><strong>隧道掉了</strong>：客户端无影响（已下载的 config 继续可用），只是后台进不去，重连 <code>ssh -fN sherpa</code></li>
<li><strong>token 泄漏</strong>：改 <code>.env</code> 的 <code>SUB_STORE_FRONTEND_BACKEND_PATH</code> 或 <code>pref.toml</code> 的 <code>api_access_token</code>，<code>docker compose up -d</code> 重启。前者要重填浏览器 <code>api=</code> 参数，后者要重生成所有订阅 URL（用短链就只改短链目标）</li>
<li><strong>证书续签自动</strong>，但 80/443 被挡会失败，到期前 30 天看下 <code>caddy-logs</code></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>NGINX 启用 HTTP/3</title>
      <link>https://zyf.im/2025/06/27/nginx-enable-http3/</link>
      <pubDate>Fri, 27 Jun 2025 10:12:12 +0000</pubDate>
      <guid>https://zyf.im/2025/06/27/nginx-enable-http3/</guid>
      <description>&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;
&lt;p&gt;HTTP/2 解决了&amp;quot;多路复用、头部压缩、优先级&amp;quot;等 HTTP/1.x 的瓶颈，但仍受 TCP 先天限制。&lt;/p&gt;
&lt;p&gt;HTTP/3 则把同样的语义搬到 QUIC。Quick UDP Internet Connections 是 Google 设计的一种基于 UDP 的多路复用安全传输层协议。标准化后，HTTP/3 = HTTP over QUIC。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;HTTP/2&lt;/th&gt;
					&lt;th&gt;HTTP/3&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;传输&lt;/td&gt;
					&lt;td&gt;TCP（必）+ TLS 1.2/1.3（多数场景）&lt;/td&gt;
					&lt;td&gt;QUIC（UDP 之上）+ TLS 1.3（强制）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;流（Stream）&lt;/td&gt;
					&lt;td&gt;Mux 在 TCP 里的逻辑流&lt;/td&gt;
					&lt;td&gt;Mux 在 QUIC 原生流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;头压缩&lt;/td&gt;
					&lt;td&gt;HPACK&lt;/td&gt;
					&lt;td&gt;QPACK（避免队头阻塞）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/2 浏览器要求必须跑在 TLS 之上（ALPN=&amp;ldquo;h2&amp;rdquo;）。栈变为 HTTP/2 → TLS → TCP → IP。&lt;/li&gt;
&lt;li&gt;HTTP/3 使用 QUIC 替换 TCP，栈变为 HTTP/3 → QUIC（拥塞/重传）→ UDP → IP，加密集成在 QUIC（基于 TLS 1.3）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mux&lt;/code&gt; 即 multiplex，多路复用。&lt;/li&gt;
&lt;li&gt;HTTP/2 必须顺序传输，而 HTTP/3 支持乱序。&lt;/li&gt;
&lt;li&gt;如果沿用 HPACK，丢一个 UDP 包就会让所有流的头部解码卡住，因此 IETF 为 HTTP/3 设计了 QPACK 来&amp;quot;避开队头阻塞（Head-of-Line Blocking, HoL）&amp;quot;。&lt;/li&gt;
&lt;li&gt;连接迁移：通过 Connection ID 识别会话，IP 或端口改变（如移动网络/Wi-Fi 切换）无需重新建立连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 HTTP/3 的网站：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="简介">简介</h2>
<p>HTTP/2 解决了&quot;多路复用、头部压缩、优先级&quot;等 HTTP/1.x 的瓶颈，但仍受 TCP 先天限制。</p>
<p>HTTP/3 则把同样的语义搬到 QUIC。Quick UDP Internet Connections 是 Google 设计的一种基于 UDP 的多路复用安全传输层协议。标准化后，HTTP/3 = HTTP over QUIC。</p>
<table>
	<thead>
			<tr>
					<th>层级</th>
					<th>HTTP/2</th>
					<th>HTTP/3</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>传输</td>
					<td>TCP（必）+ TLS 1.2/1.3（多数场景）</td>
					<td>QUIC（UDP 之上）+ TLS 1.3（强制）</td>
			</tr>
			<tr>
					<td>流（Stream）</td>
					<td>Mux 在 TCP 里的逻辑流</td>
					<td>Mux 在 QUIC 原生流</td>
			</tr>
			<tr>
					<td>头压缩</td>
					<td>HPACK</td>
					<td>QPACK（避免队头阻塞）</td>
			</tr>
	</tbody>
</table>
<ul>
<li>HTTP/2 浏览器要求必须跑在 TLS 之上（ALPN=&ldquo;h2&rdquo;）。栈变为 HTTP/2 → TLS → TCP → IP。</li>
<li>HTTP/3 使用 QUIC 替换 TCP，栈变为 HTTP/3 → QUIC（拥塞/重传）→ UDP → IP，加密集成在 QUIC（基于 TLS 1.3）。</li>
<li><code>Mux</code> 即 multiplex，多路复用。</li>
<li>HTTP/2 必须顺序传输，而 HTTP/3 支持乱序。</li>
<li>如果沿用 HPACK，丢一个 UDP 包就会让所有流的头部解码卡住，因此 IETF 为 HTTP/3 设计了 QPACK 来&quot;避开队头阻塞（Head-of-Line Blocking, HoL）&quot;。</li>
<li>连接迁移：通过 Connection ID 识别会话，IP 或端口改变（如移动网络/Wi-Fi 切换）无需重新建立连接。</li>
</ul>
<p>使用 HTTP/3 的网站：</p>
<ul>
<li><a href="https://cloudflare-quic.com/">https://cloudflare-quic.com/</a></li>
</ul>
<h2 id="开始实验">开始实验</h2>
<p>启用 HTTP/3 需要使用 HTTPS，请先参考 <a href="/2025/06/26/use-https-in-local-environment/">在本地环境使用 HTTPS ｜ ZYF.IM</a>。</p>
<p>修改 NGINX default.conf 配置文件，启用 HTTP/3：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># vi default.conf
</span></span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># for better compatibility it&#39;s recommended
</span></span></span><span class="line"><span class="cl">    <span class="c1"># to use the same port for http/3 and https
</span></span></span><span class="line"><span class="cl">    <span class="c1"># 浏览器首次只能走 TCP，因此必须提供一个 HTTPS 入口，再通过 `Alt-Svc` 升级到 HTTP/3
</span></span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Quick UDP Internet Connections
</span></span></span><span class="line"><span class="cl">    <span class="c1"># quic 关键字表示使用 UDP 监听，承载 QUIC/HTTP-3
</span></span></span><span class="line"><span class="cl">    <span class="c1"># reuseport 关键字表示复用端口，允许多个 NGINX 实例共享同一端口
</span></span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">quic</span> <span class="s">reuseport</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">http2</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 启用 HTTP/3，默认是 on
</span></span></span><span class="line"><span class="cl">    <span class="kn">http3</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">localhost</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span> <span class="s">/etc/ssl/certs/localhost.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/ssl/certs/localhost-key.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 启用 TLS 允许 0-RTT（Early Data），允许客户端在 TLS 1.3 的重连阶段携带早期数据
</span></span></span><span class="line"><span class="cl">    <span class="c1"># 对 HTTP/3 非必需，但 QUIC 天生支持 0-RTT；此处保持一致
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_early_data</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 仅启用 TLSv1.3
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_protocols</span> <span class="s">TLSv1.3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_ciphers</span> <span class="s">HIGH:!aNULL:!MD5</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_prefer_server_ciphers</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 添加 Alt-Svc 头，向客户端声明可升级到 HTTP/3
</span></span></span><span class="line"><span class="cl">    <span class="c1"># Alt-Svc 头告诉浏览器&#34;该资源在同一主机的 UDP/443 上能用 h3 协议&#34;，浏览器会自动重试
</span></span></span><span class="line"><span class="cl">    <span class="c1"># ma max-age 缓存时间，86400 秒 = 1 天
</span></span></span><span class="line"><span class="cl">    <span class="c1"># always 关键字保证即使发生 4xx/5xx 也发送 Alt-Svc，便于调试
</span></span></span><span class="line"><span class="cl">    <span class="kn">add_header</span> <span class="s">alt-svc</span> <span class="s">&#39;h3=&#34;:443&#34;</span><span class="p">;</span> <span class="kn">ma=86400&#39;</span> <span class="s">always</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">root</span> <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">index</span>  <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># docker rm -f nginx-ssl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 添加监听 UDP 端口</span>
</span></span><span class="line"><span class="cl">docker run -d --name nginx-ssl <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 443:443 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 443:443/udp <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./default.conf:/etc/nginx/conf.d/default.conf:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./localhost.pem:/etc/ssl/certs/localhost.pem:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./localhost-key.pem:/etc/ssl/certs/localhost-key.pem:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./html/:/usr/share/nginx/html/:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  nginx:alpine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># docker container restart nginx-ssl</span>
</span></span></code></pre></div><h2 id="测试">测试</h2>
<h3 id="curl">curl</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 使用支持 HTTP/3 的 curl</span>
</span></span><span class="line"><span class="cl"><span class="c1"># docker run -ti --rm alpine/curl-http3 curl -V</span>
</span></span><span class="line"><span class="cl">docker run -ti --network host --rm alpine/curl-http3 curl -I --http3 https://localhost/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HTTP/3 <span class="m">200</span>
</span></span><span class="line"><span class="cl">server: nginx/1.29.0
</span></span><span class="line"><span class="cl">date: Fri, <span class="m">27</span> Jun <span class="m">2025</span> 03:26:09 GMT
</span></span><span class="line"><span class="cl">content-type: text/html
</span></span><span class="line"><span class="cl">content-length: <span class="m">656</span>
</span></span><span class="line"><span class="cl">last-modified: Fri, <span class="m">27</span> Jun <span class="m">2025</span> 03:17:07 GMT
</span></span><span class="line"><span class="cl">etag: <span class="s2">&#34;685e0d33-290&#34;</span>
</span></span><span class="line"><span class="cl">alt-svc: <span class="nv">h3</span><span class="o">=</span><span class="s2">&#34;:443&#34;</span><span class="p">;</span> <span class="nv">ma</span><span class="o">=</span><span class="m">86400</span>
</span></span><span class="line"><span class="cl">accept-ranges: bytes
</span></span></code></pre></div><h3 id="浏览器">浏览器</h3>
<p>无法验证成功！分析原因：</p>
<ol>
<li>之前 UDP 使用的是 8443，当将相关配置都改为 443 仍然在 Chrome Network 里无法看见 h3。</li>
<li>可能 Chrome 对 localhost 或者域名解析到 127.0.0.1 的访问阻止了 HTTP/3。</li>
</ol>
<p>为了验证第 2 种情况，通过 AWS EC2 + 自己的域名 + Let&rsquo;s Encrypt 颁发的证书，再仿照本地配置的方法启用 HTTP/3。最终在 Chrome 中成功验证 HTTP/3。</p>
<p>使用 Chrome 插件 <a href="https://chromewebstore.google.com/detail/http-indicator/hgcomhbcacfkpffiphlmnlhpppcjgmbl?hl=en-US">HTTP Indicator</a>：</p>
<ul>
<li>HTTP/1.1 显示为灰色</li>
<li>HTTP/2 显示为蓝色</li>
<li>HTTP/3 显示为绿色</li>
</ul>
<p>如何配置 AWS EC2 + 自己的域名 + Let&rsquo;s Encrypt 颁发的证书，参考：</p>
<ul>
<li><a href="/2018/04/26/lets-encrypt-wildcard-certificates/">使用 Certbot 获取 Let&rsquo;s Encrypt 颁发的 TLS 证书 ｜ ZYF.IM</a></li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://nginx.org/en/docs/http/ngx_http_v3_module.html">NGINX HTTP/3 模块</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>在本地环境使用 HTTPS</title>
      <link>https://zyf.im/2025/06/26/use-https-in-local-environment/</link>
      <pubDate>Thu, 26 Jun 2025 12:00:00 +0000</pubDate>
      <guid>https://zyf.im/2025/06/26/use-https-in-local-environment/</guid>
      <description>&lt;p&gt;公网环境使用 HTTPS 参考：&lt;a href=&#34;https://zyf.im/2018/04/26/lets-encrypt-wildcard-certificates/&#34;&gt;使用 Certbot 获取 Let&amp;rsquo;s Encrypt 颁发的 TLS 证书 ｜ ZYF.IM&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;
&lt;p&gt;HTTPS（HyperText Transfer Protocol Secure）是在 HTTP 之上通过 TLS/SSL 实现加密的安全通信协议，用来保护浏览器与服务器之间的数据机密性、完整性与身份可信性。&lt;/p&gt;
&lt;h2 id=&#34;开始实验&#34;&gt;开始实验&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/FiloSottile/mkcert&#34;&gt;mkcert&lt;/a&gt; 会在本地生成一套根 CA（包含根证书和私钥），并把根证书导入系统/浏览器的信任列表。随后，它用这套 CA 的私钥为 localhost 等域名签发服务器证书；因为根证书已被信任，浏览器访问时就会把服务器证书视为可信。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew install mkcert
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mkcert -install
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mkcert localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# The certificate is at &amp;#34;./localhost.pem&amp;#34; and the key at &amp;#34;./localhost-key.pem&amp;#34; ✅&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# vi default.conf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;server&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;listen&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;443&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;ssl&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;server_name&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;localhost&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;ssl_certificate&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/etc/ssl/certs/localhost.pem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;ssl_certificate_key&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/etc/ssl/certs/localhost-key.pem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# 只允许 TLSv1.2 和 TLSv1.3 协议，显式禁止 TLSv1.0 和 TLSv1.1 协议
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;ssl_protocols&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;TLSv1.2&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;TLSv1.3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# 只保留高强度、带身份验证、且不使用 MD5 的 TLS 1.2（及以下）套件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;ssl_ciphers&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;HIGH:!aNULL:!MD5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# 优先使用服务器端加密套件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;location&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kn&#34;&gt;root&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kn&#34;&gt;index&lt;/span&gt;  &lt;span class=&#34;s&#34;&gt;index.html&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;index.htm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker run -d --name nginx-ssl &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -p 8443:443 &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -v ./default.conf:/etc/nginx/conf.d/default.conf:ro &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -v ./localhost.pem:/etc/ssl/certs/localhost.pem:ro &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -v ./localhost-key.pem:/etc/ssl/certs/localhost-key.pem:ro &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  nginx:alpine
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# docker rm -f nginx-ssl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# docker container restart nginx-ssl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;
&lt;h3 id=&#34;浏览器&#34;&gt;浏览器&lt;/h3&gt;
&lt;p&gt;浏览器访问 &lt;code&gt;https://localhost:8443/&lt;/code&gt;，如果证书是绿色的，说明成功了。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>公网环境使用 HTTPS 参考：<a href="/2018/04/26/lets-encrypt-wildcard-certificates/">使用 Certbot 获取 Let&rsquo;s Encrypt 颁发的 TLS 证书 ｜ ZYF.IM</a>。</p>
<h2 id="简介">简介</h2>
<p>HTTPS（HyperText Transfer Protocol Secure）是在 HTTP 之上通过 TLS/SSL 实现加密的安全通信协议，用来保护浏览器与服务器之间的数据机密性、完整性与身份可信性。</p>
<h2 id="开始实验">开始实验</h2>
<p><a href="https://github.com/FiloSottile/mkcert">mkcert</a> 会在本地生成一套根 CA（包含根证书和私钥），并把根证书导入系统/浏览器的信任列表。随后，它用这套 CA 的私钥为 localhost 等域名签发服务器证书；因为根证书已被信任，浏览器访问时就会把服务器证书视为可信。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install mkcert
</span></span><span class="line"><span class="cl">mkcert -install
</span></span><span class="line"><span class="cl">mkcert localhost
</span></span><span class="line"><span class="cl"><span class="c1"># The certificate is at &#34;./localhost.pem&#34; and the key at &#34;./localhost-key.pem&#34; ✅</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># vi default.conf
</span></span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">localhost</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span> <span class="s">/etc/ssl/certs/localhost.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/ssl/certs/localhost-key.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 只允许 TLSv1.2 和 TLSv1.3 协议，显式禁止 TLSv1.0 和 TLSv1.1 协议
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_protocols</span> <span class="s">TLSv1.2</span> <span class="s">TLSv1.3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 只保留高强度、带身份验证、且不使用 MD5 的 TLS 1.2（及以下）套件
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_ciphers</span> <span class="s">HIGH:!aNULL:!MD5</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 优先使用服务器端加密套件
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_prefer_server_ciphers</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">root</span> <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">index</span>  <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d --name nginx-ssl <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 8443:443 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./default.conf:/etc/nginx/conf.d/default.conf:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./localhost.pem:/etc/ssl/certs/localhost.pem:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v ./localhost-key.pem:/etc/ssl/certs/localhost-key.pem:ro <span class="se">\
</span></span></span><span class="line"><span class="cl">  nginx:alpine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># docker rm -f nginx-ssl</span>
</span></span><span class="line"><span class="cl"><span class="c1"># docker container restart nginx-ssl</span>
</span></span></code></pre></div><h2 id="测试">测试</h2>
<h3 id="浏览器">浏览器</h3>
<p>浏览器访问 <code>https://localhost:8443/</code>，如果证书是绿色的，说明成功了。</p>
<h3 id="curl">curl</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -I https://localhost:8443/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HTTP/1.1 <span class="m">200</span> OK
</span></span><span class="line"><span class="cl">Server: nginx/1.29.0
</span></span><span class="line"><span class="cl">Date: Fri, <span class="m">27</span> Jun <span class="m">2025</span> 01:54:36 GMT
</span></span><span class="line"><span class="cl">Content-Type: text/html
</span></span><span class="line"><span class="cl">Content-Length: <span class="m">615</span>
</span></span><span class="line"><span class="cl">Last-Modified: Tue, <span class="m">24</span> Jun <span class="m">2025</span> 17:57:38 GMT
</span></span><span class="line"><span class="cl">Connection: keep-alive
</span></span><span class="line"><span class="cl">ETag: <span class="s2">&#34;685ae712-267&#34;</span>
</span></span><span class="line"><span class="cl">Accept-Ranges: bytes
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># -v 显示详细信息，查看 TLS 版本</span>
</span></span><span class="line"><span class="cl">curl -I https://localhost:8443/ -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384 / <span class="o">[</span>blank<span class="o">]</span> / UNDEF
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><h2 id="深入探索">深入探索</h2>
<h3 id="http-的三大安全缺陷">HTTP 的三大安全缺陷</h3>
<blockquote>
<p>网络早已布满监听、篡改与伪装。</p>
</blockquote>
<ul>
<li><strong>明文传输</strong>：任何中间节点（Wi-Fi 热点、ISP、CDN、政府网关）都能直接读取用户名、密码、Cookie、表单内容等敏感信息。</li>
<li><strong>缺少完整性保护</strong>：中间人可随意插入、删除或修改页面内容（广告注入、恶意脚本、暗链植入），客户端亦无法察觉。</li>
<li><strong>无法验证身份</strong>：客户端无法确认自己连接的是&quot;真正的 server&quot;，攻击者可通过 DNS 欺骗、ARP 攻击或流量劫持将流量导向钓鱼站点。</li>
</ul>
<h3 id="为什么需要-https">为什么需要 HTTPS</h3>
<blockquote>
<p>TLS 通过&quot;加密 + 身份认证 + 完整性校验&quot;应对上述风险。</p>
</blockquote>
<ul>
<li>加密（Confidentiality）：对称加密保护数据内容；密钥通过非对称算法安全协商。</li>
<li>数据完整性（Integrity）：MAC（消息鉴别码）或 AEAD 算法确保内容未被篡改。</li>
<li>身份认证（Authentication）：服务器出示数字证书；浏览器验证证书链与域名匹配。</li>
</ul>
<h3 id="mac-vs-aead">MAC vs AEAD</h3>
<ul>
<li>MAC 提供数据完整性与对称实体认证，需要与独立的加密步骤结合来保障机密性。
<ul>
<li>Message Authentication Code</li>
<li>只负责&quot;认证&quot;，不加密数据；与加密算法解耦。典型实现是 HMAC-SHA1/HMAC-SHA256。</li>
</ul>
</li>
<li>AEAD 将&quot;加密 + 认证&quot;合二为一，接口更简单、实现更安全、性能更佳，是现代协议的首选。
<ul>
<li>Authenticated Encryption with Associated Data</li>
<li>加密数据 + 认证数据</li>
<li>典型实现是 AES-GCM/ChaCha20-Poly1305</li>
</ul>
</li>
<li>在 TLS/QUIC/Noise 等现代协议中，AEAD 已全面取代传统的&quot;MAC-then-Encrypt&quot;。</li>
</ul>
<h3 id="证书类型">证书类型</h3>
<p>按层级角色：</p>
<ul>
<li>根证书（Root CA）：最高级别，Subject = Issuer 自签名，预装在操作系统/浏览器的&quot;根信任库&quot;内，用于验证下级证书。</li>
<li>中级证书（Intermediate CA）：由根证书签名，用于验证下级证书，降低根密钥泄露风险。</li>
<li>最终实体证书（End Entity Certificate）：由中级证书签名，绑定具体域名或主体，用于验证服务器身份。</li>
</ul>
<p>按验证级别：</p>
<ul>
<li>DV（Domain Validation）证书仅验证域名所有权，通过自动化流程颁发，价格低廉甚至免费（如 Let&rsquo;s Encrypt）。</li>
<li>OV（Organization Validation）证书需要验证组织实体信息，在浏览器证书详情中可查看公司名称。</li>
<li>EV（Extended Validation）证书验证最为严格，地址栏会显示公司名称，但多数现代浏览器已弱化此功能的 UI 展示。</li>
</ul>
<h3 id="证书链">证书链</h3>
<p>在 HTTPS、TLS、S/MIME 等基于 X.509 的公钥基础设施（PKI）中，任何一张服务器或用户证书都必须被&quot;信任根（Root CA）&ldquo;间接或直接签名。从根证书一路向下到最终实体证书所形成的&quot;逐级签名&quot;路径，就称为证书链，也叫信任链（Chain of Trust）。</p>
<h3 id="https-在网络堆栈中的位置">HTTPS 在网络堆栈中的位置</h3>
<ol>
<li>HTTP/HTTPS 仍是应用层协议。</li>
<li>TLS（Transport Layer Security）位于 HTTP 与 TCP 之间；它把上层的明文字节（HTTP 报文）加密后再交给 TCP。</li>
<li>TCP 提供可靠的字节流；IP 负责寻址和路由；最底层是物理/链路层。</li>
<li>HTTPS = HTTP over TLS over TCP over IP。</li>
</ol>
<table>
	<thead>
			<tr>
					<th>协议</th>
					<th>主要职责</th>
					<th>典型端口</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>HTTP</td>
					<td>应用语义：URL、方法、头、状态码…</td>
					<td>80（无加密）</td>
			</tr>
			<tr>
					<td>TLS</td>
					<td>机密性、完整性、身份认证</td>
					<td>443（惯例）</td>
			</tr>
			<tr>
					<td>TCP</td>
					<td>可靠传输、流量控制、重传</td>
					<td>任意（443）</td>
			</tr>
			<tr>
					<td>IP</td>
					<td>路由、分片、寻址</td>
					<td>—</td>
			</tr>
	</tbody>
</table>
<ol>
<li>HTTPS ≠ 新协议。它只是&quot;HTTP 报文透过 TLS 隧道运输&quot;的俗称。协议栈中依然能看到标准的 HTTP 语法，只是先被 TLS 加密，运输途中看不到明文。</li>
<li>TLS 与 TCP 的独立性。TLS 不关心丢包、重传；这些由 TCP 处理。相反，TCP 不理解证书、握手；这些由 TLS 负责。</li>
</ol>
<h3 id="tls-握手过程">TLS 握手过程</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">客户端                           服务器
</span></span><span class="line"><span class="cl">   |&lt;--- SYN → SYN/ACK → ACK ---&gt;| 建立可靠的 TCP 连接
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   |--- ClientHello (TLS)    ---&gt;| 浏览器发起 TLS 握手，告诉服务器自己支持的协议版本、密码套件、随机数（Client Random）等信息
</span></span><span class="line"><span class="cl">   |&lt;-- ServerHello + 证书     ---|
</span></span><span class="line"><span class="cl">   |--- 密钥协商/Finished -------&gt;|
</span></span><span class="line"><span class="cl">   |&lt;-- Finished ----------------|
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   TLS 信道建立完成
</span></span><span class="line"><span class="cl">   |--- HTTP GET / -------------&gt;|
</span></span><span class="line"><span class="cl">   |&lt;-- HTTP/1.1 200 OK ---------|
</span></span></code></pre></div><pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">sequenceDiagram
  participant Client
  participant Server
  Client-&gt;&gt;Server: ClientHello&lt;br/&gt;– 支持的 TLS 版本、随机数、加密套件
  Server--&gt;&gt;Client: ServerHello&lt;br/&gt;– 选定版本/套件、随机数
  Server--&gt;&gt;Client: Certificate
  Server--&gt;&gt;Client: ServerHelloDone
  Client-&gt;&gt;Server: ClientKeyExchange&lt;br/&gt;– 预主密钥（用公钥加密）
  Client-&gt;&gt;Server: ChangeCipherSpec
  Client-&gt;&gt;Server: Finished（用对称密钥加密）
  Server--&gt;&gt;Client: ChangeCipherSpec
  Server--&gt;&gt;Client: Finished
  note over Client,Server: 之后开始对称加密的数据传输
</code></pre><ul>
<li>双方首先协商算法与随机数</li>
<li>客户端用服务器公钥加密预主密钥</li>
<li>双方基于随机数与预主密钥生成会话密钥</li>
<li>后续通信全部使用对称加密，提高效率</li>
</ul>
<h3 id="tls-13">TLS 1.3</h3>
<p>TLS 1.3（RFC 8446）是在 2018 年 8 月正式发布的最新版本，相比 TLS 1.2 做了深度&quot;瘦身 + 加速 + 强化安全&rdquo;。</p>
<ul>
<li>TLS 1.3 把握手变成 1-RTT 并引入 0-RTT，显著降低延迟。</li>
<li>强制 ECDHE + AEAD + SHA-2，淘汰所有过时与高危算法，默认提供前向保密。</li>
<li>握手更多字段被加密，降低被动监听价值，同时带来运维与抓包新挑战。</li>
<li>配置侧仅需保留少量推荐套件（TLS_AES_128_GCM_SHA256、TLS_CHACHA20_POLY1305_SHA256 等），比 TLS 1.2 更易运维。</li>
</ul>
<h3 id="rtt-vs-0-rtt">RTT vs. 0-RTT</h3>
<p>&ldquo;RTT&quot;本身确实是一个时间量（Round-Trip Time），指数据包从客户端发出、到达服务端，再把响应返回客户端所经历的往返延迟，常用毫秒（ms）表示。</p>
<p>在协议说明里看到的&quot;1-RTT 握手 / 2-RTT 握手&quot;属于约定俗成的缩写：把&quot;一次往返时间&quot;抽象成&quot;一次来回（round trip）&ldquo;这个计数单位来用。</p>
<h3 id="证书与密钥">证书与密钥</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">openssl version
</span></span><span class="line"><span class="cl"><span class="c1"># 看看证书</span>
</span></span><span class="line"><span class="cl">openssl x509 -in localhost.pem -noout -text
</span></span></code></pre></div><p>证书的结构：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Certificate:
</span></span><span class="line"><span class="cl">｜- Data:
</span></span><span class="line"><span class="cl">  ｜- Version: 3
</span></span><span class="line"><span class="cl">  ｜- Serial Number: ...
</span></span><span class="line"><span class="cl">  ｜- Signature Algorithm: sha256WithRSAEncryption
</span></span><span class="line"><span class="cl">  ｜- Issuer: O=mkcert development CA, OU=..., CN=mkcert ...
</span></span><span class="line"><span class="cl">  ｜- Validity:
</span></span><span class="line"><span class="cl">    ｜- Not Before: Jun 27 03:39:39 2025 GMT
</span></span><span class="line"><span class="cl">    ｜- Not After : Sep 27 03:39:39 2027 GMT
</span></span><span class="line"><span class="cl">  ｜- Subject: O=mkcert development CA, OU=...
</span></span><span class="line"><span class="cl">  ｜- Subject Public Key Info: &lt;网站的公钥&gt;
</span></span><span class="line"><span class="cl">    ｜- Public Key Algorithm: rsaEncryption
</span></span><span class="line"><span class="cl">      ｜- Public-Key: (2048 bit)
</span></span><span class="line"><span class="cl">      ｜- Modulus: &lt;模数&gt;
</span></span><span class="line"><span class="cl">      ｜- Exponent: 65537 (0x10001)
</span></span><span class="line"><span class="cl">   |- X509v3 extensions
</span></span><span class="line"><span class="cl">｜- Signature Algorithm: sha256WithRSAEncryption
</span></span><span class="line"><span class="cl">｜- Signature Value: &lt;颁发机构（CA）用自己私钥算出的数字签名，浏览器用 CA 公钥验证&gt;
</span></span></code></pre></div><p>在 RSA 体系里，公钥由两部分组成：</p>
<ul>
<li>n：模数（Modulus）</li>
<li>e：公钥指数（Exponent）</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 导出公钥</span>
</span></span><span class="line"><span class="cl">openssl x509 -in localhost.pem -pubkey -noout &gt; pubkey.pem
</span></span><span class="line"><span class="cl"><span class="c1"># 查看导出的公钥</span>
</span></span><span class="line"><span class="cl">openssl pkey -pubin -in pubkey.pem -text -noout
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 看看私钥里有什么</span>
</span></span><span class="line"><span class="cl">openssl rsa -in localhost-key.pem -text -noout
</span></span></code></pre></div><p>私钥的结构：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Private-Key: (2048 bit, 2 primes)
</span></span><span class="line"><span class="cl"># 模 n = p × q
</span></span><span class="line"><span class="cl">modulus: ...
</span></span><span class="line"><span class="cl"># e 公钥指数
</span></span><span class="line"><span class="cl">publicExponent: 65537 (0x10001)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># m = c^d mod n
</span></span><span class="line"><span class="cl">privateExponent: ...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># p 和 q 是两个大质数
</span></span><span class="line"><span class="cl">prime1: ...
</span></span><span class="line"><span class="cl">prime2: ...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># exponent1 = d mod (p-1)
</span></span><span class="line"><span class="cl">exponent1: ...
</span></span><span class="line"><span class="cl"># exponent2 = d mod (q-1)
</span></span><span class="line"><span class="cl">exponent2: ...
</span></span><span class="line"><span class="cl"># coefficient = inv(q) mod p
</span></span><span class="line"><span class="cl">coefficient: ...
</span></span></code></pre></div><h3 id="http2">HTTP/2</h3>
<p>当前使用的是 HTTP/1.1，如何启用 HTTP/2，请移步 <a href="/2017/06/06/nginx-enable-http2/">NGINX 启用 HTTP/2 ｜ ZYF.IM</a>。</p>
<h3 id="http3">HTTP/3</h3>
<p>请移步 <a href="/2025/06/27/nginx-enable-http3/">NGINX 启用 HTTP/3 ｜ ZYF.IM</a>。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://nginx.org/en/docs/http/ngx_http_ssl_module.html">NGINX HTTPS 模块</a></li>
<li><a href="http://httpforever.com/">http://httpforever.com/</a></li>
<li><a href="http://neverssl.com/">http://neverssl.com/</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>MongoDB 高手课</title>
      <link>https://zyf.im/2023/04/19/mongodb-course/</link>
      <pubDate>Wed, 19 Apr 2023 10:47:10 +0000</pubDate>
      <guid>https://zyf.im/2023/04/19/mongodb-course/</guid>
      <description>&lt;h2 id=&#34;04-特色及优势&#34;&gt;04 特色及优势&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;能力&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;数据模型&lt;/td&gt;
					&lt;td&gt;多形性（同集合不同字段）、动态性（在线改模式）、JSONSchema 治理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;开发体验&lt;/td&gt;
					&lt;td&gt;单存储区读写、反范式 + 无关联优化、API 自然&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用&lt;/td&gt;
					&lt;td&gt;Replica Set（2~50 节点，建议奇数）、自恢复、多中心容灾、滚动维护&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;横向扩展&lt;/td&gt;
					&lt;td&gt;无缝扩容、应用透明、多种数据分布策略，可达 TB-PB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;06-基本操作&#34;&gt;06 基本操作&lt;/h2&gt;
&lt;p&gt;环境与样本数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://cloud.mongodb.com/v2/&#34;&gt;cloud.mongodb.com 云服务&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/tapdata/geektime-mongodb-course/blob/master/aggregation/dump.tar.gz&#34;&gt;样本数据 dump.tar.gz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;tar -xvf dump.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mongorestore --uri&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;mongodb://root:root@10.130.0.12/?&amp;amp;authMechanism=SCRAM-SHA-1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;crud&#34;&gt;CRUD&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 插入
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fruit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;insertOne&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;apple&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fruit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;insertMany&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;apple&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;pear&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;orange&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 查询
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;fmiller&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Elizabeth Ray&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;/^f/&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;$or&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;/^f/&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;/^E/&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 投影（projection）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;/^f/&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;   &lt;span class=&#34;c1&#34;&gt;// 排除
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;find&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;/^f/&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;     &lt;span class=&#34;c1&#34;&gt;// 仅返回
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 删除
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;remove&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;abrown&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 更新
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;updateOne&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;fmiller&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;$set&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;China&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;customers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;updateMany&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;fmiller&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;$set&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;China&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 表 / 库管理
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fruit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;drop&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;show&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;collections&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dropDatabase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;show&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;dbs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;sql--mongo-操作符对照&#34;&gt;SQL ↔ Mongo 操作符对照&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;SQL&lt;/th&gt;
					&lt;th&gt;Mongo&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;a &amp;lt;&amp;gt; 1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{a: {$ne: 1}}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;a &amp;gt; 1&lt;/code&gt; / &lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{a: {$gt: 1}}&lt;/code&gt; / &lt;code&gt;{$gte: 1}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;a &amp;lt; 1&lt;/code&gt; / &lt;code&gt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{a: {$lt: 1}}&lt;/code&gt; / &lt;code&gt;{$lte: 1}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;a = 1 OR b = 1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{$or: [{a: 1}, {b: 1}]}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;a IS NULL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{a: {$exists: false}}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;a IN (1, 2, 3)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{a: {$in: [1, 2, 3]}}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;同时满足子文档条件&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;{$elemMatch: {city: &amp;quot;Rome&amp;quot;, country: &amp;quot;USA&amp;quot;}}&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;常用更新操作符&#34;&gt;常用更新操作符&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;操作&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$set&lt;/code&gt; / &lt;code&gt;$unset&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;设置 / 移除字段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$push&lt;/code&gt; / &lt;code&gt;$pop&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;数组追加 / 弹出&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$pull&lt;/code&gt; / &lt;code&gt;$pullAll&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;按匹配从数组中删除&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;$addToSet&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;不存在则添加，去重&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;08-聚合查询&#34;&gt;08 聚合查询&lt;/h2&gt;
&lt;p&gt;Aggregation Framework:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="04-特色及优势">04 特色及优势</h2>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>能力</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>数据模型</td>
					<td>多形性（同集合不同字段）、动态性（在线改模式）、JSONSchema 治理</td>
			</tr>
			<tr>
					<td>开发体验</td>
					<td>单存储区读写、反范式 + 无关联优化、API 自然</td>
			</tr>
			<tr>
					<td>高可用</td>
					<td>Replica Set（2~50 节点，建议奇数）、自恢复、多中心容灾、滚动维护</td>
			</tr>
			<tr>
					<td>横向扩展</td>
					<td>无缝扩容、应用透明、多种数据分布策略，可达 TB-PB</td>
			</tr>
	</tbody>
</table>
<h2 id="06-基本操作">06 基本操作</h2>
<p>环境与样本数据：</p>
<ul>
<li><a href="https://cloud.mongodb.com/v2/">cloud.mongodb.com 云服务</a></li>
<li><a href="https://github.com/tapdata/geektime-mongodb-course/blob/master/aggregation/dump.tar.gz">样本数据 dump.tar.gz</a></li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tar -xvf dump.tar.gz
</span></span><span class="line"><span class="cl">mongorestore --uri<span class="o">=</span><span class="s2">&#34;mongodb://root:root@10.130.0.12/?&amp;authMechanism=SCRAM-SHA-1&#34;</span>
</span></span></code></pre></div><h3 id="crud">CRUD</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// 插入
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">fruit</span><span class="p">.</span><span class="nx">insertOne</span><span class="p">({</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;apple&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">fruit</span><span class="p">.</span><span class="nx">insertMany</span><span class="p">([{</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;apple&#34;</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;pear&#34;</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;orange&#34;</span> <span class="p">}])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 查询
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="s2">&#34;fmiller&#34;</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Elizabeth Ray&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="sr">/^f/</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span> <span class="nx">$or</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">username</span><span class="o">:</span> <span class="sr">/^f/</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="sr">/^E/</span> <span class="p">}]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 投影（projection）
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="sr">/^f/</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">email</span><span class="o">:</span> <span class="mi">0</span> <span class="p">})</span>   <span class="c1">// 排除
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="sr">/^f/</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="mi">1</span> <span class="p">})</span>     <span class="c1">// 仅返回
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 删除
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">remove</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="s2">&#34;abrown&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 更新
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">updateOne</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="s2">&#34;fmiller&#34;</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;China&#34;</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">customers</span><span class="p">.</span><span class="nx">updateMany</span><span class="p">({</span> <span class="nx">username</span><span class="o">:</span> <span class="s2">&#34;fmiller&#34;</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;China&#34;</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 表 / 库管理
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">fruit</span><span class="p">.</span><span class="nx">drop</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nx">show</span> <span class="nx">collections</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">dropDatabase</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nx">show</span> <span class="nx">dbs</span>
</span></span></code></pre></div><h3 id="sql--mongo-操作符对照">SQL ↔ Mongo 操作符对照</h3>
<table>
	<thead>
			<tr>
					<th>SQL</th>
					<th>Mongo</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>a &lt;&gt; 1</code></td>
					<td><code>{a: {$ne: 1}}</code></td>
			</tr>
			<tr>
					<td><code>a &gt; 1</code> / <code>&gt;=</code></td>
					<td><code>{a: {$gt: 1}}</code> / <code>{$gte: 1}</code></td>
			</tr>
			<tr>
					<td><code>a &lt; 1</code> / <code>&lt;=</code></td>
					<td><code>{a: {$lt: 1}}</code> / <code>{$lte: 1}</code></td>
			</tr>
			<tr>
					<td><code>a = 1 OR b = 1</code></td>
					<td><code>{$or: [{a: 1}, {b: 1}]}</code></td>
			</tr>
			<tr>
					<td><code>a IS NULL</code></td>
					<td><code>{a: {$exists: false}}</code></td>
			</tr>
			<tr>
					<td><code>a IN (1, 2, 3)</code></td>
					<td><code>{a: {$in: [1, 2, 3]}}</code></td>
			</tr>
			<tr>
					<td>同时满足子文档条件</td>
					<td><code>{$elemMatch: {city: &quot;Rome&quot;, country: &quot;USA&quot;}}</code></td>
			</tr>
	</tbody>
</table>
<h3 id="常用更新操作符">常用更新操作符</h3>
<table>
	<thead>
			<tr>
					<th>操作</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>$set</code> / <code>$unset</code></td>
					<td>设置 / 移除字段</td>
			</tr>
			<tr>
					<td><code>$push</code> / <code>$pop</code></td>
					<td>数组追加 / 弹出</td>
			</tr>
			<tr>
					<td><code>$pull</code> / <code>$pullAll</code></td>
					<td>按匹配从数组中删除</td>
			</tr>
			<tr>
					<td><code>$addToSet</code></td>
					<td>不存在则添加，去重</td>
			</tr>
	</tbody>
</table>
<h2 id="08-聚合查询">08 聚合查询</h2>
<p>Aggregation Framework:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">pipeline</span> <span class="o">=</span> <span class="p">[</span><span class="nx">stage1</span><span class="p">,</span> <span class="nx">stage2</span><span class="p">,</span> <span class="p">...]</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="o">&lt;</span><span class="nx">collection</span><span class="o">&gt;</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">(</span><span class="nx">pipeline</span><span class="p">,</span> <span class="p">{</span> <span class="nx">option</span> <span class="p">})</span>
</span></span></code></pre></div><p>常用 stage：</p>
<table>
	<thead>
			<tr>
					<th>Stage</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>$match</code></td>
					<td>过滤</td>
			</tr>
			<tr>
					<td><code>$project</code></td>
					<td>投影</td>
			</tr>
			<tr>
					<td><code>$sort</code></td>
					<td>排序</td>
			</tr>
			<tr>
					<td><code>$group</code></td>
					<td>分组</td>
			</tr>
			<tr>
					<td><code>$skip</code> / <code>$limit</code></td>
					<td>分页</td>
			</tr>
			<tr>
					<td><code>$lookup</code></td>
					<td>左外连接</td>
			</tr>
	</tbody>
</table>
<h2 id="09-聚合查询实验">09 聚合查询实验</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// 全集合总计
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nx">$group</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="kc">null</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$sum</span><span class="o">:</span> <span class="s2">&#34;$total&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { _id: null, total: NumberDecimal(&#34;44019609&#34;) }
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 2019 Q1 已完成订单总金额（订单金额 + 运费）和订单数
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">status</span><span class="o">:</span> <span class="s2">&#34;completed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">orderDate</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$gte</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s2">&#34;2019-01-01&#34;</span><span class="p">),</span> <span class="nx">$lt</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s2">&#34;2019-04-01&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">$group</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">_id</span><span class="o">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">total</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$sum</span><span class="o">:</span> <span class="s2">&#34;$total&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="nx">shippingFee</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$sum</span><span class="o">:</span> <span class="s2">&#34;$shippingFee&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="nx">count</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$sum</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">$project</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">grandTotal</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$add</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;$total&#34;</span><span class="p">,</span> <span class="s2">&#34;$shippingFee&#34;</span><span class="p">]</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="nx">count</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nx">_id</span><span class="o">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { count: 5875, grandTotal: NumberDecimal(&#34;2636376&#34;) }
</span></span></span></code></pre></div><h2 id="10-复制集机制及原理">10 复制集机制及原理</h2>
<h3 id="角色">角色</h3>
<table>
	<thead>
			<tr>
					<th>节点</th>
					<th>数量</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>PRIMARY</td>
					<td>1</td>
					<td>接收写入、参与选举投票</td>
			</tr>
			<tr>
					<td>SECONDARY</td>
					<td>2+（建议奇数）</td>
					<td>复制主节点新数据、参与投票</td>
			</tr>
	</tbody>
</table>
<p>集群上限 50 节点，<strong>有投票权的节点最多 7 个</strong>。</p>
<h3 id="数据复制">数据复制</h3>
<ul>
<li>主节点把写操作（经必要转换）写入 <strong>oplog</strong>。</li>
<li>从节点通过在主节点上打开 <strong>tailable cursor</strong> 不断拉 oplog，本地回放。</li>
</ul>
<h3 id="故障恢复选举">故障恢复（选举）</h3>
<ul>
<li>有投票权的节点两两心跳。</li>
<li>连续 5 次心跳未收到判定失联。</li>
<li>主节点失联 → 触发选举；从节点失联 → 不选举。</li>
<li>基于 <strong>RAFT</strong> 一致性算法，需 <strong>多数投票节点存活</strong>。</li>
</ul>
<h3 id="当选条件">当选条件</h3>
<ul>
<li>与多数节点建立连接。</li>
<li>oplog 足够新。</li>
<li>优先级足够高（若有配置）。</li>
</ul>
<h3 id="节点选配项">节点选配项</h3>
<table>
	<thead>
			<tr>
					<th>参数</th>
					<th>作用</th>
					<th>注意</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>v</code></td>
					<td>是否具有投票权</td>
					<td></td>
			</tr>
			<tr>
					<td><code>priority</code></td>
					<td>优先级（越高越优先成为 primary）</td>
					<td><code>priority = 0</code> 永不成为主节点</td>
			</tr>
			<tr>
					<td><code>hidden</code></td>
					<td>复制数据但对应用不可见</td>
					<td>可投票，但 <code>priority</code> 必须为 0</td>
			</tr>
			<tr>
					<td><code>slaveDelay</code></td>
					<td>复制 N 秒前的数据（延迟节点）</td>
					<td></td>
			</tr>
	</tbody>
</table>
<h3 id="注意事项">注意事项</h3>
<ul>
<li><strong>硬件</strong>：节点配置必须一致（地位对等）；不同节点的硬件需相互独立（避免同时宕）。</li>
<li><strong>软件</strong>：所有节点版本必须一致。</li>
<li>增加节点 <strong>不会</strong> 提升写性能。</li>
</ul>
<h2 id="11-搭建-mongodb-复制集">11 搭建 MongoDB 复制集</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p runtime/data_db<span class="o">{</span>1,2,3<span class="o">}</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="o">&amp;&amp;</span> mkdir -p runtime/data_configdb<span class="o">{</span>1,2,3<span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">hostname -f
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">rs.status<span class="o">()</span>
</span></span></code></pre></div><h2 id="12-全家桶">12 全家桶</h2>
<ul>
<li><a href="https://www.mongodb.com/docs/launch-manage/">launch-manage | mongodb</a></li>
<li><a href="https://www.mongodb.com/docs/database-tools/">database-tools | mongodb</a></li>
</ul>
<h2 id="13-模型设计基础">13 模型设计基础</h2>
<p>三要素：<strong>Entity（实体）/ Attribute（属性）/ Relationship（关系）</strong>。</p>
<p>三层模型映射：</p>
<table>
	<thead>
			<tr>
					<th>层次</th>
					<th>概念</th>
					<th>对应内容</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>概念 CDM</td>
					<td>业务对象</td>
					<td>对象</td>
			</tr>
			<tr>
					<td>逻辑 LDM</td>
					<td>实体 / 属性 / 关系</td>
					<td>实体、属性、关系</td>
			</tr>
			<tr>
					<td>物理 PDM</td>
					<td>数据库结构</td>
					<td>表结构、字段列表、主外键</td>
			</tr>
	</tbody>
</table>
<h2 id="14-json-文档模型设计">14 JSON 文档模型设计</h2>
<p>无模式：物理建模过程可省。</p>
<p>设计原则：</p>
<ul>
<li><strong>Performance</strong> 性能</li>
<li><strong>Ease of Development</strong> 开发易用</li>
</ul>
<h2 id="15-基础设计">15 基础设计</h2>
<p>流程：业务需求 / 逻辑模型 ──逻辑导向──→ <strong>基础建模</strong> → 集合 / 字段 / 基础形状 → 引用及关联 → 最终模式。</p>
<p>约束：</p>
<ul>
<li>单文档最大 <strong>16 MB</strong>。</li>
<li>默认 <strong>内嵌为主</strong>。</li>
</ul>
<h2 id="16-工况细化">16 工况细化</h2>
<p>流程：技术需求 / 读写比例 ──技术导向──→ <strong>工况细化</strong> → 引用及关联。</p>
<p>引用模式 <code>$lookup</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">contacts</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="nx">$lookup</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;groups&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">localField</span><span class="o">:</span> <span class="s2">&#34;groups_ids&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">foreignField</span><span class="o">:</span> <span class="s2">&#34;groups_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">as</span><span class="o">:</span> <span class="s2">&#34;groups&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">])</span>
</span></span></code></pre></div><h3 id="何时改用引用">何时改用引用</h3>
<ul>
<li>内嵌文档太大。</li>
<li>内嵌文档或数组元素频繁修改。</li>
<li>内嵌数组元素持续增长且无封顶。</li>
</ul>
<h3 id="引用方式的限制">引用方式的限制</h3>
<ul>
<li>没有主外键约束检查。</li>
<li><code>$lookup</code> 只支持 <strong>left outer join</strong>。</li>
<li><code>$lookup</code> 的 <code>from</code> 不能是分片表。</li>
</ul>
<h2 id="17-模式套用">17 模式套用</h2>
<p>流程：经验 / 学习 ──模式导向──→ <strong>套用设计模式</strong> → 优化的模型。</p>
<p><strong>分桶设计（Bucket Pattern）</strong>：时序数据按时间段聚合到一个文档内。</p>
<ul>
<li>大幅减少文档总数。</li>
<li>大幅减少索引占用空间。</li>
</ul>
<h2 id="18-设计模式集锦">18 设计模式集锦</h2>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>模式</th>
					<th>做法</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>字段繁多、索引爆炸（多语言多国家属性）</td>
					<td>列转行</td>
					<td>把列转成数组元素，一条索引解决所有该类查询</td>
			</tr>
			<tr>
					<td>模式持续演进，需管理不同版本</td>
					<td>Schema Version</td>
					<td>文档加 <code>schema_version</code> 字段</td>
			</tr>
			<tr>
					<td>网页点击流量等粗粒度统计</td>
					<td>近似计算</td>
					<td><code>if random(0,9) == 0 increment by 10</code></td>
			</tr>
			<tr>
					<td>业绩 / 游戏排名等精确统计</td>
					<td>预聚合</td>
					<td>模型中加统计字段，每次写入时同步更新</td>
			</tr>
	</tbody>
</table>
<h2 id="19-写操作事务-writeconcern">19 写操作事务 writeConcern</h2>
<p><a href="https://www.mongodb.com/docs/manual/reference/write-concern/">Write Concern | mongodb</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">{ w: &lt;value&gt;, j: &lt;boolean&gt;, wtimeout: &lt;number&gt; }
</span></span></code></pre></div><p><code>w</code> 取值：</p>
<ul>
<li><code>1</code>：写入主节点即返回。</li>
<li>数字 <code>n</code>：等待 n 个节点确认。</li>
<li><code>&quot;majority&quot;</code>：等待多数节点确认。</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="s2">&#34;majority&#34;</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="mi">3</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="mi">4</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="c1">// [Error] 100 - Not enough data-bearing nodes
</span></span></span></code></pre></div><h3 id="实验延迟节点--writeconcern-超时">实验：延迟节点 + writeConcern 超时</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// 配置延迟节点（5s 延迟、不参选）
</span></span></span><span class="line"><span class="cl"><span class="nx">conf</span> <span class="o">=</span> <span class="nx">rs</span><span class="p">.</span><span class="nx">conf</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nx">conf</span><span class="p">.</span><span class="nx">members</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nx">slaveDelay</span> <span class="o">=</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl"><span class="nx">conf</span><span class="p">.</span><span class="nx">members</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nx">priority</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="nx">rs</span><span class="p">.</span><span class="nx">reconfig</span><span class="p">(</span><span class="nx">conf</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="mi">3</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="c1">// → 等待 5s 才返回
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">5</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span> <span class="nx">wtimeout</span><span class="o">:</span> <span class="mi">3000</span> <span class="p">}</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="c1">// → writeConcernError: waiting for replication timed out
</span></span></span></code></pre></div><h2 id="20-读操作事务-readpreference">20 读操作事务 readPreference</h2>
<p>参考：</p>
<ul>
<li><a href="https://www.mongodb.com/docs/manual/core/read-preference/">Read Preference</a></li>
<li><a href="https://www.mongodb.com/docs/v4.4/core/read-preference-tags/">Read Preference Tags</a></li>
</ul>
<p>三种配置方式：</p>
<table>
	<thead>
			<tr>
					<th>方式</th>
					<th>写法</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>连接串</td>
					<td><code>mongodb://.../?replicaSet=rs&amp;readPreference=secondary</code></td>
			</tr>
			<tr>
					<td>驱动 API</td>
					<td><code>MongoCollection.withReadPreference(ReadPreference readPref)</code></td>
			</tr>
			<tr>
					<td>Mongo Shell</td>
					<td><code>db.collection.find({}).readPref(&quot;secondary&quot;)</code></td>
			</tr>
	</tbody>
</table>
<h3 id="实验从节点写锁后读结果差异">实验：从节点写锁后读结果差异</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// 主节点
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="mi">3</span> <span class="p">}</span> <span class="p">})</span>  <span class="c1">// 三副本确认
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">find</span><span class="p">({}).</span><span class="nx">readPref</span><span class="p">(</span><span class="s2">&#34;secondary&#34;</span><span class="p">)</span>                    <span class="c1">// 1 line
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 从节点 1、2
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">fsyncLock</span><span class="p">()</span>                                             <span class="c1">// 锁住写入
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 主节点
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">3</span> <span class="p">})</span>                              <span class="c1">// 只写主
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">find</span><span class="p">({})</span>                                          <span class="c1">// 2 lines
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">readPref</span><span class="p">(</span><span class="s2">&#34;secondary&#34;</span><span class="p">)</span>                      <span class="c1">// 1 line ← 从未同步
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 从节点 1、2
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">fsyncUnlock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 主节点
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">readPref</span><span class="p">(</span><span class="s2">&#34;secondary&#34;</span><span class="p">)</span>                      <span class="c1">// 2 lines ← 已同步
</span></span></span></code></pre></div><h2 id="21-读操作事务-readconcern">21 读操作事务 readConcern</h2>
<p><a href="https://www.mongodb.com/docs/manual/reference/read-concern/">Read Concern</a></p>
<p>开启：<code>enableMajorityReadConcern: true</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// 从节点 1、2 锁定写入，模拟主从不一致
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">fsyncLock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 主节点
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">insert</span><span class="p">({</span> <span class="nx">count</span><span class="o">:</span> <span class="mi">3</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">readConcern</span><span class="p">(</span><span class="s2">&#34;local&#34;</span><span class="p">)</span>     <span class="c1">// 可能脏读（若该写操作后被回滚）
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">readConcern</span><span class="p">(</span><span class="s2">&#34;majority&#34;</span><span class="p">)</span>  <span class="c1">// 避免脏读
</span></span></span></code></pre></div><p><code>majority</code> 近似 SQL 的 <strong>Read Committed</strong>。</p>
<h2 id="22-多文档事务">22 多文档事务</h2>
<h3 id="acid-在-mongodb-的实现">ACID 在 MongoDB 的实现</h3>
<table>
	<thead>
			<tr>
					<th>性质</th>
					<th>实现机制</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Atomicity</td>
					<td>事务本身</td>
			</tr>
			<tr>
					<td>Consistency</td>
					<td><code>writeConcern</code> + <code>readConcern</code></td>
			</tr>
			<tr>
					<td>Isolation</td>
					<td><code>readConcern</code></td>
			</tr>
			<tr>
					<td>Durability</td>
					<td>Journal + Replication</td>
			</tr>
	</tbody>
</table>
<p>约束：</p>
<ul>
<li>事务默认 <strong>60s</strong> 内必须完成。</li>
<li>多文档事务中的读 <strong>必须走主节点</strong>。</li>
</ul>
<h3 id="实验事务隔离性">实验：事务隔离性</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">drop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">insertMany</span><span class="p">([{</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">2</span> <span class="p">}]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">session</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">getMongo</span><span class="p">().</span><span class="nx">startSession</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">session</span><span class="p">.</span><span class="nx">startTransaction</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">coll</span> <span class="o">=</span> <span class="nx">session</span><span class="p">.</span><span class="nx">getDatabase</span><span class="p">(</span><span class="s1">&#39;test&#39;</span><span class="p">).</span><span class="nx">getCollection</span><span class="p">(</span><span class="s1">&#39;tx&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">coll</span><span class="p">.</span><span class="nx">updateOne</span><span class="p">({</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">2</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">y</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">coll</span><span class="p">.</span><span class="nx">find</span><span class="p">();</span>    <span class="c1">// 事务内：{x:1}, {x:2, y:1}
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">find</span><span class="p">();</span>   <span class="c1">// 事务外：{x:1}, {x:2}             ← 隔离
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">session</span><span class="p">.</span><span class="nx">commitTransaction</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">find</span><span class="p">();</span>   <span class="c1">// {x:1}, {x:2, y:1}
</span></span></span></code></pre></div><h3 id="实验可重复读snapshot">实验：可重复读（snapshot）</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">drop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">insertMany</span><span class="p">([{</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">2</span> <span class="p">}]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">session</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">getMongo</span><span class="p">().</span><span class="nx">startSession</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">session</span><span class="p">.</span><span class="nx">startTransaction</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">readConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">level</span><span class="o">:</span> <span class="s2">&#34;snapshot&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="s2">&#34;majority&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">coll</span> <span class="o">=</span> <span class="nx">session</span><span class="p">.</span><span class="nx">getDatabase</span><span class="p">(</span><span class="s1">&#39;test&#39;</span><span class="p">).</span><span class="nx">getCollection</span><span class="p">(</span><span class="s1">&#39;tx&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">coll</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">});</span>                  <span class="c1">// 首次读
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 事务外的更新
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">tx</span><span class="p">.</span><span class="nx">updateOne</span><span class="p">({</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">y</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">coll</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">});</span>                  <span class="c1">// 仍是首次的快照
</span></span></span><span class="line"><span class="cl"><span class="nx">session</span><span class="p">.</span><span class="nx">abortTransaction</span><span class="p">();</span>
</span></span></code></pre></div><h3 id="实验写冲突">实验：写冲突</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// shell 1、2 同时开事务
</span></span></span><span class="line"><span class="cl"><span class="nx">session</span><span class="p">.</span><span class="nx">startTransaction</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">readConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">level</span><span class="o">:</span> <span class="s2">&#34;snapshot&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">writeConcern</span><span class="o">:</span> <span class="p">{</span> <span class="nx">w</span><span class="o">:</span> <span class="s2">&#34;majority&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// shell 1
</span></span></span><span class="line"><span class="cl"><span class="nx">coll</span><span class="p">.</span><span class="nx">updateOne</span><span class="p">({</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">y</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">});</span>   <span class="c1">// ok
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// shell 2
</span></span></span><span class="line"><span class="cl"><span class="nx">coll</span><span class="p">.</span><span class="nx">updateOne</span><span class="p">({</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">y</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="c1">// MongoServerError: WriteConflict — 须 retry 或 abort
</span></span></span></code></pre></div><h2 id="23-change-stream">23 Change Stream</h2>
<p>类似数据库触发器，<strong>基于 oplog</strong>。</p>
<h3 id="对比传统触发器">对比传统触发器</h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>触发器</th>
					<th>Change Stream</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>触发方式</td>
					<td>同步（事务保证）</td>
					<td>异步</td>
			</tr>
			<tr>
					<td>触发位置</td>
					<td>数据库内</td>
					<td>客户端回调事件</td>
			</tr>
			<tr>
					<td>触发次数</td>
					<td>一次</td>
					<td>每个订阅事件的客户端各一次</td>
			</tr>
			<tr>
					<td>故障恢复</td>
					<td>事务回滚</td>
					<td>从断点重新触发</td>
			</tr>
	</tbody>
</table>
<h3 id="可追踪事件">可追踪事件</h3>
<ul>
<li><code>insert</code> / <code>update</code> / <code>delete</code></li>
<li><code>drop</code> / <code>rename</code> / <code>dropDatabase</code></li>
<li><code>invalidate</code>：<code>drop / rename / dropDatabase</code> 触发，并关闭 change stream</li>
</ul>
<h3 id="约束">约束</h3>
<ul>
<li>隔离级别 = <strong>可重复读</strong>。</li>
<li>集群必须开启 <strong>majority readConcern</strong>。</li>
<li>集群无法满足 <code>{w: &quot;majority&quot;}</code> 时不会触发。</li>
<li>可用聚合管道过滤事件。</li>
</ul>
<h2 id="25-分片集群机制及原理">25 分片集群机制及原理</h2>
<h3 id="进程角色">进程角色</h3>
<table>
	<thead>
			<tr>
					<th>角色</th>
					<th>进程</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>路由</td>
					<td>mongos</td>
					<td>转发请求</td>
			</tr>
			<tr>
					<td>配置</td>
					<td>mongod</td>
					<td>元数据 / 路由表</td>
			</tr>
			<tr>
					<td>数据</td>
					<td>mongod</td>
					<td>实际存数据片段</td>
			</tr>
	</tbody>
</table>
<h3 id="数据分布策略">数据分布策略</h3>
<table>
	<thead>
			<tr>
					<th>策略</th>
					<th>优点</th>
					<th>缺点</th>
					<th>典型场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>范围</td>
					<td>范围查询快</td>
					<td>数据可能倾斜</td>
					<td>自然有序的数据</td>
			</tr>
			<tr>
					<td>哈希</td>
					<td>分布均匀</td>
					<td>范围查询差</td>
					<td>日志、物联网</td>
			</tr>
			<tr>
					<td>Zone</td>
					<td>天然分区</td>
					<td>维护成本高</td>
					<td>按地区 / 业务隔离</td>
			</tr>
	</tbody>
</table>
<h2 id="26-分片集群设计">26 分片集群设计</h2>
<p>核心术语：</p>
<table>
	<thead>
			<tr>
					<th>术语</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>shard key</td>
					<td>文档中作为分片依据的一个字段</td>
			</tr>
			<tr>
					<td>doc</td>
					<td>文档</td>
			</tr>
			<tr>
					<td>chunk</td>
					<td>一个 shard key 范围内的文档块</td>
			</tr>
			<tr>
					<td>shard</td>
					<td>分片节点（一个 replica set）</td>
			</tr>
			<tr>
					<td>cluster</td>
					<td>整个分片集群</td>
			</tr>
	</tbody>
</table>
<h3 id="片键设计三条">片键设计三条</h3>
<ol>
<li>取值基数范围要<strong>大</strong>。</li>
<li>取值范围尽可能<strong>均匀</strong>。</li>
<li>对主要查询要具有 <strong>定向能力</strong>。</li>
</ol>
<p><strong>组合片键</strong> 例：<code>{user_id: 1, time: 1}</code>。</p>
<h2 id="27-分片集群搭建及扩容">27 分片集群搭建及扩容</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1 配置域名解析</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2 准备分片目录</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3 创建第 1 个分片复制集 member1:27010 member3:27010 member5:27010</span>
</span></span><span class="line"><span class="cl">mongod --bind_ip_all --replSet shard1 --shardsvr --wiredTigerCacheSizeGB <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --shardsvr               标注为分片节点</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --wiredTigerCacheSizeGB  缓存大小</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4 初始化分片复制集</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5 创建 config server 复制集 member1:27019 member3:27019 member5:27019</span>
</span></span><span class="line"><span class="cl">mongod --bind_ip_all --replSet config --configsvr --wiredTigerCacheSizeGB <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --configsvr              标注为配置节点</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 6 初始化 config server 复制集</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 7 搭建 mongos</span>
</span></span><span class="line"><span class="cl">mongos --bind_ip_all --configdb config/member1:27019,member3:27019,member5:27019
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 连接 mongos 添加分片</span>
</span></span><span class="line"><span class="cl">sh.addShard<span class="o">(</span><span class="s2">&#34;shard1/member1:27010,member3:27010,member5:27010&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 8 创建分片表</span>
</span></span><span class="line"><span class="cl">sh.enableSharding<span class="o">(</span><span class="s2">&#34;foo&#34;</span><span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">sh.shardCollection<span class="o">(</span><span class="s2">&#34;foo.bar&#34;</span>, <span class="o">{</span>_id: <span class="s2">&#34;hashed&#34;</span><span class="o">})</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">sh.status<span class="o">()</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 插入测试数据</span>
</span></span><span class="line"><span class="cl">use foo
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="o">(</span>var <span class="nv">i</span> <span class="o">=</span> 0<span class="p">;</span> i &lt; 10000<span class="p">;</span> i++<span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  db.bar.insert<span class="o">({</span> i: i <span class="o">})</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 9~11 创建并加入第 2 个分片复制集</span>
</span></span><span class="line"><span class="cl">sh.addShard<span class="o">(</span><span class="s2">&#34;shard2/member2:27011,member4:27011,member6:27011&#34;</span><span class="o">)</span>
</span></span></code></pre></div><h2 id="28-监控最佳实践">28 监控最佳实践</h2>
<p>工具：</p>
<ul>
<li><strong>MongoDB Ops Manager</strong></li>
<li><strong>Percona</strong></li>
<li>自研监控脚本</li>
</ul>
<p>数据获取：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">serverStatus</span><span class="p">()</span>              <span class="c1">// 从上次启动到现在
</span></span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">serverStatus</span><span class="p">().</span><span class="nx">opcounters</span>
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">isMaster</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nx">mongostat</span>                      <span class="c1">// CLI 工具
</span></span></span></code></pre></div><h2 id="29-备份与恢复">29 备份与恢复</h2>
<p>策略：</p>
<ul>
<li><strong>延迟节点</strong> —— 抗误删。</li>
<li><strong>全量 + Oplog 增量</strong> —— oplog 幂等可重放。</li>
<li><code>mongodump --oplog</code> 不遗漏 dump 期间的数据。</li>
<li><code>mongorestore --oplogReplay</code> 配合恢复。</li>
</ul>
<h2 id="31-安全架构">31 安全架构</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">createRole</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">   <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;readWriteRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nx">privileges</span><span class="o">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">resource</span><span class="o">:</span> <span class="p">{</span> <span class="nx">db</span><span class="o">:</span> <span class="s2">&#34;myDatabase&#34;</span><span class="p">,</span> <span class="nx">collection</span><span class="o">:</span> <span class="s2">&#34;sample&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nx">actions</span><span class="o">:</span> <span class="p">[</span> <span class="s2">&#34;find&#34;</span><span class="p">,</span> <span class="s2">&#34;insert&#34;</span><span class="p">,</span> <span class="s2">&#34;update&#34;</span><span class="p">,</span> <span class="s2">&#34;remove&#34;</span> <span class="p">]</span>
</span></span><span class="line"><span class="cl">   <span class="p">}],</span>
</span></span><span class="line"><span class="cl">   <span class="nx">roles</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">role</span><span class="o">:</span> <span class="s1">&#39;read&#39;</span><span class="p">,</span> <span class="nx">db</span><span class="o">:</span> <span class="s1">&#39;sampledb&#39;</span> <span class="p">}]</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">createUser</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">user</span><span class="o">:</span> <span class="s1">&#39;sampleUser&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">pwd</span><span class="o">:</span> <span class="s1">&#39;pwd&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">roles</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">role</span><span class="o">:</span> <span class="s1">&#39;readWriteRole&#39;</span><span class="p">,</span> <span class="nx">db</span><span class="o">:</span> <span class="s1">&#39;admin&#39;</span> <span class="p">}]</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></div><h2 id="32-安全加固实践">32 安全加固实践</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mongod --noscripting   <span class="c1"># 禁止脚本执行</span>
</span></span><span class="line"><span class="cl">mongod --auth          <span class="c1"># 强制鉴权</span>
</span></span></code></pre></div><h2 id="33-索引机制一">33 索引机制（一）</h2>
<p>核心概念速查：</p>
<table>
	<thead>
			<tr>
					<th>概念</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Index / Key / DataPage</td>
					<td>索引、键、数据页</td>
			</tr>
			<tr>
					<td>Covered Query / FETCH</td>
					<td>完全由索引返回 / 需要回表取数据</td>
			</tr>
			<tr>
					<td>IXSCAN / COLLSCAN</td>
					<td>索引扫描 / 集合扫描</td>
			</tr>
			<tr>
					<td>Big O Notation</td>
					<td>时间复杂度</td>
			</tr>
			<tr>
					<td>Query Shape</td>
					<td>查询形状（相同 shape 可复用执行计划）</td>
			</tr>
			<tr>
					<td>Index Prefix</td>
					<td>索引前缀</td>
			</tr>
			<tr>
					<td>Selectivity</td>
					<td>选择性 —— 过滤掉的比例越大越好</td>
			</tr>
	</tbody>
</table>
<h3 id="b-tree-vs-btree">B-Tree vs B+Tree</h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>B-Tree</th>
					<th>B+Tree</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>数据存放</td>
					<td>所有节点都可存 key + value</td>
					<td>仅叶子节点存 key + value</td>
			</tr>
			<tr>
					<td>叶子链表</td>
					<td>无</td>
					<td>叶子用指针串成有序链表，区间查询友好</td>
			</tr>
			<tr>
					<td>适用</td>
					<td>内存索引、缓存</td>
					<td><strong>磁盘索引</strong>（数据库、文件系统）</td>
			</tr>
	</tbody>
</table>
<p>MongoDB / MySQL InnoDB 都用 B+Tree。</p>
<h2 id="34-索引机制二">34 索引机制（二）</h2>
<p><code>.explain(true)</code> 关注字段：</p>
<table>
	<thead>
			<tr>
					<th>字段</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>executionTimeMillis</code></td>
					<td>执行毫秒数</td>
			</tr>
			<tr>
					<td><code>totalDocsExamined</code></td>
					<td>共扫描文档数</td>
			</tr>
			<tr>
					<td><code>executionStages.docsExamined</code></td>
					<td>当前阶段扫描文档数</td>
			</tr>
			<tr>
					<td><code>executionStages.inputStage.stage</code></td>
					<td>输入阶段类型</td>
			</tr>
	</tbody>
</table>
<p><strong>ESR 原则</strong>：组合索引顺序 = <strong>E</strong>qual（最前）→ <strong>S</strong>ort（中间）→ <strong>R</strong>ange（最后）。</p>
<p>后台建索引：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">member</span><span class="p">.</span><span class="nx">createIndex</span><span class="p">({</span> <span class="nx">city</span><span class="o">:</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">background</span><span class="o">:</span> <span class="kc">true</span> <span class="p">})</span>
</span></span></code></pre></div><h2 id="36-性能诊断工具">36 性能诊断工具</h2>
<p><strong><code>mongostat</code></strong> —— 实时运行状态：</p>
<table>
	<thead>
			<tr>
					<th>指标</th>
					<th>关注阈值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>dirty</td>
					<td>&lt; 5%（脏页比例）</td>
			</tr>
			<tr>
					<td>used</td>
					<td>缓存使用率</td>
			</tr>
			<tr>
					<td>qrw</td>
					<td>排队请求数</td>
			</tr>
			<tr>
					<td>con</td>
					<td>连接数</td>
			</tr>
	</tbody>
</table>
<p><strong><code>mongotop</code></strong> —— 集合维度压力。</p>
<p><strong><a href="https://github.com/rueckstiess/mtools">mtools</a></strong> —— 日志分析。</p>
<h2 id="41-应用场景及选型">41 应用场景及选型</h2>
<p>优点：</p>
<ul>
<li>横向扩展 —— 数据 / 并发增长可自动扩容。</li>
<li>灵活模型 —— 适合快速迭代、数据模型多变。</li>
<li>JSON 结构 —— 与微服务 / REST API 天然契合。</li>
</ul>
<h2 id="44-关系型数据库迁移">44 关系型数据库迁移</h2>
<p>迁移到 MongoDB 的理由：</p>
<ul>
<li>高并发（数千 ~ 十万 ops），关系型不易扩展。</li>
<li>快速迭代，关系型模式太严谨。</li>
<li>灵活的 JSON 模式。</li>
<li>大数据量需求。</li>
<li>地理位置查询。</li>
<li>多数据中心跨地域部署。</li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://time.geekbang.org/course/intro/100040001">MongoDB 高手课</a></li>
<li><a href="https://github.com/minhhungit/mongodb-cluster-docker-compose">mongodb-cluster-docker-compose</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>Kubernetes 入门实战 Part3</title>
      <link>https://zyf.im/2023/02/24/kubernetes-getting-started-part3/</link>
      <pubDate>Fri, 24 Feb 2023 17:24:14 +0000</pubDate>
      <guid>https://zyf.im/2023/02/24/kubernetes-getting-started-part3/</guid>
      <description>&lt;h2 id=&#34;24-persistentvolume-数据持久化&#34;&gt;24 PersistentVolume 数据持久化&lt;/h2&gt;
&lt;p&gt;PersistentVolume 属于集群的系统资源，是和 Node 平级的一种对象，Pod 对它没有管理权，只有使用权。&lt;/p&gt;
&lt;p&gt;StorageClass 它抽象了特定类型的存储系统（比如 Ceph、NFS），在 PVC 和 PV 之间充当“协调人”的角色，帮助 PVC 找到合适的 PV。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# vim host-path-pv-pvc.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;apiVersion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;v1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;PersistentVolume&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# 只有 10MB 容量的存储设备&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-10m-pv&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;spec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# kubectl explain PersistentVolume.spec.storageClassName&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;storageClassName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-vol&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# kubectl explain PersistentVolume.spec.accessModes&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;accessModes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ReadWriteOnce：存储卷可读可写，但只能被一个节点上的 Pod 挂载。&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ReadOnlyMany：存储卷只读不可写，可以被任意节点上的 Pod 多次挂载。&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# ReadWriteMany：存储卷可读可写，也可以被任意节点上的 Pod 多次挂载。&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;ReadWriteOnce&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;capacity&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;#  Ki/Mi/Gi&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;10Mi&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;hostPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/tmp/host-10m-pv/&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nn&#34;&gt;---&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# PVC 不表示实际的存储，而是一个“申请”或者“声明”&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;apiVersion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;v1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-5m-pvc&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;spec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;storageClassName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-vol&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;accessModes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;ReadWriteOnce&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;resources&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;requests&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;5Mi&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mkdir /tmp/host-10m-pv
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl apply -f host-path-pv-pvc.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl get pv
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# host-10m-pv   10Mi       RWO            Retain           Bound    default/host-5m-pvc   host-vol                4s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl get pvc
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# NAME          STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# host-5m-pvc   Bound    host-10m-pv   10Mi       RWO            host-vol       6s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# vim host-pvc-pod.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;apiVersion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;v1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Pod&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-pvc-pod&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;spec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;volumes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-pvc-vol&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;claimName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-5m-pvc&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;containers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ngx-pvc-pod&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;nginx:alpine&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;ports&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;containerPort&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;80&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;volumeMounts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;host-pvc-vol&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;mountPath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/tmp&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl apply -f host-pvc-pod.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl get pod -o wide
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl &lt;span class=&#34;nb&#34;&gt;exec&lt;/span&gt; -it host-pvc-pod -- sh
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; /tmp &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; touch a.md
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# check in worker node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/tmp/host-10m-pv/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;25-nfs-网络共享存储&#34;&gt;25 NFS 网络共享存储&lt;/h2&gt;
&lt;p&gt;&amp;hellip;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="24-persistentvolume-数据持久化">24 PersistentVolume 数据持久化</h2>
<p>PersistentVolume 属于集群的系统资源，是和 Node 平级的一种对象，Pod 对它没有管理权，只有使用权。</p>
<p>StorageClass 它抽象了特定类型的存储系统（比如 Ceph、NFS），在 PVC 和 PV 之间充当“协调人”的角色，帮助 PVC 找到合适的 PV。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim host-path-pv-pvc.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PersistentVolume</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 只有 10MB 容量的存储设备</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">host-10m-pv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># kubectl explain PersistentVolume.spec.storageClassName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storageClassName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="l">host-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># kubectl explain PersistentVolume.spec.accessModes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">accessModes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># ReadWriteOnce：存储卷可读可写，但只能被一个节点上的 Pod 挂载。</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># ReadOnlyMany：存储卷只读不可写，可以被任意节点上的 Pod 多次挂载。</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># ReadWriteMany：存储卷可读可写，也可以被任意节点上的 Pod 多次挂载。</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">ReadWriteOnce</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">capacity</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c">#  Ki/Mi/Gi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">storage</span><span class="p">:</span><span class="w"> </span><span class="l">10Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hostPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/tmp/host-10m-pv/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># PVC 不表示实际的存储，而是一个“申请”或者“声明”</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PersistentVolumeClaim</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">host-5m-pvc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storageClassName</span><span class="p">:</span><span class="w"> </span><span class="l">host-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">accessModes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">ReadWriteOnce</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">storage</span><span class="p">:</span><span class="w"> </span><span class="l">5Mi</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir /tmp/host-10m-pv
</span></span><span class="line"><span class="cl">kubectl apply -f host-path-pv-pvc.yml
</span></span><span class="line"><span class="cl">kubectl get pv
</span></span><span class="line"><span class="cl"><span class="c1"># NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host-10m-pv   10Mi       RWO            Retain           Bound    default/host-5m-pvc   host-vol                4s</span>
</span></span><span class="line"><span class="cl">kubectl get pvc
</span></span><span class="line"><span class="cl"><span class="c1"># NAME          STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host-5m-pvc   Bound    host-10m-pv   10Mi       RWO            host-vol       6s</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim host-pvc-pod.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">host-pvc-pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">host-pvc-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">persistentVolumeClaim</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">claimName</span><span class="p">:</span><span class="w"> </span><span class="l">host-5m-pvc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-pvc-pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">        </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">host-pvc-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/tmp</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f host-pvc-pod.yml
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it host-pvc-pod -- sh
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp <span class="o">&amp;&amp;</span> touch a.md
</span></span><span class="line"><span class="cl"><span class="c1"># check in worker node</span>
</span></span><span class="line"><span class="cl">/tmp/host-10m-pv/
</span></span></code></pre></div><h2 id="25-nfs-网络共享存储">25 NFS 网络共享存储</h2>
<p>&hellip;</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/chronolaw/k8s_study">chronolaw/k8s_study | GitHub</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Kubernetes 入门实战 Part2</title>
      <link>https://zyf.im/2023/02/22/kubernetes-getting-started-part2/</link>
      <pubDate>Wed, 22 Feb 2023 17:11:27 +0000</pubDate>
      <guid>https://zyf.im/2023/02/22/kubernetes-getting-started-part2/</guid>
      <description>&lt;h2 id=&#34;17-多节点的-kubernetes-集群&#34;&gt;17 多节点的 Kubernetes 集群&lt;/h2&gt;
&lt;p&gt;在腾讯云 TencentOS Server 3.1 (TK4) 下测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;master SA3.MEDIUM4 2 核 4GB 5Mbps&lt;/li&gt;
&lt;li&gt;worker S5.SMALL2 1 核 2GB 1Mbps&lt;/li&gt;
&lt;li&gt;worker S5.SMALL2 1 核 2GB 1Mbps&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 修改源 https://mirrors.cloud.tencent.com/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.cloud.tencent.com/repo/centos7_base.repo
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;yum clean all
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;yum makecache
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# install docker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;yum remove docker &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-client &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-client-latest &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-common &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-latest &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-latest-logrotate &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-logrotate &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            docker-engine
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;yum install -y yum-utils
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;yum-config-manager &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    --add-repo &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    https://download.docker.com/linux/centos/docker-ce.repo
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl start docker
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker -v
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker run hello-world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一些准备工作：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="17-多节点的-kubernetes-集群">17 多节点的 Kubernetes 集群</h2>
<p>在腾讯云 TencentOS Server 3.1 (TK4) 下测试：</p>
<ul>
<li>master SA3.MEDIUM4 2 核 4GB 5Mbps</li>
<li>worker S5.SMALL2 1 核 2GB 1Mbps</li>
<li>worker S5.SMALL2 1 核 2GB 1Mbps</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 修改源 https://mirrors.cloud.tencent.com/</span>
</span></span><span class="line"><span class="cl">mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
</span></span><span class="line"><span class="cl">wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.cloud.tencent.com/repo/centos7_base.repo
</span></span><span class="line"><span class="cl">yum clean all
</span></span><span class="line"><span class="cl">yum makecache
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># install docker</span>
</span></span><span class="line"><span class="cl">yum remove docker <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-client <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-client-latest <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-common <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-latest <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-latest-logrotate <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-logrotate <span class="se">\
</span></span></span><span class="line"><span class="cl">            docker-engine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">yum install -y yum-utils
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">yum-config-manager <span class="se">\
</span></span></span><span class="line"><span class="cl">    --add-repo <span class="se">\
</span></span></span><span class="line"><span class="cl">    https://download.docker.com/linux/centos/docker-ce.repo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl start docker
</span></span><span class="line"><span class="cl">docker -v
</span></span><span class="line"><span class="cl">docker run hello-world
</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="cl"><span class="c1"># 改主机名</span>
</span></span><span class="line"><span class="cl">vi /etc/hostname
</span></span><span class="line"><span class="cl"><span class="c1"># reboot</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 把 cgroup 的驱动程序改成 systemd</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 使用 Docker 作为 Kubernetes 的底层支持</span>
</span></span><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | sudo tee /etc/docker/daemon.json
</span></span></span><span class="line"><span class="cl"><span class="s">{
</span></span></span><span class="line"><span class="cl"><span class="s">  &#34;exec-opts&#34;: [&#34;native.cgroupdriver=systemd&#34;],
</span></span></span><span class="line"><span class="cl"><span class="s">  &#34;log-driver&#34;: &#34;json-file&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s">  &#34;log-opts&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s">    &#34;max-size&#34;: &#34;100m&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">  },
</span></span></span><span class="line"><span class="cl"><span class="s">  &#34;storage-driver&#34;: &#34;overlay2&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> docker
</span></span><span class="line"><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="cl">systemctl restart docker
</span></span><span class="line"><span class="cl">docker version
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 转发 IPv4 并让 iptables 看到桥接流量</span>
</span></span><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | sudo tee /etc/modules-load.d/k8s.conf
</span></span></span><span class="line"><span class="cl"><span class="s">overlay
</span></span></span><span class="line"><span class="cl"><span class="s">br_netfilter
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo modprobe overlay
</span></span><span class="line"><span class="cl">sudo modprobe br_netfilter
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># check</span>
</span></span><span class="line"><span class="cl">lsmod <span class="p">|</span> grep br_netfilter
</span></span><span class="line"><span class="cl">lsmod <span class="p">|</span> grep overlay
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 设置所需的 sysctl 参数，参数在重新启动后保持不变</span>
</span></span><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
</span></span></span><span class="line"><span class="cl"><span class="s">net.bridge.bridge-nf-call-iptables  = 1
</span></span></span><span class="line"><span class="cl"><span class="s">net.bridge.bridge-nf-call-ip6tables = 1
</span></span></span><span class="line"><span class="cl"><span class="s">net.ipv4.ip_forward                 = 1
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 应用 sysctl 参数而不重新启动</span>
</span></span><span class="line"><span class="cl">sudo sysctl --system
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
</span></span><span class="line"><span class="cl"><span class="c1"># net.bridge.bridge-nf-call-iptables = 1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># net.bridge.bridge-nf-call-ip6tables = 1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># net.ipv4.ip_forward = 1</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 关闭 Linux 的 swap 分区</span>
</span></span><span class="line"><span class="cl">swapoff -a
</span></span><span class="line"><span class="cl">sed -ri <span class="s1">&#39;/\sswap\s/s/^#?/#/&#39;</span> /etc/fstab
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># https://developer.aliyun.com/mirror/kubernetes</span>
</span></span><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF &gt; /etc/yum.repos.d/kubernetes.repo
</span></span></span><span class="line"><span class="cl"><span class="s">[kubernetes]
</span></span></span><span class="line"><span class="cl"><span class="s">name=Kubernetes
</span></span></span><span class="line"><span class="cl"><span class="s">baseurl=https://mirrors.cloud.tencent.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">yum clean all
</span></span><span class="line"><span class="cl">yum makecache
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 将 SELinux 设置为 permissive 模式（相当于将其禁用）</span>
</span></span><span class="line"><span class="cl">setenforce <span class="m">0</span>
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/^SELINUX=enforcing$/SELINUX=permissive/&#39;</span> /etc/selinux/config
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 新版本搞不定</span>
</span></span><span class="line"><span class="cl"><span class="c1"># yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes --nogpgcheck</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">yum --showduplicate list kubelet
</span></span><span class="line"><span class="cl">yum install -y kubelet-1.23.16-0 kubeadm-1.23.16-0 kubectl-1.23.16-0 --disableexcludes<span class="o">=</span>kubernetes --nogpgcheck
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> --now kubelet
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubeadm version
</span></span><span class="line"><span class="cl">kubectl version --output<span class="o">=</span>yaml
</span></span><span class="line"><span class="cl">kubelet --version
</span></span></code></pre></div><p>下载 Kubernetes 组件镜像：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># kubeadm config images list</span>
</span></span><span class="line"><span class="cl">kubeadm config images list --kubernetes-version v1.23.16
</span></span></code></pre></div><p>安装 Master 节点：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vim /etc/containerd/config.toml
</span></span><span class="line"><span class="cl"><span class="c1">#disabled_plugins = [&#34;cri&#34;]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> containerd
</span></span><span class="line"><span class="cl">systemctl restart containerd
</span></span><span class="line"><span class="cl">systemctl status containerd
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> kubelet.service
</span></span><span class="line"><span class="cl">systemctl restart kubelet
</span></span><span class="line"><span class="cl">systemctl status kubelet
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">containerd config default &gt; /etc/containerd/config.toml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">yum install -y nc
</span></span><span class="line"><span class="cl">nc 127.0.0.1 <span class="m">6443</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubeadm init -h
</span></span><span class="line"><span class="cl">kubeadm reset -f
</span></span><span class="line"><span class="cl">rm -rf ~/.kube/config
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubeadm init <span class="se">\
</span></span></span><span class="line"><span class="cl">    --image-repository<span class="o">=</span>registry.aliyuncs.com/google_containers <span class="se">\
</span></span></span><span class="line"><span class="cl">    --pod-network-cidr<span class="o">=</span>10.10.0.0/16 <span class="se">\
</span></span></span><span class="line"><span class="cl">    --kubernetes-version<span class="o">=</span>v1.23.16 <span class="se">\
</span></span></span><span class="line"><span class="cl">    --v<span class="o">=</span><span class="m">9</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run &#34;kubectl apply -f [podnetwork].yaml&#34; with one of the options listed at:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   https://kubernetes.io/docs/concepts/cluster-administration/addons/</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># success</span>
</span></span><span class="line"><span class="cl">mkdir -p <span class="nv">$HOME</span>/.kube
</span></span><span class="line"><span class="cl">sudo cp -i /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
</span></span><span class="line"><span class="cl">sudo chown <span class="k">$(</span>id -u<span class="k">)</span>:<span class="k">$(</span>id -g<span class="k">)</span> <span class="nv">$HOME</span>/.kube/config
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">KUBECONFIG</span><span class="o">=</span>/etc/kubernetes/admin.conf
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get node
</span></span><span class="line"><span class="cl"><span class="c1"># NAME     STATUS     ROLES                  AGE    VERSION</span>
</span></span><span class="line"><span class="cl"><span class="c1"># master   NotReady   control-plane,master   2m4s   v1.23.16</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># dubug</span>
</span></span><span class="line"><span class="cl">systemctl restart docker
</span></span><span class="line"><span class="cl">systemctl restart kubelet
</span></span><span class="line"><span class="cl">systemctl restart containerd
</span></span><span class="line"><span class="cl">journalctl -xeu kubelet
</span></span><span class="line"><span class="cl">crictr ps -a
</span></span><span class="line"><span class="cl">crictl --runtime-endpoint unix:///var/run/containerd/containerd.sock ps -a <span class="p">|</span> grep kube <span class="p">|</span> grep -v pause
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get pods -n kube-system
</span></span><span class="line"><span class="cl">kubectl describe pods -n kube-system
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Flannel 网络插件 https://github.com/flannel-io/flannel/tree/master</span>
</span></span><span class="line"><span class="cl"><span class="c1"># curl https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml --output kube-flannel.yml</span>
</span></span><span class="line"><span class="cl"><span class="c1">#  net-conf.json: |</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       &#34;Network&#34;: &#34;10.10.0.0/16&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       &#34;Backend&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#         &#34;Type&#34;: &#34;vxlan&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       }</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     }</span>
</span></span><span class="line"><span class="cl">kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
</span></span><span class="line"><span class="cl">kubectl get node
</span></span><span class="line"><span class="cl"><span class="c1"># NAME     STATUS   ROLES                  AGE   VERSION</span>
</span></span><span class="line"><span class="cl"><span class="c1"># master   Ready    control-plane,master   14h   v1.23.16</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># show join command in control-plane</span>
</span></span><span class="line"><span class="cl">kubeadm token create --print-join-command
</span></span><span class="line"><span class="cl"><span class="c1"># work join; 云服务记得开放入站端口</span>
</span></span><span class="line"><span class="cl">telnet 172.21.0.5 <span class="m">6443</span>
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> kubelet.service
</span></span><span class="line"><span class="cl">kubeadm join 172.21.0.5:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx --v<span class="o">=</span><span class="m">9</span>
</span></span><span class="line"><span class="cl"><span class="c1"># check in control-plane</span>
</span></span><span class="line"><span class="cl">kubectl get nodes
</span></span><span class="line"><span class="cl"><span class="c1"># NAME            STATUS   ROLES                  AGE     VERSION</span>
</span></span><span class="line"><span class="cl"><span class="c1"># master          Ready    control-plane,master   14h     v1.23.16</span>
</span></span><span class="line"><span class="cl"><span class="c1"># vm-0-9-centos   Ready    &lt;none&gt;                 3m27s   v1.23.16</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># run nginx</span>
</span></span><span class="line"><span class="cl">kubectl run ngx --image<span class="o">=</span>nginx:alpine
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl"><span class="c1"># NAME   READY   STATUS    RESTARTS   AGE   IP          NODE      NOMINATED NODE   READINESS GATES</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx    1/1     Running   0          52m   10.10.1.2   woker01   &lt;none&gt;           &lt;none&gt;</span>
</span></span></code></pre></div><h2 id="18-deployment-部署应用">18 Deployment 部署应用</h2>
<p>“单一职责”和“对象组合”。既然 Pod 管理不了自己，那么我们就再创建一个新的对象，由它来管理 Pod，采用和 Job/CronJob 一样的形式——“对象套对象”。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl api-resources
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">out</span><span class="o">=</span><span class="s2">&#34;--dry-run=client -o yaml&#34;</span>
</span></span><span class="line"><span class="cl">kubectl create deploy ngx-dep --image<span class="o">=</span>nginx:alpine <span class="nv">$out</span> &gt; ngx-dep.yml
</span></span><span class="line"><span class="cl">vim ngx-dep.yml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 定义了 Pod 的“期望数量”，Kubernetes 会自动维护 Pod 数量到正常水平</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 定义了基于 labels 筛选 Pod 的规则，它必须与 template 里 Pod 的 labels 一致</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># 贴标签</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><p>Deployment 实际上并不“持有”Pod 对象，它只是帮助 Pod 对象能够有足够的副本数量运行。</p>
<p>通过标签这种设计，Kubernetes 就解除了 Deployment 和模板里 Pod 的强绑定，把组合关系变成了“弱引用”。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># replicas: 2</span>
</span></span><span class="line"><span class="cl">kubectl apply -f ngx-dep.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get deploy
</span></span><span class="line"><span class="cl"><span class="c1"># NAME      READY   UP-TO-DATE   AVAILABLE   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep   2/2     2            2           57s</span>
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                      READY   STATUS    RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-96scb   1/1     Running   0          3m20s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-qnzbh   1/1     Running   0          3m20s</span>
</span></span></code></pre></div><ul>
<li>READY：运行的 Pod 数量，当前数量/期望数量。</li>
<li>UP-TO-DATE：当前已经更新到最新状态的 Pod 数量。</li>
<li>AVAILABLE：不仅要求已经运行，还必须是健康状态，能够正常对外提供服务，它才是我们最关心的 Deployment 指标。</li>
<li>AGE：从创建到现在所经过的时间。</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 测试自启恢复</span>
</span></span><span class="line"><span class="cl">kubectl delete pod ngx-dep-bfbb5f64b-qnzbh
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                      READY   STATUS    RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-7n724   1/1     Running   0          33s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-96scb   1/1     Running   0          4m52s</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 测试伸缩</span>
</span></span><span class="line"><span class="cl">kubectl scale --replicas<span class="o">=</span><span class="m">5</span> deploy ngx-dep
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                      READY   STATUS    RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-7n724   1/1     Running   0          77s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-7xhbs   1/1     Running   0          7s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-96scb   1/1     Running   0          5m36s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-97qp5   1/1     Running   0          7s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-dep-bfbb5f64b-vjn4q   1/1     Running   0          7s</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 筛选标签 ==、!=、in、notin</span>
</span></span><span class="line"><span class="cl">kubectl get pod -l <span class="nv">app</span><span class="o">=</span>nginx
</span></span><span class="line"><span class="cl">kubectl get pod -l <span class="s1">&#39;app in (ngx, nginx, ngx-dep)&#39;</span>
</span></span></code></pre></div><h2 id="19-daemonset-看门狗">19 DaemonSet 看门狗</h2>
<p>在 Deployment 看来，Pod 的运行环境与功能是无关的，只要 Pod 的数量足够，应用程序应该会正常工作。</p>
<p>有些场景下，要在集群里的每个节点上都运行 Pod，也就是说 Pod 的数量与节点数量保持同步。防止在集群里漂移。</p>
<p>DaemonSet 的目标是在集群的每个节点上运行且仅运行一个 Pod。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl api-resources
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># export out=&#34;--dry-run=client -o yaml&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># kubectl create deploy redis-ds --image=redis:5-alpine $out</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># kind modify DaemonSet, delete spec.replicas</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/daemonset/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># vim redis-ds.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">DaemonSet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 和 deplayment 比没有 replicas</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">redis:5-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">6379</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f redis-ds.yml
</span></span><span class="line"><span class="cl">kubectl get ds
</span></span><span class="line"><span class="cl"><span class="c1"># NAME       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds   2         2         2       2            2           &lt;none&gt;          30m</span>
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl"><span class="c1"># 两个 worker 的场景</span>
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE      NOMINATED NODE   READINESS GATES</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-9r96k            1/1     Running   0          2m52s   10.10.3.2    woker02   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-hdl28            1/1     Running   0          21m     10.10.1.11   woker01   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Master 节点却被排除在外了</span>
</span></span></code></pre></div><p>污点（taint）作用也是给节点“贴标签”。容忍度（toleration）Pod 能否“容忍”污点。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node master
</span></span><span class="line"><span class="cl"><span class="c1"># Taints:             node-role.kubernetes.io/master:NoSchedule</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 污点会拒绝 Pod 调度到本节点上运行</span>
</span></span><span class="line"><span class="cl">kubectl describe node woker01
</span></span><span class="line"><span class="cl"><span class="c1"># Taints:             &lt;none&gt;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># - 出掉 master 污点</span>
</span></span><span class="line"><span class="cl">kubectl taint node master node-role.kubernetes.io/master:NoSchedule-
</span></span><span class="line"><span class="cl"><span class="c1"># NAME       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds   3         3         3       3            3           &lt;none&gt;          31m</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Pod 添加 tolerations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># kubectl explain ds.spec.template.spec.tolerations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># vim redis-ds-t.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">DaemonSet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds-t</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds-t</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 和 deplayment 比没有 replicas</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds-t</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis-ds-t</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">redis:5-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">6379</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># 容忍 node-role.kubernetes.io/master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tolerations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">node-role.kubernetes.io/master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">effect</span><span class="p">:</span><span class="w"> </span><span class="l">NoSchedule</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">operator</span><span class="p">:</span><span class="w"> </span><span class="l">Exists</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f redis-ds-t.yml
</span></span><span class="line"><span class="cl">kubectl get ds
</span></span><span class="line"><span class="cl"><span class="c1"># NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds     2         2         2       2            2           &lt;none&gt;          41m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-t   3         3         3       3            3           &lt;none&gt;          6s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 差别在 master</span>
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE      NOMINATED NODE   READINESS GATES</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-9r96k            1/1     Running   0          23m   10.10.3.2    woker02   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-hdl28            1/1     Running   0          42m   10.10.1.11   woker01   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-t-4mptv          1/1     Running   0          80s   10.10.3.4    woker02   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-t-dpcl8          1/1     Running   0          80s   10.10.1.12   woker01   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># redis-ds-t-kdjmn          1/1     Running   0          80s   10.10.0.6    master    &lt;none&gt;           &lt;none&gt;</span>
</span></span></code></pre></div><p><a href="https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/taint-and-toleration/">https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/taint-and-toleration/</a></p>
<p>静态 Pod：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ll -a /etc/kubernetes/manifests
</span></span><span class="line"><span class="cl"><span class="c1"># -rw------- 1 root root 2274 Feb 22 12:47 etcd.yaml</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -rw------- 1 root root 3358 Feb 22 12:47 kube-apiserver.yaml</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -rw------- 1 root root 2878 Feb 22 12:47 kube-controller-manager.yaml</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -rw------- 1 root root 1465 Feb 22 12:47 kube-scheduler.yaml</span>
</span></span></code></pre></div><p>Kubernetes 的 4 个核心组件 apiserver、etcd、scheduler、controller-manager 原来都以静态 Pod 的形式存在的，这也是为什么它们能够先于 Kubernetes 集群启动的原因。</p>
<p>kubelet 会定期检查目录里的文件。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># flannel 就是一个 DaemonSet</span>
</span></span><span class="line"><span class="cl">kubectl get ns
</span></span><span class="line"><span class="cl">kubectl get ds -n kube-flannel
</span></span><span class="line"><span class="cl"><span class="c1"># NAME              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kube-flannel-ds   3         3         3       3            3           &lt;none&gt;          3h54m</span>
</span></span></code></pre></div><h2 id="20-service-服务发现">20 Service 服务发现</h2>
<p>由 kube-proxy 控制的四层负载均衡，在 TCP/IP 协议栈上转发流量。</p>
<p>Pod 的生命周期很短暂，会不停地创建销毁，所以就需要用 Service 来实现负载均衡，它由 Kubernetes 分配固定的 IP 地址，能够屏蔽后端的 Pod 变化。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">out</span><span class="o">=</span><span class="s2">&#34;--dry-run=client -o yaml&#34;</span>
</span></span><span class="line"><span class="cl">kubectl expose deploy ngx-dep --port<span class="o">=</span><span class="m">80</span> --target-port<span class="o">=</span><span class="m">80</span> <span class="nv">$ou</span>
</span></span></code></pre></div><img width="600" alt="image" src="https://user-images.githubusercontent.com/9289792/220598245-da0f7319-8c28-4c08-b003-1dd209af1ee6.png">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim ngx-svc.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">loadBalancer</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-conf</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">default.conf</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    server {
</span></span></span><span class="line"><span class="cl"><span class="sd">      listen 80;
</span></span></span><span class="line"><span class="cl"><span class="sd">      location / {
</span></span></span><span class="line"><span class="cl"><span class="sd">        default_type text/plain;
</span></span></span><span class="line"><span class="cl"><span class="sd">        return 200
</span></span></span><span class="line"><span class="cl"><span class="sd">          &#39;srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n&#39;;
</span></span></span><span class="line"><span class="cl"><span class="sd">      }
</span></span></span><span class="line"><span class="cl"><span class="sd">    }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-conf-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">configMap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-conf</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/nginx/conf.d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-conf-vol</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f ngx-svc.yml
</span></span><span class="line"><span class="cl">kubectl get svc
</span></span><span class="line"><span class="cl"><span class="c1"># NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kubernetes   ClusterIP   10.96.0.1        &lt;none&gt;        443/TCP   5h10m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-svc      ClusterIP   10.109.131.132   &lt;none&gt;        80/TCP    35s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 虚地址 10.109.131.132</span>
</span></span><span class="line"><span class="cl">kubectl describe svc ngx-svc
</span></span><span class="line"><span class="cl"><span class="c1"># Name:              ngx-svc</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Namespace:         default</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Labels:            &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Annotations:       &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Selector:          app=ngx-dep</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Type:              ClusterIP</span>
</span></span><span class="line"><span class="cl"><span class="c1"># IP Family Policy:  SingleStack</span>
</span></span><span class="line"><span class="cl"><span class="c1"># IP Families:       IPv4</span>
</span></span><span class="line"><span class="cl"><span class="c1"># IP:                10.109.131.132</span>
</span></span><span class="line"><span class="cl"><span class="c1"># IPs:               10.109.131.132</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Port:              &lt;unset&gt;  80/TCP</span>
</span></span><span class="line"><span class="cl"><span class="c1"># TargetPort:        80/TCP</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Endpoints:         10.10.1.13:80,10.10.3.5:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Session Affinity:  None</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Events:            &lt;none&gt;</span>
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl">NAME                       READY   STATUS    RESTARTS   AGE    IP           NODE
</span></span><span class="line"><span class="cl">ngx-dep-6796688696-cwm8f   1/1     Running   <span class="m">0</span>          2m4s   10.10.3.5    woker02
</span></span><span class="line"><span class="cl">ngx-dep-6796688696-khjnv   1/1     Running   <span class="m">0</span>          2m2s   10.10.1.13   woker01
</span></span><span class="line"><span class="cl"><span class="c1"># same Endpoints</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 因为 Service、 Pod 的 IP 地址都是 Kubernetes 集群的内部网段</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   所以我们需要用 kubectl exec 进入到 Pod 内部</span>
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it ngx-dep-6796688696-cwm8f -- sh
</span></span><span class="line"><span class="cl">curl 10.109.131.132
</span></span><span class="line"><span class="cl"><span class="c1"># srv : 10.10.3.5:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: ngx-dep-6796688696-cwm8f</span>
</span></span><span class="line"><span class="cl"><span class="c1"># uri : GET 10.109.131.132 /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># date: 2023-02-22T10:09:49+00:00</span>
</span></span><span class="line"><span class="cl">curl 10.109.131.132
</span></span><span class="line"><span class="cl"><span class="c1"># srv : 10.10.1.13:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: ngx-dep-6796688696-khjnv</span>
</span></span><span class="line"><span class="cl"><span class="c1"># uri : GET 10.109.131.132 /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># date: 2023-02-22T10:09:50+00:00</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 测试恢复</span>
</span></span><span class="line"><span class="cl">kubectl delete pod ngx-dep-6796688696-khjnv
</span></span><span class="line"><span class="cl">kubectl describe svc ngx-svc
</span></span><span class="line"><span class="cl"><span class="c1"># Endpoints:         10.10.1.14:80,10.10.3.5:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 之前是 10.10.1.13:80,10.10.3.5:80</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 测试扩容</span>
</span></span><span class="line"><span class="cl">kubectl scale --replicas<span class="o">=</span><span class="m">5</span> deploy ngx-dep
</span></span><span class="line"><span class="cl">kubectl describe svc ngx-svc
</span></span><span class="line"><span class="cl"><span class="c1"># Endpoints:         10.10.1.14:80,10.10.1.15:80,10.10.3.5:80 + 2 more...</span>
</span></span></code></pre></div><p>Service 对象的域名完全形式是 <code>对象.名字空间.svc.cluster.local</code>，但很多时候也可以省略后面的部分，直接写 <code>对象.名字空间</code> 甚至 <code>对象名</code> 就足够了，默认会使用对象所在的名字空间（比如这里就是 default）。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Name:              ngx-svc</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Namespace:         default</span>
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it ngx-dep-6796688696-cwm8f -- sh
</span></span><span class="line"><span class="cl">curl ngx-svc
</span></span><span class="line"><span class="cl"><span class="c1"># srv : 10.10.3.5:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: ngx-dep-6796688696-cwm8f</span>
</span></span><span class="line"><span class="cl"><span class="c1"># uri : GET ngx-svc /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># date: 2023-02-22T10:19:25+00:00</span>
</span></span><span class="line"><span class="cl">curl ngx-svc.default
</span></span><span class="line"><span class="cl"><span class="c1"># srv : 10.10.3.6:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: ngx-dep-6796688696-lpcfs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># uri : GET ngx-svc.default /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># date: 2023-02-22T10:19:41+00:00</span>
</span></span><span class="line"><span class="cl">curl ngx-svc.default.svc.cluster.local
</span></span><span class="line"><span class="cl"><span class="c1"># srv : 10.10.3.5:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: ngx-dep-6796688696-cwm8f</span>
</span></span><span class="line"><span class="cl"><span class="c1"># uri : GET ngx-svc.default.svc.cluster.local /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># date: 2023-02-22T10:20:04+00:00</span>
</span></span></code></pre></div><p>Pod 分配了域名：<code>IP 地址.名字空间.pod.cluster.local</code> IP 地址 . 改成 -。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># kubectl explain svc.spec.type</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># vim ngx-svc.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f ngx-svc.yml
</span></span><span class="line"><span class="cl">kubectl get svc
</span></span><span class="line"><span class="cl"><span class="c1"># NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kubernetes   ClusterIP   10.96.0.1        &lt;none&gt;        443/TCP        5h56m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-svc      NodePort    10.109.131.132   &lt;none&gt;        80:30916/TCP   46m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Service 的默认类型是“ClusterIP”，只能在集群内部访问，</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   如果改成“NodePort”，就会在节点上开启一个随机端口号，让外界也能够访问内部的服务。</span>
</span></span><span class="line"><span class="cl">curl localhost:30916
</span></span><span class="line"><span class="cl"><span class="c1"># srv : 10.10.1.15:80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: ngx-dep-6796688696-l6skl</span>
</span></span><span class="line"><span class="cl"><span class="c1"># uri : GET localhost /</span>
</span></span><span class="line"><span class="cl"><span class="c1"># date: 2023-02-22T10:48:32+00:00</span>
</span></span></code></pre></div><img width="600" alt="image" src="https://user-images.githubusercontent.com/9289792/220598107-2899f665-3977-4e19-894d-bd273046fbae.png">
<h2 id="21-ingress-流量总管">21 Ingress 流量总管</h2>
<p>Service 本身是没有服务能力的，它只是一些 iptables 规则，真正配置、应用这些规则的实际上是节点里的 kube-proxy 组件。</p>
<p>Ingress 也只是一些 HTTP 路由规则的集合，相当于一份静态的描述文件，真正要把这些规则在集群里实施运行，还需要有另外一个东西，这就是 Ingress Controller，它的作用就相当于 Service 的 kube-proxy，能够读取、应用 Ingress 规则，处理、调度流量。</p>
<p>Ingress Class 是插在 Ingress 和 Ingress Controller 中间，作为流量规则和控制器的协调人，解除了 Ingress 和 Ingress Controller 的强绑定关系。</p>
<p>Kubernetes 用户可以转向管理 Ingress Class，用它来定义不同的业务逻辑分组，简化 Ingress 规则的复杂度。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">out</span><span class="o">=</span><span class="s2">&#34;--dry-run=client -o yaml&#34;</span>
</span></span><span class="line"><span class="cl">kubectl create ing ngx-ing --rule<span class="o">=</span><span class="s2">&#34;ngx.test/=ngx-svc:80&#34;</span> --class<span class="o">=</span>ngx-ink <span class="nv">$out</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim ingress.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-ing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-ink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">ngx.test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># 路径的匹配方式</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># 精确匹配（Exact）或前缀匹配（Prefix）</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Exact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">loadBalancer</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">IngressClass</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx-ink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">controller</span><span class="p">:</span><span class="w"> </span><span class="l">nginx.org/ingress-controller</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f ingress.yml
</span></span><span class="line"><span class="cl"><span class="c1"># NAME      CONTROLLER                     PARAMETERS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-ink   nginx.org/ingress-controller   &lt;none&gt;       15s</span>
</span></span><span class="line"><span class="cl">kubectl get ing
</span></span><span class="line"><span class="cl"><span class="c1"># NAME      CLASS     HOSTS      ADDRESS   PORTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx-ing   ngx-ink   ngx.test             80      84s</span>
</span></span><span class="line"><span class="cl">kubectl describe ing ngx-ing
</span></span><span class="line"><span class="cl"><span class="c1"># Name:             ngx-ing</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Labels:           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Namespace:        default</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Address:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default backend:  default-http-backend:80 (&lt;error: endpoints &#34;default-http-backend&#34; not found&gt;)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Rules:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   Host        Path  Backends</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   ----        ----  --------</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   ngx.test</span>
</span></span><span class="line"><span class="cl"><span class="c1">#               /   ngx-svc:80 (10.10.1.14:80,10.10.1.15:80)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Annotations:  &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Events:       &lt;none&gt;</span>
</span></span></code></pre></div><p>在 Kubernetes 里使用 Ingress Controller：</p>
<p><a href="https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/">https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/nginxinc/kubernetes-ingress.git --branch v3.0.2
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> kubernetes-ingress/deployments
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configure RBAC</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 为Ingress控制器创建一个命名空间和一个服务账户</span>
</span></span><span class="line"><span class="cl">kubectl apply -f common/ns-and-sa.yaml
</span></span><span class="line"><span class="cl"><span class="c1"># 为服务账户创建一个集群角色和集群角色绑定</span>
</span></span><span class="line"><span class="cl">kubectl apply -f rbac/rbac.yaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create Common Resources</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 为NGINX的默认服务器创建一个带有TLS证书和密钥的秘密</span>
</span></span><span class="line"><span class="cl">kubectl apply -f common/default-server-secret.yaml
</span></span><span class="line"><span class="cl"><span class="c1"># 创建一个 config map，用于定制NGINX配置。</span>
</span></span><span class="line"><span class="cl">kubectl apply -f common/nginx-config.yaml
</span></span><span class="line"><span class="cl"><span class="c1"># 创建一个IngressClass资源</span>
</span></span><span class="line"><span class="cl">kubectl apply -f common/ingress-class.yaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create Custom Resources</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kubectl apply -f common/crds/</span>
</span></span><span class="line"><span class="cl">vim deployment/nginx-ingress.yaml
</span></span><span class="line"><span class="cl"><span class="c1"># args add:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -enable-custom-resources=false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the Ingress Controller</span>
</span></span><span class="line"><span class="cl">kubectl apply -f deployment/nginx-ingress.yaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># check</span>
</span></span><span class="line"><span class="cl">kubectl get pods --namespace<span class="o">=</span>nginx-ingress
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                             READY   STATUS    RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># nginx-ingress-5f98f8f5f9-nnkv7   1/1     Running   0          3m14s</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get Access to the Ingress Controller</span>
</span></span><span class="line"><span class="cl">kubectl create -f service/nodeport.yaml
</span></span><span class="line"><span class="cl">kubectl get service -n nginx-ingress
</span></span><span class="line"><span class="cl"><span class="c1"># NAME            TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># nginx-ingress   NodePort   10.111.210.52   &lt;none&gt;        80:31754/TCP,443:30188/TCP   5s</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># debug</span>
</span></span><span class="line"><span class="cl">kubectl get IngressClass
</span></span><span class="line"><span class="cl">kubectl get ing -n nginx-ingress
</span></span><span class="line"><span class="cl">kubectl get deploy -n nginx-ingress
</span></span><span class="line"><span class="cl">kubectl get pod -n nginx-ingress -o wide
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl describe service -n nginx-ingress -o wide
</span></span><span class="line"><span class="cl">kubectl describe pod -n nginx-ingress
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 命令kubectl port-forward，它可以直接把本地的端口映射到 Kubernetes 集群的某个 Pod 里</span>
</span></span><span class="line"><span class="cl">kubectl port-forward -n nginx-ingress nginx-ingress-5f98f8f5f9-nnkv7 8080:80 <span class="p">&amp;</span>
</span></span></code></pre></div><img width="900" alt="image" src="https://user-images.githubusercontent.com/9289792/221135639-3b31c1be-f624-4abf-8851-cff396e9be45.png">
<h2 id="22-玩转-kubernetes-2">22 玩转 Kubernetes 2</h2>
<p>Kubernetes 部署 WordPress：</p>
<img width="900" alt="image" src="https://user-images.githubusercontent.com/9289792/221116386-7f08aa66-941c-42ac-a0c3-bb8a111fc42d.png">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim wp-maria.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-cm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DATABASE</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;db&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">USER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;123&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;123&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">maria-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">maria-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">maria-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mariadb:10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mariadb</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">3306</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">envFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;MARIADB_&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">configMapRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-cm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">maria-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">3306</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">3306</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">maria-dep</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f wp-maria.yml
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">kubectl get deploy
</span></span><span class="line"><span class="cl">kubectl get svc
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim wp-app.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-cm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># DNS HOST</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;maria-svc&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">USER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;123&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">NAME</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;db&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress:5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">envFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;WORDPRESS_DB_&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">configMapRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-cm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># 指定端口</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nodePort</span><span class="p">:</span><span class="w"> </span><span class="m">30088</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># NodePort</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f wp-app.yml
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">kubectl get deploy
</span></span><span class="line"><span class="cl">kubectl get svc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl port-forward service/wp-svc 80:80 --address 0.0.0.0
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim wp-ing.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">IngressClass</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-ink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">controller</span><span class="p">:</span><span class="w"> </span><span class="l">nginx.org/ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># kubectl create ing wp-ing --rule=&#34;wp.test/=wp-svc:80&#34; --class=wp-ink $out</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-ing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">wp-ink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">wp.test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim wp-kic-dep.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-kic-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-kic-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-kic-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># kubectl explain Deployment.spec.template.spec.serviceAccountName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># kubectl explain Deployment.spec.template.spec.hostNetwork</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hostNetwork</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx/nginx-ingress:3.0.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">IfNotPresent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">readiness-port</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8081</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">9113</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">readinessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/nginx-ready</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="l">readiness-port</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">securityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">allowPrivilegeEscalation</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">runAsUser</span><span class="p">:</span><span class="w"> </span><span class="m">101</span><span class="w"> </span><span class="c">#nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">runAsNonRoot</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">capabilities</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">drop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span>- <span class="l">ALL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span>- <span class="l">NET_BIND_SERVICE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">POD_NAMESPACE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">fieldRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="l">metadata.namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">POD_NAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">fieldRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="l">metadata.name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># 默认是 nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- -<span class="l">ingress-class=wp-ink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- -<span class="l">enable-custom-resources=false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- -<span class="l">nginx-configmaps=$(POD_NAMESPACE)/nginx-config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- -<span class="l">default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-kic-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nodePort</span><span class="p">:</span><span class="w"> </span><span class="m">30080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wp-kic-dep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f wp-ing.yml -f wp-kic-dep.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get ing
</span></span><span class="line"><span class="cl">kubectl get ingressclass
</span></span><span class="line"><span class="cl">kubectl get pod -n<span class="o">=</span>nginx-ingress
</span></span><span class="line"><span class="cl">kubectl describe pod -n<span class="o">=</span>nginx-ingress
</span></span><span class="line"><span class="cl">kubectl get deploy -n<span class="o">=</span>nginx-ingress
</span></span><span class="line"><span class="cl">kubectl get svc -n<span class="o">=</span>nginx-ingress
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 在服务器上</span>
</span></span><span class="line"><span class="cl">kubectl get pod -n<span class="o">=</span>nginx-ingress -o<span class="o">=</span>wide
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                          READY   STATUS    RESTARTS   AGE   IP           NODE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># wp-kic-dep-68579bc688-d64zs   1/1     Running   0          10m   172.21.0.9   woker01</span>
</span></span><span class="line"><span class="cl">curl 172.21.0.9 -H <span class="s2">&#34;HOST: wp.test&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 在服务器外</span>
</span></span><span class="line"><span class="cl">kubectl port-forward service/wp-kic-svc -n<span class="o">=</span>nginx-ingress 80:80 --address 0.0.0.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">vim /etc/hosts
</span></span><span class="line"><span class="cl"><span class="o">[</span>master ip<span class="o">]</span> wp.test
</span></span><span class="line"><span class="cl"><span class="c1"># 游览器访问 wp.test</span>
</span></span></code></pre></div><img width="900" alt="image" src="https://user-images.githubusercontent.com/9289792/221134023-69087496-f390-4b6a-90ca-9e7dde86bfc1.png">
<h2 id="23-中级篇实操总结">23 中级篇实操总结</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># DaemonSet 模板生成</span>
</span></span><span class="line"><span class="cl">kubectl create deploy redis-ds --image<span class="o">=</span>redis:5-alpine <span class="nv">$out</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> sed <span class="s1">&#39;s/Deployment/DaemonSet/g&#39;</span> - <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> sed -e <span class="s1">&#39;/replicas/d&#39;</span> -
</span></span></code></pre></div><h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/chronolaw/k8s_study">chronolaw/k8s_study | GitHub</a></li>
<li><a href="https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/">nginx-ingress-controller Installation with Manifests | nginx.com</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>Kubernetes 入门实战 Part1</title>
      <link>https://zyf.im/2023/02/15/kubernetes-getting-started-part1/</link>
      <pubDate>Wed, 15 Feb 2023 14:46:38 +0000</pubDate>
      <guid>https://zyf.im/2023/02/15/kubernetes-getting-started-part1/</guid>
      <description>&lt;h2 id=&#34;01-初识-docker&#34;&gt;01 初识 Docker&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.docker.com/get-started/overview/&#34;&gt;https://docs.docker.com/get-started/overview/&lt;/a&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -fsSL https://get.docker.com &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo sh
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# sudo usermod -aG docker $USER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# newgrp docker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;dockerd-rootless-setuptool.sh install
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker version
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker info
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker pull busybox
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# show all images&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker images
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;02-被隔离的进程&#34;&gt;02 被隔离的进程&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 拉取轻量级 Alpine Linux 镜像&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker pull alpine
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 以交互模式运行容器并进入 shell&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker run -it alpine sh
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 查看容器内的操作系统信息&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cat /etc/os-release
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Docker 实现资源隔离的三种核心技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;namespace：隔离进程、网络、文件系统等资源视图&lt;/li&gt;
&lt;li&gt;cgroup：限制和监控资源使用（CPU、内存、磁盘 I/O 等）&lt;/li&gt;
&lt;li&gt;chroot/pivot_root：改变进程的根目录，实现文件系统隔离&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;03-容器化的应用&#34;&gt;03 容器化的应用&lt;/h2&gt;
&lt;p&gt;Build once, Run anywhere.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="01-初识-docker">01 初识 Docker</h2>
<p><a href="https://docs.docker.com/get-started/overview/">https://docs.docker.com/get-started/overview/</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSL https://get.docker.com <span class="p">|</span> sudo sh
</span></span><span class="line"><span class="cl"><span class="c1"># sudo usermod -aG docker $USER</span>
</span></span><span class="line"><span class="cl"><span class="c1"># newgrp docker</span>
</span></span><span class="line"><span class="cl">dockerd-rootless-setuptool.sh install
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker version
</span></span><span class="line"><span class="cl">docker info
</span></span><span class="line"><span class="cl">docker ps
</span></span><span class="line"><span class="cl">docker pull busybox
</span></span><span class="line"><span class="cl"><span class="c1"># show all images</span>
</span></span><span class="line"><span class="cl">docker images
</span></span></code></pre></div><h2 id="02-被隔离的进程">02 被隔离的进程</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 拉取轻量级 Alpine Linux 镜像</span>
</span></span><span class="line"><span class="cl">docker pull alpine
</span></span><span class="line"><span class="cl"><span class="c1"># 以交互模式运行容器并进入 shell</span>
</span></span><span class="line"><span class="cl">docker run -it alpine sh
</span></span><span class="line"><span class="cl"><span class="c1"># 查看容器内的操作系统信息</span>
</span></span><span class="line"><span class="cl">cat /etc/os-release
</span></span></code></pre></div><p>Docker 实现资源隔离的三种核心技术：</p>
<ul>
<li>namespace：隔离进程、网络、文件系统等资源视图</li>
<li>cgroup：限制和监控资源使用（CPU、内存、磁盘 I/O 等）</li>
<li>chroot/pivot_root：改变进程的根目录，实现文件系统隔离</li>
</ul>
<h2 id="03-容器化的应用">03 容器化的应用</h2>
<p>Build once, Run anywhere.</p>
<p>应用程序不再直接和操作系统打交道，而是封装成镜像，再交给容器环境去运行。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># remove image</span>
</span></span><span class="line"><span class="cl">docker rmi busybox
</span></span><span class="line"><span class="cl"><span class="c1"># 开启一个交互式操作的 Shell</span>
</span></span><span class="line"><span class="cl">docker run -it busybox
</span></span><span class="line"><span class="cl"><span class="c1"># 在后台运行</span>
</span></span><span class="line"><span class="cl">docker run -d busybox
</span></span><span class="line"><span class="cl"><span class="c1"># 为容器起一个名字</span>
</span></span><span class="line"><span class="cl">docker run -d --name xxx busybox
</span></span><span class="line"><span class="cl"><span class="c1"># 不保存容器，只要运行完毕就自动清除</span>
</span></span><span class="line"><span class="cl">docker run --rm busybox <span class="nb">echo</span> <span class="s2">&#34;hello docker&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker stop xxx
</span></span><span class="line"><span class="cl"><span class="c1"># remove container</span>
</span></span><span class="line"><span class="cl">docker rm xxx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># show all container</span>
</span></span><span class="line"><span class="cl">docker ps -a
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> xxx <span class="nb">echo</span> <span class="s2">&#34;hello docker&#34;</span>
</span></span></code></pre></div><h2 id="05-镜像仓库">05 镜像仓库</h2>
<ul>
<li><a href="https://github.com/docker-library/official-images">https://github.com/docker-library/official-images</a></li>
<li><a href="https://hub.docker.com/u/library">https://hub.docker.com/u/library</a></li>
</ul>
<p><code>用户名/应用名:标签</code> 官方镜像用户名是 <code>library</code>。</p>
<ul>
<li>slim 经过精简的</li>
<li>fat 包含了较多的辅助工具</li>
<li>rc 候选版本，release candidate</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t
</span></span><span class="line"><span class="cl">docker tag ngx-app chronolaw/ngx-app:1.0
</span></span><span class="line"><span class="cl">docker push chronolaw/ngx-app:1.0
</span></span></code></pre></div><p>save 和 load 这两个镜像归档命令：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker save ngx-app:latest -o ngx.tar
</span></span><span class="line"><span class="cl">docker load -i ngx.tar
</span></span></code></pre></div><h2 id="06-打破次元壁">06 打破次元壁</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d --rm --name ubu phusion/baseimage:jammy-1.0.1
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;hello&#34;</span> &gt; a.txt
</span></span><span class="line"><span class="cl"><span class="c1"># a.txt 拷贝到容器 tmp</span>
</span></span><span class="line"><span class="cl">docker cp a.txt ubu:/tmp
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it ubu bash
</span></span><span class="line"><span class="cl"><span class="c1"># 考出容器</span>
</span></span><span class="line"><span class="cl">docker cp ubu:/tmp/a.txt ./b.txt
</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="cl"><span class="c1"># --mount</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -v /tmp:/tmp:ro 只读</span>
</span></span><span class="line"><span class="cl">docker run -d --rm -v /tmp:/tmp --name ubu phusion/baseimage:jammy-1.0.1
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it ubu bash
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker pull python:alpine
</span></span><span class="line"><span class="cl">docker run -it --rm -v <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>:/tmp python:alpine sh
</span></span></code></pre></div><p>网络模式：<code>null</code> <code>host</code> <code>bridge</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># host</span>
</span></span><span class="line"><span class="cl">docker run -d --rm --net<span class="o">=</span>host --name<span class="o">=</span>ng nginx:alpine
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> ng ip addr
</span></span><span class="line"><span class="cl">docker inspect ng <span class="p">|</span> grep IPAddress
</span></span><span class="line"><span class="cl">docker stop ng
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># bridge 默认模式</span>
</span></span><span class="line"><span class="cl">docker run -d --rm --name<span class="o">=</span>ng nginx:alpine
</span></span><span class="line"><span class="cl">docker inspect ng <span class="p">|</span> grep IPAddress
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;IPAddress&#34;: &#34;172.17.0.3&#34;</span>
</span></span><span class="line"><span class="cl">docker run -d --rm --name<span class="o">=</span>rd redis
</span></span><span class="line"><span class="cl">docker inspect rd <span class="p">|</span> grep IPAddress
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;IPAddress&#34;: &#34;172.17.0.4&#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="cl">docker run -d -p 80:80 --rm nginx:alpine
</span></span><span class="line"><span class="cl">docker run -d -p 8080:80 --rm nginx:alpine
</span></span><span class="line"><span class="cl"><span class="c1"># 分别“映射”到了两个容器里的 80 端口</span>
</span></span></code></pre></div><h2 id="07-玩转-docker">07 玩转 Docker</h2>
<p>Container Image Registry</p>
<p><a href="https://registry.hub.docker.com/_/registry/">https://registry.hub.docker.com/_/registry/</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d -p 5000:5000 registry
</span></span><span class="line"><span class="cl"><span class="c1"># 使用 docker tag 命令给镜像打标签再上传</span>
</span></span><span class="line"><span class="cl">docker tag nginx:alpine 127.0.0.1:5000/nginx:alpine
</span></span><span class="line"><span class="cl">docker push 127.0.0.1:5000/nginx:alpine
</span></span><span class="line"><span class="cl"><span class="c1"># 本次重新拉取测试</span>
</span></span><span class="line"><span class="cl">docker rmi 127.0.0.1:5000/nginx:alpine
</span></span><span class="line"><span class="cl">docker pull 127.0.0.1:5000/nginx:alpine
</span></span></code></pre></div><p><a href="https://docs.docker.com/registry/spec/api/">https://docs.docker.com/registry/spec/api/</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Listing Repositories</span>
</span></span><span class="line"><span class="cl">curl 127.1:5000/v2/_catalog
</span></span><span class="line"><span class="cl"><span class="c1"># {&#34;repositories&#34;:[&#34;nginx&#34;]}</span>
</span></span><span class="line"><span class="cl">curl 127.1:5000/v2/nginx/tags/list
</span></span><span class="line"><span class="cl"><span class="c1"># {&#34;name&#34;:&#34;nginx&#34;,&#34;tags&#34;:[&#34;alpine&#34;]}</span>
</span></span></code></pre></div><p>registry 默认会把镜像存储在 Docker 内部目录 <code>/var/lib/registry</code>。</p>
<p>搭建 WordPress：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d --rm <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">MARIADB_DATABASE</span><span class="o">=</span>db <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">MARIADB_USER</span><span class="o">=</span>wp <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">MARIADB_PASSWORD</span><span class="o">=</span><span class="m">123</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">MARIADB_ROOT_PASSWORD</span><span class="o">=</span><span class="m">123</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --name mariadb <span class="se">\
</span></span></span><span class="line"><span class="cl">    mariadb:10
</span></span><span class="line"><span class="cl"><span class="c1"># 进入 mariadb</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it mariadb mysql -uwp -p123
</span></span><span class="line"><span class="cl"><span class="c1"># show mariadb ip</span>
</span></span><span class="line"><span class="cl">docker inspect mariadb <span class="p">|</span> grep IPAddress
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;IPAddress&#34;: &#34;172.17.0.2&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker run -d --rm <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">WORDPRESS_DB_HOST</span><span class="o">=</span>172.17.0.2 <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">WORDPRESS_DB_USER</span><span class="o">=</span>wp <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">WORDPRESS_DB_PASSWORD</span><span class="o">=</span><span class="m">123</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --env <span class="nv">WORDPRESS_DB_NAME</span><span class="o">=</span>db <span class="se">\
</span></span></span><span class="line"><span class="cl">    --name wp <span class="se">\
</span></span></span><span class="line"><span class="cl">    wordpress:5
</span></span><span class="line"><span class="cl">docker inspect wp <span class="p">|</span> grep IPAddress
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;IPAddress&#34;: &#34;172.17.0.4&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vim wp.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">server <span class="o">{</span>
</span></span><span class="line"><span class="cl">  listen 80<span class="p">;</span>
</span></span><span class="line"><span class="cl">  default_type text/html<span class="p">;</span>
</span></span><span class="line"><span class="cl">  location / <span class="o">{</span>
</span></span><span class="line"><span class="cl">      proxy_http_version 1.1<span class="p">;</span>
</span></span><span class="line"><span class="cl">      proxy_set_header Host <span class="nv">$host</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># wordpress server</span>
</span></span><span class="line"><span class="cl">      proxy_pass http://172.17.0.4<span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 感觉可以直接用 wordpress 的 80，无需再代理一次</span>
</span></span><span class="line"><span class="cl">docker run -d --rm <span class="se">\
</span></span></span><span class="line"><span class="cl">    -p 80:80 <span class="se">\
</span></span></span><span class="line"><span class="cl">    -v <span class="sb">`</span><span class="nb">pwd</span><span class="sb">`</span>/wp.conf:/etc/nginx/conf.d/default.conf <span class="se">\
</span></span></span><span class="line"><span class="cl">    --name ng <span class="se">\
</span></span></span><span class="line"><span class="cl">    nginx:alpine
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># show logs</span>
</span></span><span class="line"><span class="cl">docker logs mariadb
</span></span><span class="line"><span class="cl">docker logs ng
</span></span><span class="line"><span class="cl">docker logs wp
</span></span></code></pre></div><h2 id="08-入门篇总结">08 入门篇总结</h2>
<p>构建自己的镜像：</p>
<p><a href="https://github.com/chronolaw/k8s_study/blob/master/ch1/Dockerfile">https://github.com/chronolaw/k8s_study/blob/master/ch1/Dockerfile</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">ARG</span> <span class="nv">IMAGE_BASE</span><span class="o">=</span><span class="s2">&#34;nginx&#34;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ARG</span> <span class="nv">IMAGE_TAG</span><span class="o">=</span><span class="s2">&#34;1.21-alpine&#34;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">${IMAGE_BASE}:${IMAGE_TAG</span><span class="o">}</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">PATH</span><span class="o">=</span><span class="nv">$PATH</span>:/tmp<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">DEBUG</span><span class="o">=</span>OFF
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">COPY</span> ./default.conf /etc/nginx/conf.d/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> <span class="nb">cd</span> /usr/share/nginx/html <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;hello nginx&#34;</span> &gt; a.txt<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">8081</span> <span class="m">8082</span> <span class="m">8083</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/etc/nginx</span><span class="err">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t ngx-app:1.0 .
</span></span><span class="line"><span class="cl">docker run -it --rm ngx-app:1.0 sh
</span></span><span class="line"><span class="cl">docker save ngx-app:1.0 -o ngx.tar
</span></span><span class="line"><span class="cl">docker load -i ngx.tar
</span></span></code></pre></div><h2 id="09-kubernetes-环境">09 Kubernetes 环境</h2>
<p>Kubernetes 就是一个 <a href="https://kubernetes.io/">Production-Grade Container Orchestration</a> 生产级别的容器编排平台和集群管理系统。</p>
<p><a href="https://minikube.sigs.k8s.io/docs/start/">minikube</a> quickly sets up a local Kubernetes cluster on macOS, Linux, and Windows.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install minikube
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">minikube version
</span></span><span class="line"><span class="cl"><span class="c1"># minikube version: v1.37.0</span>
</span></span><span class="line"><span class="cl"><span class="c1"># commit: 65318f4cfff9c12cc87ec9eb8f4cdd57b25047f3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 统一实验环境</span>
</span></span><span class="line"><span class="cl">minikube start
</span></span><span class="line"><span class="cl"><span class="c1"># 查看状态</span>
</span></span><span class="line"><span class="cl">minikube status
</span></span><span class="line"><span class="cl"><span class="c1"># type: Control Plane</span>
</span></span><span class="line"><span class="cl"><span class="c1"># host: Running</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kubelet: Running</span>
</span></span><span class="line"><span class="cl"><span class="c1"># apiserver: Running</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kubeconfig: Configured</span>
</span></span><span class="line"><span class="cl">minikube node list
</span></span><span class="line"><span class="cl"><span class="c1"># minikube 192.168.49.2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 登录到这个节点上</span>
</span></span><span class="line"><span class="cl">minikube ssh
</span></span><span class="line"><span class="cl">uname -a
</span></span><span class="line"><span class="cl"><span class="c1"># Linux minikube 6.17.8-orbstack-00308-g8f9c941121b1 #1 SMP PREEMPT Thu Nov 20 09:34:02 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux</span>
</span></span><span class="line"><span class="cl">ip add
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># minikube 管理 Kubernetes 集群环境</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kubectl 操作实际的 Kubernetes 功能</span>
</span></span><span class="line"><span class="cl"><span class="c1"># install kubectl</span>
</span></span><span class="line"><span class="cl">minikube kubectl
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">minikube kubectl -- version
</span></span><span class="line"><span class="cl"><span class="c1"># Client Version: v1.34.0</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Kustomize Version: v5.7.1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server Version: v1.34.0</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">kubectl</span><span class="o">=</span><span class="s2">&#34;minikube kubectl --&#34;</span>
</span></span><span class="line"><span class="cl">kubectl version
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 启动一个镜像</span>
</span></span><span class="line"><span class="cl">kubectl run ngx --image<span class="o">=</span>nginx:alpine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># like docker ps</span>
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl"><span class="c1"># NAME   READY   STATUS              RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ngx    0/1     ContainerCreating   0          7s</span>
</span></span></code></pre></div><h2 id="10-kubernetes-工作机制">10 Kubernetes 工作机制</h2>
<p>Kubernetes 架构基于流行的“控制面 Control Plane / 数据面 Data Plane”设计。集群由多个“节点”（Node）组成，底层环境支持物理机或虚拟机。</p>
<p>按照功能划分，集群节点主要分为两类：负责“大脑”工作的 Master 节点（控制面），用于执行管理和调度任务；以及负责“体力”工作的 Worker 节点（数据面），用于运行具体的业务应用。</p>
<img width="600" alt="image" src="https://user-images.githubusercontent.com/9289792/219293821-a4e1620a-c024-4edf-ba29-a5bc1f8d3f57.png">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get node
</span></span><span class="line"><span class="cl"><span class="c1"># NAME       STATUS   ROLES           AGE   VERSION</span>
</span></span><span class="line"><span class="cl"><span class="c1"># minikube   Ready    control-plane   11m   v1.34.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 查看 master Component pod</span>
</span></span><span class="line"><span class="cl">kubectl get pod -n kube-system
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                               READY   STATUS    RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># coredns-66bc5c9577-6c27s           1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># etcd-minikube                      1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kube-apiserver-minikube            1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kube-controller-manager-minikube   1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kube-proxy-9v5wf                   1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># kube-scheduler-minikube            1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl"><span class="c1"># storage-provisioner                1/1     Running   0          11m</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># worker node</span>
</span></span><span class="line"><span class="cl">minikube ssh
</span></span><span class="line"><span class="cl"><span class="c1"># show kube-proxy</span>
</span></span><span class="line"><span class="cl">docker ps <span class="p">|</span> grep kube-proxy
</span></span><span class="line"><span class="cl"><span class="c1"># show kubelet, not exist docker</span>
</span></span><span class="line"><span class="cl">ps -ef <span class="p">|</span> grep kubelet
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># show addons list</span>
</span></span><span class="line"><span class="cl">minikube addons list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">minikube dashboard
</span></span><span class="line"><span class="cl"><span class="c1"># 🎉  Opening http://127.0.0.1:50733/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...</span>
</span></span></code></pre></div><img width="600" alt="image" src="https://user-images.githubusercontent.com/9289792/219294564-12655273-044c-4d56-a99a-d6f6e7cf7525.png">
<h2 id="11-yaml">11 YAML</h2>
<p>YAML 在语法上是 JSON 的超集，常被用作 Kubernetes 的配置文件格式。</p>
<p>其次，我们需要理解两种思维模式：Shell 脚本和 Dockerfile 是典型的 <strong>“命令式”</strong>，它告诉机器“第一步做什么，第二步做什么”；而 Kubernetes 推崇 <strong>“声明式”</strong>，它只告诉机器“我想要什么样的结果”，具体的步骤由系统自己完成。</p>
<p>最后，Kubernetes 的核心组件 apiserver 借鉴了 HTTP 协议中“资源”的概念，使用了标准的 RESTful 风格。在 K8s 的体系里，所有被管理的内容（如 Pod, Service）都被抽象成了 <strong>“API 对象”</strong> 来进行操作。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 查看 kubectl api-service 支持的所有对象</span>
</span></span><span class="line"><span class="cl">kubectl api-resources
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 显示出详细的命令执行过程</span>
</span></span><span class="line"><span class="cl">kubectl get pod --v<span class="o">=</span><span class="m">9</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 自带的 API 文档 https://kubernetes.io/docs/reference/kubernetes-api/</span>
</span></span><span class="line"><span class="cl">kubectl explain pod
</span></span><span class="line"><span class="cl">kubectl explain pod.metadata
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 命令式</span>
</span></span><span class="line"><span class="cl">kubectl run ngx --image<span class="o">=</span>nginx:alpine
</span></span><span class="line"><span class="cl"><span class="c1"># 转换为 YAML 声名式</span>
</span></span><span class="line"><span class="cl">kubectl run ngx --image<span class="o">=</span>nginx:alpine --dry-run<span class="o">=</span>client -o yaml &gt; ngx-pod.yml
</span></span><span class="line"><span class="cl">kubectl apply -f ngx-pod.yml
</span></span><span class="line"><span class="cl">kubectl delete -f ngx-pod.yml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dnsPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterFirst</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><h2 id="12-pod">12 Pod</h2>
<p>既然 Docker 容器提倡“一个容器只跑一个进程”，那么当我们需要多个紧密协作的进程（多应用联合运行）共同工作时，我们不能破坏单容器单进程的隔离原则（不破坏容器隔离），所以我们需要在容器之上引入一个更高层级的抽象对象（收纳舱/Pod），让这些容器在共享网络和存储的同时，依然保持镜像层面的独立。</p>
<img width="600" alt="image" src="https://user-images.githubusercontent.com/9289792/219309091-7436a44b-c0e4-4e8f-8dc1-7a208122ae7d.png">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f ngx-pod.yml
</span></span><span class="line"><span class="cl">kubectl logs ngx
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">kubectl describe pod ngx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># cp file to pod</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;aaa&#39;</span> &gt; a.txt
</span></span><span class="line"><span class="cl">kubectl cp a.txt ngx:/tmp
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># exec need --</span>
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it ngx -- sh
</span></span><span class="line"><span class="cl">cat /tmp/a.txt
</span></span></code></pre></div><h2 id="13-job-cronjob-离线业务">13 Job CronJob 离线业务</h2>
<p>单一职责 意味着每个容器应当只运行一个核心进程。避免“大而全”的巨型容器，保持容器功能的单一和纯粹，才能让它们像乐高积木一样被灵活复用和独立升级。</p>
<p>组合优于继承 意味着应该通过“组合”多个独立的容器（Pod 模式）来构建复杂的应用，而不是通过“继承”基础镜像并在其上层层叠加所有功能（巨型镜像）。这种松耦合的方式，允许应用在运行时通过 Sidecar 等模式灵活协作，而不是被固化在同一个镜像文件里。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create job echo-job --image<span class="o">=</span>busybox --dry-run<span class="o">=</span>client -o yaml &gt; echo-job.yml
</span></span><span class="line"><span class="cl"><span class="c1"># 告诉 k8s 我想创建一个名为 echo-job 的任务（Job）。</span>
</span></span><span class="line"><span class="cl"><span class="c1"># --dry-run=client 告诉 kubectl，“你假装执行一下创建命令，检查一下语法，但是千万不要真的把请求发给 Kubernetes API Server，也不要真的在集群里创建这个 Job”</span>
</span></span><span class="line"><span class="cl">vim echo-job.yml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">busybox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># 补充输出</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;echo&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;hello world&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># -----</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Never</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f echo-job.yml
</span></span><span class="line"><span class="cl">kubectl get job
</span></span><span class="line"><span class="cl"><span class="c1"># NAME       COMPLETIONS   DURATION   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># echo-job   1/1           16s        33s</span>
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl"><span class="c1"># NAME             READY   STATUS      RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># echo-job-59sgn   0/1     Completed   0          68s</span>
</span></span><span class="line"><span class="cl">kubectl logs echo-job-59sgn
</span></span><span class="line"><span class="cl"><span class="c1"># hello world</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sleep-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 设置 Pod 运行的超时时间</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">activeDeadlineSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 设置 Pod 的失败重试次数</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">backoffLimit</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Job 完成需要运行多少个 Pod，默认是 1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">completions</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># 它与 completions 相关，表示允许并发运行的 Pod 数量，避免过多占用资源</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">parallelism</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">busybox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># 随机休眠</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">            </span>- <span class="l">sh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- -<span class="l">c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="l">sleep $(($RANDOM % 10 + 1)) &amp;&amp; echo done</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Never</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f sleep-job.yaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get pod -w
</span></span><span class="line"><span class="cl"><span class="c1"># NAME              READY   STATUS      RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># sleep-job-92m4d   0/1     Completed   0          30s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># sleep-job-g8pmj   0/1     Completed   0          15s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># sleep-job-tsncl   0/1     Completed   0          30s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># sleep-job-x4qlp   0/1     Completed   0          15s</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># cronjob</span>
</span></span><span class="line"><span class="cl">kubectl create cj echo-cj --image<span class="o">=</span>busybox --schedule<span class="o">=</span><span class="s2">&#34;*/1 * * * *&#34;</span> --dry-run<span class="o">=</span>client -o yaml &gt; echo-cj.yaml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">CronJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo-cj</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jobTemplate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo-cj</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">busybox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo-cj</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="c"># 补充输出</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/bin/echo&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;hello&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;world&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">OnFailure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;*/1 * * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f echo-cj.yaml
</span></span><span class="line"><span class="cl">kubectl get cj
</span></span><span class="line"><span class="cl"><span class="c1"># NAME      SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># echo-cj   */1 * * * *   False     1        5s              8s</span>
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                     READY   STATUS      RESTARTS   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># echo-cj-27942326-qng4j   0/1     Completed   0          93s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># echo-cj-27942327-vlrvs   0/1     Completed   0          33s</span>
</span></span></code></pre></div><h2 id="14-configmap-secret-管理配置信息">14 ConfigMap Secret 管理配置信息</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create cm info --from-literal<span class="o">=</span><span class="nv">name</span><span class="o">=</span>zhao --dry-run<span class="o">=</span>client -o yaml &gt; cm.yml
</span></span><span class="line"><span class="cl">kubectl apply -f cm.yml
</span></span><span class="line"><span class="cl">kubectl get cm
</span></span><span class="line"><span class="cl">kubectl describe cm info
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create secret generic user --from-literal<span class="o">=</span><span class="nv">name</span><span class="o">=</span>root --dry-run<span class="o">=</span>client -o yaml &gt; secret.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># -n 去掉字符串里隐含的换行符</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;root&#34;</span> <span class="p">|</span> base64
</span></span><span class="line"><span class="cl">kubectl apply -f secret.yml
</span></span><span class="line"><span class="cl">kubectl get secret
</span></span><span class="line"><span class="cl">kubectl describe secret user
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl explain pod.spec.containers.env.valueFrom
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">creationTimestamp</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">NAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">configMapKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">info</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">SNAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dnsPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterFirst</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f env-pod.yml
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it ngx -- sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nv">$NAME</span> <span class="nv">$SNAME</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># 以 Volume 的方式使用 ConfigMap/Secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vol-pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Volume 属于 Pod 与 containers 同级</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cm-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">configMap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">info</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sec-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># 挂载到容器里的某个路径下</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/tmp/cm-items</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cm-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/tmp/sec-items</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sec-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ngx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dnsPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterFirst</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vim vol-pod.yml
</span></span><span class="line"><span class="cl">kubectl apply -f vol-pod.yml
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it vol-pod -- sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat /tmp/cm-items/name
</span></span><span class="line"><span class="cl">cat /tmp/sec-items/name
</span></span><span class="line"><span class="cl"><span class="c1"># ConfigMap 和 Secret 都变成了目录的形式，而它们里面的 Key-Value 变成了一个个的文件，而文件名就是 Key。</span>
</span></span></code></pre></div><h2 id="15-玩转-kubernetes">15 玩转 Kubernetes</h2>
<img width="600" alt="image" src="https://user-images.githubusercontent.com/9289792/219537942-31e778eb-888d-4331-8d65-03ae4095b003.png">
<p>搭建 WordPress 环境：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim mariadb-pod.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-cm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DATABASE</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;db&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">USER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;123&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;123&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l">database</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mariadb:10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">IfNotPresent</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">        </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">3306</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">envFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;MARIADB_&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">configMapRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">maria-cm</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f mariadb-pod.yml
</span></span><span class="line"><span class="cl"><span class="c1"># 获取 IP 地址需要加上参数 -o wide</span>
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl"><span class="c1"># NAME        READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES</span>
</span></span><span class="line"><span class="cl"><span class="c1"># maria-pod   1/1     Running   0          64s   172.17.0.5   minikube   &lt;none&gt;           &lt;none&gt;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vim wp-pod.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-cm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># MariaDB Pod 的 IP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;172.17.0.5&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">USER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;wp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;123&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">NAME</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;db&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l">website</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress:5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-pod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">IfNotPresent</span><span class="w">
</span></span></span><span class="line"><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="cl"><span class="w">        </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">envFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;WORDPRESS_DB_&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">configMapRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">wp-cm</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f wp-pod.yml
</span></span><span class="line"><span class="cl">kubectl get pod -o wide
</span></span><span class="line"><span class="cl"><span class="c1"># NAME        READY   STATUS    RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES</span>
</span></span><span class="line"><span class="cl"><span class="c1"># maria-pod   1/1     Running   0          160m    172.17.0.5   minikube   &lt;none&gt;           &lt;none&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># wp-pod      1/1     Running   0          2m47s   172.17.0.6   minikube   &lt;none&gt;           &lt;none&gt;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 本地的 “8080” 映射到 WordPress Pod 的“80”</span>
</span></span><span class="line"><span class="cl">kubectl port-forward wp-pod 8080:80 <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Forwarding from 127.0.0.1:8080 -&gt; 80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Forwarding from [::1]:8080 -&gt; 80</span>
</span></span><span class="line"><span class="cl"><span class="c1"># fg</span>
</span></span></code></pre></div><h2 id="16-初级篇总结">16 初级篇总结</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">minikube version
</span></span><span class="line"><span class="cl">minikube status
</span></span><span class="line"><span class="cl">minikube start --kubernetes-version<span class="o">=</span>v1.23.3
</span></span><span class="line"><span class="cl">minikube node list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl version
</span></span><span class="line"><span class="cl">kubectl run ngx --image<span class="o">=</span>nginx:alpine
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># apiserver 等核心组件是在 kube-system 名字空间</span>
</span></span><span class="line"><span class="cl">kubectl get pod -n kube-system
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl api-resources
</span></span><span class="line"><span class="cl">kubectl explain pod.metadata
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">out</span><span class="o">=</span><span class="s2">&#34;--dry-run=client -o yaml&#34;</span>
</span></span><span class="line"><span class="cl">kubectl run ngx --image<span class="o">=</span>nginx:alpine <span class="nv">$out</span> &gt; pod.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl apply -f ngx-pod.yml
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">kubectl logs ngx-pod
</span></span><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it ngx-pod -- sh
</span></span><span class="line"><span class="cl">kubectl delete -f ngx-pod
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create job echo-job --image<span class="o">=</span>busybox <span class="nv">$out</span>
</span></span><span class="line"><span class="cl">kubectl apply -f job.yml
</span></span><span class="line"><span class="cl">kubectl get job
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">kubectl logs echo-job-l52l7
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create cj echo-cj --image<span class="o">=</span>busybox --schedule<span class="o">=</span><span class="s2">&#34;* * * * *&#34;</span> <span class="nv">$out</span>
</span></span><span class="line"><span class="cl">kubectl apply -f cronjob.yml
</span></span><span class="line"><span class="cl">kubectl get cj
</span></span><span class="line"><span class="cl">kubectl get pod
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create cm info --from-literal<span class="o">=</span><span class="nv">k</span><span class="o">=</span>v <span class="nv">$out</span>
</span></span><span class="line"><span class="cl">kubectl get cm
</span></span><span class="line"><span class="cl">kubectl describe cm info
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create secret generic user --from-literal<span class="o">=</span><span class="nv">name</span><span class="o">=</span>root <span class="nv">$out</span>
</span></span><span class="line"><span class="cl">kubectl get secret
</span></span><span class="line"><span class="cl">kubectl describe secret user
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nv">cm9vdA</span><span class="o">==</span> <span class="p">|</span> base64 -d
</span></span></code></pre></div><h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/chronolaw/k8s_study">chronolaw/k8s_study | GitHub</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>Go 语言第一课</title>
      <link>https://zyf.im/2023/02/01/go-language-lesson-one/</link>
      <pubDate>Wed, 01 Feb 2023 11:44:27 +0000</pubDate>
      <guid>https://zyf.im/2023/02/01/go-language-lesson-one/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文是 2023 年学习 Tony Bai《Go 语言第一课》的章节笔记整理，对应 Go 1.17 时代。2026 主流 Go 版本已到 1.22+，泛型（1.18）、&lt;code&gt;go.work&lt;/code&gt; 工作区（1.18）、&lt;code&gt;slog&lt;/code&gt;（1.21）、&lt;code&gt;slices&lt;/code&gt; / &lt;code&gt;maps&lt;/code&gt; 标准库（1.21）等新内容本文未覆盖，读到 module / vendor 等命令前请先看官方最新文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;02-设计哲学&#34;&gt;02 设计哲学&lt;/h2&gt;
&lt;p&gt;设计哲学之于编程语言，就好比一个人的价值观之于这个人的行为。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单：Go 生产力的源泉。&lt;/li&gt;
&lt;li&gt;显式：Go 希望开发人员 明确知道自己在做什么；显式的基于值比较的错误处理方案。&lt;/li&gt;
&lt;li&gt;组合：类型嵌入 (Type Embedding)。&lt;/li&gt;
&lt;li&gt;并发：面向多核、原生支持并发、用户层轻量级线程 goroutine。&lt;/li&gt;
&lt;li&gt;面向工程：将解决工程问题作为 Go 的 设计原则之一，这些问题包括：程序构建慢、依赖管理失控、代码难于理 解、跨语言构建难等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;03-配好环境&#34;&gt;03 配好环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://go.dev/doc/devel/release&#34;&gt;https://go.dev/doc/devel/release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://golang.google.cn/dl/&#34;&gt;https://golang.google.cn/dl/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;安装多个-go-版本&#34;&gt;安装多个 Go 版本&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Go 1.17+ 安装 binary 用 go install（go get 已废弃）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go install golang.org/dl/go1.21.5@latest
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go1.21.5 download
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go1.21.5 version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;配置-go&#34;&gt;配置 Go&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go env
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go &lt;span class=&#34;nb&#34;&gt;help&lt;/span&gt; environment
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;04-go-程序的结构&#34;&gt;04 Go 程序的结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import &amp;quot;fmt&amp;quot;&lt;/code&gt; 一行中 &lt;code&gt;fmt&lt;/code&gt; 代表的是包的导入路径 (Import)，它表示的是标准库下的 fmt 目录，整个 import 声明语句的含义是导入标准库 fmt 目录下的包&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fmt.Println&lt;/code&gt; 函数调用一行中的 &lt;code&gt;fmt&lt;/code&gt; 代表的则是包名。&lt;/li&gt;
&lt;li&gt;通常导入路径的最后一个分段名与包名是相同的，这也很容易让人误解 import 声明语句中的 &lt;code&gt;fmt&lt;/code&gt; 指的是包名，其实并不是这样的。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;gofmt main.go
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;go-module&#34;&gt;Go module&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go mod init
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go mod tidy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;05-go-项目的布局标准&#34;&gt;05 Go 项目的布局标准&lt;/h2&gt;
&lt;p&gt;loccount 工具&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>本文是 2023 年学习 Tony Bai《Go 语言第一课》的章节笔记整理，对应 Go 1.17 时代。2026 主流 Go 版本已到 1.22+，泛型（1.18）、<code>go.work</code> 工作区（1.18）、<code>slog</code>（1.21）、<code>slices</code> / <code>maps</code> 标准库（1.21）等新内容本文未覆盖，读到 module / vendor 等命令前请先看官方最新文档。</p>
</blockquote>
<h2 id="02-设计哲学">02 设计哲学</h2>
<p>设计哲学之于编程语言，就好比一个人的价值观之于这个人的行为。</p>
<ul>
<li>简单：Go 生产力的源泉。</li>
<li>显式：Go 希望开发人员 明确知道自己在做什么；显式的基于值比较的错误处理方案。</li>
<li>组合：类型嵌入 (Type Embedding)。</li>
<li>并发：面向多核、原生支持并发、用户层轻量级线程 goroutine。</li>
<li>面向工程：将解决工程问题作为 Go 的 设计原则之一，这些问题包括：程序构建慢、依赖管理失控、代码难于理 解、跨语言构建难等。</li>
</ul>
<h2 id="03-配好环境">03 配好环境</h2>
<ul>
<li><a href="https://go.dev/doc/devel/release">https://go.dev/doc/devel/release</a></li>
<li><a href="https://golang.google.cn/dl/">https://golang.google.cn/dl/</a></li>
</ul>
<h3 id="安装多个-go-版本">安装多个 Go 版本</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Go 1.17+ 安装 binary 用 go install（go get 已废弃）</span>
</span></span><span class="line"><span class="cl">go install golang.org/dl/go1.21.5@latest
</span></span><span class="line"><span class="cl">go1.21.5 download
</span></span><span class="line"><span class="cl">go1.21.5 version
</span></span></code></pre></div><h3 id="配置-go">配置 Go</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go env
</span></span><span class="line"><span class="cl">go <span class="nb">help</span> environment
</span></span></code></pre></div><h2 id="04-go-程序的结构">04 Go 程序的结构</h2>
<ul>
<li><code>import &quot;fmt&quot;</code> 一行中 <code>fmt</code> 代表的是包的导入路径 (Import)，它表示的是标准库下的 fmt 目录，整个 import 声明语句的含义是导入标准库 fmt 目录下的包</li>
<li><code>fmt.Println</code> 函数调用一行中的 <code>fmt</code> 代表的则是包名。</li>
<li>通常导入路径的最后一个分段名与包名是相同的，这也很容易让人误解 import 声明语句中的 <code>fmt</code> 指的是包名，其实并不是这样的。</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gofmt main.go
</span></span></code></pre></div><h3 id="go-module">Go module</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go mod init
</span></span><span class="line"><span class="cl">go mod tidy
</span></span></code></pre></div><h2 id="05-go-项目的布局标准">05 Go 项目的布局标准</h2>
<p>loccount 工具</p>
<p><a href="https://github.com/golang/go">https://github.com/golang/go</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tree -LF <span class="m">1</span> .
</span></span></code></pre></div><h2 id="06-解决包依赖管理">06 解决包依赖管理</h2>
<p>GOPATH -&gt; Vendor -&gt; Go Module</p>
<h3 id="gopath">GOPATH</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go env
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">GOPATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/go&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">go get github.com/sirupsen/logrus
</span></span></code></pre></div><h3 id="vendor">vendor</h3>
<ul>
<li>Go 项目必须放在 GOPATH 环境变量配置的路径下，庞大的 vendor 目录需要提交到代码仓库，不仅占用代码仓库空间，减慢仓库下载和更新的速度，而且还会干扰代码评审，对实施代码统计等开发者效能工具也有比较大影响。</li>
<li>你还需要手工管理 vendor 下面的 Go 依赖包，包括项目依赖包的分析、版本的记 录、依赖包获取和存放，等等，最让开发者头疼的就是这一点。</li>
</ul>
<h3 id="go-module-1">Go Module</h3>
<p>Go Module 与 go.mod 是一一对应的。go.mod 文件所在的顶层目录也被称为 module 的根目录，module 根目录以及它子目录 下的所有 Go 包均归属于这个 Go Module，这个 module 也被称为 main module。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="s">&#34;github.com/sirupsen/logrus&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logrus</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;hello, go module mode&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go mod init
</span></span><span class="line"><span class="cl">go mod tidy
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">module</span><span class="w"> </span><span class="k">go</span><span class="o">-</span><span class="nx">lesson</span><span class="o">-</span><span class="nx">one</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="mf">1.17</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="w"> </span><span class="nx">github</span><span class="p">.</span><span class="nx">com</span><span class="o">/</span><span class="nx">sirupsen</span><span class="o">/</span><span class="nx">logrus</span><span class="w"> </span><span class="nx">v1</span><span class="mf">.9.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="w"> </span><span class="nx">golang</span><span class="p">.</span><span class="nx">org</span><span class="o">/</span><span class="nx">x</span><span class="o">/</span><span class="nx">sys</span><span class="w"> </span><span class="nx">v0</span><span class="mf">.0.0</span><span class="o">-</span><span class="mi">20220715151400</span><span class="o">-</span><span class="nx">c0bba94af5f8</span><span class="w"> </span><span class="c1">// indirect</span><span class="w">
</span></span></span></code></pre></div><p>major.minor.patch</p>
<p>Go 的语义导入版本机制：将包主版本号引入到包导入路径中。v0、v1 时不加入路径。</p>
<p>因此甚至可以同时依赖一个包的两个不兼容版本。真实例子是 <code>gopkg.in/yaml</code>，v2 和 v3 路径不同，可以共存：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">yaml</span><span class="w"> </span><span class="s">&#34;gopkg.in/yaml.v2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">yamlv3</span><span class="w"> </span><span class="s">&#34;gopkg.in/yaml.v3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>Go 会在该项目依赖项的所有版本中，选出符合项目整体要求的“最小版本”。这与 PHP Composer 最新最大 (Latest Greatest) 版本 相反。</p>
<h2 id="07-go-module-操作">07 Go Module 操作</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go list -m all
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">go list -m -versions github.com/sirupsen/logrus
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 指定版本 升降级</span>
</span></span><span class="line"><span class="cl">go get github.com/sirupsen/logrus@v1.7.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 指定版本 升降级</span>
</span></span><span class="line"><span class="cl">go mod edit -require<span class="o">=</span>github.com/sirupsen/logrus@v1.7.0
</span></span><span class="line"><span class="cl">go mod tidy
</span></span></code></pre></div><h3 id="使用-vendor-机制">使用 vendor 机制</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go mod vendor
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">go build -mod<span class="o">=</span>vendor
</span></span><span class="line"><span class="cl"><span class="c1"># 顶层目录下存在 vendor 目录，那么 go build 默认也会优先基于 vendor 构建，除非：</span>
</span></span><span class="line"><span class="cl">go build -mod<span class="o">=</span>mod
</span></span></code></pre></div><h2 id="08-go-程序的执行次序">08 Go 程序的执行次序</h2>
<p>可执行程序的 main 包必须定义 main 函数，否则 Go 编译器会报错。</p>
<p>除了 main 包外，其他包也可以拥有自己的名为 main 的函数 或方法。</p>
<h3 id="init-函数">init 函数</h3>
<p>除了前面讲过的 main.main 函数之外，Go 语言还有一个特殊函数，它就是用于进行包初始化的 init 函数了。main 函数之前，常量和变量初 始化之后。每个 init 函数在整个 Go 程序生命周期内仅会被执行一次。Go 包可以拥有不止一个 init 函数。</p>
<p>Go 在进行包初始化的过程中，会采用“深度优先”的原则，递归初始化各个包的 依赖包。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">package main
</span></span><span class="line"><span class="cl">|- import pkg1
</span></span><span class="line"><span class="cl">    |- import pkg2
</span></span><span class="line"><span class="cl">        |- const
</span></span><span class="line"><span class="cl">        |- var
</span></span><span class="line"><span class="cl">        |- init()
</span></span><span class="line"><span class="cl">    |- const
</span></span><span class="line"><span class="cl">    |- var
</span></span><span class="line"><span class="cl">    |- init()
</span></span><span class="line"><span class="cl">|- const
</span></span><span class="line"><span class="cl">|- var
</span></span><span class="line"><span class="cl">|- init()
</span></span><span class="line"><span class="cl">|- main()
</span></span></code></pre></div><h3 id="init-函数的用途">init 函数的用途</h3>
<ul>
<li>重置包级变量值。被用于检查包级变量的初始状态。</li>
<li>实现对包级变量的复杂初始化。</li>
<li>在 init 函数中实现“注册模式”。通过在 init 函数中注册自己的实现的模式，就有效降低了 Go 包对外的直接 暴露，尤其是包级变量的暴露，从而避免了外部通过包级变量对包状态的改动。</li>
</ul>
<h2 id="09-构建一个-web-服务">09 构建一个 Web 服务</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="s">&#34;net/http&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;Hello World&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;:8888&#34;</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl localhost:8888
</span></span><span class="line"><span class="cl">Hello World
</span></span></code></pre></div><blockquote>
<p><a href="https://github.com/imzyf/go-bookstore">https://github.com/imzyf/go-bookstore</a></p>
</blockquote>
<h2 id="10-变量声明">10 变量声明</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 将变量名放在了类型的前面</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 修饰关键字 变量名 类型 初值</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 省略类型信息的声明</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 显式赋予变量初值</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">int32</span><span class="p">(</span><span class="mi">13</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 声明多个</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w"> </span><span class="sc">&#39;A&#39;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 短变量声明</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">a</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">b</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="sc">&#39;A&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">c</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 声明多个</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w"> </span><span class="sc">&#39;A&#39;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;hello&#34;</span><span class="w">
</span></span></span></code></pre></div><h3 id="go-语言的两类变量">Go 语言的两类变量</h3>
<ul>
<li>包级变量 (package variable)</li>
<li>局部变量 (local variable)</li>
</ul>
<h3 id="包级变量的声明形式">包级变量的声明形式</h3>
<p>包级变量只能使用带有 var 关键字的变量声明形式，不能使用短变量声明形式，但在形式细节上可以有一定灵活度。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="kt">int32</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">17</span><span class="w"> </span><span class="c1">// 显式指定类型</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">f</span><span class="w"> </span><span class="kt">float32</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mf">3.14</span><span class="w"> </span><span class="c1">// 显式指定类型</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">13</span><span class="w"> </span><span class="c1">// 使用默认类型</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">int32</span><span class="p">(</span><span class="mi">17</span><span class="p">)</span><span class="w"> </span><span class="c1">// 显式指定类型</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">f</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">float32</span><span class="p">(</span><span class="mf">3.14</span><span class="p">)</span><span class="w"> </span><span class="c1">// 显式指定类型</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="kt">int32</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">f</span><span class="w"> </span><span class="kt">float64</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 声明聚类</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">netGo</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">netCgo</span><span class="w"> </span><span class="kt">bool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">aLongTimeAgo</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nf">Unix</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">noDeadline</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">{}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">noCancel</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kd">struct</span><span class="p">{})(</span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 就近原则</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 尽可能在靠近第一次使用变量的位置声明这个变量</span><span class="w">
</span></span></span></code></pre></div><h3 id="局部变量的声明形式">局部变量的声明形式</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 延迟初始化的局部变量</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 显式初始化的局部变量</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">a</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">17</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">f</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">float32</span><span class="p">(</span><span class="mf">3.14</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">s</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;hello, gopher!&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 尽量在分支控制时使用短变量声明形式</span><span class="w">
</span></span></span></code></pre></div><h2 id="11-代码块-block-与作用域-scope">11 代码块 Block 与作用域 Scope</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 变量遮蔽</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">foo</span><span class="p">(</span><span class="nx">n</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">a</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">a</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">n</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;a =&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">a</span><span class="p">)</span><span class="w"> </span><span class="c1">// 11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">foo</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;after calling foo, a =&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">a</span><span class="p">)</span><span class="w"> </span><span class="c1">// 11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><ul>
<li>宇宙代码块 (Universe Block)</li>
<li>包代码块 (Package Block)</li>
<li>文件代码块 (File Block)</li>
<li>分支控制语句隐式代码块</li>
<li>switch/select 的子句隐式代码块</li>
</ul>
<p>一个标识符的作用域就是指：这个标识符在被声明后可以被有效使用的源码区域。</p>
<p>导出标识符：</p>
<ul>
<li>声明在包代码块中</li>
<li>它名字第一个字符是一个大写的 Unicode 字符</li>
</ul>
<blockquote>
<p><a href="https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter11/main.go">https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter11/main.go</a></p>
</blockquote>
<h2 id="12-数值类型">12 数值类型</h2>
<h3 id="整型">整型</h3>
<p>Go 采用补码 (2&rsquo;s complement) 作为整型的比特位编码方法。Go 的补码是通过原码逐位取反后再加 1 得到的。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">uint8  1 0 0 0 0 0 0 1 = 129     // 无符号，按二进制直读
</span></span><span class="line"><span class="cl">int8   1 0 0 0 0 0 0 1 = -127    // 有符号，首位为符号位，按补码解释
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">// -127 的补码推导（原码 → 反码 → 补码）：
</span></span><span class="line"><span class="cl">0 1 1 1 1 1 1 1     // 127 的原码
</span></span><span class="line"><span class="cl">1 0 0 0 0 0 0 0     // 逐位取反
</span></span><span class="line"><span class="cl">1 0 0 0 0 0 0 1     // +1 得到 -127 的补码
</span></span></code></pre></div><h3 id="整型的溢出问题">整型的溢出问题</h3>
<p>经典坑：用 <code>int8</code> 做循环计数器，到 127 自增直接 wrap 到 -128，条件永远成立 → 死循环。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">int8</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="mi">127</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// i == 127 时 i++ 溢出回 -128，i &lt;= 127 永远 true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Go <strong>运行时不会对算术溢出报错</strong>，静默 wrap 是常态。修法：扩大类型（<code>int16</code> / <code>int</code>）或改成 <code>i &lt; 127</code>。</p>
<blockquote>
<p><a href="https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter12/main.go">https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter12/main.go</a></p>
</blockquote>
<h3 id="浮点型">浮点型</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">IEEE 754
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">符号位 Sign  阶码 Exponent  尾数 Mantissa
</span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>\bit 位\</th>
					<th>符号位</th>
					<th>阶码</th>
					<th>阶码偏移值</th>
					<th>尾数</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>单精度 float32</td>
					<td>1</td>
					<td>8</td>
					<td>127</td>
					<td>23</td>
			</tr>
			<tr>
					<td>双精度 float64</td>
					<td>1</td>
					<td>11</td>
					<td>1023</td>
					<td>52</td>
			</tr>
	</tbody>
</table>
<p>eg: 139.8125</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">步骤一：把整数部分和小数部分分别转换为二进制（d 表示十进制，b 表示二进制）：
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">整数部分：139d =&gt; 10001011b
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">小数部分：0.8125d =&gt; 0.1101b（十进制小数转二进制用「乘 2 取整」竖式）
</span></span><span class="line"><span class="cl">0.8125 * 2 = 1.625 …… 1
</span></span><span class="line"><span class="cl">0.625  * 2 = 1.25  …… 1
</span></span><span class="line"><span class="cl">0.25   * 2 = 0.5   …… 0
</span></span><span class="line"><span class="cl">0.5    * 2 = 1     …… 1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">139.8125d -&gt; 10001011.1101b
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">步骤二：移动小数点，直到整数部分只剩一个 1。
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">10001011.1101b -&gt; 1.00010111101b
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">小数点向左移了 7 位，指数为 7，尾数为 00010111101b。
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">步骤三：计算阶码。float32 单精度浮点数的阶码偏移值：
</span></span><span class="line"><span class="cl">阶码 = 指数 + 偏移值
</span></span><span class="line"><span class="cl">偏移值 = 2^(e-1) - 1，e 为阶码 bit 数。float32 有 8 位阶码：2^(8-1)-1 = 127。
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">阶码 = 7 + 127 = 134d = 10000110b
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">步骤四：拼装符号位、阶码、尾数。
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">符号位 0
</span></span><span class="line"><span class="cl">阶码   10000110
</span></span><span class="line"><span class="cl">尾数   00010111101 → 不足 23 位右侧补零 → 00010111101000000000000
</span></span></code></pre></div><p><code>139.8125</code> -&gt; <code>0_10000110_00010111101000000000000</code></p>
<ul>
<li><a href="https://stackoverflow.com/questions/75357159/go-float32bit-result-not-expected">Go Float32bit() result not expected | stackoverflow</a></li>
</ul>
<h3 id="复数型">复数型</h3>
<p>Go 内置 <code>complex64</code> / <code>complex128</code>，实部和虚部分别是 float32 / float64。日常业务代码几乎用不到，主要面向科学计算 / 信号处理。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">c</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">complex</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span><span class="w">   </span><span class="c1">// 3+4i</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">real</span><span class="p">(</span><span class="nx">c</span><span class="p">),</span><span class="w"> </span><span class="nb">imag</span><span class="p">(</span><span class="nx">c</span><span class="p">)</span><span class="w">     </span><span class="c1">// 3, 4</span><span class="w">
</span></span></span></code></pre></div><h3 id="创建自定义的数值类型">创建自定义的数值类型</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">MyInt</span><span class="w"> </span><span class="kt">int32</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">m</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">n</span><span class="w"> </span><span class="kt">int32</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">MyInt</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">m</span><span class="w"> </span><span class="c1">// error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">MyInt</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n</span><span class="w"> </span><span class="c1">// error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nf">MyInt</span><span class="p">(</span><span class="nx">m</span><span class="p">)</span><span class="w"> </span><span class="c1">// ok</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nf">MyInt</span><span class="p">(</span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="c1">// ok</span><span class="w">
</span></span></span></code></pre></div><p>MyInt 类型的底层类型是 int32，所以它的数值性质与 int32 完全相同，但它 们仍然是完全不同的两种类型。</p>
<h3 id="类型别名-type-alias">类型别名 (Type Alias)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">MyInt</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kt">int32</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">n</span><span class="w"> </span><span class="kt">int32</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">MyInt</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">n</span><span class="w">
</span></span></span></code></pre></div><p>通过类型别名语法定义的新类型与原类型别无二致，可以完全相互替代。</p>
<h2 id="13-字符串类型">13 字符串类型</h2>
<p>why-what-how</p>
<p>非原生字符串：</p>
<ul>
<li>不是原生类型，编译器不会对它进行类型校验，导致类型安全性差;</li>
<li>字符串操作时要时刻考虑结尾的 <code>\0</code>，防止缓冲区溢出;</li>
<li>以字符数组形式定义的“字符串”，它的值是可变的，在并发场景中需要考虑同步问题;</li>
<li>获取一个字符串的长度代价较大，通常是 O(n) 时间复杂度;</li>
<li>C 语言没有内置对非 ASCII 字符 (如中文字符) 的支持。</li>
</ul>
<p>string 类型的数据是不可变的，提高了字符串的并发安全性和存储利用率（同一个字符串值分配同一块存储）。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">s</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">&#34;hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">s</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="sc">&#39;k&#39;</span><span class="w"> </span><span class="c1">// cannot assign to s[0] (value of type byte)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">s</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">&#34;gopher&#34;</span><span class="w"> </span><span class="c1">// ok</span><span class="w">
</span></span></span></code></pre></div><p>没有结尾 <code>\0</code>，而且获取长度的时间复杂度是常数时间，消除了获取字符串长度的开销。</p>
<p>反引号原生支持“所见即所得”的原始字符串，大大降低构造多行字符串时的心智负担。</p>
<p>对非 ASCII 字符提供原生支持，消除了源码在不同环境下显示乱码的可能。Unicode 字符是以 UTF-8 编码格式存储在内存。</p>
<p>通过单引号括起的字符字面值：</p>
<p><a href="https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter13/main.go">https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter13/main.go</a></p>
<p>UTF-8 编码解决的是 Unicode 码点值在计算机中如何存储和表示 (位模式) 的问题。UTF-8 方案使用变长度字节，从 1 个到 4 个不等。</p>
<p>一个 rune 存储一个 unicode 码点或 utf-32 的四字节编码；从字节视角，string 对应的底层存储存放的是 utf8 编码。</p>
<p>Go 字符串类型的内部标示</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// StringHeader 是一个 string 的运行时表示</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// string 类型其实是一个“描述符”</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">StringHeader</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">// 一个指向底层存储的指针</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Data</span><span class="w"> </span><span class="kt">uintptr</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">// 字符串的长度</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Len</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>直接将 string 类型通过函数或方法参数传入也不会带来太多的开销。因为传入的仅仅是一个“描述符”，而不是真正的字符串数据。</p>
<h3 id="go-字符串类型的常见操作">Go 字符串类型的常见操作</h3>
<p>下标操作；下标操作，我们获取的是字符串中特定下标上的字节，而不是字符。</p>
<p>字符迭代：</p>
<ul>
<li><code>for</code> 迭代，字节视角的迭代</li>
<li>字符串中 Unicode 字符的码点值，以及该字符在字符串中的偏移值（字节视角）</li>
</ul>
<p>字符串连接；<code>+</code> <code>+=</code> <code>strings.Builder</code> <code>strings.Join</code> <code>fmt.Sprintf</code>。</p>
<p>字符串比较；= =、!= 、&gt;=、&lt;=、&gt; 和 &lt;。</p>
<p>字符串转换；string -&gt; <code>[]rune</code> <code>[]byte</code></p>
<h2 id="14-常量">14 常量</h2>
<ul>
<li>支持无类型常量</li>
<li>支持隐式自动转型</li>
<li>可用于实现枚举</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">myInt</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 无类型常量 (Untyped Constant)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">const</span><span class="w"> </span><span class="nx">n</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">13</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">var</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">myInt</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">// 隐式转型</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">a</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 无类型常量 + 隐式转型：使得在 Go 这样的具有强类型系统的语言，在处理表达式混合数据类型运算的时候具有比较大的灵活性，代码编写也得到了一定程度的简化。</span><span class="w">
</span></span></span></code></pre></div><p>Go 的 const 语法提供了“隐式重复前一个非空表达式”的机制。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Apple</span><span class="p">,</span><span class="w"> </span><span class="nx">Banana</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kc">iota</span><span class="p">,</span><span class="w"> </span><span class="kc">iota</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">10</span><span class="w"> </span><span class="c1">// 0, 10 (iota = 0)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Strawberry</span><span class="p">,</span><span class="w"> </span><span class="nx">Grape</span><span class="w"> </span><span class="c1">// 1, 11 (iota = 1)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">Pear</span><span class="p">,</span><span class="w"> </span><span class="nx">Watermelon</span><span class="w"> </span><span class="c1">// 2, 12 (iota = 2)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">const</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">_</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kc">iota</span><span class="w"> </span><span class="c1">// 略过 iota = 0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">IPV6_V6ONLY</span><span class="w"> </span><span class="c1">// 1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">SOMAXCONN</span><span class="w"> </span><span class="c1">// 2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">SO_ERROR</span><span class="w"> </span><span class="c1">// 3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><h2 id="15-数组与切片">15 数组与切片</h2>
<p>数组是一个固定长度的、由同构类型元素组成的连续序列。不仅是逻辑上的连续序列，而且在实际内存分配时也占据着一整块内存。</p>
<p>切片不定长同构数据类型。切片可以看成是数组的“描述符”（句柄），为数组打开了一个访问与修改的“窗口”。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 切片</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">slice</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">array</span><span class="w"> </span><span class="nx">unsafe</span><span class="p">.</span><span class="nx">Pointer</span><span class="w"> </span><span class="c1">// 指向底层数组的指针</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">len</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="c1">// 切片的长度，即切片中当前元素的个数</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nx">cap</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="c1">// 底层数组的长度，也是切片的最大容量，cap 值永远大于等于 len 值</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">sl1</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="w"> </span><span class="c1">// 是声明，未初始化，是 nil 值，底层没有分配内存空间</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">sl2</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="p">{}</span><span class="w"> </span><span class="c1">// 初始化了，不是 nil 值，底层分配了内存空间，有地址。</span><span class="w">
</span></span></span></code></pre></div><h2 id="16-map-类型">16 map 类型</h2>
<p>一组无序的键值对。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">map</span><span class="p">[</span><span class="nx">key_type</span><span class="p">]</span><span class="nx">value_type</span><span class="w">
</span></span></span></code></pre></div><p>key 的类型必须支持“==”和“!=”两种比较操作符。</p>
<h2 id="references">References</h2>
<ul>
<li>极客时间《Go 语言第一课》by Tony Bai —— 本文是该课程的章节学习笔记整理，原课程见 <a href="https://time.geekbang.org/column/intro/100093501">time.geekbang.org/column/intro/100093501</a></li>
<li><a href="https://go.dev/play/">The Go Playground</a></li>
</ul>
<p>&ndash; EOF &mdash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>MySQL 实战 45 讲</title>
      <link>https://zyf.im/2022/08/14/mysql-course-45/</link>
      <pubDate>Sun, 14 Aug 2022 15:53:16 +0000</pubDate>
      <guid>https://zyf.im/2022/08/14/mysql-course-45/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;http://gk.link/a/11AmM&#34;&gt;MySQL 实战 45 讲 | 林晓斌&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;01--基础架构一条-sql-查询语句是如何执行的&#34;&gt;01 | 基础架构：一条 SQL 查询语句是如何执行的？&lt;/h2&gt;
&lt;img alt=&#34;image&#34; src=&#34;https://user-images.githubusercontent.com/9289792/184530380-826038a0-335a-45f7-a56e-cff7b336b4a4.png&#34; style=&#34;width: 480px&#34; /&gt;
&lt;p&gt;请求自上而下经过 Server 层 → Engine 层。Server 层承担连接、解析、优化、执行，所有引擎共享；Engine 层负责数据存取，可插拔。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;组件&lt;/th&gt;
					&lt;th&gt;职责&lt;/th&gt;
					&lt;th&gt;备注&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;连接器&lt;/td&gt;
					&lt;td&gt;建立 / 鉴权 / 维持连接&lt;/td&gt;
					&lt;td&gt;长短连接由客户端控制，服务端 &lt;code&gt;wait_timeout&lt;/code&gt; 兜底&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;查询缓存&lt;/td&gt;
					&lt;td&gt;命中即直接返回&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;8.0 已移除&lt;/strong&gt;；表一更新整张缓存失效，不建议用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;分析器&lt;/td&gt;
					&lt;td&gt;词法 + 语法 + 预处理；列 / 表名校验在此发生&lt;/td&gt;
					&lt;td&gt;报 &lt;code&gt;Unknown column &#39;k&#39; in &#39;field list&#39;&lt;/code&gt; 就在此&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;优化器&lt;/td&gt;
					&lt;td&gt;选索引、决定 join 顺序&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;执行器&lt;/td&gt;
					&lt;td&gt;鉴权后调引擎接口；返回结果集&lt;/td&gt;
					&lt;td&gt;引擎扫描行数 ≠ &lt;code&gt;rows_examined&lt;/code&gt;（见 §05）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;常用查询&#34;&gt;常用查询&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SHOW&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PROCESSLIST&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;                       &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- 当前连接
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wait_timeout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;                  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- 28800s = 8h
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SHOW&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;VARIABLES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;LIKE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;wait_timeout&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;query_cache_type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;              &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- 8.0 之前可观察是否开启
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;清理连接占用的资源但保留连接：调接口函数 &lt;code&gt;mysql_reset_connection()&lt;/code&gt;，不是 SQL 语句。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><a href="http://gk.link/a/11AmM">MySQL 实战 45 讲 | 林晓斌</a></p>
</blockquote>
<h2 id="01--基础架构一条-sql-查询语句是如何执行的">01 | 基础架构：一条 SQL 查询语句是如何执行的？</h2>
<img alt="image" src="https://user-images.githubusercontent.com/9289792/184530380-826038a0-335a-45f7-a56e-cff7b336b4a4.png" style="width: 480px" />
<p>请求自上而下经过 Server 层 → Engine 层。Server 层承担连接、解析、优化、执行，所有引擎共享；Engine 层负责数据存取，可插拔。</p>
<table>
	<thead>
			<tr>
					<th>组件</th>
					<th>职责</th>
					<th>备注</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>连接器</td>
					<td>建立 / 鉴权 / 维持连接</td>
					<td>长短连接由客户端控制，服务端 <code>wait_timeout</code> 兜底</td>
			</tr>
			<tr>
					<td>查询缓存</td>
					<td>命中即直接返回</td>
					<td><strong>8.0 已移除</strong>；表一更新整张缓存失效，不建议用</td>
			</tr>
			<tr>
					<td>分析器</td>
					<td>词法 + 语法 + 预处理；列 / 表名校验在此发生</td>
					<td>报 <code>Unknown column 'k' in 'field list'</code> 就在此</td>
			</tr>
			<tr>
					<td>优化器</td>
					<td>选索引、决定 join 顺序</td>
					<td></td>
			</tr>
			<tr>
					<td>执行器</td>
					<td>鉴权后调引擎接口；返回结果集</td>
					<td>引擎扫描行数 ≠ <code>rows_examined</code>（见 §05）</td>
			</tr>
	</tbody>
</table>
<h3 id="常用查询">常用查询</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">PROCESSLIST</span><span class="p">;</span><span class="w">                       </span><span class="c1">-- 当前连接
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">wait_timeout</span><span class="p">;</span><span class="w">                  </span><span class="c1">-- 28800s = 8h
</span></span></span><span class="line"><span class="cl"><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;wait_timeout&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">query_cache_type</span><span class="p">;</span><span class="w">              </span><span class="c1">-- 8.0 之前可观察是否开启
</span></span></span></code></pre></div><p>清理连接占用的资源但保留连接：调接口函数 <code>mysql_reset_connection()</code>，不是 SQL 语句。</p>
<blockquote>
<p>为什么 <code>SELECT k FROM ...</code> 在表没有 <code>k</code> 列时报错在分析器而非执行器？因为字段定义是元数据，分析器读元数据就能判断列是否存在，无需打开表。</p>
</blockquote>
<p>参考：<a href="https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_query_cache_type">sysvar_query_cache_type</a></p>
<h2 id="02--日志系统一条-sql-更新语句是如何执行的">02 | 日志系统：一条 SQL 更新语句是如何执行的？</h2>
<p><strong>WAL（Write-Ahead Logging）</strong>：先写日志，再写磁盘。日志顺序写，数据随机写。</p>
<h3 id="redo-log-vs-binlog">redo log vs binlog</h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>redo log</th>
					<th>binlog</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>归属</td>
					<td>InnoDB 引擎专属</td>
					<td>Server 层，所有引擎共享</td>
			</tr>
			<tr>
					<td>内容</td>
					<td><strong>物理</strong>：哪个数据页做了什么修改</td>
					<td><strong>逻辑</strong>：行变更的语义（语句 / 行格式）</td>
			</tr>
			<tr>
					<td>写入方式</td>
					<td>循环写，固定大小</td>
					<td>追加写，写满切下一个</td>
			</tr>
			<tr>
					<td>用途</td>
					<td>事务、crash-safe</td>
					<td>归档、主从同步、恢复</td>
			</tr>
			<tr>
					<td>共享性</td>
					<td>只有 InnoDB 自己用（私有物理格式）</td>
					<td>其他数据库 / 引擎可消费（语义通用）</td>
			</tr>
	</tbody>
</table>
<img alt="image" src="https://user-images.githubusercontent.com/9289792/184621469-ce7bea97-685a-421f-a582-0d571badc226.png" style="width: 480px" />
<h3 id="两阶段提交">两阶段提交</h3>
<p>binlog 写入夹在 redo log 的 <code>prepare → commit</code> 之间，使两个日志的提交状态保持一致。崩溃恢复决策：</p>
<table>
	<thead>
			<tr>
					<th>崩溃时机</th>
					<th>redo log 状态</th>
					<th>binlog 状态</th>
					<th>恢复决策</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>binlog 写入前崩</td>
					<td>prepare</td>
					<td>未写</td>
					<td>回滚</td>
			</tr>
			<tr>
					<td>binlog 已写、commit 前崩</td>
					<td>prepare</td>
					<td>已写</td>
					<td>提交</td>
			</tr>
			<tr>
					<td>commit 之后崩</td>
					<td>commit</td>
					<td>已写</td>
					<td>直接提交</td>
			</tr>
	</tbody>
</table>
<h3 id="关键参数">关键参数</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">innodb_flush_log_at_trx_commit</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 1 推荐：每事务持久化 redo
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">sync_binlog</span><span class="p">;</span><span class="w">                     </span><span class="c1">-- 1 推荐：每事务持久化 binlog
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">max_binlog_size</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">1024</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">1024</span><span class="p">;</span><span class="w">   </span><span class="c1">-- MB
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">innodb_page_size</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">1024</span><span class="p">;</span><span class="w">         </span><span class="c1">-- 16（默认页大小 KB）
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">binlog_format</span><span class="p">;</span><span class="w">                   </span><span class="c1">-- STATEMENT / ROW / MIXED
</span></span></span></code></pre></div><p>binlog 格式：<code>STATEMENT</code> 记 SQL（时间函数等不确定语义有风险）；<code>ROW</code> 记前后两行内容，体积大但确定。</p>
<p>记录以 <strong>页（page）</strong> 为单位存取，默认 16KB —— 读一行实际读 16KB。</p>
<p>参考：</p>
<ul>
<li><a href="https://segmentfault.com/a/1190000042041728">彻底搞懂三大 MySQL 日志</a></li>
<li><a href="https://blog.51cto.com/u_3664660/3212550">搞懂 mysql 日志系统 binlog,redolog,undolog</a></li>
</ul>
<h2 id="03--事务隔离为什么你改了我还看不见">03 | 事务隔离：为什么你改了我还看不见？</h2>
<h3 id="三种读异常">三种读异常</h3>
<table>
	<thead>
			<tr>
					<th>异常</th>
					<th>含义</th>
					<th>出现于隔离级别</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>脏读</td>
					<td>读到其他事务<strong>未提交</strong>的数据</td>
					<td>READ UNCOMMITTED</td>
			</tr>
			<tr>
					<td>不可重复读</td>
					<td>同一记录前后读取内容不一致</td>
					<td>READ UNCOMMITTED、READ COMMITTED</td>
			</tr>
			<tr>
					<td>幻读</td>
					<td>同一范围前后读取行数不一致</td>
					<td>READ UNCOMMITTED、READ COMMITTED；InnoDB RR 因 MVCC 已避免</td>
			</tr>
	</tbody>
</table>
<p>脏读示例：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- session1
</span></span></span><span class="line"><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">k</span><span class="o">=</span><span class="mi">2</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">        </span><span class="c1">-- 未提交
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session2
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">UNCOMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">k</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">          </span><span class="c1">-- 2 ← 脏读
</span></span></span></code></pre></div><p>不可重复读示例：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- session1
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">COMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">k</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">          </span><span class="c1">-- 1
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session2
</span></span></span><span class="line"><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">k</span><span class="o">=</span><span class="mi">2</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">         </span><span class="c1">-- 已提交
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session1
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">k</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">          </span><span class="c1">-- 2 ← 不可重复读
</span></span></span></code></pre></div><p>幻读示例（RC 下）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- session1
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">COMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><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">T1</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">k</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">    </span><span class="c1">-- 1
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session2
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="p">(</span><span class="n">k</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session1
</span></span></span><span class="line"><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">T1</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">k</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">    </span><span class="c1">-- 2 ← 幻读
</span></span></span></code></pre></div><h3 id="四种隔离级别">四种隔离级别</h3>
<table>
	<thead>
			<tr>
					<th>级别</th>
					<th>视图创建时机</th>
					<th>阻挡的异常</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>READ UNCOMMITTED</td>
					<td>无视图，直接读最新值</td>
					<td>—</td>
			</tr>
			<tr>
					<td>READ COMMITTED</td>
					<td>每条 SQL 开始时</td>
					<td>脏读</td>
			</tr>
			<tr>
					<td>REPEATABLE READ</td>
					<td>事务首条语句执行时</td>
					<td>脏读 + 不可重复读（+ 幻读，仅 InnoDB）</td>
			</tr>
			<tr>
					<td>SERIALIZABLE</td>
					<td>加读 / 写锁强制串行</td>
					<td>脏读 + 不可重复读 + 幻读</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p>RR 在标准 SQL 里无法避免幻读，但 InnoDB 在 RR 下叠加 <strong>MVCC + Next-Key Lock</strong> 后能避免（仅 snapshot read；<code>FOR UPDATE</code> 等当前读另议，见 §08）。</p>
</blockquote>
<h3 id="常用配置">常用配置</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">tx_isolation</span><span class="p">;</span><span class="w">                                  </span><span class="c1">-- 当前隔离级别
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">COMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">autocommit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">                                     </span><span class="c1">-- 关闭自动提交
</span></span></span><span class="line"><span class="cl"><span class="k">COMMIT</span><span class="w"> </span><span class="k">WORK</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">CHAIN</span><span class="p">;</span><span class="w">                                  </span><span class="c1">-- 等价 commit; begin;
</span></span></span></code></pre></div><h3 id="长事务的影响与避免">长事务的影响与避免</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 持续超过 60s 的事务
</span></span></span><span class="line"><span class="cl"><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">information_schema</span><span class="p">.</span><span class="n">innodb_trx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">TIME_TO_SEC</span><span class="p">(</span><span class="n">timediff</span><span class="p">(</span><span class="n">now</span><span class="p">(),</span><span class="w"> </span><span class="n">trx_started</span><span class="p">))</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">60</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>避免清单：</p>
<ul>
<li>确认 <code>autocommit = 1</code>；框架别习惯性把所有语句 <code>BEGIN/COMMIT</code> 包起来。</li>
<li><code>SET MAX_EXECUTION_TIME</code> 给单语句兜底。</li>
<li>监控 <code>information_schema.innodb_trx</code>，设长事务阈值告警。</li>
<li>开发期开 <code>general_log</code>，提前发现问题。</li>
</ul>
<p>参考：</p>
<ul>
<li><a href="https://www.zhihu.com/question/47007926">幻读、可重复读的真实用例 | zhihu</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html">SET TRANSACTION Statement | mysql</a></li>
</ul>
<h2 id="04--深入浅出索引上">04 | 深入浅出索引（上）</h2>
<h3 id="索引数据结构对比">索引数据结构对比</h3>
<table>
	<thead>
			<tr>
					<th>结构</th>
					<th>等值</th>
					<th>范围</th>
					<th>适用场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>哈希表</td>
					<td>优</td>
					<td>不支持</td>
					<td>等值查询为主</td>
			</tr>
			<tr>
					<td>有序数组</td>
					<td>优</td>
					<td>优</td>
					<td><strong>静态数据</strong></td>
			</tr>
			<tr>
					<td>平衡 BST</td>
					<td>O(log N)</td>
					<td>O(log N)</td>
					<td>内存索引</td>
			</tr>
			<tr>
					<td><strong>N 叉树（B+）</strong></td>
					<td>高度低</td>
					<td>高度低</td>
					<td>InnoDB 磁盘索引</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p>估算：InnoDB 一个 16KB 页放约 1170 个 <code>BIGINT</code> 索引项（16K / (8B 数据 + 6B 子树指针)）。N 叉 B+ 树深度极浅，3 层即覆盖十亿行级别。</p>
</blockquote>
<h3 id="主键索引-vs-二级索引">主键索引 vs 二级索引</h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>主键索引（聚簇 / clustered）</th>
					<th>二级索引（secondary）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>叶子节点</td>
					<td>整行数据</td>
					<td>主键值</td>
			</tr>
			<tr>
					<td>查询路径</td>
					<td>一次定位</td>
					<td>命中后 <strong>回表</strong> 到主键索引</td>
			</tr>
	</tbody>
</table>
<p>覆盖索引可避免回表，见 §05。</p>
<h3 id="自增主键的两条好处">自增主键的两条好处</h3>
<ul>
<li><strong>写性能</strong>：始终追加，不挪记录，不触发页分裂。</li>
<li><strong>存储</strong>：主键短 → 二级索引叶子节点小，整体空间小。</li>
</ul>
<h3 id="何时业务字段适合做主键">何时业务字段适合做主键</h3>
<p>KV 场景：<strong>只有一个唯一索引</strong>且就是它，没有二级索引带来的回表与膨胀代价。</p>
<h3 id="叶子节点存的是什么">叶子节点存的是什么</h3>
<p>InnoDB 叶子节点 = 一个 <strong>页</strong>（默认 16KB），页里存多行 <strong>行数据</strong>。&ldquo;叶子存页&quot;是结构描述；&ldquo;叶子存行&quot;是内容描述，二者并不矛盾。</p>
<h3 id="为什么二级索引不直接存行地址">为什么二级索引不直接存行地址</h3>
<p>MyISAM 走的就是这条路（&ldquo;堆组织表&rdquo;）。InnoDB 选主键而非地址，是为了 <strong>避免页分裂时所有索引都得改主键记录地址</strong>。</p>
<h2 id="05--深入浅出索引下">05 | 深入浅出索引（下）</h2>
<h3 id="覆盖索引">覆盖索引</h3>
<p>查询所需列全部包含在索引中 → 无需回表。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><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">T</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">k</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>若 <code>k</code> 列值为 <code>1, 2, 3, 5, 6, 7</code>：引擎实际读 3 条（3 / 5 / 6 终止），Server 拿到 2 条 —— 这就是 explain 的 <code>rows</code> 与引擎扫描数不等的来源。</p>
<h3 id="最左前缀原则">最左前缀原则</h3>
<p>B+ 树索引可用最左前缀定位记录。</p>
<p><strong>索引顺序的第一判据</strong>：如果调整顺序能少维护一个索引，就采纳。</p>
<blockquote>
<p>常查 <code>(name, age)</code> 与 <code>(age)</code> 时，建 <code>(name, age) + (age)</code>，不是 <code>(age, name) + (age)</code> —— 前者总空间小。</p>
</blockquote>
<h3 id="索引下推index-condition-pushdown-56">索引下推（Index Condition Pushdown, 5.6+）</h3>
<p>联合索引 <code>(name, age)</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><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">tuser</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;张%&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">age</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">ismale</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>版本</th>
					<th>行为</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>&lt; 5.6</td>
					<td>仅用 <code>name</code> 定位前缀，符合 <code>张%</code> 的全部回表后再筛 <code>age</code></td>
			</tr>
			<tr>
					<td>≥ 5.6 ICP</td>
					<td>索引内部就判 <code>age = 10</code>，不满足直接跳过，<strong>减少回表次数</strong></td>
			</tr>
	</tbody>
</table>
<img alt="image" src="https://user-images.githubusercontent.com/9289792/185831784-b6f62f71-a0f7-49c8-afe4-dda0f2dc335a.png" style="width: 420px" />
<p>联合索引按列顺序排序：<code>(name, age)</code> 先按 <code>name</code>，再按 <code>age</code>。</p>
<p><strong>第一性原则</strong>：在满足查询需求前提下，尽量少访问资源。</p>
<h2 id="06--全局锁和表锁给表加个字段怎么这么难">06 | 全局锁和表锁：给表加个字段怎么这么难？</h2>
<h3 id="全局锁">全局锁</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">FLUSH</span><span class="w"> </span><span class="n">TABLES</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">LOCK</span><span class="p">;</span><span class="w">    </span><span class="c1">-- FTWRL：整库只读
</span></span></span><span class="line"><span class="cl"><span class="c1">-- ... 备份 ...
</span></span></span><span class="line"><span class="cl"><span class="n">UNLOCK</span><span class="w"> </span><span class="n">TABLES</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>代价：</p>
<ul>
<li>主库执行 → 业务停摆。</li>
<li>从库执行 → 不消化主库 binlog，主从延迟。</li>
</ul>
<p>更优解：<code>mysqldump --single-transaction</code> 在 RR 下开事务，靠 MVCC 拿一致性视图，过程中可正常写。仍需 FTWRL 的场景：引擎不支持 RR（如 MyISAM）。</p>
<h3 id="ftwrl-vs-set-global-readonlytrue">FTWRL vs <code>set global readonly=true</code></h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>FTWRL</th>
					<th>readonly</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>副作用</td>
					<td>仅设全局读锁</td>
					<td>readonly 常被业务用作主备判断，影响面大</td>
			</tr>
			<tr>
					<td>异常恢复</td>
					<td>客户端断开 → 自动释放</td>
					<td>客户端断开 → 长期 readonly，库不可写</td>
			</tr>
	</tbody>
</table>
<h3 id="表级锁两类">表级锁两类</h3>
<h4 id="lock-tables"><code>LOCK TABLES</code></h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">LOCK</span><span class="w"> </span><span class="n">TABLES</span><span class="w"> </span><span class="n">T1</span><span class="w"> </span><span class="k">READ</span><span class="p">,</span><span class="w"> </span><span class="n">T2</span><span class="w"> </span><span class="k">WRITE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 当前线程：只能读 T1、读写 T2，其他表禁访
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 其他线程：写 T1、读写 T2 都被阻塞
</span></span></span><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">OPEN</span><span class="w"> </span><span class="n">TABLES</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">In_use</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h4 id="mdlmetadata-lock-隐式">MDL（Metadata Lock）—— 隐式</h4>
<table>
	<thead>
			<tr>
					<th>操作</th>
					<th>加锁</th>
					<th>互斥关系</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>增删改查</td>
					<td>MDL 读锁</td>
					<td>读读不互斥</td>
			</tr>
			<tr>
					<td>结构变更 DDL</td>
					<td>MDL 写锁</td>
					<td>读写、写写互斥（排他）</td>
			</tr>
	</tbody>
</table>
<p>事务中的 MDL 锁在 <strong>语句开始时申请</strong>、<strong>事务提交时才释放</strong>。</p>
<h3 id="典型雪崩给小表加字段卡死全库">典型雪崩：给小表加字段卡死全库</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- session1 长事务持有 MDL 读锁未释放
</span></span></span><span class="line"><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">T</span><span class="p">;</span><span class="w">                </span><span class="c1">-- MDL 读锁
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session3 发起 DDL
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">T</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">c2</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></span><span class="line"><span class="cl"><span class="c1">-- ← 拿不到 MDL 写锁，阻塞，并排到 MDL 等待队列首位
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- session4 后续任何对 T 的读写
</span></span></span><span class="line"><span class="cl"><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">T</span><span class="p">;</span><span class="w">                </span><span class="c1">-- ← 排在 session3 之后，全部阻塞
</span></span></span></code></pre></div><img alt="image" src="https://user-images.githubusercontent.com/9289792/185884836-f36feff9-1668-4f37-ad06-1fff9d9c18ef.png" style="width: 420px" />
<p>写锁请求会阻塞 <strong>之后所有</strong> 对该表的读写请求 —— 一个长事务足以拖垮全库。</p>
<h3 id="安全加字段">安全加字段</h3>
<ol>
<li><strong>先解长事务</strong>：<code>information_schema.innodb_trx</code> 找超时事务。</li>
<li><strong>DDL 加超时退避</strong>：</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">tbl_name</span><span class="w"> </span><span class="n">NOWAIT</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="p">...;</span><span class="w">     </span><span class="c1">-- 拿不到锁立即放弃
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">tbl_name</span><span class="w"> </span><span class="n">WAIT</span><span class="w"> </span><span class="mi">30</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="p">...;</span><span class="w">     </span><span class="c1">-- 最多等 30s
</span></span></span></code></pre></div><blockquote>
<p>实测：MySQL 5.7.30 不支持 <code>NOWAIT / WAIT</code>；腾讯云 MySQL 5.7.18-txsql 支持。参见 <a href="https://cloud.tencent.com/document/product/236/48838">腾讯云 NOWAIT 语法</a>。</p>
</blockquote>
<h3 id="online-ddl-的-lock-选项">Online DDL 的 LOCK 选项</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">T</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">c2</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 class="w"> </span><span class="k">LOCK</span><span class="o">=</span><span class="k">EXCLUSIVE</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>LOCK 选项</th>
					<th>并发查询</th>
					<th>并发 DML</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>NONE</td>
					<td>✓</td>
					<td>✓</td>
			</tr>
			<tr>
					<td>SHARED</td>
					<td>✓</td>
					<td>✗</td>
			</tr>
			<tr>
					<td>DEFAULT</td>
					<td>尽量都允许</td>
					<td>尽量都允许</td>
			</tr>
			<tr>
					<td>EXCLUSIVE</td>
					<td>✗</td>
					<td>✗</td>
			</tr>
	</tbody>
</table>
<h2 id="07--行锁功过怎么减少行锁对性能的影响">07 | 行锁功过：怎么减少行锁对性能的影响？</h2>
<h3 id="两阶段锁协议">两阶段锁协议</h3>
<p>InnoDB 事务的行锁在需要时才加，<strong>直到事务结束才释放</strong>。</p>
<p>由此导出一条工程规则：<strong>把最易冲突的锁放最后</strong>。</p>
<p>例：转账 + 记日志（按 ③①② 顺序执行）</p>
<table>
	<thead>
			<tr>
					<th>顺序</th>
					<th>步骤</th>
					<th>冲突度</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>③ 先</td>
					<td>记交易日志 INSERT</td>
					<td>低</td>
			</tr>
			<tr>
					<td>① 中</td>
					<td>扣顾客 A 余额</td>
					<td>中</td>
			</tr>
			<tr>
					<td>② 后</td>
					<td>加影院 B 余额</td>
					<td><strong>高</strong>（很多人同时买）</td>
			</tr>
	</tbody>
</table>
<p>把&quot;加影院余额&quot;放最后，热点行锁持有时间从整段事务缩到末尾一小段。</p>
<h3 id="死锁与检测">死锁与检测</h3>
<img alt="image" src="https://user-images.githubusercontent.com/9289792/186377931-53a1b737-3149-4985-820f-87ffd1319158.png" style="width: 420px" />
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">innodb_lock_wait_timeout</span><span class="p">;</span><span class="w">       </span><span class="c1">-- 默认 50s（图中环境设为 7200 = 2h）
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">@@</span><span class="n">innodb_deadlock_detect</span><span class="p">;</span><span class="w">         </span><span class="c1">-- 1 开启主动检测；触发回滚 1213
</span></span></span></code></pre></div><p><strong>主动检测的代价</strong>：每个新被阻塞的线程要做 O(n) 环路检查。1000 并发更新同行 → 百万次量级的检测，CPU 烧穿。</p>
<p><strong>热点行</strong>的解决：<strong>拆分多行</strong>，把一个余额账户拆成 N 个子账户分摊并发，应用层聚合。</p>
<h2 id="08--事务到底是隔离的还是不隔离的">08 | 事务到底是隔离的还是不隔离的？</h2>
<h3 id="begin-不等于事务起点"><code>BEGIN</code> 不等于事务起点</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">                                       </span><span class="c1">-- 仅声明，未启动
</span></span></span><span class="line"><span class="cl"><span class="k">START</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="n">CONSISTENT</span><span class="w"> </span><span class="n">SNAPSHOT</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 立即拍快照，事务真正启动
</span></span></span></code></pre></div><p><code>BEGIN</code> 之后第一条对 InnoDB 表的操作才真正开启事务。</p>
<h3 id="两个视图别混淆">两个&quot;视图&quot;别混淆</h3>
<table>
	<thead>
			<tr>
					<th>名称</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>view</td>
					<td><code>CREATE VIEW</code> 定义的虚拟表，调用时执行查询语句</td>
			</tr>
			<tr>
					<td>consistent read view</td>
					<td>InnoDB MVCC 用的一致性读视图，支撑 RC / RR 隔离级别</td>
			</tr>
	</tbody>
</table>
<p>RR 隔离级别下，事务启动时拍 <strong>整库</strong> 快照。</p>
<h3 id="transaction-id">transaction id</h3>
<p>InnoDB 每个事务有唯一 <code>transaction id</code>，事务开启时向事务系统申请，<strong>严格递增</strong>。MVCC 用它判断数据行版本对当前事务是否可见。</p>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>PHP float 精度</title>
      <link>https://zyf.im/2020/05/09/php-float-precision/</link>
      <pubDate>Sat, 09 May 2020 17:32:34 +0000</pubDate>
      <guid>https://zyf.im/2020/05/09/php-float-precision/</guid>
      <description>&lt;h2 id=&#34;实例-1&#34;&gt;实例 1&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1.1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gettype&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(6) &amp;#34;double&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(1.1)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;实例-2&#34;&gt;实例 2&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;123456789.1100110011&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(123456789.11001)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%.11f&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(21) &amp;#34;123456789.11001099646&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;123456789.11001&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(123456789.11001)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%.11f&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(21) &amp;#34;123456789.11000999808&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;123456789.1100110011&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(123456789.11001)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(15) &amp;#34;123456789.11001&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(123456789.11001)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%.11f&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(21) &amp;#34;123456789.11000999808&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(false) - 说明 $a 还是携带着 float 的精度
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(true)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;实例-3&#34;&gt;实例 3&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// # 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;120085&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1200.85&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(false)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// # 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;120085&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1200.85&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(false)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// # 3
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;120081&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1200.81&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(true)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// # 4
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;120085&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1200.85&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(1.4551915228367E-11)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;实例-4&#34;&gt;实例 4&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.9&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(true)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(false)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%.20f&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$a&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(22) &amp;#34;1.00000000000000000000&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%.20f&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(22) &amp;#34;0.09999999999999997780&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;0.5&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.25&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.25&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(true) 0.5 二进制 0.1，0.25 二进制 0.01
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;0.25&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.25&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;0.5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// bool(true)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;实例-5&#34;&gt;实例 5&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;19.99&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(1999)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// int(1998) !!!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// string(4) &amp;#34;1999&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// int(1999)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;round&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// float(1999)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;var_dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;round&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$n&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// int(1999)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;19.99 * 100&lt;/code&gt; 实际结果是 &lt;code&gt;1998.9999999999998…&lt;/code&gt;，&lt;code&gt;(int)&lt;/code&gt; 走的是向零截断而非四舍五入，所以掉到 &lt;code&gt;1998&lt;/code&gt;。打印时 PHP 默认精度只显示到 &lt;code&gt;1999&lt;/code&gt;，肉眼上看不出来这个陷阱。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="实例-1">实例 1</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$a</span> <span class="o">=</span> <span class="mf">1.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">gettype</span><span class="p">(</span><span class="nv">$a</span><span class="p">));</span> <span class="c1">// string(6) &#34;double&#34;
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$a</span><span class="p">);</span> <span class="c1">// float(1.1)
</span></span></span></code></pre></div><h2 id="实例-2">实例 2</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$a</span> <span class="o">=</span> <span class="s2">&#34;123456789.1100110011&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$a</span> <span class="o">=</span> <span class="p">(</span><span class="nx">float</span><span class="p">)</span> <span class="nv">$a</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$a</span><span class="p">);</span> <span class="c1">// float(123456789.11001)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%.11f&#39;</span><span class="p">,</span> <span class="nv">$a</span><span class="p">));</span> <span class="c1">// string(21) &#34;123456789.11001099646&#34;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$b</span> <span class="o">=</span> <span class="mf">123456789.11001</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$b</span><span class="p">);</span> <span class="c1">// float(123456789.11001)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%.11f&#39;</span><span class="p">,</span> <span class="nv">$b</span><span class="p">));</span> <span class="c1">// string(21) &#34;123456789.11000999808&#34;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$c</span> <span class="o">=</span> <span class="s1">&#39;123456789.1100110011&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$c</span> <span class="o">=</span> <span class="p">(</span><span class="nx">float</span><span class="p">)</span> <span class="nv">$c</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$c</span><span class="p">);</span> <span class="c1">// float(123456789.11001)
</span></span></span><span class="line"><span class="cl"><span class="nv">$c</span> <span class="o">=</span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="nv">$c</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$c</span><span class="p">);</span> <span class="c1">// string(15) &#34;123456789.11001&#34;
</span></span></span><span class="line"><span class="cl"><span class="nv">$c</span> <span class="o">=</span> <span class="p">(</span><span class="nx">float</span><span class="p">)</span> <span class="nv">$c</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$c</span><span class="p">);</span> <span class="c1">// float(123456789.11001)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%.11f&#39;</span><span class="p">,</span> <span class="nv">$c</span><span class="p">));</span> <span class="c1">// string(21) &#34;123456789.11000999808&#34;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$a</span> <span class="o">===</span> <span class="nv">$b</span><span class="p">);</span> <span class="c1">// bool(false) - 说明 $a 还是携带着 float 的精度
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$b</span> <span class="o">===</span> <span class="nv">$c</span><span class="p">);</span> <span class="c1">// bool(true)
</span></span></span></code></pre></div><h2 id="实例-3">实例 3</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="c1">// # 1
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mi">120085</span> <span class="o">===</span> <span class="mf">1200.85</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span> <span class="c1">// bool(false)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// # 2
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mi">120085</span> <span class="o">==</span> <span class="mf">1200.85</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span> <span class="c1">// bool(false)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// # 3
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mi">120081</span> <span class="o">==</span> <span class="mf">1200.81</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span> <span class="c1">// bool(true)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// # 4
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mi">120085</span> <span class="o">-</span> <span class="mf">1200.85</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span> <span class="c1">// float(1.4551915228367E-11)
</span></span></span></code></pre></div><h2 id="实例-4">实例 4</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$a</span> <span class="o">=</span> <span class="mf">0.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$b</span> <span class="o">=</span> <span class="mf">0.9</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$c</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nv">$a</span> <span class="o">+</span> <span class="nv">$b</span><span class="p">)</span> <span class="o">==</span> <span class="nv">$c</span><span class="p">);</span> <span class="c1">// bool(true)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nv">$c</span> <span class="o">-</span> <span class="nv">$b</span><span class="p">)</span> <span class="o">==</span> <span class="nv">$a</span><span class="p">);</span> <span class="c1">// bool(false)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%.20f&#39;</span><span class="p">,</span> <span class="nv">$a</span> <span class="o">+</span> <span class="nv">$b</span><span class="p">));</span> <span class="c1">// string(22) &#34;1.00000000000000000000&#34;
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%.20f&#39;</span><span class="p">,</span> <span class="nv">$c</span> <span class="o">-</span> <span class="nv">$b</span><span class="p">));</span> <span class="c1">// string(22) &#34;0.09999999999999997780&#34;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="mf">0.5</span> <span class="o">-</span> <span class="mf">0.25</span><span class="p">)</span> <span class="o">===</span> <span class="mf">0.25</span><span class="p">);</span> <span class="c1">// bool(true) 0.5 二进制 0.1，0.25 二进制 0.01
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="mf">0.25</span> <span class="o">+</span> <span class="mf">0.25</span><span class="p">)</span> <span class="o">===</span> <span class="mf">0.5</span><span class="p">);</span> <span class="c1">// bool(true)
</span></span></span></code></pre></div><h2 id="实例-5">实例 5</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$n</span> <span class="o">=</span> <span class="mf">19.99</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nv">$n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span> <span class="c1">// float(1999)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nx">int</span><span class="p">)</span> <span class="p">(</span><span class="nv">$n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">));</span> <span class="c1">// int(1998) !!!
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nx">string</span><span class="p">)</span> <span class="p">(</span><span class="nv">$n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">));</span> <span class="c1">// string(4) &#34;1999&#34;
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nx">int</span><span class="p">)</span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="p">(</span><span class="nv">$n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">));</span> <span class="c1">// int(1999)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">round</span><span class="p">(</span><span class="nv">$n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">));</span> <span class="c1">// float(1999)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nx">int</span><span class="p">)</span> <span class="nx">round</span><span class="p">(</span><span class="nv">$n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">));</span> <span class="c1">// int(1999)
</span></span></span></code></pre></div><p><code>19.99 * 100</code> 实际结果是 <code>1998.9999999999998…</code>，<code>(int)</code> 走的是向零截断而非四舍五入，所以掉到 <code>1998</code>。打印时 PHP 默认精度只显示到 <code>1999</code>，肉眼上看不出来这个陷阱。</p>
<h2 id="分析">分析</h2>
<p>看文档：</p>
<ul>
<li><a href="https://www.php.net/manual/zh/function.gettype.php">gettype | php.net</a></li>
<li><a href="https://www.php.net/manual/zh/language.types.float.php">Float 浮点型 | php.net</a></li>
</ul>
<blockquote>
<p>浮点型（也叫浮点数 float，双精度数 double 或实数 real）
浮点数的字长和平台相关，尽管通常最大值是 1.8e308 并具有 14 位十进制数字的精度（64 位 IEEE 格式）。
所以永远不要相信浮点数结果精确到了最后一位，也永远不要比较两个浮点数是否相等。如果确实需要更高的精度，应该使用任意精度数学函数或者 gmp 函数。</p>
</blockquote>
<p>实例 1：PHP 中 <code>float</code> 与 <code>double</code> 是同一种类型 —— 底层 C 实现统一用 double 存储。</p>
<p>实例 2、3：float 的比较结果<strong>视情况而定</strong>，<strong>永远不要相信浮点数结果精确到了最后一位</strong>。</p>
<p>实例 4：浮点数转为二进制时可能造成精度丢失。<code>0.1</code> 和 <code>0.9</code> 在二进制都是无穷循环小数，二者相加时舍入误差恰好抵消落到 <code>1.0</code>；而 <code>1 - 0.9</code> 的误差被保留并放大，所以与 <code>0.1</code> 的二进制近似值不再相等。</p>
<ul>
<li>Arbitrary-precision arithmetic library for PHP <a href="https://github.com/brick/math">https://github.com/brick/math</a></li>
</ul>
<h2 id="浮点数转二进制方法">浮点数转二进制方法</h2>
<p>整数部分采用除以 2 取余方法，小数部分采用乘以 2 取整方法。</p>
<p>例如：把数字 8.5 转为二进制：</p>
<p>整数部分是 8：</p>
<ul>
<li>8/2=4 8%2=0</li>
<li>4/2=2 4%2=0</li>
<li>2/2=1 2%2=0</li>
<li>1 比 2 小，因此不需要计算下去，整数 8 的二进制为 1000</li>
</ul>
<p>小数部分是 0.5：</p>
<ul>
<li>0.5x2 = 1.0</li>
<li>因取整后小数部分为 0，因此不需要再计算下去，小数 0.5 的二进制为 0.1</li>
</ul>
<p><code>8.5</code> 的二进制为 <code>1000.1</code>。</p>
<p>计算数字 0.9 的二进制：</p>
<ul>
<li>0.9x2 = 1.8</li>
<li>0.8x2 = 1.6</li>
<li>0.6x2 = 1.2</li>
<li>0.2x2 = 0.4</li>
<li>0.4x2 = 0.8</li>
<li>0.8x2 = 1.6</li>
<li>&hellip; 之后不断循环下去，当截取精度为 N 时，N 后的数会被舍去，导致精度丢失。</li>
</ul>
<p>实例 4 中 <code>0.9</code> 在转为二进制时精度丢失，导致比较时出现错误。</p>
<blockquote>
<p>你看似有穷的小数，在计算机的二进制表示里却是无穷的。</p>
</blockquote>
<p>计算数字 0.25 的二进制：</p>
<ul>
<li>0.25x2 = 0.5</li>
<li>0.5x2 = 1.0</li>
</ul>
<p><code>0.25</code> 的二进制为 <code>0.01</code>。</p>
<h2 id="float-比较方法">float 比较方法</h2>
<h3 id="使用-round-方法处理后再比较">使用 round 方法处理后再比较</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mi">120085</span> <span class="o">==</span> <span class="nx">round</span><span class="p">(</span><span class="mf">1200.85</span> <span class="o">*</span> <span class="mi">100</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="c1">// bool(true)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mf">12008.5</span> <span class="o">===</span> <span class="nx">round</span><span class="p">(</span><span class="mf">1200.85</span> <span class="o">*</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="c1">// bool(true)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="mf">1200.85</span> <span class="o">===</span> <span class="nx">round</span><span class="p">(</span><span class="mf">1200.8499999</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="c1">// bool(true)
</span></span></span></code></pre></div><h3 id="使用高精度运算方法">使用高精度运算方法</h3>
<p>见文档 <a href="https://www.php.net/manual/zh/ref.bc.php">BC 数学 函数 | php.net</a>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="mi">1</span> <span class="o">-</span> <span class="mf">0.9</span><span class="p">)</span> <span class="o">==</span> <span class="mf">0.1</span><span class="p">);</span> <span class="c1">// bool(false)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">(</span><span class="nx">bcsub</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mf">0.9</span><span class="p">,</span> <span class="mi">40</span><span class="p">)</span> <span class="o">==</span> <span class="mf">0.1</span><span class="p">);</span> <span class="c1">// bool(true)
</span></span></span><span class="line"><span class="cl"><span class="nx">var_dump</span><span class="p">((</span><span class="nx">float</span><span class="p">)</span> <span class="nx">bcsub</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mf">0.9</span><span class="p">,</span> <span class="mi">40</span><span class="p">)</span> <span class="o">===</span> <span class="mf">0.1</span><span class="p">);</span> <span class="c1">// bool(true)
</span></span></span></code></pre></div><h2 id="references">References</h2>
<ul>
<li><a href="https://www.codecasts.com/blog/post/php-tricky-floats-comparison-with-int">PHP 浮点型与整型比较的小坑 | codecasts</a></li>
<li><a href="https://blog.csdn.net/fdipzone/article/details/48106065">php 浮点数比较方法 | csdn</a></li>
<li><a href="https://www.laruence.com/2013/03/26/2884.html">PHP 浮点数的一个常见问题的解答 | laruence</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>Lonicera Framework</title>
      <link>https://zyf.im/2019/12/19/lonicera-framework/</link>
      <pubDate>Thu, 19 Dec 2019 16:46:06 +0000</pubDate>
      <guid>https://zyf.im/2019/12/19/lonicera-framework/</guid>
      <description>&lt;p&gt;项目代码：&lt;a href=&#34;https://github.com/imzyf/lonicera&#34;&gt;imzyf/lonicera | GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lonicera Framework - Every French soldier carries a marshal’s baton in his knapsack.&lt;/p&gt;
&lt;h2 id=&#34;mvc&#34;&gt;MVC&lt;/h2&gt;
&lt;p&gt;MVC 模式的目的是实现一种动态的程序设计，使后续对程序的修改和扩展简化，并且使程序某一部分的重复利用成为可能。&lt;/p&gt;
&lt;h2 id=&#34;lonicera-01&#34;&gt;Lonicera 0.1&lt;/h2&gt;
&lt;h3 id=&#34;bootstrap&#34;&gt;bootstrap&lt;/h3&gt;
&lt;p&gt;index.php 单一入口模式。&lt;/p&gt;
&lt;p&gt;启动 PHP 内置 Web 服务器：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;php -S localhost:7070
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;路由器层&#34;&gt;路由器层&lt;/h3&gt;
&lt;p&gt;更偏向于使用 PATH_INFO 方式来访问。&lt;/p&gt;
&lt;p&gt;从传统 URL 参数模式的访问地址进行解析，提取里面的 group、controller、action、param 4 个参数，随后交给 bootstrap 进行 dispatch 处理。&lt;/p&gt;
&lt;h3 id=&#34;数据模型&#34;&gt;数据模型&lt;/h3&gt;
&lt;p&gt;用 PDO 来实现连接数据库。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ORM Object Relational Mapping 对象与数据库的映射叫作对象关系映射&lt;/li&gt;
&lt;li&gt;PO Persistent Object 把一个数据库中的表的一行记录对应的对象称为持久对象&lt;/li&gt;
&lt;li&gt;BO Business Object 业务对象 把业务逻辑封装为一个对象&lt;/li&gt;
&lt;li&gt;VO Value Object 值对象 界面显示的数据对象&lt;/li&gt;
&lt;li&gt;DTO Data Transfer Object 用在热呵呵需要数据传输的地方&lt;/li&gt;
&lt;li&gt;DAO Data Access Object 指代 Active Record 模式中的数据对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传统的 ORM 模式提倡数据对象和负责持久化的代码的分开，但是这并没有坚持数据操作的工作量。还有一种 ORM 模式叫作 Active Record。在 Active Record 中，模型层集成了 ORM 的功能，他们及代表实体，包含因为业务逻辑，又是数据对象，并负责把自己存储到数据库中。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>项目代码：<a href="https://github.com/imzyf/lonicera">imzyf/lonicera | GitHub</a></p>
<p>Lonicera Framework - Every French soldier carries a marshal’s baton in his knapsack.</p>
<h2 id="mvc">MVC</h2>
<p>MVC 模式的目的是实现一种动态的程序设计，使后续对程序的修改和扩展简化，并且使程序某一部分的重复利用成为可能。</p>
<h2 id="lonicera-01">Lonicera 0.1</h2>
<h3 id="bootstrap">bootstrap</h3>
<p>index.php 单一入口模式。</p>
<p>启动 PHP 内置 Web 服务器：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">php -S localhost:7070
</span></span></code></pre></div><h3 id="路由器层">路由器层</h3>
<p>更偏向于使用 PATH_INFO 方式来访问。</p>
<p>从传统 URL 参数模式的访问地址进行解析，提取里面的 group、controller、action、param 4 个参数，随后交给 bootstrap 进行 dispatch 处理。</p>
<h3 id="数据模型">数据模型</h3>
<p>用 PDO 来实现连接数据库。</p>
<ul>
<li>ORM Object Relational Mapping 对象与数据库的映射叫作对象关系映射</li>
<li>PO Persistent Object 把一个数据库中的表的一行记录对应的对象称为持久对象</li>
<li>BO Business Object 业务对象 把业务逻辑封装为一个对象</li>
<li>VO Value Object 值对象 界面显示的数据对象</li>
<li>DTO Data Transfer Object 用在热呵呵需要数据传输的地方</li>
<li>DAO Data Access Object 指代 Active Record 模式中的数据对象</li>
</ul>
<p>传统的 ORM 模式提倡数据对象和负责持久化的代码的分开，但是这并没有坚持数据操作的工作量。还有一种 ORM 模式叫作 Active Record。在 Active Record 中，模型层集成了 ORM 的功能，他们及代表实体，包含因为业务逻辑，又是数据对象，并负责把自己存储到数据库中。</p>
<p>Active Record 模式中的数据对象不再是 PO 对象，而是 DAO。</p>
<p>一系列的数据库操作组合起来，称之为 Service。Service 向下负责与数据库打交道，向上负责接收页面传递的参数以及数据的传输。理论上应该对 DAO 进项抽象到一个 Service 中。</p>
<h3 id="视图层">视图层</h3>
<p>PHP MVC 中的显示层开始朝着轻量化、API 化发展了。</p>
<p>增强一个类通常途径：</p>
<ol>
<li>使用 <code>__call</code> <code>__set</code> <code>__get</code> 等魔术方法</li>
<li>使用反射</li>
<li>使用 <code>trait</code></li>
<li>使用继承和组合</li>
</ol>
<h3 id="初步改进">初步改进</h3>
<p><code>spl_autoload_register</code> 统一加载文件。</p>
<h2 id="lonicera-02">Lonicera 0.2</h2>
<h3 id="引入异常机制">引入异常机制</h3>
<p>同时设置 <code>set_error_handler</code> 和 <code>set_exception_handler</code>。PHP 同时存在错误和异常两个互不包含的概念。</p>
<h3 id="拦截器与插件">拦截器与插件</h3>
<p><code>dispatch</code> 中处理。使用正则匹配、<code>call_user_func</code>。</p>
<h3 id="request-增强与安全防御">Request 增强与安全防御</h3>
<p>包装 <code>$_REQUEST</code>。</p>
<h2 id="lonicera-03">Lonicera 0.3</h2>
<h3 id="composer-类加载机制">Composer 类加载机制</h3>
<p><code>PSR-4</code>。</p>
<h3 id="model-增强">Model 增强</h3>
<p><code>illuminata/database</code>。</p>
<h3 id="控制反转与依赖注入">控制反转与依赖注入</h3>
<p><code>Inversion of Control</code> <code>IoC</code>，<code>Dependency Injection</code> <code>DI</code>。</p>
<ul>
<li>谁控制谁？IoC 容器控制了对象。</li>
<li>控制什么？主要控制了外部资源获取（不只是对象创建，还包括比如文件等）。</li>
</ul>
<p>传统应用程序是：由我们自己在对象中主动控制去直接获取依赖对象，而反转则是：由容器来帮忙创建及注入依赖对象。由于是容器帮我们查找及注入依赖对象，对象只是被动接受依赖对象，所以是反转。</p>
<ul>
<li>哪些方面反转了？依赖对象的获取被反转了。</li>
</ul>
<p>在我们使用 UserOrder 对象的时候，不再需要手动去创建 User 对象和 Order 对象了，而是直接问 IoC 容器去要 UserOrder 对象，IoC 容器会负责查找 UserOrder 的依赖并替我们创建 User 对象和 Order 对象，并管理它们。也就是说和我们打交道的是 IoC 容器。</p>
<p>依赖注入，是组件之间依赖关系由容器在运行期决定形象来说，即由容器动态地将某个依赖关系注入到组件之中。依赖注入的目的并非是为软件系统带来更多功能，而是为了提高组件重用的效率，并为系统搭建一个灵活、可扩展的平台。</p>
<p>通过依赖注入机制，我们只需要通过简单的配置，而无需任何代码就可指定目标需要的资源，完成自身的业务逻辑，而不需要关心具体的资源来自何处，由谁实现。</p>
<ul>
<li>谁依赖于谁？当然是应用程序依赖于 IoC 容器。</li>
<li>为什么需要依赖？应用程序需要 IoC 容器来提供对象需要的外部资源。</li>
<li>谁注入谁？IoC 容器注入应用程序某个对象，应用程序依赖的对象。</li>
<li>注入了什么？注入某个对象所需要的外部资源 (包括对象、资源、常量数据)。</li>
</ul>
<p>IoC 和 DI 是两个相辅相成的概念，IoC 的实现是使用了 DI，而 DI 的目的是为了实现 IoC。实际上，它们是同一个概念的不同角度描述。</p>
<p>为什么要这么做，这么做有什么意义呢？依赖注入帮我们降低了创建对象的成本，使得对象之间松耦合。</p>
<p><code>composer</code> 通过 <code>composer.json</code> 来管理第三方依赖，从这个角度讲，<code>composer</code> 就是一个 IoC 容器。</p>
<p><code>PSR-11</code> <code>Psr\Contanier\ContainerInterface</code>。</p>
<h2 id="db-sql">DB SQL</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">NAMES</span><span class="w"> </span><span class="n">utf8mb4</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">FOREIGN_KEY_CHECKS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- ----------------------------
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Table structure for user
</span></span></span><span class="line"><span class="cl"><span class="c1">-- ----------------------------
</span></span></span><span class="line"><span class="cl"><span class="k">DROP</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="o">`</span><span class="k">user</span><span class="o">`</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="o">`</span><span class="k">user</span><span class="o">`</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="w"> </span><span class="n">unsigned</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="n">AUTO_INCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">name</span><span class="o">`</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">COLLATE</span><span class="w"> </span><span class="n">utf8mb4_unicode_ci</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="w"> </span><span class="k">COMMENT</span><span class="w"> </span><span class="s1">&#39;名字&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="o">`</span><span class="n">age</span><span class="o">`</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="w"> </span><span class="n">unsigned</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="s1">&#39;0&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w"> </span><span class="n">ENGINE</span><span class="o">=</span><span class="n">InnoDB</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">CHARSET</span><span class="o">=</span><span class="n">utf8mb4</span><span class="w"> </span><span class="k">COLLATE</span><span class="o">=</span><span class="n">utf8mb4_unicode_ci</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">FOREIGN_KEY_CHECKS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>最左前缀原理与相关优化</title>
      <link>https://zyf.im/2019/05/25/the-left-prefix-index-rule/</link>
      <pubDate>Sat, 25 May 2019 11:05:46 +0000</pubDate>
      <guid>https://zyf.im/2019/05/25/the-left-prefix-index-rule/</guid>
      <description>以 MySQL 8 为视角讲组合索引的最左前缀规则，并对比 Skip Scan、Functional Key Parts 等 8.0 新特性对老规则的修正。</description>
      <content:encoded><![CDATA[<p>MySQL 中的索引可以按一定顺序引用多个列，这种索引叫做联合索引。形式上，一个联合索引是一个有序元组 <code>&lt;a1, a2, …, an&gt;</code>，其中每个元素是表中的一列；单列索引可视为元素数为 1 的特例。</p>
<p>下文以 <strong>MySQL 8.0 + <a href="https://dev.mysql.com/doc/employee/en/employees-installation.html">Employees Sample Database</a></strong> 为实验环境。MySQL 5.7 的历史情况会在相关章节穿插说明 —— 5.7 已结束官方支持，但很多旧项目仍在用，行为差异值得标出来。</p>
<p>以 <code>employees.titles</code> 为例，查看其索引。注意 8.0 比 5.7 多了 <code>Visible</code> 与 <code>Expression</code> 两列：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="k">Table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">Non_unique</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Key_name</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Seq_in_index</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Column_name</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Collation</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Cardinality</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sub_part</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Packed</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Null</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Index_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Comment</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Index_comment</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Visible</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Expression</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w">          </span><span class="mi">0</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">  </span><span class="o">|</span><span class="w">            </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">emp_no</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">A</span><span class="w">         </span><span class="o">|</span><span class="w">      </span><span class="mi">301292</span><span class="w"> </span><span class="o">|</span><span class="w">     </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">   </span><span class="o">|</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">BTREE</span><span class="w">      </span><span class="o">|</span><span class="w">         </span><span class="o">|</span><span class="w">               </span><span class="o">|</span><span class="w"> </span><span class="n">YES</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w">          </span><span class="mi">0</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">  </span><span class="o">|</span><span class="w">            </span><span class="mi">2</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">title</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">A</span><span class="w">         </span><span class="o">|</span><span class="w">      </span><span class="mi">442605</span><span class="w"> </span><span class="o">|</span><span class="w">     </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">   </span><span class="o">|</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">BTREE</span><span class="w">      </span><span class="o">|</span><span class="w">         </span><span class="o">|</span><span class="w">               </span><span class="o">|</span><span class="w"> </span><span class="n">YES</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w">          </span><span class="mi">0</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">  </span><span class="o">|</span><span class="w">            </span><span class="mi">3</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">from_date</span><span class="w">   </span><span class="o">|</span><span class="w"> </span><span class="n">A</span><span class="w">         </span><span class="o">|</span><span class="w">      </span><span class="mi">442605</span><span class="w"> </span><span class="o">|</span><span class="w">     </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">   </span><span class="o">|</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">BTREE</span><span class="w">      </span><span class="o">|</span><span class="w">         </span><span class="o">|</span><span class="w">               </span><span class="o">|</span><span class="w"> </span><span class="n">YES</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
</span></span></span></code></pre></div><ul>
<li><code>Visible</code>：8.0 引入的 <strong>Invisible Index</strong>。<code>ALTER TABLE ... ALTER INDEX idx INVISIBLE</code> 让索引对优化器不可见但物理结构仍在，适合「灰度删除索引」的场景 —— 先标为不可见观察一段时间，没出问题再真删。</li>
<li><code>Expression</code>：8.0.13+ 的 <strong>Functional Key Parts</strong>（函数索引）会在这一列展示表达式，后文会用到。</li>
</ul>
<h2 id="全列匹配">全列匹配</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;10009&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;1995-02-18&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">               </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">const</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">159</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">const</span><span class="p">,</span><span class="n">const</span><span class="p">,</span><span class="n">const</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w">   </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">  </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+
</span></span></span></code></pre></div><p>对索引所有列进行精确匹配（这里精确匹配指 <code>=</code> 或 <code>IN</code>）时，索引被完整用到。</p>
<p>理论上索引对顺序敏感，但优化器会自动调整 <code>WHERE</code> 子句的条件顺序去匹配可用索引。把上面三个条件颠倒顺序，执行计划完全一致：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;1995-02-18&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;10009&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 结果同上：const, key_len=159
</span></span></span></code></pre></div><h2 id="最左前缀匹配">最左前缀匹配</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;10009&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">   </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">4</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">const</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">3</span><span class="w"> </span><span class="o">|</span><span class="w">   </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">  </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------+
</span></span></span></code></pre></div><p>精确匹配索引最左连续若干列时，索引可被用到 —— 但只用到匹配的那部分前缀。这里 <code>key_len=4</code> 说明只走了第一列 <code>emp_no</code>。</p>
<h2 id="中间某个条件未提供">中间某个条件未提供</h2>
<p>查询用到了索引中列的精确匹配，但中间列没有提供：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;10009&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;1995-02-18&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">   </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">4</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">const</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">10</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Using</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
</span></span></span></code></pre></div><p>默认情况下，中间列 <code>title</code> 缺失导致 <code>from_date</code> 无法拼回最左前缀，索引只走了第一列，<code>from_date</code> 退化为 <code>Using where</code> 过滤。MySQL 8 提供了三种解法：</p>
<h3 id="方法一补辅助索引">方法一：补辅助索引</h3>
<p>最直接的解法是为这种查询模式建一个辅助索引 <code>&lt;emp_no, from_date&gt;</code>，跳过 <code>title</code>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_emp_from</span><span class="w"> </span><span class="p">(</span><span class="n">emp_no</span><span class="p">,</span><span class="w"> </span><span class="n">from_date</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><h3 id="方法二手工填坑in-枚举">方法二：手工填坑（IN 枚举）</h3>
<p>如果中间列基数很低，可以用 <code>IN</code> 枚举把它的空位填上，让查询重新形成完整的最左前缀。<code>title</code> 只有 7 个不同值：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 7 rows
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;10009&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;Senior Engineer&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Staff&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Engineer&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Senior Staff&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Assistant Engineer&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Technique Leader&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Manager&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;1995-02-18&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">range</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">159</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">7</span><span class="w"> </span><span class="o">|</span><span class="w">   </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Using</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span></code></pre></div><p><code>key_len=159</code> 说明三列全用到，但 <code>type=range</code> 暴露了实际行为：7 次 key 查找。如果 <code>title</code> 取值很多，这条路就走不通了。</p>
<h3 id="方法三让优化器自己跳--index-skip-scan8013">方法三：让优化器自己跳 —— Index Skip Scan（8.0.13+）</h3>
<p>MySQL 8.0.13 起加入 <strong>Index Skip Scan</strong>，思路与方法二一致：优化器自动枚举被跳列的所有可能值，重组前缀。命中时 <code>EXPLAIN</code> 的 <code>Extra</code> 会出现 <code>Using index for skip scan</code>。</p>
<p>触发条件比较苛刻（参见 <a href="https://dev.mysql.com/doc/refman/8.0/en/range-optimization.html#range-access-skip-scan">Range Optimization · Skip Scan</a>）：</p>
<ul>
<li>单表查询、覆盖索引</li>
<li>不使用 <code>GROUP BY</code> / <code>DISTINCT</code></li>
<li>被跳列前后都有等值或范围条件</li>
<li><strong>被跳列基数低</strong>（高基数枚举代价反而高）</li>
</ul>
<p>不满足就回到方法一或方法二。可用 hint 强制 / 禁用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ SKIP_SCAN(titles PRIMARY) */</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="cm">/*+ NO_SKIP_SCAN(titles PRIMARY) */</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><strong>5.7 历史</strong>：没有 Skip Scan，遇到「中间列缺失」只有方法一、方法二两条路。</p>
</blockquote>
<h2 id="查询条件没有指定索引第一列">查询条件没有指定索引第一列</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;1995-02-18&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w">   </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">ALL</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">          </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">    </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">442605</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">10</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Using</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+
</span></span></span></code></pre></div><p>没有最左前缀，常规情况下索引用不上，只能全表扫。理论上 8.0 的 Skip Scan 也能跳过前导列，但 <code>emp_no</code> 基数高达 30 万，枚举代价比全表扫还贵 —— 优化器会拒绝。Skip Scan 不是万灵药，<strong>前导列基数低</strong>是硬性前提。</p>
<h2 id="匹配某列的前缀字符串">匹配某列的前缀字符串</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10009</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;Senior%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">range</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">156</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w">   </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Using</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span></code></pre></div><p>通配符 <code>%</code> 不在开头时可以走索引，被识别为范围扫描。</p>
<h2 id="范围查询">范围查询</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;10010&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">range</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">4</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="o">|</span><span class="w">   </span><span class="mi">14</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">10</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Using</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
</span></span></span></code></pre></div><p>范围列本身可用索引，但范围列<strong>之后</strong>的列在同一索引中无法继续利用 —— 一个索引最多只能用一个范围列。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s1">&#39;10010&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;1986-01-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;1986-12-31&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- key_len=4，只有 emp_no 走了索引
</span></span></span></code></pre></div><p><code>EXPLAIN</code> 不区分「范围扫描」和「多值精确匹配」，两者在 <code>type</code> 都显示为 <code>range</code>。<code>BETWEEN</code> 也不必然是范围 —— 区间窄且值连续时，优化器可能把它当 <code>IN</code> 处理：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;10001&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;10010&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="s1">&#39;1986-01-01&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="s1">&#39;1986-12-31&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- key_len=159，三列全用到
</span></span></span></code></pre></div><p><code>emp_no BETWEEN 10001 AND 10010</code> 被展开成 10 个等值点，于是后面的列也跟上来了。对比闭区间和开区间：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- key_len=4
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10000</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">10011</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- key_len=159
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">10001</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="mi">10010</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior Engineer&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span></code></pre></div><p>整数类型上，<strong>闭区间</strong> <code>&gt;= AND &lt;=</code> 等价于可枚举集合，优化器能把它当多值匹配；<strong>开区间</strong> <code>&gt; AND &lt;</code> 走纯 range，后续列无法跟上。结论：写整数闭区间用 <code>&gt;=</code> / <code>&lt;=</code>，给优化器留出多值匹配的可能。</p>
<h2 id="查询条件中含有函数或表达式">查询条件中含有函数或表达式</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;10009&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">LEFT</span><span class="p">(</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">select_type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">table</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="n">partitions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">possible_keys</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">key</span><span class="w">     </span><span class="o">|</span><span class="w"> </span><span class="n">key_len</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">   </span><span class="o">|</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Extra</span><span class="w">       </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
</span></span></span><span class="line"><span class="cl"><span class="o">|</span><span class="w">  </span><span class="mi">1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">SIMPLE</span><span class="w">      </span><span class="o">|</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">NULL</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">ref</span><span class="w">  </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">4</span><span class="w">       </span><span class="o">|</span><span class="w"> </span><span class="n">const</span><span class="w"> </span><span class="o">|</span><span class="w">    </span><span class="mi">3</span><span class="w"> </span><span class="o">|</span><span class="w">   </span><span class="mi">100</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="k">Using</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">+</span><span class="c1">----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+
</span></span></span></code></pre></div><p>被函数包裹的列默认无法走索引（这里 <code>title</code> 退化为 <code>Using where</code>）。<code>WHERE emp_no - 1 = 10000</code> 同理 —— 即便它等价于 <code>emp_no = 10001</code>，优化器也不会做常量折叠，只能全表扫。</p>
<h3 id="解法functional-key-parts8013">解法：Functional Key Parts（8.0.13+）</h3>
<p>8.0.13 起可以为表达式建索引：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">titles</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_title_left6</span><span class="w"> </span><span class="p">((</span><span class="k">LEFT</span><span class="p">(</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p">)));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">employees</span><span class="p">.</span><span class="n">titles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">LEFT</span><span class="p">(</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Senior&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 现在能命中 idx_title_left6
</span></span></span></code></pre></div><p>底层实现是 <strong>隐式虚拟生成列 + 普通 B-Tree 索引</strong>。需要注意：</p>
<ul>
<li>表达式必须和查询里<strong>完全一致</strong>才会被匹配 —— <code>LEFT(title, 6)</code> 与 <code>SUBSTRING(title, 1, 6)</code> 不互通。</li>
<li>函数索引继承所有生成列的限制（字符集、长度上限、不能引用其他生成列等），且会占用表的列数上限。</li>
<li>改 SQL 也是一条路：把 <code>emp_no - 1 = 10000</code> 写成 <code>emp_no = 10001</code>，不需要新索引就能命中。能改 SQL 优先改 SQL。</li>
</ul>
<blockquote>
<p><strong>5.7 历史</strong>：没有 Functional Key Parts，只能改 SQL 或者用「显式生成列 + 普通索引」曲线救国。</p>
</blockquote>
<h2 id="explain-analyze从执行计划到实测耗时">EXPLAIN ANALYZE：从执行计划到实测耗时</h2>
<p>要量化两种写法的差距，5.7 时代常用 <code>SHOW PROFILES</code>（已 deprecated），8.0.18+ 推荐 <code>EXPLAIN ANALYZE</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">employees</span><span class="p">.</span><span class="n">titles</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">emp_no</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;10009&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">from_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;1995-02-18&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>输出会给出每个迭代器节点的 <strong>实际行数 / 实际耗时 / 循环次数</strong>，而不是 <code>EXPLAIN</code> 的「估算」。配合 <code>FORMAT=TREE</code> 还能直观看出 join 顺序与算子嵌套，是 8.0 调优的首选工具。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/range-optimization.html#range-access-skip-scan">8.2.1.5 Range Optimization · Skip Scan</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/create-index.html#create-index-functional-key-parts">13.1.15 CREATE INDEX · Functional Key Parts</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html">13.1.20.9 Invisible Indexes</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/explain.html">13.8.2 EXPLAIN Statement · EXPLAIN ANALYZE</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/show-profile.html">13.7.5.30 SHOW PROFILE Syntax</a>（已 deprecated）</li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>回顾 2018</title>
      <link>https://zyf.im/2018/12/31/review-2018/</link>
      <pubDate>Mon, 31 Dec 2018 21:00:00 +0000</pubDate>
      <guid>https://zyf.im/2018/12/31/review-2018/</guid>
      <description>&lt;iframe frameborder=&#34;no&#34; border=&#34;0&#34; marginwidth=&#34;0&#34; marginheight=&#34;0&#34; width=298 height=52 src=&#34;//music.163.com/outchain/player?type=2&amp;id=476618833&amp;auto=0&amp;height=32&#34;&gt;&lt;/iframe&gt;
&lt;p&gt;重新翻阅的自己工作邮件的发件箱，回顾一年工作。新年伊始自己还是在开发 P 项目的 iOS App，开始写 Q&amp;amp;A 功能。一些不算太难的 tableView 布局的需求，对我来说，都是头大的问题。&lt;/p&gt;
&lt;p&gt;这段时期招聘时的面试，竟成为我学习 App 开发的一扇小窗。&lt;/p&gt;
&lt;p&gt;年前收到了奖金还是挺开心的。leader 新年寄语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要发声，要当主力&lt;/li&gt;
&lt;li&gt;当有好的想法时，要学会说服别人&lt;/li&gt;
&lt;li&gt;要有耐心，Yifan 需要时间的沉淀&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前两点意思差不多，这段时间思想上困扰我的是：自己对自己的定位是一个初级工程师，认为会的东西、经验不多，见识少，我尊重比我年长的工程师的想法与观点，也相信他们是经过长远思考过的。这个思维设定，我觉得没有什么问题。但是也许有人忽略了 &lt;strong&gt;责任&lt;/strong&gt;，对方案负责，对项目负责。开发方案一再重建性修改，接口结构没有规范。&lt;/p&gt;
&lt;p&gt;2017 的总结说胜利属于伏地魔，本想苟着发育，这时发现：苟是苟不住的，这个世界 &lt;strong&gt;需要英雄&lt;/strong&gt; carry。&lt;/p&gt;
&lt;p&gt;后来我感觉应该将公司看做一个舞台，舞台上有灯光、音效就要利用，展示自己、锻炼自己，即使是出糗，那就整理整理再来一次，who care? 成长是最重要的。&lt;/p&gt;
&lt;p&gt;Course 模块是前工程师用 Objective-C 写的，离职后一直没有再维护，过年期间自己重构了所有 Objective-C 的代码，项目完全转为了一个纯 Swift 项目。使用 Realm 作为数据本地化方案，选择的原因也很朴素，GitHub 哪个星多我就优先选用什么。&lt;/p&gt;
&lt;p&gt;后来参考 &lt;a href=&#34;https://github.com/6ag&#34;&gt;Jack Feng - 6ag&lt;/a&gt; 的几个 Swift 开源项目，新创建了 P 项目的工具 App，也对主项目结构做了重新的整理，在这里再次特别感谢。&lt;/p&gt;
&lt;p&gt;西安运营部的成立，加多了 C 项目的后台需求，难以都顾及项目两头。使用 laravel-admin 搭建新后台，也开始使用 Docker 部署项目，感觉从此离不开 Docker 了，像极了遇到 Git 时的感觉。&lt;/p&gt;
&lt;p&gt;C 项目主站的前端是在服务端渲染，非常传统的模式，多个工程师转手也是十分混乱。参考了 &lt;a href=&#34;https://baijunyao.com/&#34;&gt;白俊遥&lt;/a&gt; 工程的博客、laravel 项目，修改了项目结构，添加了 gulp 工具制定了工作流，虽然没有实现前后端的完全分离，但终究是向现代化前端走出了一步。&lt;/p&gt;
&lt;p&gt;转眼就到了年中调薪，公司不含糊，薪水涨到了我满意的值。这对我很关键，调整的不单单是我的薪水，也调整了我的心态。因为当时我认为没有强的工程师、甚至归我负责，却拿着比我还高的薪水。现在总算是有了一个平衡。&lt;/p&gt;
&lt;p&gt;不断向 C 项目投入更多的资源，项目 &lt;strong&gt;指标&lt;/strong&gt; 的要求越来越多。比如：优化了项目、优化了查询，到底优化了多少，怎么量化？这些在之前一直不被重视，改好了就都算叫优化了。项目中出现的问题错误，都要查找真实具体的原因，而不是说一个可能的什么原因，就当解答的了。我一开始也很难适应这些，但心里是认同的。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=298 height=52 src="//music.163.com/outchain/player?type=2&id=476618833&auto=0&height=32"></iframe>
<p>重新翻阅的自己工作邮件的发件箱，回顾一年工作。新年伊始自己还是在开发 P 项目的 iOS App，开始写 Q&amp;A 功能。一些不算太难的 tableView 布局的需求，对我来说，都是头大的问题。</p>
<p>这段时期招聘时的面试，竟成为我学习 App 开发的一扇小窗。</p>
<p>年前收到了奖金还是挺开心的。leader 新年寄语：</p>
<ul>
<li>要发声，要当主力</li>
<li>当有好的想法时，要学会说服别人</li>
<li>要有耐心，Yifan 需要时间的沉淀</li>
</ul>
<p>前两点意思差不多，这段时间思想上困扰我的是：自己对自己的定位是一个初级工程师，认为会的东西、经验不多，见识少，我尊重比我年长的工程师的想法与观点，也相信他们是经过长远思考过的。这个思维设定，我觉得没有什么问题。但是也许有人忽略了 <strong>责任</strong>，对方案负责，对项目负责。开发方案一再重建性修改，接口结构没有规范。</p>
<p>2017 的总结说胜利属于伏地魔，本想苟着发育，这时发现：苟是苟不住的，这个世界 <strong>需要英雄</strong> carry。</p>
<p>后来我感觉应该将公司看做一个舞台，舞台上有灯光、音效就要利用，展示自己、锻炼自己，即使是出糗，那就整理整理再来一次，who care? 成长是最重要的。</p>
<p>Course 模块是前工程师用 Objective-C 写的，离职后一直没有再维护，过年期间自己重构了所有 Objective-C 的代码，项目完全转为了一个纯 Swift 项目。使用 Realm 作为数据本地化方案，选择的原因也很朴素，GitHub 哪个星多我就优先选用什么。</p>
<p>后来参考 <a href="https://github.com/6ag">Jack Feng - 6ag</a> 的几个 Swift 开源项目，新创建了 P 项目的工具 App，也对主项目结构做了重新的整理，在这里再次特别感谢。</p>
<p>西安运营部的成立，加多了 C 项目的后台需求，难以都顾及项目两头。使用 laravel-admin 搭建新后台，也开始使用 Docker 部署项目，感觉从此离不开 Docker 了，像极了遇到 Git 时的感觉。</p>
<p>C 项目主站的前端是在服务端渲染，非常传统的模式，多个工程师转手也是十分混乱。参考了 <a href="https://baijunyao.com/">白俊遥</a> 工程的博客、laravel 项目，修改了项目结构，添加了 gulp 工具制定了工作流，虽然没有实现前后端的完全分离，但终究是向现代化前端走出了一步。</p>
<p>转眼就到了年中调薪，公司不含糊，薪水涨到了我满意的值。这对我很关键，调整的不单单是我的薪水，也调整了我的心态。因为当时我认为没有强的工程师、甚至归我负责，却拿着比我还高的薪水。现在总算是有了一个平衡。</p>
<p>不断向 C 项目投入更多的资源，项目 <strong>指标</strong> 的要求越来越多。比如：优化了项目、优化了查询，到底优化了多少，怎么量化？这些在之前一直不被重视，改好了就都算叫优化了。项目中出现的问题错误，都要查找真实具体的原因，而不是说一个可能的什么原因，就当解答的了。我一开始也很难适应这些，但心里是认同的。</p>
<p>C 项目数据源重新整理，终于摸到了一次算法的边。“你觉的算法没用，是因为你不会。”数据标记、正确率、召回率、特征词提取、文本相似度、词条打分、背包算法…… 一段最让人回忆的开发经历，与业务开发完全不同的模式。</p>
<p>C 项目衍生出新项目 G，我一个人做，一次实践 Laravel 框架的机会。</p>
<p>再次和 Z 哥合作了一个 App 项目，私活做的真是的太累了，极不推荐。其实这段时期工作上并没有在做 iOS，因为这个项目没让自己 iOS 的技能凉下来，为后面回归 iOS 项目保持了状态。</p>
<p>参加了年度的 Swift 大会，在群里简单了很多大佬、看过博客的工程师，没想到他们都很乐意加微信。也发现他们相互都认识，这就是圈子吧。</p>
<p>2018 是世界杯年，我不懂球就是看个热闹，最开心的是公司有活动基金，我拉着不少新同事们吃喝了不少。冬季后发展成了和同事的每周的火锅，真挺快乐的。</p>
<p>买了心心念的 Switch 叫的闪送，上午下单一会后，就像收外卖一样收了机子。荒野之息是好玩，呃… 男主竟然叫林克不是塞尔达，你敢想？</p>
<p>因为 MSI 的胜利开始关注 RNG LPL。这年 LPL 后面的比赛我基本都看了，中午还会看 dys 的直播实况。洲际赛 RW 救世主真是精彩，“当没人相信你的时候，你自己相信了自己，并且赢得了胜利，这便是成为英雄的道路。”登峰造極 2018 我们是冠军。</p>
<p>永利*暴雷崩盘，损失不少，派出所报案。家里，多年的期房要发钥匙了，感恩父母。一次团建向比比求婚成功。</p>
<p>这年最后一天是在三亚，吃到了棒棒的早晨，去了蜈支洲岛，感受了乘风破浪，遇到了一个有趣的东北司机，参加了酒店的晚会，看了喝酒赢到的电影。</p>
<p>2018 Happy Ending.</p>
<blockquote>
<p>做了一个梦 我们 iG 拿冠军了 啊哈哈哈…
我说是不是假的 确实是个梦吧，
但是就是当时虽然在做 但是特别开心。
所以我们尽量就是自己努力一下，
然后每个人发挥好一点的话，
我觉得真的有可能 也可以拿冠军的。</p>
</blockquote>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>MySQL 日期/时间类型选型与时区处理</title>
      <link>https://zyf.im/2018/05/25/select-the-appropriate-date-type-in-mysql/</link>
      <pubDate>Fri, 25 May 2018 16:00:00 +0000</pubDate>
      <guid>https://zyf.im/2018/05/25/select-the-appropriate-date-type-in-mysql/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;适用版本：MySQL 8.0+ / Laravel 10+。MySQL 5.7 已 EOL，文中不再覆盖。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;一句话结论&#34;&gt;一句话结论&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;「物理时刻」（事件发生于何时）→ &lt;strong&gt;&lt;code&gt;TIMESTAMP&lt;/code&gt;&lt;/strong&gt;，自动跟随会话时区转换&lt;/li&gt;
&lt;li&gt;「字面值」（合同上写的、用户输入的日期）→ &lt;strong&gt;&lt;code&gt;DATETIME&lt;/code&gt;&lt;/strong&gt;，原样存取不转换&lt;/li&gt;
&lt;li&gt;仅日期 → &lt;code&gt;DATE&lt;/code&gt;；仅时长 → &lt;code&gt;TIME&lt;/code&gt;；JS/跨语言毫秒戳 → &lt;code&gt;BIGINT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;永远不要用 &lt;code&gt;VARCHAR&lt;/code&gt; 存日期&lt;/strong&gt;——无强制约束、无法范围查询、排序按字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;选型对照表&#34;&gt;选型对照表&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;范围&lt;/th&gt;
					&lt;th&gt;字节&lt;/th&gt;
					&lt;th&gt;时区行为&lt;/th&gt;
					&lt;th&gt;小数秒&lt;/th&gt;
					&lt;th&gt;典型场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;DATETIME&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;1000-01-01 00:00:00&lt;/code&gt; ~ &lt;code&gt;9999-12-31 23:59:59&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;5 + fsp&lt;/td&gt;
					&lt;td&gt;字符串原样存取，&lt;strong&gt;不转换&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;DATETIME(0~6)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;合同生效时点、用户填写的本地时间、历史事件&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;TIMESTAMP&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;1970-01-01 00:00:01 UTC&lt;/code&gt; ~ &lt;code&gt;2038-01-19 03:14:07 UTC&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;4 + fsp&lt;/td&gt;
					&lt;td&gt;写入：会话时区 → UTC；读取：UTC → 会话时区&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;TIMESTAMP(0~6)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt;、日志、跨时区事件戳&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;DATE&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;1000-01-01&lt;/code&gt; ~ &lt;code&gt;9999-12-31&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;生日、纪念日、报表日期&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;TIME&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;-838:59:59&lt;/code&gt; ~ &lt;code&gt;838:59:59&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;3 + fsp&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;TIME(0~6)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;营业时长、计时器&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;YEAR&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;1901&lt;/code&gt; ~ &lt;code&gt;2155&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;罕用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;BIGINT&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;INT64&lt;/td&gt;
					&lt;td&gt;8&lt;/td&gt;
					&lt;td&gt;应用层控制&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;JS 毫秒戳、Unix 微秒戳、跨语言互操作&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;小数秒（fsp，fractional seconds precision）&lt;/strong&gt; 0~6 位，每 2 位多占 1 字节。Laravel 11+ 默认就用 &lt;code&gt;DATETIME(6)&lt;/code&gt; / &lt;code&gt;TIMESTAMP(6)&lt;/code&gt;。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>适用版本：MySQL 8.0+ / Laravel 10+。MySQL 5.7 已 EOL，文中不再覆盖。</p>
</blockquote>
<h2 id="一句话结论">一句话结论</h2>
<ul>
<li>「物理时刻」（事件发生于何时）→ <strong><code>TIMESTAMP</code></strong>，自动跟随会话时区转换</li>
<li>「字面值」（合同上写的、用户输入的日期）→ <strong><code>DATETIME</code></strong>，原样存取不转换</li>
<li>仅日期 → <code>DATE</code>；仅时长 → <code>TIME</code>；JS/跨语言毫秒戳 → <code>BIGINT</code></li>
<li><strong>永远不要用 <code>VARCHAR</code> 存日期</strong>——无强制约束、无法范围查询、排序按字符串</li>
</ul>
<h2 id="选型对照表">选型对照表</h2>
<table>
	<thead>
			<tr>
					<th>类型</th>
					<th>范围</th>
					<th>字节</th>
					<th>时区行为</th>
					<th>小数秒</th>
					<th>典型场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>DATETIME</code></td>
					<td><code>1000-01-01 00:00:00</code> ~ <code>9999-12-31 23:59:59</code></td>
					<td>5 + fsp</td>
					<td>字符串原样存取，<strong>不转换</strong></td>
					<td><code>DATETIME(0~6)</code></td>
					<td>合同生效时点、用户填写的本地时间、历史事件</td>
			</tr>
			<tr>
					<td><code>TIMESTAMP</code></td>
					<td><code>1970-01-01 00:00:01 UTC</code> ~ <code>2038-01-19 03:14:07 UTC</code></td>
					<td>4 + fsp</td>
					<td>写入：会话时区 → UTC；读取：UTC → 会话时区</td>
					<td><code>TIMESTAMP(0~6)</code></td>
					<td><code>created_at</code> / <code>updated_at</code>、日志、跨时区事件戳</td>
			</tr>
			<tr>
					<td><code>DATE</code></td>
					<td><code>1000-01-01</code> ~ <code>9999-12-31</code></td>
					<td>3</td>
					<td>—</td>
					<td>—</td>
					<td>生日、纪念日、报表日期</td>
			</tr>
			<tr>
					<td><code>TIME</code></td>
					<td><code>-838:59:59</code> ~ <code>838:59:59</code></td>
					<td>3 + fsp</td>
					<td>—</td>
					<td><code>TIME(0~6)</code></td>
					<td>营业时长、计时器</td>
			</tr>
			<tr>
					<td><code>YEAR</code></td>
					<td><code>1901</code> ~ <code>2155</code></td>
					<td>1</td>
					<td>—</td>
					<td>—</td>
					<td>罕用</td>
			</tr>
			<tr>
					<td><code>BIGINT</code></td>
					<td>INT64</td>
					<td>8</td>
					<td>应用层控制</td>
					<td>—</td>
					<td>JS 毫秒戳、Unix 微秒戳、跨语言互操作</td>
			</tr>
	</tbody>
</table>
<p><strong>小数秒（fsp，fractional seconds precision）</strong> 0~6 位，每 2 位多占 1 字节。Laravel 11+ 默认就用 <code>DATETIME(6)</code> / <code>TIMESTAMP(6)</code>。</p>
<p><strong>2038 注意</strong>：<code>TIMESTAMP</code> 已剩 12 年寿命。新表对长期存储字段倾向 <code>DATETIME(6)</code>；<code>created_at</code> 当前用 <code>TIMESTAMP</code> 仍可（届时官方会延或可迁移），但<strong>业务时间字段直接上 <code>DATETIME</code></strong> 更稳。</p>
<h3 id="自动初始化default--on-update">自动初始化（DEFAULT / ON UPDATE）</h3>
<p><code>DATETIME</code> 与 <code>TIMESTAMP</code> 都支持自动初始化与自动更新——这点 5.6 之前只有 <code>TIMESTAMP</code> 能用。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(</span><span class="mi">6</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">updated_at</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="k">UPDATE</span><span class="w"> </span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(</span><span class="mi">6</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">paid_at</span><span class="w">    </span><span class="n">DATETIME</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>精度必须在 <code>DEFAULT</code> / <code>ON UPDATE</code> 间一致。</p>
<h2 id="时区机制核心">时区机制（核心）</h2>
<h3 id="三个变量">三个变量</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><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;%time_zone%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- system_time_zone: OS 时区（mysqld 启动时确定，运行期改 OS 不生效）
</span></span></span><span class="line"><span class="cl"><span class="c1">-- time_zone:        MySQL 实际使用的时区，&#39;SYSTEM&#39; 表示跟随 system_time_zone
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 每个连接还有一份 @@session.time_zone
</span></span></span></code></pre></div><h3 id="核心规则">核心规则</h3>
<table>
	<thead>
			<tr>
					<th>类型</th>
					<th>写入</th>
					<th>读取</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>DATETIME</code></td>
					<td>字符串原样落盘</td>
					<td>字符串原样返回</td>
			</tr>
			<tr>
					<td><code>TIMESTAMP</code></td>
					<td>用<strong>会话时区</strong>解释字符串 → 转 UTC 落盘</td>
					<td>UTC → <strong>会话时区</strong>字符串</td>
			</tr>
	</tbody>
</table>
<p>推论：</p>
<ul>
<li>同一行 <code>TIMESTAMP</code>，UTC 会话读到 <code>2026-05-17 12:00:00</code>，<code>+08:00</code> 会话读到 <code>2026-05-17 20:00:00</code>——<strong>同一物理时刻的两种表达</strong>。</li>
<li>同一行 <code>DATETIME</code>，两个会话读到的字符串一模一样——<strong>MySQL 不知道这字符串是哪个时区的</strong>。</li>
</ul>
<h3 id="修改时区">修改时区</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">time_zone</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;+00:00&#39;</span><span class="p">;</span><span class="w">      </span><span class="c1">-- 全实例（不需要 FLUSH PRIVILEGES，与权限无关）
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">time_zone</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;+08:00&#39;</span><span class="p">;</span><span class="w">              </span><span class="c1">-- 当前会话
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">time_zone</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Asia/Shanghai&#39;</span><span class="p">;</span><span class="w">       </span><span class="c1">-- 仅在已导入 IANA 表后可用
</span></span></span></code></pre></div><p>固化到配置：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/mysql/mysql.conf.d/mysqld.cnf</span>
</span></span><span class="line"><span class="cl"><span class="k">[mysqld]</span>
</span></span><span class="line"><span class="cl"><span class="na">default-time-zone</span> <span class="o">=</span> <span class="s">&#39;+00:00&#39;</span>
</span></span></code></pre></div><h3 id="导入-iana-时区表">导入 IANA 时区表</h3>
<p>默认安装下 MySQL 只认 <code>+08:00</code> 这种数字偏移；要用 <code>Asia/Shanghai</code> 这类 IANA 名（处理夏令时更安全），先从系统 zoneinfo 导一次：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mysql_tzinfo_to_sql /usr/share/zoneinfo <span class="p">|</span> mysql -u root -p mysql
</span></span></code></pre></div><blockquote>
<p>⚠️ <strong>不要用 <code>CST</code> 命名时区。</strong> MySQL 报告的 <code>CST</code> 是歧义缩写——可能是 China / Central / Cuba Standard Time 三者之一，跨平台行为不可控。一律用 <code>+08:00</code> 或 IANA 名。</p>
</blockquote>
<h2 id="laravel-实战">Laravel 实战</h2>
<h3 id="默认行为速查">默认行为速查</h3>
<table>
	<thead>
			<tr>
					<th>项</th>
					<th>默认</th>
					<th>位置</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>App 时区</td>
					<td><code>UTC</code></td>
					<td><code>config/app.php</code> → <code>timezone</code></td>
			</tr>
			<tr>
					<td><code>timestamps()</code> 列</td>
					<td><code>TIMESTAMP</code></td>
					<td><code>Schema::create</code> → <code>$table-&gt;timestamps()</code></td>
			</tr>
			<tr>
					<td><code>softDeletes()</code></td>
					<td><code>TIMESTAMP</code></td>
					<td><code>$table-&gt;softDeletes()</code></td>
			</tr>
			<tr>
					<td>Eloquent date cast</td>
					<td><code>Carbon</code> 实例</td>
					<td><code>created_at</code> / <code>updated_at</code> 自动；其它字段在 <code>casts()</code> 显式</td>
			</tr>
			<tr>
					<td>序列化精度（Laravel 11+）</td>
					<td>微秒</td>
					<td><code>DATETIME(6)</code> / <code>TIMESTAMP(6)</code></td>
			</tr>
			<tr>
					<td>序列化格式</td>
					<td>UTC ISO-8601（<code>YYYY-MM-DDTHH:MM:SS.uuuuuuZ</code>）</td>
					<td>默认</td>
			</tr>
	</tbody>
</table>
<h3 id="推荐做法全球化项目">推荐做法（全球化项目）</h3>
<ol>
<li>
<p><strong>存储侧统一 UTC</strong>：<code>mysqld default-time-zone='+00:00'</code>、<code>config('app.timezone') = 'UTC'</code>。Laravel 官方原话「strongly encouraged」。</p>
</li>
<li>
<p><strong><code>created_at</code> / <code>updated_at</code> 走 Laravel 默认 <code>TIMESTAMP</code></strong>——自动跟随会话时区。</p>
</li>
<li>
<p><strong>业务时间字段（合同生效、活动开始、订单期望送达）用 <code>DATETIME(6)</code></strong>——这些是「字面值」，不该被时区影响。</p>
</li>
<li>
<p><strong>展示层按用户时区转换</strong>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$model</span><span class="o">-&gt;</span><span class="na">created_at</span>
</span></span><span class="line"><span class="cl">  <span class="o">-&gt;</span><span class="na">setTimezone</span><span class="p">(</span><span class="nx">auth</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">user</span><span class="p">()</span><span class="o">-&gt;</span><span class="na">timezone</span> <span class="o">??</span> <span class="s1">&#39;UTC&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">-&gt;</span><span class="na">format</span><span class="p">(</span><span class="s1">&#39;Y-m-d H:i:s&#39;</span><span class="p">);</span>
</span></span></code></pre></div></li>
</ol>
<h3 id="容易踩的坑">容易踩的坑</h3>
<ol>
<li>
<p><strong><code>created_at</code> 序列化总是 UTC，不受 <code>app.timezone</code> 影响。</strong> 这是 Laravel 12 文档明确写的——<code>timestamp</code> cast 字段「always formatted in UTC, regardless of the application&rsquo;s timezone setting」。想本地化展示请走 cast 自定义或显式 <code>setTimezone</code>。</p>
</li>
<li>
<p><strong><code>Carbon::now()</code> vs <code>DB::raw('NOW()')</code> 不等价。</strong> 前者用 PHP <code>app.timezone</code>，后者用 MySQL 会话时区。两边不一致时会写入错位的「物理时刻」。统一 UTC 是最稳的根治方案。</p>
</li>
<li>
<p><strong>PDO 连接默认不发 <code>SET time_zone</code>，沿用 <code>@@global.time_zone</code>。</strong> 想在连接级隔离，在 <code>config/database.php</code> 的 mysql 驱动加：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="s1">&#39;options&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="nx">PDO</span><span class="o">::</span><span class="na">MYSQL_ATTR_INIT_COMMAND</span> <span class="o">=&gt;</span> <span class="s2">&#34;SET time_zone = &#39;+00:00&#39;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">],</span>
</span></span></code></pre></div></li>
<li>
<p><strong><code>$table-&gt;timestamp('paid_at')</code> 不会自动加 <code>ON UPDATE CURRENT_TIMESTAMP</code></strong>——需要时显式 <code>-&gt;useCurrentOnUpdate()</code>，默认值用 <code>-&gt;useCurrent()</code>。</p>
</li>
<li>
<p><strong>JS 互操作存毫秒</strong>：用 <code>BIGINT</code> 比让 JS 解析 MySQL DATETIME 字符串少很多麻烦。</p>
</li>
</ol>
<h2 id="references">References</h2>
<ul>
<li>MySQL 8.0 Reference — <a href="https://dev.mysql.com/doc/refman/8.0/en/date-and-time-types.html">Date and Time Data Types</a></li>
<li>MySQL 8.0 Reference — <a href="https://dev.mysql.com/doc/refman/8.0/en/datetime.html">DATE, DATETIME, and TIMESTAMP</a></li>
<li>MySQL 8.0 Reference — <a href="https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html">Automatic Initialization and Updating for TIMESTAMP and DATETIME</a></li>
<li>MySQL 8.0 Reference — <a href="https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html">MySQL Server Time Zone Support</a></li>
<li>Laravel — <a href="https://laravel.com/docs/12.x/eloquent-mutators#date-casting">Date Casting, Serialization, and Timezones</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>【Modern PHP】笔记</title>
      <link>https://zyf.im/2018/05/08/modern-php-reading-notes/</link>
      <pubDate>Tue, 08 May 2018 17:00:00 +0000</pubDate>
      <guid>https://zyf.im/2018/05/08/modern-php-reading-notes/</guid>
      <description>&lt;p&gt;又回到 PHP Web 开发，使用 Laravel 框架，重读《Modern PHP》。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PHP 正在重生。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;特性&#34;&gt;特性&lt;/h2&gt;
&lt;h3 id=&#34;命名空间&#34;&gt;命名空间&lt;/h3&gt;
&lt;p&gt;声明命名空间：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;php&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;namespace&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Oreilly\ModernPHP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;导入和别名：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;php&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;use&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Symfony\Component\HttpFoundation\Response&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$r&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Res&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Oops&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;400&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$r&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;send&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PHP 5.6 开始可以导入函数和常量：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;?&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;php&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;use&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;Namespace&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;\functionName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;use&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;constant&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;Namespace&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;\CONST_NAME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;functionName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;CONST_NAME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;使用接口&#34;&gt;使用接口&lt;/h3&gt;
&lt;p&gt;接口是两个 PHP 对象之间的契约，其目的不是让一个对象依赖另一个对象的身份，而是依赖另一个对象的能力。&lt;/p&gt;
&lt;p&gt;使用接口编写更加灵活，能委托别人实现细节。&lt;/p&gt;
&lt;h3 id=&#34;性状-trait&#34;&gt;性状 trait&lt;/h3&gt;
&lt;p&gt;性状是类的部分实现，可以混入一个或者多个现有的 PHP 类中。性状有两个作用：表明类可以做什么（像是接口）；提供模块化实现（像是类）。&lt;/p&gt;
&lt;p&gt;如果想让两个无关的 PHP 类具有类似的行为，应该怎么呢？性状就是为了解决这种问题而诞生的。性状能把模块化的实现方式注入多个无关的类中。而且性状还能促进代码的重用。&lt;/p&gt;
&lt;p&gt;这与创建一个接口，两个无关的类实现这个接口的优势在于：不用写相同的实现代码，符合 DRY 原则。&lt;/p&gt;
&lt;p&gt;PHP 解释器在编译时会把性状复制粘贴到类的定义体中，但是不会处理这个操作引入的不兼容问题。如果性状假定类中有特定的属性和方法（在性状中没有定义），要确保相应的类中有对应的属性和方法。&lt;/p&gt;
&lt;h3 id=&#34;生成器&#34;&gt;生成器&lt;/h3&gt;
&lt;p&gt;Generator 是 PHP 5.5.0 引入的功能。生成器是简单的迭代器，仅此而已。&lt;/p&gt;
&lt;p&gt;PHP 生成器不要求类实现 Iterator 接口，从而减轻了类的负担。生成器会根据需求计算并产生要迭代的值。这对应该的性能有重大影响。假如标准的 PHP 迭代器经常在内存中执行迭代操作，这要预先计算出数据集，性能低；此时我们可以使用生成器，即时计算并产出后续值，不占用宝贵的内存资源。&lt;/p&gt;
&lt;p&gt;PHP 生成器不能满足所有迭代操作的需求，因为如果不查询，生成器永远不知道下一个要迭代的值是什么，在生成器中无法后退和快进。生成器还是一次性，无法多次迭代同一个生成器。不过，如果需要，可以重建或克隆生成器。&lt;/p&gt;
&lt;p&gt;PHP 生成器是 PHP 函数，只不过要在函数中一次或者多次使用 yield 关键字。生成器从不返回值，值产出值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;function&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;myGenerator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;value1&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;value2&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;yield&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;value3&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;foreach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;myGenerator&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;  &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$yieldedValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;k&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$yieldedValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;PHP_EOL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;value1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;value2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;value3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用生成器处理 CSV：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>又回到 PHP Web 开发，使用 Laravel 框架，重读《Modern PHP》。</p>
<blockquote>
<p>PHP 正在重生。</p>
</blockquote>
<h2 id="特性">特性</h2>
<h3 id="命名空间">命名空间</h3>
<p>声明命名空间：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl"><span class="k">namespace</span> <span class="nx">Oreilly\ModernPHP</span><span class="p">;</span>
</span></span></code></pre></div><p>导入和别名：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">Symfony\Component\HttpFoundation\Response</span> <span class="k">as</span> <span class="nx">Res</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$r</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Res</span><span class="p">(</span><span class="s1">&#39;Oops&#39;</span><span class="p">,</span> <span class="mi">400</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nv">$r</span><span class="o">-&gt;</span><span class="na">send</span><span class="p">();</span>
</span></span></code></pre></div><p>PHP 5.6 开始可以导入函数和常量：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">func</span> <span class="k">Namespace</span><span class="nx">\functionName</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">use</span> <span class="nx">constant</span> <span class="k">Namespace</span><span class="nx">\CONST_NAME</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">functionName</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> <span class="nx">CONST_NAME</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="使用接口">使用接口</h3>
<p>接口是两个 PHP 对象之间的契约，其目的不是让一个对象依赖另一个对象的身份，而是依赖另一个对象的能力。</p>
<p>使用接口编写更加灵活，能委托别人实现细节。</p>
<h3 id="性状-trait">性状 trait</h3>
<p>性状是类的部分实现，可以混入一个或者多个现有的 PHP 类中。性状有两个作用：表明类可以做什么（像是接口）；提供模块化实现（像是类）。</p>
<p>如果想让两个无关的 PHP 类具有类似的行为，应该怎么呢？性状就是为了解决这种问题而诞生的。性状能把模块化的实现方式注入多个无关的类中。而且性状还能促进代码的重用。</p>
<p>这与创建一个接口，两个无关的类实现这个接口的优势在于：不用写相同的实现代码，符合 DRY 原则。</p>
<p>PHP 解释器在编译时会把性状复制粘贴到类的定义体中，但是不会处理这个操作引入的不兼容问题。如果性状假定类中有特定的属性和方法（在性状中没有定义），要确保相应的类中有对应的属性和方法。</p>
<h3 id="生成器">生成器</h3>
<p>Generator 是 PHP 5.5.0 引入的功能。生成器是简单的迭代器，仅此而已。</p>
<p>PHP 生成器不要求类实现 Iterator 接口，从而减轻了类的负担。生成器会根据需求计算并产生要迭代的值。这对应该的性能有重大影响。假如标准的 PHP 迭代器经常在内存中执行迭代操作，这要预先计算出数据集，性能低；此时我们可以使用生成器，即时计算并产出后续值，不占用宝贵的内存资源。</p>
<p>PHP 生成器不能满足所有迭代操作的需求，因为如果不查询，生成器永远不知道下一个要迭代的值是什么，在生成器中无法后退和快进。生成器还是一次性，无法多次迭代同一个生成器。不过，如果需要，可以重建或克隆生成器。</p>
<p>PHP 生成器是 PHP 函数，只不过要在函数中一次或者多次使用 yield 关键字。生成器从不返回值，值产出值。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">function</span> <span class="nf">myGenerator</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">yield</span> <span class="s1">&#39;value1&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">yield</span> <span class="s1">&#39;value2&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">yield</span> <span class="s1">&#39;value3&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">foreach</span> <span class="p">(</span><span class="nx">myGenerator</span><span class="p">()</span>  <span class="k">as</span> <span class="nv">$yieldedValue</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="k">echo</span> <span class="nv">$yieldedValue</span><span class="p">,</span> <span class="nx">PHP_EOL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">value1</span>
</span></span><span class="line"><span class="cl"><span class="nx">value2</span>
</span></span><span class="line"><span class="cl"><span class="nx">value3</span>
</span></span></code></pre></div><p>使用生成器处理 CSV：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">function</span> <span class="nf">getRows</span><span class="p">(</span><span class="nv">$file</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$handle</span> <span class="o">=</span> <span class="nx">fopen</span><span class="p">(</span><span class="nv">$file</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$handle</span> <span class="o">===</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span> <span class="k">new</span> <span class="nx">Exception</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="nx">feof</span><span class="p">(</span><span class="nv">$handle</span><span class="p">)</span> <span class="o">===</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="nx">fgetcsv</span><span class="p">(</span><span class="nv">$handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">fclose</span><span class="p">(</span><span class="nv">$handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">foreach</span> <span class="p">(</span><span class="nx">getRows</span><span class="p">(</span><span class="s1">&#39;data.csv&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="nv">$row</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">print_r</span><span class="p">(</span><span class="nv">$row</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="闭包">闭包</h3>
<p>理论上讲，闭包和匿名函数是不同的概念。不过，PHP 将其视作相同的概念。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$closure</span> <span class="o">=</span> <span class="k">function</span> <span class="p">(</span><span class="nv">$name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;Hello %s&#39;</span><span class="p">,</span> <span class="nv">$name</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">echo</span> <span class="nv">$closure</span><span class="p">(</span><span class="s2">&#34;Josh&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>我们之所以能调用 $closure 变量，是因为这个变量的值是一个闭包，而且闭包对象实现了 <code>__invoke()</code> 魔术方法。只要变量名后有 ()，PHP 就会查找并调用 <code>__invoke()</code> 方法。</p>
<p>PHP 闭包常被当做函数和方法的回调使用。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$numbersPlusOne</span> <span class="o">=</span> <span class="nx">array_map</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="nv">$number</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$number</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">print_r</span><span class="p">(</span><span class="nv">$numbersPlusOne</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// [2,3,4]
</span></span></span></code></pre></div><p>在有闭包之前，只能单独创建具名函数，然后使用名称引用那个函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$numbersPlusOne</span> <span class="o">=</span> <span class="nx">array_map</span><span class="p">(</span><span class="s1">&#39;incrementNumber&#39;</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// 如果只需要使用一次回调，没必要单独定义。把闭包当成回调使用，写出的代码更整洁、更清晰。
</span></span></span></code></pre></div><p>使用 use 关键字附加闭包状态：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="k">function</span> <span class="nf">enclosePerson</span><span class="p">(</span><span class="nv">$name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">function</span> <span class="p">(</span><span class="nv">$doCommand</span><span class="p">)</span> <span class="k">use</span> <span class="p">(</span><span class="nv">$name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">sprintf</span><span class="p">(</span><span class="s1">&#39;%s, %s&#39;</span><span class="p">,</span> <span class="nv">$name</span><span class="p">,</span> <span class="nv">$doCommand</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 把字符串 Clay 封装到闭包里
</span></span></span><span class="line"><span class="cl"><span class="nv">$clay</span> <span class="o">=</span> <span class="nx">enclosePerson</span><span class="p">(</span><span class="s1">&#39;Clay&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 传入参数，调用闭包
</span></span></span><span class="line"><span class="cl"><span class="k">echo</span> <span class="nv">$clay</span><span class="p">(</span><span class="s1">&#39;get me sweet tea!&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// &#34;Clay, get me sweet tea!&#34;
</span></span></span></code></pre></div><p>具名函数 enclosePerson() 有个名为 $name 的参数，这个函数返回一个闭包对象，而且这个闭包封装了 $name 参数。即便返回的闭包对象跳出了 enclosePerson() 函数的作用域，它也会记住 $name 参数的值，因为 $name 变量仍在闭包中。</p>
<p>PHP 闭包是对象。闭包对象的默认状态没什么用，不过有一个 <code>__invoke()</code> 魔术方法和 <code>bindTo()</code> 方法。</p>
<h3 id="zend-opcache-字节码">Zend OPcache 字节码</h3>
<p>字节码缓存能存储预先编译好的 PHP 字节码。这意味着，请求 PHP 脚本时，PHP 解释器不用每次都读取、解析和编译 PHP 代码。</p>
<h3 id="内置的-http-服务器">内置的 HTTP 服务器</h3>
<p>启动这个服务器：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">php</span> <span class="o">-</span><span class="nx">S</span> <span class="nx">localhost</span><span class="o">:</span><span class="mi">4000</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 让 PHP Web 服务器监听所有接口
</span></span></span><span class="line"><span class="cl"><span class="nx">php</span> <span class="o">-</span><span class="nx">S</span> <span class="mf">0.0</span><span class="o">.</span><span class="mf">0.0</span><span class="o">:</span><span class="mi">4000</span>
</span></span></code></pre></div><h2 id="标准">标准</h2>
<h3 id="psr-是什么">PSR 是什么</h3>
<p>PHP Standards Recommendation.</p>
<ul>
<li>PSR-1 基本的代码风格</li>
<li>PSR-2 严格的代码风格</li>
<li>PSR-3 日志记录器接口</li>
<li>PSR-4 自动加载</li>
</ul>
<h2 id="组件">组件</h2>
<h3 id="查找组件">查找组件</h3>
<ul>
<li><a href="https://github.com/ziadoz/awesome-php#text-editors-and-ides">Awesome PHP</a></li>
<li><a href="https://packagist.org">Packagist</a></li>
</ul>
<h2 id="良好实践">良好实践</h2>
<h3 id="流">流</h3>
<p>流式数据的种类各异，每种类型需要独特的协议，以便读写数据。称这些协议为流封装协议。</p>
<ol>
<li>开始通信</li>
<li>读取数据</li>
<li>写入数据</li>
<li>结束通信</li>
</ol>
<p>指定协议和目标的方法是使用流标识符：</p>
<p><code>&lt;scheme&gt;://&lt;target&gt;</code></p>
<p>使用 HTTP 流封装协议创建了一个与 Flickr API 通信的 PHP 流：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span>
</span></span><span class="line"><span class="cl"><span class="nv">$json</span> <span class="o">=</span> <span class="nx">file_get_contents</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;http://api.flickr.com/services/feeds/photos_public.gne?format=json&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>不要误以为这是普通的网页 URL，file_get_contents() 函数的字符串参数其实是一个流标识符。http 协议会让 PHP 使用 HTTP 流封装协议。在这个参数中，http 之后是流的目标。很多 PHP 开发者不知道普通的 URL 其实是 PHP 流封装协议标识的伪装。</p>
<p>我们使用 file_get_contents() fopen() fwrite() 和 fclose() 函数读写文件系统。因为 PHP 默认使用的流封装协议是 file://，使用我们很少认为这些函数使用的是 PHP 流。</p>
<p>隐式使用 file:// 流封装协议：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nv">$handle</span> <span class="o">=</span> <span class="nx">fopen</span><span class="p">(</span><span class="s1">&#39;/etc/hosts&#39;</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">feof</span><span class="p">(</span><span class="nv">$handle</span><span class="p">)</span> <span class="o">!==</span> <span class="nx">ture</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">echo</span> <span class="nx">fgets</span><span class="p">(</span><span class="nv">$handle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">fclose</span><span class="p">(</span><span class="nv">$handle</span><span class="p">);</span>
</span></span></code></pre></div><p>显示使用 file:// 流封装协议：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="nv">$handle</span> <span class="o">=</span> <span class="nx">fopen</span><span class="p">(</span><span class="s1">&#39;file://etc/hosts&#39;</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span></code></pre></div><p>我们通常会省略 file:// 封装协议，这是 PHP 使用的默认值。</p>
<p>编写命令行脚本的 PHP 开发者会感激 php:// 流封装协议。这个流封装协议的作用是与 PHP 脚本的标准输入、标准输出和标准错误文件描描述符通信。</p>
<p>php://stdin 只读 PHP 流，其中的数据来自标准输入。例如，接收命令行传入脚本的信息。</p>
<p>php://stdout 把数据写入当前的缓冲区。这个流只能写，无法读或寻址。</p>
<p>php://memory 从系统内存中读取数据，或者把数据写入系统内存。缺点是，可用内存是有限的。使用 php://temp 流更安全。</p>
<p>php://temp 和 php://memory 类似，不过没有可以内存时，PHP 会把数据写入临时文件。</p>
<h3 id="错误和异常">错误和异常</h3>
<p>提到了 <a href="https://github.com/Seldaek/monolog">Monolog</a> 记录日志。</p>
<h2 id="调优">调优</h2>
<h3 id="内存">内存</h3>
<p><em>一共能分配给 PHP 多少内存？</em></p>
<p>Linode 2GB 的 sever 留 512MB 给 PHP。</p>
<p><em>单个 PHP 进程平均消耗多少内存？</em></p>
<p>使用 top 命令查看。一般 PHP 进程消耗 5 ~ 20MB 内存。</p>
<p><em>能负担的起多少个 PHP-FPM 进程？</em></p>
<p>假设 PHP 分配了 512MB 内存，每个 PHP 平均消耗 15MB 内存，从而确定能负担的起 34 个进程。</p>
<p>压力测试工具：</p>
<ul>
<li><a href="https://httpd.apache.org/docs/2.4/programs/ab.html">ab - Apache HTTP server benchmarking tool</a></li>
<li><a href="https://www.joedog.org/siege-home/">Siege</a></li>
</ul>
<h3 id="zend-opcache">Zend OPcache</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">opcache</span><span class="o">.</span><span class="nx">memory_consumption</span> <span class="o">=</span> <span class="mi">64</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 为操作码缓存分配的内存量（单位 MB）。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">opcache</span><span class="o">.</span><span class="nx">interned_strings_buffer</span> <span class="o">=</span> <span class="mi">16</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 用来存储驻留字符串的内存量（单位 MB，默认 4MB）。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">opcache</span><span class="o">.</span><span class="nx">max_accelerated_file</span> <span class="o">=</span> <span class="mi">4000</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 操作码缓存中最多能存储多少个 PHP 脚本。这个值一定比 PHP 应用中的文件数量大。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">opcache</span><span class="o">.</span><span class="nx">validate_timestamps</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 为 1 时，一段时间后 PHP 会检查 PHP 脚本的内容是否变化。检查的时间间隔由 revalidate_freq 指定。
</span></span></span><span class="line"><span class="cl"><span class="c1"># 开发环境设为 1，在生成环境中为 0。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">opcache</span><span class="o">.</span><span class="nx">revalidate_freq</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 设置多久检查一次 PHP 脚本的内容是否有变化。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">opcache</span><span class="o">.</span><span class="nx">fast_shutdown</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 这么设置能让操作码使用更快的停机步骤，把对象析构和内存释放交给 Zend Engine 的内存管理器完成。
</span></span></span></code></pre></div><h3 id="文件上传">文件上传</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">file_uploads</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="nx">upload_max_filesize</span> <span class="o">=</span> <span class="mi">10</span><span class="nx">M</span>
</span></span><span class="line"><span class="cl"><span class="nx">max_file_uploads</span> <span class="o">=</span> <span class="mi">3</span>
</span></span></code></pre></div><p>如果需要上传非常大的文件，还要调整 nginx 虚拟主机配置中的 client_max_body_size 设置。</p>
<h3 id="会话处理">会话处理</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">session</span><span class="o">.</span><span class="nx">save_handler</span> <span class="o">=</span> <span class="s1">&#39;memcached&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nx">session</span><span class="o">.</span><span class="nx">save_path</span> <span class="o">=</span> <span class="s1">&#39;127.0.0.2:11211&#39;</span>
</span></span></code></pre></div><h3 id="缓冲输出">缓冲输出</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">output_buffering</span> <span class="o">=</span> <span class="mi">4096</span>
</span></span><span class="line"><span class="cl"><span class="nx">implicit_flush</span> <span class="o">=</span> <span class="k">false</span>
</span></span></code></pre></div><p>确保使用的值是 4（32 位系统）或者 8（64 位系统）的倍数。</p>
<h3 id="真实路径缓存">真实路径缓存</h3>
<p>realpath cache，PHP 会缓存应用使用的文件路径，这样每次包含或者导入文件时就无需不断搜索包含路径了。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="cl"><span class="nx">realpath_cache_size</span> <span class="o">=</span> <span class="mi">64</span><span class="nx">k</span>
</span></span></code></pre></div><h2 id="部署">部署</h2>
<p>提到了 <a href="http://capistranorb.com/">Capistrano</a> 待研究。</p>
<h2 id="测试">测试</h2>
<ul>
<li>PHPUnit</li>
<li>Xdebug</li>
<li>使用 Travis CI 持续测试</li>
</ul>
<h2 id="分析">分析</h2>
<ul>
<li>XHProf 较新的 PHP 应用分析器</li>
<li>XHGUI</li>
<li>New Relic</li>
<li>Blackfire</li>
</ul>
<h2 id="hhvm-和-hack">HHVM 和 Hack</h2>
<p>Hip-Hop Virtual Machine.</p>
<p>Hack 是一门建立在 PHP 之上的编程语音，引入了静态类型，新的数据结构和额外的接口，同时还能向后兼容现有的动态类型 PHP 代码。</p>
<p>动态类型和静态类型，二者之间的区别在于何时检查 PHP 类型。动态类型在运行时检查类型，而静态类型在编译时检查类型。</p>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>用 Certbot 自动化 Let&#39;s Encrypt TLS 证书</title>
      <link>https://zyf.im/2018/04/26/lets-encrypt-wildcard-certificates/</link>
      <pubDate>Thu, 26 Apr 2018 16:00:00 +0000</pubDate>
      <guid>https://zyf.im/2018/04/26/lets-encrypt-wildcard-certificates/</guid>
      <description>&lt;h2 id=&#34;certbot-和-lets-encrypt-的关系&#34;&gt;Certbot 和 Let&amp;rsquo;s Encrypt 的关系&lt;/h2&gt;
&lt;h3 id=&#34;lets-encrypt&#34;&gt;Let&amp;rsquo;s Encrypt&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个免费、自动化、开放的公共证书颁发机构（CA）。&lt;/li&gt;
&lt;li&gt;通过 ACME（Automatic Certificate Management Environment，RFC 8555）协议向域名所有者颁发 DV（Domain Validation）TLS / SSL 证书。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;certbot&#34;&gt;Certbot&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;由 Electronic Frontier Foundation（EFF）维护的开源 ACME 客户端。&lt;/li&gt;
&lt;li&gt;主要目标是简化与 Let&amp;rsquo;s Encrypt 之间的交互：
&lt;ul&gt;
&lt;li&gt;自动化域名验证（HTTP-01、DNS-01、TLS-ALPN-01）&lt;/li&gt;
&lt;li&gt;安装并续期证书&lt;/li&gt;
&lt;li&gt;更新 Web 服务器配置（Apache、Nginx、Lighttpd 等）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Certbot 也能与任何兼容 ACME 的 CA 通信，不限于 Let&amp;rsquo;s Encrypt。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;acme-挑战怎么选&#34;&gt;ACME 挑战怎么选&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;挑战&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
					&lt;th&gt;限制&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;HTTP-01&lt;/td&gt;
					&lt;td&gt;80 端口可达、单域名 / 多域名（最常用）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不能签 wildcard&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;DNS-01&lt;/td&gt;
					&lt;td&gt;任意域名 —— &lt;strong&gt;wildcard 唯一选项&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;需要 DNS provider API 或手动加 TXT&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;TLS-ALPN-01&lt;/td&gt;
					&lt;td&gt;443 可达但 80 被封，常用于反代后端&lt;/td&gt;
					&lt;td&gt;较少用，需要 ACME-aware 反代支持&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;典型工作流程&#34;&gt;典型工作流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;解析参数并检测服务器类型。&lt;/li&gt;
&lt;li&gt;选择并执行挑战（HTTP-01 例：在 &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt; 下写入 token）。&lt;/li&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt 回访验证域名归属。&lt;/li&gt;
&lt;li&gt;验证通过后签发证书；Certbot 下载并安装到本地。&lt;/li&gt;
&lt;li&gt;创建 systemd timer（snap 安装会自动配置）周期性 &lt;code&gt;certbot renew&lt;/code&gt; 续期。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;开始实验&#34;&gt;开始实验&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;实验环境：Amazon Linux 2023（AL2023）。AL2023 没有官方 &lt;code&gt;certbot&lt;/code&gt; 包，社区维护的 snap 仓库是当前最省事的安装路径。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="certbot-和-lets-encrypt-的关系">Certbot 和 Let&rsquo;s Encrypt 的关系</h2>
<h3 id="lets-encrypt">Let&rsquo;s Encrypt</h3>
<ul>
<li>一个免费、自动化、开放的公共证书颁发机构（CA）。</li>
<li>通过 ACME（Automatic Certificate Management Environment，RFC 8555）协议向域名所有者颁发 DV（Domain Validation）TLS / SSL 证书。</li>
</ul>
<h3 id="certbot">Certbot</h3>
<ul>
<li>由 Electronic Frontier Foundation（EFF）维护的开源 ACME 客户端。</li>
<li>主要目标是简化与 Let&rsquo;s Encrypt 之间的交互：
<ul>
<li>自动化域名验证（HTTP-01、DNS-01、TLS-ALPN-01）</li>
<li>安装并续期证书</li>
<li>更新 Web 服务器配置（Apache、Nginx、Lighttpd 等）</li>
</ul>
</li>
<li>Certbot 也能与任何兼容 ACME 的 CA 通信，不限于 Let&rsquo;s Encrypt。</li>
</ul>
<h3 id="acme-挑战怎么选">ACME 挑战怎么选</h3>
<table>
	<thead>
			<tr>
					<th>挑战</th>
					<th>适用场景</th>
					<th>限制</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>HTTP-01</td>
					<td>80 端口可达、单域名 / 多域名（最常用）</td>
					<td><strong>不能签 wildcard</strong></td>
			</tr>
			<tr>
					<td>DNS-01</td>
					<td>任意域名 —— <strong>wildcard 唯一选项</strong></td>
					<td>需要 DNS provider API 或手动加 TXT</td>
			</tr>
			<tr>
					<td>TLS-ALPN-01</td>
					<td>443 可达但 80 被封，常用于反代后端</td>
					<td>较少用，需要 ACME-aware 反代支持</td>
			</tr>
	</tbody>
</table>
<h3 id="典型工作流程">典型工作流程</h3>
<ol>
<li>解析参数并检测服务器类型。</li>
<li>选择并执行挑战（HTTP-01 例：在 <code>/.well-known/acme-challenge/</code> 下写入 token）。</li>
<li>Let&rsquo;s Encrypt 回访验证域名归属。</li>
<li>验证通过后签发证书；Certbot 下载并安装到本地。</li>
<li>创建 systemd timer（snap 安装会自动配置）周期性 <code>certbot renew</code> 续期。</li>
</ol>
<h2 id="开始实验">开始实验</h2>
<blockquote>
<p>实验环境：Amazon Linux 2023（AL2023）。AL2023 没有官方 <code>certbot</code> 包，社区维护的 snap 仓库是当前最省事的安装路径。</p>
</blockquote>
<h3 id="启动-ec2-并配置-nginx">启动 EC2 并配置 nginx</h3>
<ul>
<li>Instance type: t2.nano</li>
<li>Username: ec2-user</li>
<li>Security Groups: 放行 HTTP 和 HTTPS</li>
<li>Public IPv4: 35.86.90.4</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf update -y
</span></span><span class="line"><span class="cl">sudo dnf install -y nginx
</span></span><span class="line"><span class="cl">nginx -v
</span></span><span class="line"><span class="cl"><span class="c1"># nginx version: nginx/1.28.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now nginx
</span></span></code></pre></div><p>直接访问 <a href="http://35.86.90.4">http://35.86.90.4</a>，会看到默认的 nginx 欢迎页面。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vi /etc/nginx/conf.d/default.conf
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="s">[::]:80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">tmp.yifans.net</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">root</span>         <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">error_page</span> <span class="mi">404</span> <span class="s">/404.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">=</span> <span class="s">/404.html</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">error_page</span> <span class="mi">500</span> <span class="mi">502</span> <span class="mi">503</span> <span class="mi">504</span> <span class="s">/50x.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">=</span> <span class="s">/50x.html</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nginx -t
</span></span><span class="line"><span class="cl">sudo systemctl reload nginx
</span></span></code></pre></div><p>把域名 <code>tmp.yifans.net</code> 解析到 <code>35.86.90.4</code>，访问 <a href="http://tmp.yifans.net">http://tmp.yifans.net</a> 同样看到 nginx 欢迎页面，浏览器显示 Not Secure。</p>
<h3 id="安装-certbot">安装 certbot</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># AL2023 暂无官方 snapd，用社区维护的仓库</span>
</span></span><span class="line"><span class="cl">sudo wget -O /etc/yum.repos.d/snapd.repo <span class="se">\
</span></span></span><span class="line"><span class="cl">    https://bboozzoo.github.io/snapd-amazon-linux/al2023/snapd.repo
</span></span><span class="line"><span class="cl">sudo dnf install -y snapd
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now snapd.socket
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo snap install --classic certbot
</span></span><span class="line"><span class="cl">sudo ln -s /snap/bin/certbot /usr/bin/certbot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">certbot --version
</span></span><span class="line"><span class="cl"><span class="c1"># certbot 4.1.1</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 前置 nginx 配置中要有 server_name tmp.yifans.net;</span>
</span></span><span class="line"><span class="cl">sudo certbot --nginx -d tmp.yifans.net
</span></span><span class="line"><span class="cl"><span class="c1"># 全自动完成</span>
</span></span></code></pre></div><p>访问 <a href="http://tmp.yifans.net">http://tmp.yifans.net</a> 会自动跳转 https。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 看看被 certbot 自动修改的 nginx 配置</span>
</span></span><span class="line"><span class="cl">cat /etc/nginx/conf.d/default.conf
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">tmp.yifans.net</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">root</span>         <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">error_page</span> <span class="mi">404</span> <span class="s">/404.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">=</span> <span class="s">/404.html</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">error_page</span> <span class="mi">500</span> <span class="mi">502</span> <span class="mi">503</span> <span class="mi">504</span> <span class="s">/50x.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">=</span> <span class="s">/50x.html</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">ssl</span> <span class="s">ipv6only=on</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span> <span class="s">/etc/letsencrypt/live/tmp.yifans.net/fullchain.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/letsencrypt/live/tmp.yifans.net/privkey.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">    <span class="kn">include</span> <span class="s">/etc/letsencrypt/options-ssl-nginx.conf</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_dhparam</span> <span class="s">/etc/letsencrypt/ssl-dhparams.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">if</span> <span class="s">(</span><span class="nv">$host</span> <span class="p">=</span> <span class="s">tmp.yifans.net)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="s">[::]:80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">tmp.yifans.net</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">return</span> <span class="mi">404</span><span class="p">;</span> <span class="c1"># managed by Certbot
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><blockquote>
<p><strong>注意</strong>：Certbot 默认不启用 HTTP/2。要开 HTTP/2 在 <code>listen 443 ssl;</code> 后手动加一行 <code>http2 on;</code>（NGINX 1.25.1+）或改用老语法 <code>listen 443 ssl http2;</code>。详见 <a href="/2017/06/06/nginx-enable-http2/">NGINX 启用 HTTP/2</a>。</p>
</blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 看看 certbot 预置的 SSL 参数</span>
</span></span><span class="line"><span class="cl">cat /etc/letsencrypt/options-ssl-nginx.conf
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># Contents are based on https://ssl-config.mozilla.org
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">ssl_session_cache</span> <span class="s">shared:le_nginx_SSL:10m</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 内存里建一个 10 MiB 共享会话缓存。
</span></span></span><span class="line"><span class="cl"><span class="c1"># 加速同客户端后续的 TLS 握手；约可保存 40000 个会话条目。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">ssl_session_timeout</span> <span class="mi">1440m</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 单位是分钟（1440 m = 24 h），缓存条目过期时间。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">ssl_session_tickets</span> <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 关闭 TLS Session Tickets。
</span></span></span><span class="line"><span class="cl"><span class="c1"># 早期实现 ticket 加密密钥不便轮换，会损害前向安全；Mozilla modern 至今仍建议关闭。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">ssl_protocols</span> <span class="s">TLSv1.2</span> <span class="s">TLSv1.3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 只允许 1.2 / 1.3；关掉 SSLv3、TLS 1.0 / 1.1。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">ssl_prefer_server_ciphers</span> <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 让客户端从交集里挑套件。仅开放现代套件的前提下，这样兼容性更好。
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">ssl_ciphers</span> <span class="s">&#34;ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 仅保留 GCM / ChaCha20-Poly1305 这一代 AEAD 套件，弃用 CBC / RC4 / 3DES。
</span></span></span><span class="line"><span class="cl"><span class="c1"># ECDHE-ECDSA：椭圆曲线 DH + ECDSA 证书
</span></span></span><span class="line"><span class="cl"><span class="c1"># ECDHE-RSA：  椭圆曲线 DH + RSA  证书
</span></span></span><span class="line"><span class="cl"><span class="c1"># DHE-RSA：    传统 DH，给极旧设备兜底
</span></span></span></code></pre></div><p><code>ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;</code> 告诉 NGINX：DHE（Diffie-Hellman Ephemeral）握手时别用默认的 1024 位弱参数，改用 Certbot 预生成的更安全 DH 参数文件。</p>
<p>DHE 是 TLS 的一种密钥交换算法，做两件事：</p>
<ul>
<li>让客户端和服务器在公网信道上安全地协商一把对称加密用的临时密钥。</li>
<li>提供<strong>前向安全（Forward Secrecy）</strong>：即便日后服务器长期私钥泄露，过去抓到的密文仍然解不开。</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 看看证书</span>
</span></span><span class="line"><span class="cl">sudo openssl x509 -in /etc/letsencrypt/live/tmp.yifans.net/fullchain.pem -noout -text <span class="p">|</span> grep yifans.net
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        Subject: <span class="nv">CN</span><span class="o">=</span>tmp.yifans.net
</span></span><span class="line"><span class="cl">                DNS:tmp.yifans.net
</span></span></code></pre></div><h3 id="只获取证书certonly">只获取证书（certonly）</h3>
<p>把 <code>tmp-certonly.yifans.net</code> 解析到 <code>35.86.90.4</code>，让 HTTP-01 挑战能回访。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vi /etc/nginx/conf.d/certonly.conf
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="s">[::]:80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">tmp-certonly.yifans.net</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">root</span>         <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nginx -t
</span></span><span class="line"><span class="cl">sudo systemctl reload nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo certbot certonly --nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 选项里勾选要签的域名</span>
</span></span><span class="line"><span class="cl">1: tmp.yifans.net
</span></span><span class="line"><span class="cl">2: tmp-certonly.yifans.net
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Successfully received certificate.
</span></span><span class="line"><span class="cl">Certificate is saved at: /etc/letsencrypt/live/tmp-certonly.yifans.net/fullchain.pem
</span></span><span class="line"><span class="cl">Key is saved at:         /etc/letsencrypt/live/tmp-certonly.yifans.net/privkey.pem
</span></span><span class="line"><span class="cl">This certificate expires on 2025-10-01.
</span></span></code></pre></div><p><code>certonly</code> 模式只拿证书，<strong>不会</strong>改你的 nginx 配置，需要手动在 server block 里加 <code>ssl_certificate</code> 等指令。</p>
<h3 id="wildcard-证书">Wildcard 证书</h3>
<ul>
<li>Wildcard <strong>强制走 DNS-01</strong>：HTTP-01 没法证明你对 <code>*.example.com</code> 下所有子域的控制权。</li>
<li><code>*.example.com</code> 不覆盖 <code>example.com</code> 本身，也不覆盖 <code>hello.goodbye.example.com</code>。</li>
<li><code>*.goodbye.example.com</code> 可覆盖 <code>hello.goodbye.example.com</code>。</li>
<li>不可以申请 <code>*.*.example.com</code>，只能一个 <code>*</code>。</li>
</ul>
<p>有 DNS API 的话可用 <a href="https://eff-certbot.readthedocs.io/en/latest/using.html#dns-plugins">DNS Plugins</a>，没有就 manual：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo certbot certonly --manual -d <span class="s1">&#39;*.tmp.yifans.net&#39;</span> --preferred-challenges dns
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Please deploy a DNS TXT record under the name:
</span></span><span class="line"><span class="cl">  _acme-challenge.tmp.yifans.net.
</span></span><span class="line"><span class="cl">with the following value: ...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 用 dig 验证再回车，否则 Certbot 失败后会消耗速率额度</span>
</span></span><span class="line"><span class="cl"><span class="c1"># https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.tmp.yifans.net.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Successfully received certificate.
</span></span><span class="line"><span class="cl">Certificate is saved at: /etc/letsencrypt/live/tmp.yifans.net-0001/fullchain.pem
</span></span><span class="line"><span class="cl">Key is saved at:         /etc/letsencrypt/live/tmp.yifans.net-0001/privkey.pem
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo openssl x509 -in /etc/letsencrypt/live/tmp.yifans.net-0001/fullchain.pem -noout -text <span class="p">|</span> grep yifans.net
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        Subject: <span class="nv">CN</span><span class="o">=</span>*.tmp.yifans.net
</span></span><span class="line"><span class="cl">                DNS:*.tmp.yifans.net
</span></span></code></pre></div><p>挂到 nginx：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">*.tmp.yifans.net</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">root</span>         <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">http2</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span>     <span class="s">/etc/letsencrypt/live/tmp.yifans.net-0001/fullchain.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/letsencrypt/live/tmp.yifans.net-0001/privkey.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">include</span> <span class="s">/etc/letsencrypt/options-ssl-nginx.conf</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_dhparam</span> <span class="s">/etc/letsencrypt/ssl-dhparams.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">*.tmp.yifans.net</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nginx -t <span class="o">&amp;&amp;</span> sudo systemctl reload nginx
</span></span></code></pre></div><p>把 <code>1.tmp.yifans.net</code> 解析到 <code>35.86.90.4</code>，访问 <a href="https://1.tmp.yifans.net">https://1.tmp.yifans.net</a>，Chrome 证书信息里能看到 <code>Common Name (CN) *.tmp.yifans.net</code>。</p>
<h3 id="续期">续期</h3>
<p>snap 安装的 Certbot 自带 systemd timer，每天跑两次，<strong>只在 30 天内到期时</strong>才真正续期：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl list-timers <span class="p">|</span> grep certbot
</span></span><span class="line"><span class="cl">sudo certbot renew --dry-run            <span class="c1"># 演练一次，不真签发</span>
</span></span></code></pre></div><p>如果是 <code>certonly</code> 模式（Certbot 不管 nginx），需要让续期后自动 reload：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 一次性：</span>
</span></span><span class="line"><span class="cl">sudo certbot renew --deploy-hook <span class="s1">&#39;systemctl reload nginx&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 或永久放在 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh：</span>
</span></span><span class="line"><span class="cl"><span class="c1">#!/bin/sh</span>
</span></span><span class="line"><span class="cl">systemctl reload nginx
</span></span></code></pre></div><h2 id="几个常被踩到的坑">几个常被踩到的坑</h2>
<ul>
<li>
<p><strong>速率限制</strong>。Let&rsquo;s Encrypt 每个注册域名每周最多 50 张证书。调试或写脚本时务必先用 <code>--test-cert</code>（staging 服务器），免得被 ban 一周。staging 颁发的证书浏览器不信任，但能验通整个流程。</p>
</li>
<li>
<p><strong>证书有效期 90 天</strong>。Certbot 在 30 天阈值内才会真正续期，所以 <code>renew</code> 跑了&quot;没动作&quot;是正常的。</p>
</li>
<li>
<p><strong>ECDSA 默认</strong>。Let&rsquo;s Encrypt 2023 起对新证书默认签 ECDSA P-256（更小更快）；想要 RSA 反而要显式 <code>--key-type rsa</code>。</p>
</li>
<li>
<p><strong>HSTS 不会自动配</strong>。拿到证书只完成了一半，浏览器仍会接受 http 跳转；建议在 server block 加：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">add_header</span> <span class="s">Strict-Transport-Security</span> <span class="s">&#34;max-age=31536000</span><span class="p">;</span> <span class="k">includeSubDomains&#34;</span> <span class="s">always</span><span class="p">;</span>
</span></span></code></pre></div><p>正式开 HSTS 前先用较短 <code>max-age</code> 灰度，避免误配把整个域锁死。</p>
</li>
<li>
<p><strong>改了证书别忘 reload</strong>。<code>--nginx</code> 插件帮你做了；<code>certonly</code> 自己做（见上面 deploy-hook）。</p>
</li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://certbot.eff.org/instructions?ws=nginx&amp;os=snap">Nginx on Linux (snap) | Certbot</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8555">RFC 8555 - Automatic Certificate Management Environment</a></li>
<li><a href="https://letsencrypt.org/docs/rate-limits/">Let&rsquo;s Encrypt Rate Limits</a></li>
<li><a href="https://ssl-config.mozilla.org/">Mozilla SSL Configuration Generator</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>【Git 权威指南】读书笔记 - 协同</title>
      <link>https://zyf.im/2018/01/17/got-git-reading-notes-harmony/</link>
      <pubDate>Wed, 17 Jan 2018 19:00:00 +0000</pubDate>
      <guid>https://zyf.im/2018/01/17/got-git-reading-notes-harmony/</guid>
      <description>&lt;p&gt;【Git 权威指南】&amp;ldquo;和声&amp;rdquo;+&amp;ldquo;协同模型&amp;quot;两章的精炼合并版：远程协议、推送与冲突解决、里程碑共享、分支协同、工作流模型、共用代码（submodule / subtree）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文沿用书中 &lt;code&gt;master&lt;/code&gt; 作为默认分支名；Git 2.28+ 默认改为 &lt;code&gt;main&lt;/code&gt;，行为完全一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;远程协议与版本库&#34;&gt;远程协议与版本库&lt;/h2&gt;
&lt;h3 id=&#34;协议选择&#34;&gt;协议选择&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;协议&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
					&lt;th&gt;现状&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;SSH&lt;/td&gt;
					&lt;td&gt;推 + 拉，需公钥认证&lt;/td&gt;
					&lt;td&gt;协作首选&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HTTPS&lt;/td&gt;
					&lt;td&gt;推 + 拉，凭证由 helper 缓存&lt;/td&gt;
					&lt;td&gt;网络受限环境首选；个人项目常用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;本地&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;file://&lt;/code&gt; 或裸路径&lt;/td&gt;
					&lt;td&gt;备份 / 镜像&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;git://&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;只读匿名协议&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;已弃用&lt;/strong&gt;，GitHub 2022 年关停&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;FTP / RSYNC&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;已弃用&lt;/strong&gt;，忘了它们&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh://&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;user@&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;example.com&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;:port&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;/path/to/repo.git
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;user@&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;example.com:path/to/repo.git          &lt;span class=&#34;c1&#34;&gt;# SSH 简写&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;https://example.com/path/to/repo.git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;远程版本库管理&#34;&gt;远程版本库管理&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote -v                                &lt;span class=&#34;c1&#34;&gt;# 列出 fetch/push URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote add &amp;lt;name&amp;gt; &amp;lt;url&amp;gt;                  &lt;span class=&#34;c1&#34;&gt;# 注册&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote set-url &amp;lt;name&amp;gt; &amp;lt;url&amp;gt;              &lt;span class=&#34;c1&#34;&gt;# 改 URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote set-url --push &amp;lt;name&amp;gt; &amp;lt;url&amp;gt;       &lt;span class=&#34;c1&#34;&gt;# 单独改 push URL（读写分离）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote rename &amp;lt;old&amp;gt; &amp;lt;new&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote rm &amp;lt;name&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git remote update                            &lt;span class=&#34;c1&#34;&gt;# 拉取所有 remote 的更新&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git config remote.&amp;lt;name&amp;gt;.skipDefaultUpdate &lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;   &lt;span class=&#34;c1&#34;&gt;# 排除某个 remote&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;fetch--pull-的关系&#34;&gt;fetch / pull 的关系&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git pull = git fetch + git merge        # 默认
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git pull --rebase = git fetch + git rebase
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;习惯用 rebase 整理历史的人，全局开启：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>【Git 权威指南】&ldquo;和声&rdquo;+&ldquo;协同模型&quot;两章的精炼合并版：远程协议、推送与冲突解决、里程碑共享、分支协同、工作流模型、共用代码（submodule / subtree）。</p>
<blockquote>
<p>本文沿用书中 <code>master</code> 作为默认分支名；Git 2.28+ 默认改为 <code>main</code>，行为完全一致。</p>
</blockquote>
<h2 id="远程协议与版本库">远程协议与版本库</h2>
<h3 id="协议选择">协议选择</h3>
<table>
	<thead>
			<tr>
					<th>协议</th>
					<th>用途</th>
					<th>现状</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>SSH</td>
					<td>推 + 拉，需公钥认证</td>
					<td>协作首选</td>
			</tr>
			<tr>
					<td>HTTPS</td>
					<td>推 + 拉，凭证由 helper 缓存</td>
					<td>网络受限环境首选；个人项目常用</td>
			</tr>
			<tr>
					<td>本地</td>
					<td><code>file://</code> 或裸路径</td>
					<td>备份 / 镜像</td>
			</tr>
			<tr>
					<td><code>git://</code></td>
					<td>只读匿名协议</td>
					<td><strong>已弃用</strong>，GitHub 2022 年关停</td>
			</tr>
			<tr>
					<td>FTP / RSYNC</td>
					<td>—</td>
					<td><strong>已弃用</strong>，忘了它们</td>
			</tr>
	</tbody>
</table>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh://<span class="o">[</span>user@<span class="o">]</span>example.com<span class="o">[</span>:port<span class="o">]</span>/path/to/repo.git
</span></span><span class="line"><span class="cl"><span class="o">[</span>user@<span class="o">]</span>example.com:path/to/repo.git          <span class="c1"># SSH 简写</span>
</span></span><span class="line"><span class="cl">https://example.com/path/to/repo.git
</span></span></code></pre></div><h3 id="远程版本库管理">远程版本库管理</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git remote -v                                <span class="c1"># 列出 fetch/push URL</span>
</span></span><span class="line"><span class="cl">git remote add &lt;name&gt; &lt;url&gt;                  <span class="c1"># 注册</span>
</span></span><span class="line"><span class="cl">git remote set-url &lt;name&gt; &lt;url&gt;              <span class="c1"># 改 URL</span>
</span></span><span class="line"><span class="cl">git remote set-url --push &lt;name&gt; &lt;url&gt;       <span class="c1"># 单独改 push URL（读写分离）</span>
</span></span><span class="line"><span class="cl">git remote rename &lt;old&gt; &lt;new&gt;
</span></span><span class="line"><span class="cl">git remote rm &lt;name&gt;
</span></span><span class="line"><span class="cl">git remote update                            <span class="c1"># 拉取所有 remote 的更新</span>
</span></span><span class="line"><span class="cl">git config remote.&lt;name&gt;.skipDefaultUpdate <span class="nb">true</span>   <span class="c1"># 排除某个 remote</span>
</span></span></code></pre></div><h3 id="fetch--pull-的关系">fetch / pull 的关系</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">git pull = git fetch + git merge        # 默认
</span></span><span class="line"><span class="cl">git pull --rebase = git fetch + git rebase
</span></span></code></pre></div><p>习惯用 rebase 整理历史的人，全局开启：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global pull.rebase <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git config --global branch.autosetuprebase always   <span class="c1"># 新追踪分支自动设 rebase</span>
</span></span></code></pre></div><blockquote>
<p><code>pull.rebase=true</code> 是 2.6+ 后协同的现代默认姿势，避免 <code>git pull</code> 平白塞进一堆 <code>Merge branch 'main'</code> 的噪声。</p>
</blockquote>
<h2 id="推送与冲突解决">推送与冲突解决</h2>
<h3 id="推送基础">推送基础</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git push                                <span class="c1"># 推当前分支到追踪的上游</span>
</span></span><span class="line"><span class="cl">git push -u origin &lt;branch&gt;             <span class="c1"># 首次推并建立 upstream</span>
</span></span><span class="line"><span class="cl">git push origin dev:master              <span class="c1"># 显式 refspec：本地 dev → 远端 master</span>
</span></span></code></pre></div><h3 id="非快进推送force-的现代姿势">非快进推送：force 的现代姿势</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git push -f                                       <span class="c1"># 危险：会盖掉别人推的东西</span>
</span></span><span class="line"><span class="cl">git push --force-with-lease                       <span class="c1"># 推荐：只在远端确实是你看到的版本时才覆盖</span>
</span></span><span class="line"><span class="cl">git push --force-with-lease --force-if-includes   <span class="c1"># 2.30+ 更严格：还要求你 fetch 过最新</span>
</span></span></code></pre></div><blockquote>
<p><code>--force-with-lease</code> 是 <code>-f</code> 的安全替代品。它检测远端是否被别人推了新东西，是就拒推 —— 多人协作改 rebase 后强推必备。</p>
</blockquote>
<p>服务端禁止非快进推送（用于受保护分支）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git --git-dir<span class="o">=</span>/path/to/shared.git config receive.denyNonFastForwards <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git --git-dir<span class="o">=</span>/path/to/shared.git config receive.denyDeletes <span class="nb">true</span>
</span></span></code></pre></div><blockquote>
<p>真正的权限控制还是交给 GitLab / GitHub 服务端的 protected branch / push rule，这里只是底层兜底。</p>
</blockquote>
<h3 id="冲突解决的实际流程">冲突解决的实际流程</h3>
<p>合并 / 变基遇冲突时，冲突文件长这样：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
</span></span><span class="line"><span class="cl">我这边的版本
</span></span><span class="line"><span class="cl">=======
</span></span><span class="line"><span class="cl">对方分支的版本
</span></span><span class="line"><span class="cl">&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature
</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="cl">git status                       <span class="c1"># 列出 &#34;both modified&#34; 的文件</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 编辑器里手动取舍，或调工具</span>
</span></span><span class="line"><span class="cl">git mergetool                    <span class="c1"># 调用 vimdiff / opendiff / vscode 等</span>
</span></span><span class="line"><span class="cl">git diff --check                 <span class="c1"># 检查是否还有遗留 &lt;&lt;&lt;&lt;&lt;&lt;&lt;</span>
</span></span><span class="line"><span class="cl">git add &lt;files&gt;
</span></span><span class="line"><span class="cl">git commit                       <span class="c1"># merge 用 commit；rebase 用 git rebase --continue</span>
</span></span><span class="line"><span class="cl">git merge --abort                <span class="c1"># 想反悔</span>
</span></span><span class="line"><span class="cl">git rebase --abort
</span></span></code></pre></div><h3 id="合并策略关键陷阱-x-ours-vs--s-ours">合并策略关键陷阱：<code>-X ours</code> vs <code>-s ours</code></h3>
<p>写法相近，<strong>效果完全不同</strong>：</p>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>行为</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>git merge -X ours &lt;branch&gt;</code></td>
					<td>recursive 策略 + ours 选项。<strong>只在冲突 hunk</strong> 上偏向本方，非冲突的对方改动正常合入</td>
			</tr>
			<tr>
					<td><code>git merge -s ours &lt;branch&gt;</code></td>
					<td>ours 策略。<strong>对方分支整个内容全部丢弃</strong>，只记录一笔合并提交</td>
			</tr>
	</tbody>
</table>
<p>后者常用于&quot;我宣告这个分支已废弃但要在历史上标一笔&rdquo;。误用会神不知鬼不觉丢光对方所有改动。</p>
<h3 id="合并方式fast-forward-还是-no-ff">合并方式：fast-forward 还是 no-ff</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git merge feature                <span class="c1"># 默认：如果可以 ff 就 ff，分支痕迹消失</span>
</span></span><span class="line"><span class="cl">git merge --no-ff feature        <span class="c1"># 强制产生合并提交，保留分支拓扑</span>
</span></span><span class="line"><span class="cl">git config --global merge.ff <span class="nb">false</span>
</span></span></code></pre></div><p>团队偏好&quot;看得见每个 feature 分支&quot;的，统一关掉 ff；偏好线性历史的，反过来用 rebase + ff。</p>
<h2 id="里程碑共享">里程碑共享</h2>
<h3 id="tag-不会随分支自动推">tag 不会随分支自动推</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git push origin v1.0                     <span class="c1"># 推单个 tag</span>
</span></span><span class="line"><span class="cl">git push origin --tags                   <span class="c1"># 推所有本地 tag</span>
</span></span><span class="line"><span class="cl">git push origin refs/tags/v*             <span class="c1"># refspec 通配</span>
</span></span><span class="line"><span class="cl">git push origin :refs/tags/v1.0          <span class="c1"># 删除远端 tag</span>
</span></span><span class="line"><span class="cl">git fetch --tags                         <span class="c1"># 主动拉所有 tag</span>
</span></span></code></pre></div><blockquote>
<p>默认 <code>git fetch</code> 只会带回与已 fetch 的 commit <strong>可达</strong>的 tag，孤立 tag 不会自动同步。
远端有同名 tag 而本地没有时会建立；本地已存在则<strong>不会覆盖</strong>，需要 <code>git fetch --tags -f</code>。</p>
</blockquote>
<h3 id="命名语义化版本">命名：语义化版本</h3>
<p><a href="https://semver.org/lang/zh-CN/">Semantic Versioning 2.0</a> —— <code>MAJOR.MINOR.PATCH</code>：</p>
<ul>
<li><strong>MAJOR</strong> 不兼容改动</li>
<li><strong>MINOR</strong> 向下兼容的新功能</li>
<li><strong>PATCH</strong> 向下兼容的修复</li>
</ul>
<p>预发布 / 编译信息：<code>1.0.0-beta.1+20251010</code>。</p>
<h2 id="分支协同">分支协同</h2>
<h3 id="本地分支基本操作">本地分支基本操作</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git branch                              <span class="c1"># 列本地</span>
</span></span><span class="line"><span class="cl">git branch -r                           <span class="c1"># 列远程跟踪</span>
</span></span><span class="line"><span class="cl">git branch -a                           <span class="c1"># 全部</span>
</span></span><span class="line"><span class="cl">git branch -vv                          <span class="c1"># 含 upstream 关系</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git switch &lt;branch&gt;                     <span class="c1"># 切分支（2.23+，比 checkout 语义清晰）</span>
</span></span><span class="line"><span class="cl">git switch -c &lt;new&gt; <span class="o">[</span>&lt;start&gt;<span class="o">]</span>           <span class="c1"># 创建并切换</span>
</span></span><span class="line"><span class="cl">git branch -d &lt;branch&gt;                  <span class="c1"># 安全删（未合并会拒绝）</span>
</span></span><span class="line"><span class="cl">git branch -D &lt;branch&gt;                  <span class="c1"># 强删</span>
</span></span><span class="line"><span class="cl">git branch -m &lt;old&gt; &lt;new&gt;               <span class="c1"># 重命名</span>
</span></span></code></pre></div><h3 id="远程跟踪分支">远程跟踪分支</h3>
<p><code>origin/main</code> 这种引用住在 <code>refs/remotes/</code> 命名空间，是 fetch 后服务端最新状态的<strong>本地快照</strong>，不是真正的分支 —— 不能直接 commit 到它上面。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git switch hello-1.x                    <span class="c1"># DWIM：本地无同名分支时，自动基于 origin/hello-1.x 建并跟踪</span>
</span></span><span class="line"><span class="cl">git switch -c local-feat origin/feat    <span class="c1"># 显式基于远程分支建跟踪分支</span>
</span></span><span class="line"><span class="cl">git branch --set-upstream-to<span class="o">=</span>origin/main main   <span class="c1"># 给已存在分支补设 upstream</span>
</span></span><span class="line"><span class="cl">git fetch --prune                       <span class="c1"># 清理远端已删的跟踪分支</span>
</span></span></code></pre></div><blockquote>
<p><strong>DWIM (Do What I Mean)</strong>：<code>git switch &lt;name&gt;</code> 时若本地无同名分支但远端恰有一个 <code>origin/&lt;name&gt;</code>，Git 会自动创建本地分支并设 upstream。多个 remote 都有时不触发，避免歧义。</p>
</blockquote>
<h3 id="rebase-整理本地分支">rebase 整理本地分支</h3>
<p><code>dev</code> 基于老的 <code>master</code>，希望以最新 <code>master</code> 为基底再推送：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git switch dev
</span></span><span class="line"><span class="cl">git fetch origin
</span></span><span class="line"><span class="cl">git rebase origin/master
</span></span><span class="line"><span class="cl"><span class="c1"># 冲突 → 解决 → git add → git rebase --continue</span>
</span></span><span class="line"><span class="cl">git push --force-with-lease             <span class="c1"># 历史被重写，必须 force（用 with-lease 才安全）</span>
</span></span></code></pre></div><blockquote>
<p>共享分支 rebase 后必须 <code>--force-with-lease</code>，且最好提前在团队里 broadcast 一下。
个人独享分支随便 rebase；团队共享主干永远 merge 不 rebase。</p>
</blockquote>
<h3 id="git-worktree并行多分支">git worktree：并行多分支</h3>
<p>不再需要&quot;克隆好几份仓库&quot;或者 <code>git stash</code> 来回切：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add ../proj-hotfix hotfix         <span class="c1"># 在隔壁目录开一个挂在 hotfix 分支的工作区</span>
</span></span><span class="line"><span class="cl">git worktree list
</span></span><span class="line"><span class="cl">git worktree remove ../proj-hotfix
</span></span></code></pre></div><p>每个 worktree 共享同一 <code>.git/objects</code>，但有独立工作区和独立 HEAD。Code review、跑长测试、临时改 hotfix 都不打断主工作区。</p>
<h3 id="分支安全配置">分支安全配置</h3>
<table>
	<thead>
			<tr>
					<th>配置</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>core.logallrefupdates=true</code></td>
					<td>给每条分支记 reflog（带工作区的库默认开；裸库默认关，建议手动打开）</td>
			</tr>
			<tr>
					<td><code>receive.denyNonFastForwards=true</code></td>
					<td>服务端拒绝非快进推送</td>
			</tr>
			<tr>
					<td><code>receive.denyDeletes=true</code></td>
					<td>服务端拒绝删除分支</td>
			</tr>
	</tbody>
</table>
<h2 id="工作流模型">工作流模型</h2>
<p>经典三种，选用按团队规模和发布节奏：</p>
<table>
	<thead>
			<tr>
					<th>模型</th>
					<th>适用</th>
					<th>关键约束</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Trunk-based</strong></td>
					<td>持续部署、CI 强的小到中团队</td>
					<td>所有人直推 <code>main</code>（或短命 feature 分支）+ feature flag</td>
			</tr>
			<tr>
					<td><strong>GitHub Flow</strong></td>
					<td>SaaS 持续交付，最常见</td>
					<td><code>main</code> 永远可部署，feature 分支走 PR → 合并 → 部署</td>
			</tr>
			<tr>
					<td><strong>GitFlow</strong></td>
					<td>有明确版本发布周期的产品（客户端、SDK）</td>
					<td><code>main</code> + <code>develop</code> + <code>release/*</code> + <code>hotfix/*</code>，重</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p>Topgit、卖主分支 (Vendor Branch) 是 2010 年前的产物，现代项目用包管理器（npm / composer / cargo）+ submodule / subtree 覆盖了这些场景，可忽略。</p>
</blockquote>
<h2 id="共用代码submodule-vs-subtree">共用代码：submodule vs subtree</h2>
<p>两者解决同一问题（在一个仓库里嵌套引用另一个仓库），但取舍完全不同：</p>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>submodule</th>
					<th>subtree</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>子项目代码物理位置</td>
					<td>独立 <code>.git</code>，靠 commit 指针引用</td>
					<td><strong>直接嵌入主仓库</strong></td>
			</tr>
			<tr>
					<td>克隆体验</td>
					<td>需要 <code>--recurse-submodules</code> 或 init</td>
					<td>克隆主仓即拿全</td>
			</tr>
			<tr>
					<td>主仓体积</td>
					<td>小（只存指针）</td>
					<td>大（含子项目历史）</td>
			</tr>
			<tr>
					<td>协作者上手成本</td>
					<td>高（需理解 submodule + detached HEAD）</td>
					<td>低（看起来就是普通目录）</td>
			</tr>
			<tr>
					<td>双向同步（向上游回推）</td>
					<td>直接在子仓库 push 即可</td>
					<td>麻烦（<code>git subtree push</code> 提取相关 commit）</td>
			</tr>
			<tr>
					<td>适合</td>
					<td>真正独立的依赖，少改</td>
					<td>共享代码模板、内嵌工具脚本</td>
			</tr>
	</tbody>
</table>
<h3 id="submodule-标准姿势">submodule 标准姿势</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule add &lt;url&gt; &lt;path&gt;          <span class="c1"># 添加；生成 .gitmodules</span>
</span></span><span class="line"><span class="cl">git clone --recurse-submodules &lt;url&gt;    <span class="c1"># 一次克隆主仓 + 所有子模块（推荐）</span>
</span></span><span class="line"><span class="cl">git submodule update --init --recursive <span class="c1"># 已克隆的仓库补拉子模块</span>
</span></span><span class="line"><span class="cl">git submodule update --remote           <span class="c1"># 把子模块拉到其追踪分支的最新</span>
</span></span><span class="line"><span class="cl">git submodule status
</span></span></code></pre></div><blockquote>
<p><strong>submodule 头号陷阱</strong>：每个 submodule 始终处于 detached HEAD。要在子模块里改东西，先 <code>git -C &lt;path&gt; switch &lt;branch&gt;</code> 切到分支，提交后<strong>先推子仓库，再推主仓库</strong> —— 顺序反了别人会拉不到对应 commit。</p>
</blockquote>
<h3 id="subtree-标准姿势">subtree 标准姿势</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 引入：把 S 项目作为主仓的 lib/s 子目录</span>
</span></span><span class="line"><span class="cl">git subtree add --prefix<span class="o">=</span>lib/s &lt;s-url&gt; main --squash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 拉上游更新</span>
</span></span><span class="line"><span class="cl">git subtree pull --prefix<span class="o">=</span>lib/s &lt;s-url&gt; main --squash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 把主仓内对 lib/s 的改动回推到 S 项目</span>
</span></span><span class="line"><span class="cl">git subtree push --prefix<span class="o">=</span>lib/s &lt;s-url&gt; feature-branch
</span></span></code></pre></div><p><code>--squash</code> 把上游历史压成一笔 commit，主仓 <code>git log</code> 干净，代价是主仓再也看不到子项目的提交图。<strong>统一加或统一不加</strong>，混用会撞合并冲突。</p>
<h3 id="从已有项目剥离子模块">从已有项目剥离子模块</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. 从 P 项目里抽出 lib/s 的所有 commit，存到临时分支</span>
</span></span><span class="line"><span class="cl">git subtree split -P lib/s -b extracted
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. 创建独立的 S 仓库</span>
</span></span><span class="line"><span class="cl">mkdir ../s <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ../s <span class="o">&amp;&amp;</span> git init
</span></span><span class="line"><span class="cl">git pull ../P extracted
</span></span><span class="line"><span class="cl">git remote add origin &lt;s-url&gt;
</span></span><span class="line"><span class="cl">git push -u origin main
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. P 项目里删掉原目录，改用 subtree 引用</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ../P
</span></span><span class="line"><span class="cl">git rm -rf lib/s <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;Extract lib/s to standalone repo&#34;</span>
</span></span><span class="line"><span class="cl">git subtree add --prefix<span class="o">=</span>lib/s &lt;s-url&gt; main --squash
</span></span></code></pre></div><blockquote>
<p>一个常见用途：把构建产物推到 GitHub Pages</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git subtree push --prefix<span class="o">=</span>dist origin gh-pages
</span></span></code></pre></div></blockquote>
<h2 id="补丁文件交互">补丁文件交互</h2>
<p>少数场景（邮件协作 / 没有共同远端 / 跨网隔离环境）会用补丁文件传递提交：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git format-patch -s HEAD~3..HEAD         <span class="c1"># 把最近 3 个提交导成 3 个 *.patch 文件，-s 加 Signed-off-by</span>
</span></span><span class="line"><span class="cl">git format-patch -s origin/main..HEAD    <span class="c1"># 导出 fork 后所有新增提交</span>
</span></span><span class="line"><span class="cl">git am &lt; user1.mbox                      <span class="c1"># 收方导入，保留 author 与 message</span>
</span></span><span class="line"><span class="cl">git apply foo.patch                      <span class="c1"># 仅打补丁，不产生 commit，author 信息丢失</span>
</span></span><span class="line"><span class="cl">git apply --check foo.patch              <span class="c1"># 试跑，不真改</span>
</span></span></code></pre></div><p>Linux 内核、Git 本身都还在用邮件 + <code>git format-patch</code> / <code>git am</code> 流程；日常 GitHub / GitLab 协作用 PR / MR 即可。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="http://www.worldhello.net/gotgit/03-git-harmony">3. Git 和声 — GotGit</a></li>
<li><a href="http://www.worldhello.net/gotgit/04-git-model/">4. 协同模型 — GotGit</a></li>
<li><a href="https://semver.org/lang/zh-CN/">Semantic Versioning 2.0.0</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>回顾 2017</title>
      <link>https://zyf.im/2017/12/31/review-2017/</link>
      <pubDate>Sun, 31 Dec 2017 22:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/12/31/review-2017/</guid>
      <description>&lt;p&gt;2017 关键词：Birds、离职、狼人杀、白洋淀、iOS、自如。&lt;/p&gt;
&lt;iframe frameborder=&#34;no&#34; border=&#34;0&#34; marginwidth=&#34;0&#34; marginheight=&#34;0&#34; width=298 height=52 src=&#34;//music.163.com/outchain/player?type=2&amp;id=423314746&amp;auto=0&amp;height=32&#34;&gt;&lt;/iframe&gt;
&lt;h2 id=&#34;birds&#34;&gt;Birds&lt;/h2&gt;
&lt;p&gt;Birds 是自己编写时间最长的一个 Web 项目。项目接手、改版、重构、迭代、盈利，上半年每天工作都是面对它。&lt;/p&gt;
&lt;!-- more --&gt;
&lt;p&gt;Birds 良好的表现与需求互相推动，这点很重要，假如有需求而项目没有成长，或项目成长没了需求，对工程来说都只能是纸上谈兵、无事可做。项目中的规范的重要性，无论是代码还是数据库，只要项目不是夭折终将显现，对于规范性我绝不会开倒车，做任何妥协，种种经历只说明：这里欠的东西总会还。如何应对需求的千变万化，小结几点：模块化、组件化思想，功能多可配置，功能可拔插，功能方法粒度要小；不要对程序肆意进行打补丁式的修改，减少对流程入侵；当某一处的逻辑增加时要及时重新修改、定义流程；对需求目的的正确理解更是能高效开发。&lt;/p&gt;
&lt;p&gt;对前端重新认识的一年。现在的前端和大学时刚刚接触时，已经大不一样了。前端能做的事情越来越多，很多业务逻辑都在前端处理，服务端只需要提供接口，分工合作更容易。浅尝 React Vue Webpack 后，也许前端才是自己的归宿。&lt;/p&gt;
&lt;h2 id=&#34;离职&#34;&gt;离职&lt;/h2&gt;
&lt;p&gt;离职的不是我。老人走，新人来，新人走。离职一词在我身边跟了一年都没有停下来。同事离职的原因很多，离开北京、公司发展、自我发展&amp;hellip; 马云说离职的原因就两个：1. 钱没给够。2. 受了委屈。我起初是认同这一说法的，但是后来觉得“人”或者说“人的思想”或者说“人性”在其中影响作用非常的大。相同的事情不同的角度看，结果相去甚远，如果基本的理念不同，那事情基本是无法调和的。&lt;/p&gt;
&lt;p&gt;对做的事情的认同感，对自我的定位与认识，对现实情况的冷静思考，这几点需要反复咀嚼。&lt;/p&gt;
&lt;p&gt;分享一篇文章：&lt;a href=&#34;http://stormzhang.com/2017/06/26/leave-if-no-experts/&#34;&gt;公司没大牛带，需要离职么？&lt;/a&gt;。对于刚入门的我们，这是一个很有代表性的问题。太多时候我们期待别人，忘了自己。公司有大牛，能跟着学是幸运，没有是常态。先接受了这个设定，会更容易找提高自己的方法。有没有大牛并不重要，我们是为了那个更好的自己。&lt;/p&gt;
&lt;h2 id=&#34;狼人杀&#34;&gt;狼人杀&lt;/h2&gt;
&lt;p&gt;2017 火的游戏很多：狼人杀、守望先锋、王者荣耀、吃鸡。狼人杀这一杀，好像杀到了毕业前和同学舍友在一起臭 high 的日子。可说骚话、互相 diss、互相吹捧，真真假假其乐无穷。最赤鸡的是鱼炸出了个女盆友，各路同学亲上加亲，无法克说。&lt;/p&gt;
&lt;h2 id=&#34;白洋淀&#34;&gt;白洋淀&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我想 你说
你不要在孤单 让我做你的伴&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;白洋淀去过四次，那里的温泉过去两次，年中 Team Building 再喜加一，而这一次让这里有了美丽故事。喷泉、沙发，出现了对的人。&lt;/p&gt;
&lt;h2 id=&#34;ios&#34;&gt;iOS&lt;/h2&gt;
&lt;p&gt;React Native 的一波尝试后，走上了移动开发的道。Swift 让上 iOS 车的门槛降低不少，Swift API 趋向稳定，可以说这是最好的时候。GitHub 应该是再也放不下了，外文的书籍、文档确实靠谱啊，推荐一个教程网站 &lt;a href=&#34;https://www.raywenderlich.com/category/ios&#34;&gt;iOS Tutorials - Ray Wenderlich&lt;/a&gt; 真的零基础入门。&lt;/p&gt;
&lt;p&gt;我相信：一个人语言的界限，就是他世界的边界。&lt;/p&gt;
&lt;h2 id=&#34;自如&#34;&gt;自如&lt;/h2&gt;
&lt;p&gt;一场大火让无数人无家可归，年末换房也受牵连。被自如圈粉，其他中介的房子真的太 low 了。自如订房子居然也要抢，一波三折、失而复得。在没抢到房子时，真的想走，想离开这里了，很凄凉。最终是在公司对面住下了，过上了在家吃中饭晚饭的日子。&lt;/p&gt;
&lt;h2 id=&#34;2018-happy-new-year&#34;&gt;2018 Happy New Year&lt;/h2&gt;
&lt;p&gt;2017 自己是幸运的，2018 猥琐发育，不要浪，胜利属于伏地魔。&lt;/p&gt;
&lt;p&gt;&amp;ndash; EOF &amp;ndash;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>2017 关键词：Birds、离职、狼人杀、白洋淀、iOS、自如。</p>
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=298 height=52 src="//music.163.com/outchain/player?type=2&id=423314746&auto=0&height=32"></iframe>
<h2 id="birds">Birds</h2>
<p>Birds 是自己编写时间最长的一个 Web 项目。项目接手、改版、重构、迭代、盈利，上半年每天工作都是面对它。</p>
<!-- more -->
<p>Birds 良好的表现与需求互相推动，这点很重要，假如有需求而项目没有成长，或项目成长没了需求，对工程来说都只能是纸上谈兵、无事可做。项目中的规范的重要性，无论是代码还是数据库，只要项目不是夭折终将显现，对于规范性我绝不会开倒车，做任何妥协，种种经历只说明：这里欠的东西总会还。如何应对需求的千变万化，小结几点：模块化、组件化思想，功能多可配置，功能可拔插，功能方法粒度要小；不要对程序肆意进行打补丁式的修改，减少对流程入侵；当某一处的逻辑增加时要及时重新修改、定义流程；对需求目的的正确理解更是能高效开发。</p>
<p>对前端重新认识的一年。现在的前端和大学时刚刚接触时，已经大不一样了。前端能做的事情越来越多，很多业务逻辑都在前端处理，服务端只需要提供接口，分工合作更容易。浅尝 React Vue Webpack 后，也许前端才是自己的归宿。</p>
<h2 id="离职">离职</h2>
<p>离职的不是我。老人走，新人来，新人走。离职一词在我身边跟了一年都没有停下来。同事离职的原因很多，离开北京、公司发展、自我发展&hellip; 马云说离职的原因就两个：1. 钱没给够。2. 受了委屈。我起初是认同这一说法的，但是后来觉得“人”或者说“人的思想”或者说“人性”在其中影响作用非常的大。相同的事情不同的角度看，结果相去甚远，如果基本的理念不同，那事情基本是无法调和的。</p>
<p>对做的事情的认同感，对自我的定位与认识，对现实情况的冷静思考，这几点需要反复咀嚼。</p>
<p>分享一篇文章：<a href="http://stormzhang.com/2017/06/26/leave-if-no-experts/">公司没大牛带，需要离职么？</a>。对于刚入门的我们，这是一个很有代表性的问题。太多时候我们期待别人，忘了自己。公司有大牛，能跟着学是幸运，没有是常态。先接受了这个设定，会更容易找提高自己的方法。有没有大牛并不重要，我们是为了那个更好的自己。</p>
<h2 id="狼人杀">狼人杀</h2>
<p>2017 火的游戏很多：狼人杀、守望先锋、王者荣耀、吃鸡。狼人杀这一杀，好像杀到了毕业前和同学舍友在一起臭 high 的日子。可说骚话、互相 diss、互相吹捧，真真假假其乐无穷。最赤鸡的是鱼炸出了个女盆友，各路同学亲上加亲，无法克说。</p>
<h2 id="白洋淀">白洋淀</h2>
<blockquote>
<p>我想 你说
你不要在孤单 让我做你的伴</p>
</blockquote>
<p>白洋淀去过四次，那里的温泉过去两次，年中 Team Building 再喜加一，而这一次让这里有了美丽故事。喷泉、沙发，出现了对的人。</p>
<h2 id="ios">iOS</h2>
<p>React Native 的一波尝试后，走上了移动开发的道。Swift 让上 iOS 车的门槛降低不少，Swift API 趋向稳定，可以说这是最好的时候。GitHub 应该是再也放不下了，外文的书籍、文档确实靠谱啊，推荐一个教程网站 <a href="https://www.raywenderlich.com/category/ios">iOS Tutorials - Ray Wenderlich</a> 真的零基础入门。</p>
<p>我相信：一个人语言的界限，就是他世界的边界。</p>
<h2 id="自如">自如</h2>
<p>一场大火让无数人无家可归，年末换房也受牵连。被自如圈粉，其他中介的房子真的太 low 了。自如订房子居然也要抢，一波三折、失而复得。在没抢到房子时，真的想走，想离开这里了，很凄凉。最终是在公司对面住下了，过上了在家吃中饭晚饭的日子。</p>
<h2 id="2018-happy-new-year">2018 Happy New Year</h2>
<p>2017 自己是幸运的，2018 猥琐发育，不要浪，胜利属于伏地魔。</p>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>【Git 权威指南】读书笔记 - 独奏</title>
      <link>https://zyf.im/2017/07/19/got-git-reading-notes-solo/</link>
      <pubDate>Wed, 19 Jul 2017 16:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/07/19/got-git-reading-notes-solo/</guid>
      <description>&lt;p&gt;【Git 权威指南】&amp;ldquo;初识 Git&amp;rdquo; + &amp;ldquo;独奏&amp;quot;两章的精炼合并本：背景、安装、初始化、暂存区、对象模型、引用游标（reset / checkout / stash）、里程碑、文件操作、历史查看、改变历史、克隆。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文沿用书中 &lt;code&gt;master&lt;/code&gt; 作为默认分支名。Git 2.28+ 默认改为 &lt;code&gt;main&lt;/code&gt;，行为完全一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;前传diffpatch分布式&#34;&gt;前传：diff、patch、分布式&lt;/h2&gt;
&lt;p&gt;Git 之前，源码协作靠 &lt;code&gt;diff&lt;/code&gt; + &lt;code&gt;patch&lt;/code&gt; 这套老组合：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;diff -u hello.old hello.new &amp;gt; hello.patch     &lt;span class=&#34;c1&#34;&gt;# -u: unified 格式，带上下文，是补丁的必备参数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;patch hello.old &amp;lt; hello.patch                  &lt;span class=&#34;c1&#34;&gt;# 升级到 new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;patch -R hello.old &amp;lt; hello.patch               &lt;span class=&#34;c1&#34;&gt;# -R: 反向，回到 old&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;patch -p1 &amp;lt; changes.patch                      &lt;span class=&#34;c1&#34;&gt;# -p1: 跳过路径首层，用于跨目录补丁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;补丁文件长这样：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;--- hello.old   2026-05-14 10:00:00
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+++ hello.new   2026-05-14 10:05:00
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gu&#34;&gt;@@ -1,3 +1,3 @@
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; hello
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;-world
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;+git
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; !
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Git 的 &lt;code&gt;git diff&lt;/code&gt; / &lt;code&gt;git apply&lt;/code&gt; / &lt;code&gt;git format-patch&lt;/code&gt; / &lt;code&gt;git am&lt;/code&gt; 就是这套机制的强化版 —— 加了对象寻址、历史图谱、合并算法。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>【Git 权威指南】&ldquo;初识 Git&rdquo; + &ldquo;独奏&quot;两章的精炼合并本：背景、安装、初始化、暂存区、对象模型、引用游标（reset / checkout / stash）、里程碑、文件操作、历史查看、改变历史、克隆。</p>
<blockquote>
<p>本文沿用书中 <code>master</code> 作为默认分支名。Git 2.28+ 默认改为 <code>main</code>，行为完全一致。</p>
</blockquote>
<h2 id="前传diffpatch分布式">前传：diff、patch、分布式</h2>
<p>Git 之前，源码协作靠 <code>diff</code> + <code>patch</code> 这套老组合：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">diff -u hello.old hello.new &gt; hello.patch     <span class="c1"># -u: unified 格式，带上下文，是补丁的必备参数</span>
</span></span><span class="line"><span class="cl">patch hello.old &lt; hello.patch                  <span class="c1"># 升级到 new</span>
</span></span><span class="line"><span class="cl">patch -R hello.old &lt; hello.patch               <span class="c1"># -R: 反向，回到 old</span>
</span></span><span class="line"><span class="cl">patch -p1 &lt; changes.patch                      <span class="c1"># -p1: 跳过路径首层，用于跨目录补丁</span>
</span></span></code></pre></div><p>补丁文件长这样：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- hello.old   2026-05-14 10:00:00
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ hello.new   2026-05-14 10:05:00
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -1,3 +1,3 @@
</span></span></span><span class="line"><span class="cl"> hello
</span></span><span class="line"><span class="cl"><span class="gd">-world
</span></span></span><span class="line"><span class="cl"><span class="gi">+git
</span></span></span><span class="line"><span class="cl"> !
</span></span></code></pre></div><p>Git 的 <code>git diff</code> / <code>git apply</code> / <code>git format-patch</code> / <code>git am</code> 就是这套机制的强化版 —— 加了对象寻址、历史图谱、合并算法。</p>
<p><strong>分布式</strong>是 Git 与 CVS / SVN 的根本分野：克隆即完整版本库，提交、查日志、建分支、合并、回退全部本地完成，不依赖网络。这是 Git &ldquo;快&quot;的根因，也是为什么提交必须用 SHA-1 寻址 —— 没有中心写入点能分配递增编号（详见下文「SHA-1 是怎么算出来的」）。</p>
<h2 id="安装">安装</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install git                                     <span class="c1"># macOS</span>
</span></span><span class="line"><span class="cl">sudo apt install git git-doc git-svn git-email gitk  <span class="c1"># Debian / Ubuntu</span>
</span></span></code></pre></div><p><code>git-svn</code> / <code>git-email</code> / <code>gitk</code> 依赖 Perl / Tk，发行版常单独打包。装好 <code>git-doc</code> 后可在浏览器查帮助：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git <span class="nb">help</span> -w &lt;subcommand&gt;
</span></span></code></pre></div><h2 id="git-初始化">Git 初始化</h2>
<h3 id="全局配置用户身份与别名">全局配置：用户身份与别名</h3>
<p>提交时记入 author / committer 信息，全局配置一次即可（写入 <code>~/.gitconfig</code>）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Jiang Xin&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email jiangxin@ossxp.com
</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="cl">git config --global alias.br branch
</span></span><span class="line"><span class="cl">git config --global alias.ci commit
</span></span><span class="line"><span class="cl">git config --global alias.co checkout
</span></span><span class="line"><span class="cl">git config --global alias.st status
</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="cl">git config --global core.quotepath <span class="nb">false</span>
</span></span></code></pre></div><p>否则 <code>git status</code> 会把 <code>中文.md</code> 显示成 <code>\344\270\255\346\226\207.md</code>。</p>
<h3 id="创建仓库">创建仓库</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git init demo               <span class="c1"># 等价于 mkdir demo &amp;&amp; cd demo &amp;&amp; git init</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> demo
</span></span><span class="line"><span class="cl">ls -aF                      <span class="c1"># → ./  ../  .git/</span>
</span></span></code></pre></div><p><code>.git</code> 是版本库目录，其所在目录称为 <strong>工作区</strong>。</p>
<p>子目录中执行 Git 命令时，Git 沿目录向上递归查找 <code>.git</code>。可用 <code>strace</code> 验证：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">strace -e <span class="s1">&#39;trace=file&#39;</span> git status
</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="cl">git rev-parse --git-dir          <span class="c1"># → /path/to/demo/.git</span>
</span></span><span class="line"><span class="cl">git rev-parse --show-toplevel    <span class="c1"># → /path/to/demo</span>
</span></span></code></pre></div><h3 id="三层配置文件">三层配置文件</h3>
<table>
	<thead>
			<tr>
					<th>层级</th>
					<th>路径</th>
					<th>命令</th>
					<th>优先级</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>仓库</td>
					<td><code>&lt;repo&gt;/.git/config</code></td>
					<td><code>git config -e</code></td>
					<td>最高</td>
			</tr>
			<tr>
					<td>全局</td>
					<td><code>~/.gitconfig</code></td>
					<td><code>git config -e --global</code></td>
					<td>中</td>
			</tr>
			<tr>
					<td>系统</td>
					<td><code>/etc/gitconfig</code></td>
					<td><code>git config -e --system</code></td>
					<td>最低</td>
			</tr>
	</tbody>
</table>
<p>配置文件是 INI 格式，可读可写：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config core.bare              <span class="c1"># 读</span>
</span></span><span class="line"><span class="cl">git config a.b something          <span class="c1"># 写 → [a] b = something</span>
</span></span><span class="line"><span class="cl">git config x.y.z others           <span class="c1"># 写 → [x &#34;y&#34;] z = others</span>
</span></span></code></pre></div><p><code>git config</code> 也能操作任意 INI 文件：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">GIT_CONFIG</span><span class="o">=</span>test.ini git config a.b.c.d <span class="s2">&#34;hello, world&#34;</span>
</span></span></code></pre></div><h3 id="修补错误的提交者信息">修补错误的提交者信息</h3>
<p>漏配 <code>user.name</code> 时已经提交了？重置后 amend：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Jiang Xin&#34;</span>
</span></span><span class="line"><span class="cl">git commit --amend --allow-empty --reset-author
</span></span></code></pre></div><ul>
<li><code>--amend</code>：修补最近一次提交，不产生新提交。</li>
<li><code>--allow-empty</code>：允许空白提交（修补后无文件变化时需要）。</li>
<li><code>--reset-author</code>：同时重置 Author 与 AuthorDate；不加只会改 Committer。</li>
</ul>
<blockquote>
<p>Git 不做提交者身份认证，&ldquo;谁都能写任意 name/email&rdquo; 是分布式特性。权限控制交给 GitLab / GitHub 等服务端。</p>
</blockquote>
<h2 id="git-暂存区stage--index">Git 暂存区（stage / index）</h2>
<h3 id="工作区--暂存区--版本库">工作区 → 暂存区 → 版本库</h3>
<p>修改文件后必须先 <code>add</code> 再 <code>commit</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add welcome.txt
</span></span><span class="line"><span class="cl">git diff                     <span class="c1"># 工作区 vs 暂存区</span>
</span></span><span class="line"><span class="cl">git diff --cached            <span class="c1"># 暂存区 vs HEAD</span>
</span></span><span class="line"><span class="cl">git status -s                <span class="c1"># 简洁状态：?? 未跟踪 / A  新增 / M  暂存 /  M 仅工作区改 / MM 都改</span>
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;which version checked in?&#34;</span>
</span></span><span class="line"><span class="cl">git log --pretty<span class="o">=</span>oneline
</span></span></code></pre></div><blockquote>
<p><code>git commit -a</code> 自动 add 所有已跟踪文件的改动后提交，会丢掉&quot;挑选改动入提交&quot;的能力，慎用。
一次提交只做一件事。把一天改动一锅端，等于把 Git 当文件备份系统。</p>
</blockquote>
<h3 id="index-文件的本质">index 文件的本质</h3>
<p><code>.git/index</code> 是一棵虚拟工作区目录树，记录每个跟踪文件的名字、时间戳、长度，以及指向 <code>.git/objects/</code> 中实际内容的对象 ID。</p>
<p><img alt="git stage" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202431-33a64480-8658-11ea-8771-07b2bf81f657.png"></p>
<p>关键操作的边界：</p>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>暂存区</th>
					<th>工作区</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>git reset HEAD</code></td>
					<td>重置为 HEAD</td>
					<td>不变</td>
			</tr>
			<tr>
					<td><code>git rm --cached &lt;file&gt;</code></td>
					<td>删除</td>
					<td>不变</td>
			</tr>
			<tr>
					<td><code>git checkout -- &lt;file&gt;</code></td>
					<td>不变</td>
					<td>用暂存区覆盖 ⚠</td>
			</tr>
			<tr>
					<td><code>git checkout HEAD -- &lt;file&gt;</code></td>
					<td>用 HEAD 覆盖</td>
					<td>用 HEAD 覆盖 ⚠</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p>Git 2.23+ 推荐 <code>git restore</code> / <code>git restore --staged</code> 替代 <code>checkout</code>，语义更清晰：<code>restore</code> 只动文件，<code>switch</code> 只切分支。</p>
</blockquote>
<h3 id="查看暂存区与-head-的目录树">查看暂存区与 HEAD 的目录树</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git ls-tree -l HEAD          <span class="c1"># HEAD 的 tree</span>
</span></span><span class="line"><span class="cl">git ls-files -s              <span class="c1"># 暂存区的 tree</span>
</span></span></code></pre></div><p><img alt="git diff" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202327-06599680-8658-11ea-8329-93b424bc3726.png"></p>
<h2 id="git-对象模型">Git 对象模型</h2>
<p>Git 对象库 <code>.git/objects/</code> 中只有四种对象：<code>blob</code>（文件内容）、<code>tree</code>（目录）、<code>commit</code>（提交）、<code>tag</code>（附注标签）。每个对象用 40 位 SHA-1 唯一寻址。</p>
<h3 id="三种对象的关系">三种对象的关系</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git log -1 --pretty<span class="o">=</span>raw
</span></span><span class="line"><span class="cl"><span class="c1"># commit  e695606fc5e31b2ff9038a48a3d363f4c21a3d86   ← 本次提交</span>
</span></span><span class="line"><span class="cl"><span class="c1"># tree    f58da9a820e3fd9d84ab2ca2f1b467ac265038f9   ← 提交对应的目录快照</span>
</span></span><span class="line"><span class="cl"><span class="c1"># parent  a0c641e92b10d8bcca1ed1bf84ca80340fdefee6   ← 上一次提交</span>
</span></span></code></pre></div><p><code>git cat-file</code> 查类型与内容：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git cat-file -t e695606      <span class="c1"># → commit</span>
</span></span><span class="line"><span class="cl">git cat-file -t f58d         <span class="c1"># → tree</span>
</span></span><span class="line"><span class="cl">git cat-file -t fd3c06       <span class="c1"># → blob</span>
</span></span><span class="line"><span class="cl">git cat-file -p fd3c06       <span class="c1"># → 文件内容</span>
</span></span></code></pre></div><p>对象在磁盘上按 SHA-1 前 2 位分目录、后 38 位作文件名：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ls .git/objects/e6/95606fc5e31b2ff9038a48a3d363f4c21a3d86
</span></span></code></pre></div><p><img alt="git objects" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202330-08235a00-8658-11ea-9040-b280b00b7e43.png"></p>
<h3 id="head-与-master-的关系">HEAD 与 master 的关系</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat .git/HEAD                          <span class="c1"># → ref: refs/heads/master</span>
</span></span><span class="line"><span class="cl">cat .git/refs/heads/master             <span class="c1"># → e695606fc5e31b2ff9038a48a3d363f4c21a3d86</span>
</span></span></code></pre></div><ul>
<li><code>HEAD</code> 是符号引用，指向当前分支。</li>
<li><code>refs/heads/master</code> 是分支文件，内容为最新提交的 SHA-1。</li>
<li>在 master 分支上时，<code>HEAD</code> ≡ <code>master</code> ≡ <code>refs/heads/master</code>。</li>
</ul>
<p><code>.git/refs/</code> 是引用命名空间，<code>refs/heads/</code> 下是分支，<code>refs/tags/</code> 下是标签。</p>
<p><img alt="git refs" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202610-78ca7680-8658-11ea-9b85-651555b8f005.png"></p>
<h3 id="sha-1-是怎么算出来的">SHA-1 是怎么算出来的</h3>
<p>Git 对象 ID = <code>SHA1(&quot;&lt;type&gt; &lt;size&gt;\0&lt;content&gt;&quot;)</code>。</p>
<p>注意 Git 不是直接对内容做 SHA-1，而是先在前面拼一个 header：<code>&lt;type&gt;</code> 是 <code>commit</code> / <code>tree</code> / <code>blob</code> / <code>tag</code> 之一，<code>&lt;size&gt;</code> 是内容字节数，<code>\0</code> 是分隔符。<strong>因为 header 里带 type，同样的字节当 blob 和当 tree 算出的 ID 不同</strong> —— 这就是 Git 区分四类对象的方式。</p>
<p>手工复算 commit，验证公式：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git cat-file commit HEAD <span class="p">|</span> wc -c                          <span class="c1"># → 234   content 字节数</span>
</span></span><span class="line"><span class="cl"><span class="o">(</span> <span class="nb">printf</span> <span class="s2">&#34;commit 234\000&#34;</span><span class="p">;</span> git cat-file commit HEAD <span class="o">)</span> <span class="p">|</span> sha1sum
</span></span><span class="line"><span class="cl"><span class="c1"># → e695606fc5e31b2ff9038a48a3d363f4c21a3d86             手工算的</span>
</span></span><span class="line"><span class="cl">git rev-parse HEAD
</span></span><span class="line"><span class="cl"><span class="c1"># → e695606fc5e31b2ff9038a48a3d363f4c21a3d86             Git 自己给的，完全一致</span>
</span></span></code></pre></div><p>小括号 <code>( ; )</code> 把 header 和 content 串成一段流喂给 <code>sha1sum</code>。把 <code>commit</code> 换成 <code>blob</code> / <code>tree</code>、<code>size</code> 换成对应字节数，同样成立。<code>git hash-object</code> 就是这套&quot;拼 header + SHA-1&quot;的官方封装。</p>
<p>由此可推出 Git 几个核心性质：</p>
<ul>
<li>任何字节级改动都会产生新 ID；commit 里又包含 parent ID，所以改一处历史会让后续所有 commit ID 雪崩 —— 这是 Git 不可篡改的根。</li>
<li>不同 type 的对象 ID 永不冲突，因为 header 不同。</li>
<li>部分 SHA-1 前缀（默认 7 位）就能寻址，是因为单仓库的对象数远小于 16⁷ ≈ 2.7 亿。</li>
</ul>
<blockquote>
<p>SHA-1 不是数学上唯一，但 160 位空间 + 抗碰撞设计使实际工程中可视为唯一。Git 2.29+ 已在向 SHA-256 演进。</p>
</blockquote>
<h3 id="提交的引用语法">提交的引用语法</h3>
<p>集中式系统能用递增编号，是因为只有一个写入点；Git 是分布式的，必须用全球可重现的对象 ID 寻址。</p>
<table>
	<thead>
			<tr>
					<th>写法</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>master</code> / <code>refs/heads/master</code></td>
					<td>分支最新提交</td>
			</tr>
			<tr>
					<td><code>HEAD</code></td>
					<td>当前分支最新提交</td>
			</tr>
			<tr>
					<td><code>HEAD^</code> / <code>HEAD~1</code></td>
					<td>父提交</td>
			</tr>
			<tr>
					<td><code>HEAD^^</code> / <code>HEAD~2</code></td>
					<td>祖父提交</td>
			</tr>
			<tr>
					<td><code>HEAD^2</code></td>
					<td>第二个父提交（merge 才有）</td>
			</tr>
			<tr>
					<td><code>a573106^{tree}</code></td>
					<td>该提交对应的 tree</td>
			</tr>
			<tr>
					<td><code>a573106:path/to/file</code></td>
					<td>该提交里某个文件的 blob</td>
			</tr>
			<tr>
					<td><code>:path/to/file</code></td>
					<td>暂存区里的 blob</td>
			</tr>
			<tr>
					<td><code>:/Commit A</code></td>
					<td>在提交日志中搜索字串的提交</td>
			</tr>
	</tbody>
</table>
<p>部分 SHA-1 前缀（不冲突即可）也能寻址：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git cat-file -p e695
</span></span><span class="line"><span class="cl">git cat-file -p e695^
</span></span><span class="line"><span class="cl">git rev-parse e695^<span class="o">{</span>tree<span class="o">}</span>
</span></span></code></pre></div><h2 id="引用游标reset--checkout--stash">引用游标：reset / checkout / stash</h2>
<h3 id="分支是一个游标">分支是一个游标</h3>
<p>引用 <code>refs/heads/master</code> 就是一个游标，每次新 commit 就指向新的提交 ID。<code>git reset</code> 可以把它拨向任意已存在的提交：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reset --hard HEAD^         <span class="c1"># 拨回到上一个 commit</span>
</span></span></code></pre></div><blockquote>
<p><code>--hard</code> 会同时改写引用、暂存区、工作区。改之前先 <code>git stash</code> 或 commit 当前改动。</p>
</blockquote>
<h3 id="reset-的三个动作">reset 的三个动作</h3>
<p><code>git reset [--soft|--mixed|--hard] [&lt;commit&gt;]</code> 内部按需执行三步，参数决定走到第几步（不带参数等价 <code>--mixed</code>）：</p>
<table>
	<thead>
			<tr>
					<th>参数</th>
					<th style="text-align: center">① 移动引用</th>
					<th style="text-align: center">② 重置暂存区</th>
					<th style="text-align: center">③ 重置工作区</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--soft</code></td>
					<td style="text-align: center">✓</td>
					<td style="text-align: center"></td>
					<td style="text-align: center"></td>
			</tr>
			<tr>
					<td><code>--mixed</code></td>
					<td style="text-align: center">✓</td>
					<td style="text-align: center">✓</td>
					<td style="text-align: center"></td>
			</tr>
			<tr>
					<td><code>--hard</code></td>
					<td style="text-align: center">✓</td>
					<td style="text-align: center">✓</td>
					<td style="text-align: center">✓</td>
			</tr>
	</tbody>
</table>
<p><img alt="git reset" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202770-b5966d80-8658-11ea-92e4-c348b8f12313.png"></p>
<p>常用配方：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reset HEAD &lt;file&gt;          <span class="c1"># 撤出暂存区（git add 的反操作），工作区不动</span>
</span></span><span class="line"><span class="cl">git reset --soft HEAD^         <span class="c1"># 撤销最近一次 commit，改动留在暂存区，便于修改后重新 commit</span>
</span></span><span class="line"><span class="cl">git reset --hard HEAD^         <span class="c1"># 彻底丢掉最近一次 commit + 所有未提交改动</span>
</span></span></code></pre></div><blockquote>
<p><code>git commit --amend</code> 等价于 <code>git reset --soft HEAD^ &amp;&amp; git commit -e -F .git/COMMIT_EDITMSG</code>。
<code>&lt;paths&gt;</code> 与引用同名时易冲突，可用 <code>--</code> 分隔：<code>git reset HEAD -- &lt;paths&gt;</code>。</p>
</blockquote>
<h3 id="用-reflog-救回错误的重置">用 reflog 救回错误的重置</h3>
<p>带工作区的版本库默认开启 <code>core.logallrefupdates=true</code>，所有引用变更都记录在 <code>.git/logs/</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reflog show master <span class="p">|</span> head -5
</span></span><span class="line"><span class="cl"><span class="c1"># 9e8a761 master@{0}: 9e8a761: updating HEAD</span>
</span></span><span class="line"><span class="cl"><span class="c1"># e695606 master@{1}: HEAD^: updating HEAD</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 4902dc3 master@{2}: commit: does master follow this new commit?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git reset --hard master@<span class="o">{</span>2<span class="o">}</span>    <span class="c1"># 拨回到两步之前</span>
</span></span></code></pre></div><p>只要 reflog 还在（默认 90 天），任何&quot;丢失&quot;的提交都能找回来。</p>
<h3 id="检出与分离头指针">检出与分离头指针</h3>
<p><code>git reset</code> 改的是分支游标；<code>git checkout &lt;commit&gt;</code> 改的是 <code>HEAD</code> 本身，分支游标不动。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git checkout HEAD^
</span></span><span class="line"><span class="cl"><span class="c1"># You are in &#39;detached HEAD&#39; state.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat .git/HEAD
</span></span><span class="line"><span class="cl"><span class="c1"># 3175afde9450a1dc40b09d05a012b45e967cb80f   ← 直接是 SHA-1，不再是 &#34;ref: refs/heads/xxx&#34;</span>
</span></span></code></pre></div><p>这就是 <strong>分离头指针</strong>：<code>HEAD</code> 指向具体提交而非引用。此时可以正常 commit，但一旦 <code>checkout</code> 到其他分支，这些提交没有任何引用追踪，过期后会被 gc 清除。要救回这些&quot;游离提交&rdquo;：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git checkout master
</span></span><span class="line"><span class="cl">git merge &lt;游离提交的 SHA-1&gt;
</span></span></code></pre></div><blockquote>
<p>Git 2.23+ 推荐 <code>git switch</code> / <code>git restore</code> 替代 <code>checkout</code>。<code>switch</code> 只切分支，<code>restore</code> 只动文件 —— <code>checkout</code> 一词多义，新命令把这两个意图拆开了。</p>
</blockquote>
<h3 id="暂存进度-stash">暂存进度 stash</h3>
<p>中途要切分支，但工作区一堆改动不想提交：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git stash                       <span class="c1"># 默认只暂存已跟踪文件；-u 含未跟踪，-a 含被忽略</span>
</span></span><span class="line"><span class="cl">git stash list                  <span class="c1"># 查看 stash 栈</span>
</span></span><span class="line"><span class="cl">git stash pop                   <span class="c1"># 恢复并删除最近一笔</span>
</span></span><span class="line"><span class="cl">git stash apply stash@<span class="o">{</span>1<span class="o">}</span>       <span class="c1"># 恢复但不删除指定 stash</span>
</span></span><span class="line"><span class="cl">git stash drop stash@<span class="o">{</span>1<span class="o">}</span>        <span class="c1"># 删除指定 stash</span>
</span></span><span class="line"><span class="cl">git stash branch &lt;name&gt;         <span class="c1"># 基于某次 stash 创建分支</span>
</span></span></code></pre></div><p>机制：每次 stash 就是写在引用 <code>refs/stash</code> 下的一笔 commit，该引用的变更走 reflog（<code>.git/logs/refs/stash</code>），所以才有 <code>stash@{n}</code> 这种和 reflog 一样的语法。一次 stash 其实是两笔 commit —— 一个保存暂存区，一个 merge commit 保存工作区，恢复时分别还原。</p>
<h2 id="里程碑-tag">里程碑 tag</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git tag -m <span class="s2">&#34;v1.0 release&#34;</span> v1.0          <span class="c1"># 附注 tag，会生成独立的 tag 对象</span>
</span></span><span class="line"><span class="cl">git tag v1.0-lw                          <span class="c1"># 轻量 tag，只是引用文件指向某 commit</span>
</span></span><span class="line"><span class="cl">git rev-parse refs/tags/v1.0
</span></span></code></pre></div><p><code>git describe</code> 给当前 HEAD 自动生成可读版本号：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git describe
</span></span><span class="line"><span class="cl"><span class="c1"># v1.0-2-g8861c65</span>
</span></span><span class="line"><span class="cl"><span class="c1">#  └┬─┘ │ └──┬──┘</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 最近tag │   该提交 SHA-1 前缀</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       距 tag 的提交数</span>
</span></span></code></pre></div><p>刚打完 tag 时只输出 tag 名；之后每次 commit 都会拼上&quot;距离 + SHA-1&quot;后缀。</p>
<h2 id="文件操作">文件操作</h2>
<h3 id="删除rm-vs-git-rm">删除：<code>rm</code> vs <code>git rm</code></h3>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th style="text-align: center">工作区</th>
					<th style="text-align: center">暂存区</th>
					<th style="text-align: center">版本库（commit 后）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>rm &lt;file&gt;</code></td>
					<td style="text-align: center">删</td>
					<td style="text-align: center">─</td>
					<td style="text-align: center">─</td>
			</tr>
			<tr>
					<td><code>git rm &lt;file&gt;</code></td>
					<td style="text-align: center">删</td>
					<td style="text-align: center">删</td>
					<td style="text-align: center">下次删</td>
			</tr>
			<tr>
					<td><code>git rm --cached &lt;file&gt;</code></td>
					<td style="text-align: center">─</td>
					<td style="text-align: center">删</td>
					<td style="text-align: center">下次删</td>
			</tr>
	</tbody>
</table>
<p><code>git add</code> 两个聚合参数的差别：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add -u                     <span class="c1"># 已跟踪文件的所有改动（含删除），不含新文件</span>
</span></span><span class="line"><span class="cl">git add -A                     <span class="c1"># 所有改动（含删除、含新文件）</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="cl">git cat-file -p HEAD~1:welcome.txt &gt; welcome.txt
</span></span><span class="line"><span class="cl">git add welcome.txt <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;restore welcome.txt&#34;</span>
</span></span></code></pre></div><p>也可以用 <code>git ls-files --with-tree=HEAD~1</code> 列出旧版本中的文件。</p>
<h3 id="移动--重命名">移动 / 重命名</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git mv welcome.txt README       <span class="c1"># 等价于 mv + git rm + git add</span>
</span></span></code></pre></div><h3 id="gitignore">.gitignore</h3>
<p>语法要点：</p>
<ul>
<li><code>#</code> 行首是注释。</li>
<li>通配符：<code>*</code> 任意字符、<code>?</code> 单字符、<code>[abc]</code> 字符范围。</li>
<li>末尾 <code>/</code> 只匹配目录；不带 <code>/</code> 同名文件和目录都忽略。</li>
<li>行首 <code>!</code> 反向匹配（不忽略）。</li>
<li>路径以 <code>/</code> 开头表示根锚定，否则任意层级都匹配。</li>
</ul>
<pre tabindex="0"><code class="language-gitignore" data-lang="gitignore">*.a            # 忽略所有 .a
!lib.a         # 但 lib.a 例外
/TODO          # 仅根目录的 TODO，子目录的不算
build/         # 任意层级的 build 目录
doc/*.txt      # doc/notes.txt 命中，doc/server/arch.txt 不命中
</code></pre><blockquote>
<p><code>.gitignore</code> <strong>只对未跟踪文件有效</strong>。已入库的文件后续改动不会被忽略，得先 <code>git rm --cached &lt;file&gt;</code> 再加入忽略。</p>
</blockquote>
<p>三种作用域，从私到公：</p>
<table>
	<thead>
			<tr>
					<th>作用域</th>
					<th>路径</th>
					<th>共享性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>本仓库私有</td>
					<td><code>.git/info/exclude</code></td>
					<td>仅本机此仓库</td>
			</tr>
			<tr>
					<td>全局私有</td>
					<td><code>core.excludesfile</code> 指向的文件</td>
					<td>仅本机所有仓库</td>
			</tr>
			<tr>
					<td>项目共享</td>
					<td>仓库内 <code>.gitignore</code></td>
					<td>入库后所有协作者</td>
			</tr>
	</tbody>
</table>
<p>强制把被忽略的文件加入暂存：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add -f hello.h
</span></span></code></pre></div><h3 id="归档-git-archive">归档 git archive</h3>
<p>直接 <code>tar</code> 工作区会把 <code>.git/</code> 和忽略文件全打进去。用 <code>git archive</code> 从提交快照打包：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git archive -o latest.zip HEAD
</span></span><span class="line"><span class="cl">git archive -o partial.tar HEAD src doc
</span></span><span class="line"><span class="cl">git archive --format<span class="o">=</span>tar --prefix<span class="o">=</span>foo-1.0/ v1.0 <span class="p">|</span> gzip &gt; foo-1.0.tar.gz
</span></span></code></pre></div><h2 id="历史查看">历史查看</h2>
<h3 id="版本范围语法-git-rev-list">版本范围语法 git rev-list</h3>
<p><code>git rev-parse</code> 把表达式解析为 SHA-1；<code>git rev-list</code> 在此基础上列出版本范围。最常用是&quot;取差集&rdquo;：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rev-list --oneline G..D            <span class="c1"># D 的历史 - G 的历史</span>
</span></span><span class="line"><span class="cl">git rev-list --oneline ^G D            <span class="c1"># 等价写法，^X = 排除 X 及其历史</span>
</span></span><span class="line"><span class="cl">git rev-list --oneline D F             <span class="c1"># D 和 F 的历史的并集</span>
</span></span></code></pre></div><p>口诀：<code>A..B</code> 等价于 <code>^A B</code>，看 B 比 A 多了什么。</p>
<p><code>git rev-parse</code> 还有一组配置探测开关，写脚本经常用到：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rev-parse --git-dir                <span class="c1"># .git 目录位置</span>
</span></span><span class="line"><span class="cl">git rev-parse --show-toplevel          <span class="c1"># 工作区根</span>
</span></span><span class="line"><span class="cl">git rev-parse --show-cdup              <span class="c1"># 到根的相对深度</span>
</span></span><span class="line"><span class="cl">git rev-parse --symbolic --branches    <span class="c1"># 列出所有分支名</span>
</span></span></code></pre></div><h3 id="浏览日志-git-log">浏览日志 git log</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git log --oneline -5                   <span class="c1"># 最近 5 条单行</span>
</span></span><span class="line"><span class="cl">git log --graph --oneline              <span class="c1"># 字符版分支图</span>
</span></span><span class="line"><span class="cl">git log -p &lt;file&gt;                      <span class="c1"># 含 diff</span>
</span></span><span class="line"><span class="cl">git log --stat                         <span class="c1"># 含变更摘要</span>
</span></span><span class="line"><span class="cl">git log --pretty<span class="o">=</span>raw -1                <span class="c1"># 看 tree/parent/author 原始信息</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="cl">git config --global alias.glog <span class="s2">&#34;log --graph --oneline --decorate&#34;</span>
</span></span></code></pre></div><h3 id="差异比较-git-diff">差异比较 git diff</h3>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>比较</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>git diff</code></td>
					<td>工作区 vs 暂存区</td>
			</tr>
			<tr>
					<td><code>git diff --cached</code></td>
					<td>暂存区 vs HEAD</td>
			</tr>
			<tr>
					<td><code>git diff HEAD</code></td>
					<td>工作区 vs HEAD</td>
			</tr>
			<tr>
					<td><code>git diff A</code></td>
					<td>工作区 vs 提交 A</td>
			</tr>
			<tr>
					<td><code>git diff --cached A</code></td>
					<td>暂存区 vs 提交 A</td>
			</tr>
			<tr>
					<td><code>git diff A B</code></td>
					<td>提交 A vs 提交 B</td>
			</tr>
			<tr>
					<td><code>git diff A B -- &lt;paths&gt;</code></td>
					<td>指定路径下的差异</td>
			</tr>
			<tr>
					<td><code>git diff &lt;path1&gt; &lt;path2&gt;</code></td>
					<td>仓库外的两个文件</td>
			</tr>
			<tr>
					<td><code>git diff --word-diff</code></td>
					<td>逐词比较，非逐行</td>
			</tr>
	</tbody>
</table>
<h3 id="文件追溯-git-blame">文件追溯 git blame</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git blame README                <span class="c1"># 每行首显示引入该行的 commit + author</span>
</span></span><span class="line"><span class="cl">git blame -L 6,+5 README        <span class="c1"># 只看第 6 行起的 5 行</span>
</span></span></code></pre></div><h3 id="二分查找-git-bisect">二分查找 git bisect</h3>
<p>二分定位&quot;哪个提交引入了 bug&quot;：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bisect start
</span></span><span class="line"><span class="cl">git bisect bad HEAD                    <span class="c1"># 当前是坏的</span>
</span></span><span class="line"><span class="cl">git bisect good v1.0                   <span class="c1"># v1.0 时是好的</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Git 自动 checkout 中间提交，跑测试后告诉它 good / bad</span>
</span></span><span class="line"><span class="cl">git bisect good
</span></span><span class="line"><span class="cl">git bisect bad
</span></span><span class="line"><span class="cl"><span class="c1"># ... 直到锁定第一个坏提交</span>
</span></span><span class="line"><span class="cl">git bisect reset
</span></span></code></pre></div><h2 id="改变历史">改变历史</h2>
<blockquote>
<p>历史一旦推送到共享仓库就别再改 —— 协作者会全部出错。改历史只对未推送的本地分支或独享分支安全。</p>
</blockquote>
<h3 id="单步amend">单步：amend</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git commit --amend                     <span class="c1"># 修补最近一次 commit（改文件或改 message）</span>
</span></span><span class="line"><span class="cl">git commit --amend --no-edit           <span class="c1"># 只补文件，不改 message</span>
</span></span></code></pre></div><h3 id="多步reset-soft-重写">多步：reset &ndash;soft 重写</h3>
<p>把最近 N 个 commit 揉成一个：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reset --soft HEAD~3
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;squashed 3 commits&#34;</span>
</span></span></code></pre></div><h3 id="拣选-cherry-pick">拣选 cherry-pick</h3>
<p>从别处挑一个 commit 重放到当前分支：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git cherry-pick &lt;commit&gt;
</span></span></code></pre></div><p>机制：把目标提交导出为补丁，在当前 HEAD 上重放，得到内容一致但 SHA-1 不同的新提交。</p>
<h3 id="变基-rebase">变基 rebase</h3>
<p>把一段提交&quot;嫁接&quot;到新的基点上：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rebase --onto &lt;newbase&gt; &lt;since&gt; &lt;till&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># 把 (since, till] 这段提交摘出来，逐个重放到 newbase 之上</span>
</span></span></code></pre></div><p>执行步骤：先 <code>checkout &lt;till&gt;</code>（detached HEAD），把范围内的提交写到临时文件，硬重置到 <code>&lt;newbase&gt;</code>，再按顺序重放。遇冲突暂停 —— 解决后 <code>git rebase --continue</code>；跳过当前 <code>git rebase --skip</code>；放弃 <code>git rebase --abort</code>。</p>
<p>交互式变基是日常最常用的&quot;改历史瑞士军刀&quot;：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rebase -i HEAD~5
</span></span><span class="line"><span class="cl"><span class="c1"># pick   保留</span>
</span></span><span class="line"><span class="cl"><span class="c1"># reword 改 message</span>
</span></span><span class="line"><span class="cl"><span class="c1"># edit   中途停下手改</span>
</span></span><span class="line"><span class="cl"><span class="c1"># squash 合并到上一个</span>
</span></span><span class="line"><span class="cl"><span class="c1"># fixup  合并到上一个，丢弃 message</span>
</span></span><span class="line"><span class="cl"><span class="c1"># drop   删除该 commit</span>
</span></span></code></pre></div><h3 id="丢弃远古历史">丢弃远古历史</h3>
<p>只想保留最近 100 个提交，扔掉之前的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. 用某次提交的 tree 直接造一个孤儿提交（无 parent）</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Initial squashed commit&#34;</span> <span class="p">|</span> git commit-tree HEAD~100^<span class="o">{</span>tree<span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="c1"># → 输出新 commit 的 SHA-1，记为 $ORPHAN</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. 把 HEAD~100 之后的所有提交嫁接到孤儿之上</span>
</span></span><span class="line"><span class="cl">git rebase --onto <span class="nv">$ORPHAN</span> HEAD~100 master
</span></span></code></pre></div><p>之前的历史与新历史完全断开，gc 后磁盘释放。</p>
<h3 id="反转-revert">反转 revert</h3>
<p><code>revert</code> 生成一个反向提交来&quot;撤销&quot;指定提交，不改写历史，安全用于共享分支：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git revert HEAD                <span class="c1"># 撤销最近一次</span>
</span></span><span class="line"><span class="cl">git revert &lt;commit&gt;            <span class="c1"># 撤销指定提交</span>
</span></span></code></pre></div><h2 id="git-克隆">Git 克隆</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone &lt;url&gt; &lt;dir&gt;                  <span class="c1"># 含工作区</span>
</span></span><span class="line"><span class="cl">git clone --bare &lt;url&gt; &lt;dir.git&gt;       <span class="c1"># 裸库，无工作区</span>
</span></span><span class="line"><span class="cl">git clone --mirror &lt;url&gt; &lt;dir.git&gt;     <span class="c1"># 裸库 + 注册上游，可持续 git fetch 同步</span>
</span></span></code></pre></div><p>裸库目录习惯加 <code>.git</code> 后缀，<code>core.bare=true</code>，用于服务端或备份点：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git init --bare /srv/git/demo.git      <span class="c1"># 新建空裸库</span>
</span></span><span class="line"><span class="cl">git push /srv/git/demo.git              <span class="c1"># 工作区可直接 push 进去</span>
</span></span></code></pre></div><p><code>--mirror</code> 比 <code>--bare</code> 多一步：把上游所有 refs 完整镜像（含 remote-tracking、tag、note），并设置 <code>remote.origin.fetch = +refs/*:refs/*</code>，所以可以反复 <code>git fetch</code> 增量同步 —— 这是用裸库做镜像 / 备份的标准姿势。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="http://www.worldhello.net/gotgit/01-meet-git/index.html">1. 初识 Git — GotGit</a></li>
<li><a href="http://www.worldhello.net/gotgit/02-git-solo/index.html">2. Git 独奏 — GotGit</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>NGINX 启用 HTTP/2</title>
      <link>https://zyf.im/2017/06/06/nginx-enable-http2/</link>
      <pubDate>Tue, 06 Jun 2017 21:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/06/06/nginx-enable-http2/</guid>
      <description>&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;
&lt;p&gt;HTTP/2（RFC 7540，2015 年发布）是自 1999 年 HTTP/1.1 以来的第一次重大升级，目标是在不改变应用语义的前提下，改善延迟、并发和网络效率。浏览器端基本只在 TLS 上启用（h2），明文 h2c 仅用于内部服务间通信。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;h2：HTTP/2 over TLS（最常见，浏览器只支持这一种）&lt;/li&gt;
&lt;li&gt;h2c：HTTP/2 over TCP（无加密）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;设计背景&#34;&gt;设计背景&lt;/h3&gt;
&lt;p&gt;移动端和富媒体时代请求量激增，HTTP/1.1 的队头阻塞、多个 TCP 连接并行带来的慢启动 / 拥塞竞争成为瓶颈。&lt;/p&gt;
&lt;p&gt;谷歌的 SPDY 原型验证了「单连接、多路复用、压缩头部」的思路；IETF 在 SPDY 3.1 基础上标准化为 HTTP/2。&lt;/p&gt;
&lt;h3 id=&#34;核心特性&#34;&gt;核心特性&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;特性&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;二进制分帧（Binary Framing）&lt;/td&gt;
					&lt;td&gt;把 HTTP 报文拆成小型 Frame，机器易解析、可并行&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多路复用（Multiplexing）&lt;/td&gt;
					&lt;td&gt;多个请求 / 响应共享一个 TCP 连接，互不阻塞&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;流与优先级（Stream &amp;amp; Priority）&lt;/td&gt;
					&lt;td&gt;每个请求是双向 Stream，可指定权重与依赖&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;头部压缩（HPACK）&lt;/td&gt;
					&lt;td&gt;静态 Huffman + 动态表，大幅减少重复 Header 字节&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;服务器推送（Server Push）&lt;/td&gt;
					&lt;td&gt;Chrome 106（2022-09）已移除，由 103 Early Hints 取代&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;流量控制&lt;/td&gt;
					&lt;td&gt;端到端窗口，避免单个大文件占满带宽&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;多路复用解决的是&lt;strong&gt;应用层&lt;/strong&gt;队头阻塞 —— TCP 层的队头阻塞依然存在（任意一个包丢失，连接上所有 stream 全停），这正是 HTTP/3 改走 QUIC over UDP 要解决的问题。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="简介">简介</h2>
<p>HTTP/2（RFC 7540，2015 年发布）是自 1999 年 HTTP/1.1 以来的第一次重大升级，目标是在不改变应用语义的前提下，改善延迟、并发和网络效率。浏览器端基本只在 TLS 上启用（h2），明文 h2c 仅用于内部服务间通信。</p>
<ul>
<li>h2：HTTP/2 over TLS（最常见，浏览器只支持这一种）</li>
<li>h2c：HTTP/2 over TCP（无加密）</li>
</ul>
<h3 id="设计背景">设计背景</h3>
<p>移动端和富媒体时代请求量激增，HTTP/1.1 的队头阻塞、多个 TCP 连接并行带来的慢启动 / 拥塞竞争成为瓶颈。</p>
<p>谷歌的 SPDY 原型验证了「单连接、多路复用、压缩头部」的思路；IETF 在 SPDY 3.1 基础上标准化为 HTTP/2。</p>
<h3 id="核心特性">核心特性</h3>
<table>
	<thead>
			<tr>
					<th>特性</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>二进制分帧（Binary Framing）</td>
					<td>把 HTTP 报文拆成小型 Frame，机器易解析、可并行</td>
			</tr>
			<tr>
					<td>多路复用（Multiplexing）</td>
					<td>多个请求 / 响应共享一个 TCP 连接，互不阻塞</td>
			</tr>
			<tr>
					<td>流与优先级（Stream &amp; Priority）</td>
					<td>每个请求是双向 Stream，可指定权重与依赖</td>
			</tr>
			<tr>
					<td>头部压缩（HPACK）</td>
					<td>静态 Huffman + 动态表，大幅减少重复 Header 字节</td>
			</tr>
			<tr>
					<td>服务器推送（Server Push）</td>
					<td>Chrome 106（2022-09）已移除，由 103 Early Hints 取代</td>
			</tr>
			<tr>
					<td>流量控制</td>
					<td>端到端窗口，避免单个大文件占满带宽</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p>多路复用解决的是<strong>应用层</strong>队头阻塞 —— TCP 层的队头阻塞依然存在（任意一个包丢失，连接上所有 stream 全停），这正是 HTTP/3 改走 QUIC over UDP 要解决的问题。</p>
</blockquote>
<h2 id="nginx-配置">NGINX 配置</h2>
<p>启用 HTTP/2 需要 HTTPS，本地证书参考 <a href="/2025/06/26/use-https-in-local-environment/">在本地环境使用 HTTPS ｜ ZYF.IM</a>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="c1"># /etc/nginx/conf.d/default.conf
</span></span></span><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">http2</span> <span class="no">on</span><span class="p">;</span>                                <span class="c1"># NGINX 1.25.1+ 的新语法
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">localhost</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate</span>     <span class="s">/etc/ssl/certs/localhost.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_certificate_key</span> <span class="s">/etc/ssl/certs/localhost-key.pem</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_protocols</span> <span class="s">TLSv1.2</span> <span class="s">TLSv1.3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">ssl_ciphers</span> <span class="s">HIGH:!aNULL:!MD5</span><span class="p">;</span>            <span class="c1"># demo 用，生产请见下文
</span></span></span><span class="line"><span class="cl">    <span class="kn">ssl_prefer_server_ciphers</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">root</span>  <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker container restart nginx-ssl
</span></span></code></pre></div><h2 id="几个常被问到的细节">几个常被问到的细节</h2>
<ul>
<li><strong><code>http2 on;</code> 的版本要求</strong>。NGINX 1.25.1（2023-05）才引入独立的 <code>http2</code> 指令，更早的版本要写 <code>listen 443 ssl http2;</code>。老语法仍可用，但已被官方建议替换。</li>
<li><strong>ALPN 协商</strong>。HTTP/2 over TLS 必须靠 ALPN 在 TLS 握手时协商出 <code>h2</code>。OpenSSL 1.0.2+ 默认支持，NGINX 启用 TLS 即自动具备，<strong>不需要额外指令</strong>。</li>
<li><strong>cipher 列表别复制 demo</strong>。<code>HIGH:!aNULL:!MD5</code> 是 NGINX 默认列表，还包含一些过时套件。生产环境请用 <a href="https://ssl-config.mozilla.org/">Mozilla SSL Configuration Generator</a> 生成 intermediate 或 modern 配置，自动剔除 RC4、3DES、CBC mode 等弱套件。</li>
<li><strong>Server Push 已死，用 103 Early Hints</strong>。Chrome 106 正式移除 push 支持。替代方案是服务器在主响应前发一个 <code>103 Early Hints</code> 响应，携带 <code>Link: &lt;/app.css&gt;; rel=preload</code>，浏览器立即开始拉取。NGINX 1.13.9+ 通过 <code>ngx_http_v2_module</code> 的 <code>http2_push_preload</code> 已不推荐，HTTP 层的 Early Hints 需要应用配合。</li>
<li><strong>HPACK → QPACK</strong>。HTTP/2 的 HPACK 因动态表有顺序依赖，会被 TCP 队头阻塞拖累；HTTP/3 改用 QPACK 解耦编解码顺序。这是 HTTP/3 性能优势的关键设计之一。</li>
</ul>
<h2 id="测试">测试</h2>
<h3 id="浏览器">浏览器</h3>
<p>访问 <code>https://localhost:8443/</code>，Chrome DevTools Network 面板勾选 <code>Protocol</code> 列，看到 <code>h2</code> 即生效。</p>
<p>或装 <a href="https://chromewebstore.google.com/detail/http-indicator/hgcomhbcacfkpffiphlmnlhpppcjgmbl?hl=en-US">HTTP Indicator</a> 扩展：HTTP/1.1 灰、HTTP/2 蓝、HTTP/3 绿。</p>
<h3 id="curl">curl</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># -I (--head) 只返回响应头</span>
</span></span><span class="line"><span class="cl">curl -I https://localhost:8443/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HTTP/2 <span class="m">200</span>
</span></span><span class="line"><span class="cl">server: nginx/1.29.0
</span></span><span class="line"><span class="cl">date: Fri, <span class="m">27</span> Jun <span class="m">2025</span> 01:56:55 GMT
</span></span><span class="line"><span class="cl">content-type: text/html
</span></span><span class="line"><span class="cl">content-length: <span class="m">615</span>
</span></span></code></pre></div><h3 id="nghttp--h2load">nghttp / h2load</h3>
<p><code>nghttp2</code> 工具包提供更底层的视图，能打出 ALPN 协商结果、HPACK 表、stream 详情：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install nghttp2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">nghttp -nv https://localhost:8443/    <span class="c1"># 单请求 + 详细协议输出</span>
</span></span><span class="line"><span class="cl">h2load -n <span class="m">1000</span> -c <span class="m">10</span> https://localhost:8443/   <span class="c1"># 1000 请求 / 10 连接的压测</span>
</span></span></code></pre></div><p><code>h2load</code> 还能直接对比 h1 / h2 / h3 的吞吐 (<code>--h1</code>、<code>--alpn-list=h3,h2</code>)，调参时比 <code>ab</code> 更贴近真实场景。</p>
<h2 id="http3">HTTP/3</h2>
<p>继续看 <a href="/2025/06/27/nginx-enable-http3/">NGINX 启用 HTTP/3 ｜ ZYF.IM</a>。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://nginx.org/en/docs/http/ngx_http_v2_module.html">NGINX HTTP/2 模块</a></li>
<li><a href="https://ssl-config.mozilla.org/">Mozilla SSL Configuration Generator</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8297">RFC 8297 - 103 Early Hints</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>【摔跤吧，爸爸】随笔</title>
      <link>https://zyf.im/2017/05/14/i-am-proud-of-you/</link>
      <pubDate>Sun, 14 May 2017 13:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/05/14/i-am-proud-of-you/</guid>
      <description>&lt;p&gt;周末看了《摔跤吧，爸爸》，也是第一次独自电影院看电影，试写一篇影评纪念下。&lt;/p&gt;
&lt;p&gt;注意：&lt;strong&gt;严重剧透预警。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主角爸爸是印度全国摔跤冠军，一心想着为国家赢取一枚金牌。可自己没能实现梦想，把梦想转移给了自己还未出世的孩子。可事与愿违，想要男孩的主角爸爸的前三个孩子都是女孩，第四个还是女孩。主角爸爸近乎要放弃为国争取金牌的梦想时，却意外看的了大女儿、二女儿身上的摔跤天赋。&lt;/p&gt;
&lt;!-- more --&gt;
&lt;p&gt;主角妈妈在得知主角爸爸准备将女儿们训练为拳击手时，道出了“你不能将你的梦想施加在女儿们身上”，主角爸爸沉思片刻“给我一年时间，其间你不要插手，如果没成功，我将有永远放弃我的梦想”。在主角爸爸说出这句话之前，我是反对父母将自己的梦想让孩子去实现的，也更没有去争取过孩子们的想法，现实中这样做的父母我是常为 Loser. 但是主角爸爸的回答让我看到的不是一个：被自己个人梦想冲昏头，用孩子的全部去成就自己的父亲。主角爸爸是讲道理的，一年后如果失败也会甘愿放弃。坚持与固执的区别也许就在此。&lt;/p&gt;
&lt;p&gt;主角爸爸变身为女儿们的魔鬼摔跤教练，每天除了上学就是训练，跑步时衣服不合适换男生衣服，找侄子当陪练，没有力量加餐牛奶、鸡肉，长头发难清理剪成了寸头。受苦中的女儿们找妈妈求情，主角妈妈遵守承诺，不干预主角爸爸的训练。这里真的要给主角妈妈点赞，后面女儿们的成功，女儿们、主角爸爸的努力在明面放着，而主角妈妈的守诺不干预，对主角爸爸的信赖，是同样的伟大。不然那柔弱的耳边风不知乱了多少坚定的意志。&lt;/p&gt;
&lt;p&gt;训练太辛苦的女儿们采用消极怠工对抗主角爸爸。破坏闹钟、破坏场地、假摔示弱。而旷工一天参加好友的婚礼现场被主角爸爸收拾。在与好友诉苦时，才发现了：对比其他女孩一望到底的家庭妇女人生，主角爸爸的狠心是那样让人羡慕。&lt;/p&gt;
&lt;p&gt;观念改变的女儿们开始主动训练。大女儿首次参赛就初露锋芒，差一点将男孩对手打败。逐渐成长的大女儿愈战愈勇的直到赢得全国冠军。全国冠军都会进入国家体育学校学习，大女儿也要离开主角爸爸接受新教练的训练，备战世界大赛。从小镇走出来的大女儿，没了主角爸爸的严格管束，开始着迷于这外面多彩的世界。吃油炸食品、留起头发、逛街、电影院。假期回家的大女儿用自己新学的技巧击败了主角爸爸，更是产生自我膨胀，对主角爸爸传统技术技巧的不屑与不信任。&lt;/p&gt;
&lt;p&gt;国际大赛上大女儿频频失利，无能教练将此归为命运。与此同时坚信主角爸爸训练方式的二儿女也获得全国冠军，进入国家队。在二儿女和主角妈妈的劝解下，一通电话化解了大女儿和主角爸爸的隔阂。主角爸爸亲自来到体育学校，为大女儿备战下次的国际大赛。&lt;/p&gt;
&lt;p&gt;主角爸爸在仔细研究大女儿对手后，制定了针对性的战术帮助大女儿杀入决赛。无能教练为了不让主角爸爸抢了自己的功劳，在决赛开始前将主角爸爸骗入小黑屋，无法让主角爸爸指导大女儿比赛。决赛第二局大女儿没能把握赛点，让对手追平。主角爸爸继续被困，只能默默祈祷。决赛最后一局，大女儿大比分落后，最后 10 秒大女儿脑海闪现着主角爸爸的叮咛，一击 5 分绝杀翻盘，赢得世界冠军。冠军的国歌声奏起，小黑屋中的主角爸爸也被路人解救，冲入赛场的主角爸爸与大女儿四目相对，此时这已经不是大女儿或主角爸爸哪一个人的冠军，而是所有正在与命运抗战者的胜利。&lt;/p&gt;
&lt;p&gt;主角爸爸：“你是我的骄傲”&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;电影的专业手法和技术自己不懂，但是自己有个感觉：影片后期有大量的摔跤比赛，时不时有种看奥运赛的感觉，但是自己并没有感到乏味。电影故事情节很寻常，但是娓娓道来，很多细节小事让节奏不拖沓。故事人物有主角爸爸的理性和坚持、主角妈妈的守诺不干预、女儿们自我的成长、侄子的酱油加醋、无能教练损人为己&lt;/p&gt;
&lt;p&gt;电影给我也留下了几个现实问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父母的认识经验一定比子女高的多，主角爸爸在看到女儿们的摔跤天赋时，狠心训练与女儿本身的意愿如何选择？&lt;/li&gt;
&lt;li&gt;父母逐渐变老，他们真的跟不上我们了吗？&lt;/li&gt;
&lt;li&gt;如何辨别无能教练的瞎指挥？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;ndash; EOF &amp;ndash;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>周末看了《摔跤吧，爸爸》，也是第一次独自电影院看电影，试写一篇影评纪念下。</p>
<p>注意：<strong>严重剧透预警。</strong></p>
<p>主角爸爸是印度全国摔跤冠军，一心想着为国家赢取一枚金牌。可自己没能实现梦想，把梦想转移给了自己还未出世的孩子。可事与愿违，想要男孩的主角爸爸的前三个孩子都是女孩，第四个还是女孩。主角爸爸近乎要放弃为国争取金牌的梦想时，却意外看的了大女儿、二女儿身上的摔跤天赋。</p>
<!-- more -->
<p>主角妈妈在得知主角爸爸准备将女儿们训练为拳击手时，道出了“你不能将你的梦想施加在女儿们身上”，主角爸爸沉思片刻“给我一年时间，其间你不要插手，如果没成功，我将有永远放弃我的梦想”。在主角爸爸说出这句话之前，我是反对父母将自己的梦想让孩子去实现的，也更没有去争取过孩子们的想法，现实中这样做的父母我是常为 Loser. 但是主角爸爸的回答让我看到的不是一个：被自己个人梦想冲昏头，用孩子的全部去成就自己的父亲。主角爸爸是讲道理的，一年后如果失败也会甘愿放弃。坚持与固执的区别也许就在此。</p>
<p>主角爸爸变身为女儿们的魔鬼摔跤教练，每天除了上学就是训练，跑步时衣服不合适换男生衣服，找侄子当陪练，没有力量加餐牛奶、鸡肉，长头发难清理剪成了寸头。受苦中的女儿们找妈妈求情，主角妈妈遵守承诺，不干预主角爸爸的训练。这里真的要给主角妈妈点赞，后面女儿们的成功，女儿们、主角爸爸的努力在明面放着，而主角妈妈的守诺不干预，对主角爸爸的信赖，是同样的伟大。不然那柔弱的耳边风不知乱了多少坚定的意志。</p>
<p>训练太辛苦的女儿们采用消极怠工对抗主角爸爸。破坏闹钟、破坏场地、假摔示弱。而旷工一天参加好友的婚礼现场被主角爸爸收拾。在与好友诉苦时，才发现了：对比其他女孩一望到底的家庭妇女人生，主角爸爸的狠心是那样让人羡慕。</p>
<p>观念改变的女儿们开始主动训练。大女儿首次参赛就初露锋芒，差一点将男孩对手打败。逐渐成长的大女儿愈战愈勇的直到赢得全国冠军。全国冠军都会进入国家体育学校学习，大女儿也要离开主角爸爸接受新教练的训练，备战世界大赛。从小镇走出来的大女儿，没了主角爸爸的严格管束，开始着迷于这外面多彩的世界。吃油炸食品、留起头发、逛街、电影院。假期回家的大女儿用自己新学的技巧击败了主角爸爸，更是产生自我膨胀，对主角爸爸传统技术技巧的不屑与不信任。</p>
<p>国际大赛上大女儿频频失利，无能教练将此归为命运。与此同时坚信主角爸爸训练方式的二儿女也获得全国冠军，进入国家队。在二儿女和主角妈妈的劝解下，一通电话化解了大女儿和主角爸爸的隔阂。主角爸爸亲自来到体育学校，为大女儿备战下次的国际大赛。</p>
<p>主角爸爸在仔细研究大女儿对手后，制定了针对性的战术帮助大女儿杀入决赛。无能教练为了不让主角爸爸抢了自己的功劳，在决赛开始前将主角爸爸骗入小黑屋，无法让主角爸爸指导大女儿比赛。决赛第二局大女儿没能把握赛点，让对手追平。主角爸爸继续被困，只能默默祈祷。决赛最后一局，大女儿大比分落后，最后 10 秒大女儿脑海闪现着主角爸爸的叮咛，一击 5 分绝杀翻盘，赢得世界冠军。冠军的国歌声奏起，小黑屋中的主角爸爸也被路人解救，冲入赛场的主角爸爸与大女儿四目相对，此时这已经不是大女儿或主角爸爸哪一个人的冠军，而是所有正在与命运抗战者的胜利。</p>
<p>主角爸爸：“你是我的骄傲”</p>
<hr>
<p>电影的专业手法和技术自己不懂，但是自己有个感觉：影片后期有大量的摔跤比赛，时不时有种看奥运赛的感觉，但是自己并没有感到乏味。电影故事情节很寻常，但是娓娓道来，很多细节小事让节奏不拖沓。故事人物有主角爸爸的理性和坚持、主角妈妈的守诺不干预、女儿们自我的成长、侄子的酱油加醋、无能教练损人为己</p>
<p>电影给我也留下了几个现实问题：</p>
<ul>
<li>父母的认识经验一定比子女高的多，主角爸爸在看到女儿们的摔跤天赋时，狠心训练与女儿本身的意愿如何选择？</li>
<li>父母逐渐变老，他们真的跟不上我们了吗？</li>
<li>如何辨别无能教练的瞎指挥？</li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>MySQL 管理用户与访问授权</title>
      <link>https://zyf.im/2017/04/11/mysql-manage-user-and-grant/</link>
      <pubDate>Tue, 11 Apr 2017 11:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/04/11/mysql-manage-user-and-grant/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;适用版本：MySQL 8.0+。5.7 已于 2023-10-31 EOL，本文不再覆盖其语法（如 &lt;code&gt;IDENTIFIED BY&lt;/code&gt; 嵌在 &lt;code&gt;GRANT&lt;/code&gt; 里、&lt;code&gt;PASSWORD()&lt;/code&gt; 函数等均已移除）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;速查&#34;&gt;速查&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- 建用户 + 授权（8.0 必须分两步，GRANT 不再支持隐式建用户/带密码）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tom&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IDENTIFIED&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;pwd123&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ALL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIVILEGES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tom&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WITH&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;GRANT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;OPTION&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- 查 / 改密 / 删
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SHOW&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GRANTS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FOR&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tom&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tom&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IDENTIFIED&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;new_pwd&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;DROP&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tom&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;用户&#34;&gt;用户&lt;/h2&gt;
&lt;h3 id=&#34;创建&#34;&gt;创建&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;USER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;username&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;host&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IDENTIFIED&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;password&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;字段&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;username&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;账号名&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;host&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;允许从哪个主机登陆。&lt;code&gt;localhost&lt;/code&gt; 仅本机；&lt;code&gt;%&lt;/code&gt; 任意远端；&lt;code&gt;192.168.1.%&lt;/code&gt; 限定网段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;password&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;登陆密码，可省略（不建议）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;8.0 的默认认证插件是 &lt;strong&gt;&lt;code&gt;caching_sha2_password&lt;/code&gt;&lt;/strong&gt;（5.7 是 &lt;code&gt;mysql_native_password&lt;/code&gt;）。绝大多数现代客户端已支持；但 PHP &lt;code&gt;mysqlnd&lt;/code&gt; &amp;lt; 7.2.8、部分老 ORM/客户端连不上时，单独为该账号回退插件：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>适用版本：MySQL 8.0+。5.7 已于 2023-10-31 EOL，本文不再覆盖其语法（如 <code>IDENTIFIED BY</code> 嵌在 <code>GRANT</code> 里、<code>PASSWORD()</code> 函数等均已移除）。</p>
</blockquote>
<h2 id="速查">速查</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 建用户 + 授权（8.0 必须分两步，GRANT 不再支持隐式建用户/带密码）
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;pwd123&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="k">PRIVILEGES</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="o">*</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="k">GRANT</span><span class="w"> </span><span class="k">OPTION</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 查 / 改密 / 删
</span></span></span><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">GRANTS</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;new_pwd&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">DROP</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="用户">用户</h2>
<h3 id="创建">创建</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;username&#39;</span><span class="o">@</span><span class="s1">&#39;host&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;password&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>字段</th>
					<th>含义</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>username</code></td>
					<td>账号名</td>
			</tr>
			<tr>
					<td><code>host</code></td>
					<td>允许从哪个主机登陆。<code>localhost</code> 仅本机；<code>%</code> 任意远端；<code>192.168.1.%</code> 限定网段</td>
			</tr>
			<tr>
					<td><code>password</code></td>
					<td>登陆密码，可省略（不建议）</td>
			</tr>
	</tbody>
</table>
<p>8.0 的默认认证插件是 <strong><code>caching_sha2_password</code></strong>（5.7 是 <code>mysql_native_password</code>）。绝大多数现代客户端已支持；但 PHP <code>mysqlnd</code> &lt; 7.2.8、部分老 ORM/客户端连不上时，单独为该账号回退插件：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="n">mysql_native_password</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;pwd&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>例子：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;dog&#39;</span><span class="o">@</span><span class="s1">&#39;localhost&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;123456&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;192.168.1.%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;123456&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">;</span><span class="w">   </span><span class="c1">-- 空密码，等价于无密码
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">                    </span><span class="c1">-- 无密码
</span></span></span></code></pre></div><h3 id="改密--锁定--过期">改密 / 锁定 / 过期</h3>
<p><code>ALTER USER</code> 是 8.0 修改账号属性的统一入口（<code>SET PASSWORD</code> 与 <code>PASSWORD()</code> 已废）。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 改他人密码
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;new_pwd&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 改自己当前密码
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="k">USER</span><span class="p">()</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;new_pwd&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 强制下次登陆改密
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">PASSWORD</span><span class="w"> </span><span class="n">EXPIRE</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 密码过期策略：每 90 天强制改一次
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">PASSWORD</span><span class="w"> </span><span class="n">EXPIRE</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="k">DAY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 账号上锁 / 解锁
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">ACCOUNT</span><span class="w"> </span><span class="k">LOCK</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">ACCOUNT</span><span class="w"> </span><span class="n">UNLOCK</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 连续失败登录自动锁定（8.0.19+）：5 次失败锁 1 天
</span></span></span><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">FAILED_LOGIN_ATTEMPTS</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="n">PASSWORD_LOCK_TIME</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="删除">删除</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">DROP</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;username&#39;</span><span class="o">@</span><span class="s1">&#39;host&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h2 id="权限">权限</h2>
<h3 id="授予">授予</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="n">priv_type</span><span class="w"> </span><span class="p">[(</span><span class="n">col_list</span><span class="p">)]</span><span class="w"> </span><span class="p">[,</span><span class="w"> </span><span class="n">priv_type</span><span class="w"> </span><span class="p">...]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">ON</span><span class="w"> </span><span class="p">[</span><span class="n">object_type</span><span class="p">]</span><span class="w"> </span><span class="k">level</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">TO</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="p">[,</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="p">...]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">[</span><span class="k">WITH</span><span class="w"> </span><span class="k">GRANT</span><span class="w"> </span><span class="k">OPTION</span><span class="p">];</span><span class="w">
</span></span></span></code></pre></div><p>按层级（<code>level</code>）从大到小：</p>
<table>
	<thead>
			<tr>
					<th>层级</th>
					<th>写法</th>
					<th>例子</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>全局</td>
					<td><code>*.*</code></td>
					<td><code>GRANT ALL PRIVILEGES ON *.* TO 'pig'@'%'</code></td>
			</tr>
			<tr>
					<td>库</td>
					<td><code>db.*</code></td>
					<td><code>GRANT ALL ON my_db.* TO 'pig'@'%'</code></td>
			</tr>
			<tr>
					<td>表</td>
					<td><code>db.tbl</code></td>
					<td><code>GRANT SELECT, INSERT ON test.user TO 'pig'@'%'</code></td>
			</tr>
			<tr>
					<td>列</td>
					<td><code>db.tbl</code> + 列名</td>
					<td><code>GRANT SELECT (name, email) ON test.user TO 'pig'@'%'</code></td>
			</tr>
			<tr>
					<td>存储例程</td>
					<td><code>PROCEDURE db.x</code> / <code>FUNCTION db.x</code></td>
					<td><code>GRANT EXECUTE ON PROCEDURE app.calc TO 'pig'@'%'</code></td>
			</tr>
	</tbody>
</table>
<p><code>WITH GRANT OPTION</code> 允许被授权者把自己的权限再传给别人。</p>
<h3 id="查看">查看</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">GRANTS</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">GRANTS</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">CURRENT_USER</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">GRANTS</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="s1">&#39;app_read&#39;</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 含指定 role 展开后的权限
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 从系统视图反查（适合脚本）
</span></span></span><span class="line"><span class="cl"><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">information_schema</span><span class="p">.</span><span class="n">user_privileges</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">GRANTEE</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;&#39;pig&#39;@&#39;%&#39;&#34;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="撤销">撤销</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">REVOKE</span><span class="w"> </span><span class="n">priv_type</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="k">level</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="k">user</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">REVOKE</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="k">PRIVILEGES</span><span class="p">,</span><span class="w"> </span><span class="k">GRANT</span><span class="w"> </span><span class="k">OPTION</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="s1">&#39;pig&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">  </span><span class="c1">-- 一把全收
</span></span></span></code></pre></div><p><strong>核心规则：REVOKE 的层级必须与 GRANT 时完全一致。</strong> 表级授权只能用表级回收，库级只能用库级回收——彼此不能跨级抵消。</p>
<table>
	<thead>
			<tr>
					<th>当初 GRANT</th>
					<th>用这条 REVOKE</th>
					<th>结果</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>GRANT SELECT ON test.user TO 'pig'@'%'</code></td>
					<td><code>REVOKE SELECT ON *.* FROM 'pig'@'%'</code></td>
					<td>❌ 不生效</td>
			</tr>
			<tr>
					<td><code>GRANT SELECT ON *.* TO 'pig'@'%'</code></td>
					<td><code>REVOKE SELECT ON test.user FROM 'pig'@'%'</code></td>
					<td>❌ 不生效</td>
			</tr>
			<tr>
					<td><code>GRANT SELECT ON test.user TO 'pig'@'%'</code></td>
					<td><code>REVOKE SELECT ON test.user FROM 'pig'@'%'</code></td>
					<td>✅ 生效</td>
			</tr>
	</tbody>
</table>
<h3 id="flush-privileges">FLUSH PRIVILEGES?</h3>
<p><code>CREATE USER</code> / <code>GRANT</code> / <code>REVOKE</code> / <code>ALTER USER</code> / <code>DROP USER</code> 都会自动重载授权表，<strong>不需要</strong> <code>FLUSH PRIVILEGES</code>。只有当你绕过 DCL、直接 <code>INSERT/UPDATE/DELETE</code> <code>mysql.user</code> 等系统表时（强烈不推荐）才必须手动刷新。</p>
<h2 id="角色">角色</h2>
<p>角色（ROLE）是命名的一组权限。给一批用户授同样权限时，比一条条 GRANT 简洁得多——一改全改。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 1. 建角色 + 给角色授权
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="s1">&#39;app_read&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;app_write&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="k">SELECT</span><span class="w">                    </span><span class="k">ON</span><span class="w"> </span><span class="n">app_db</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;app_read&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="k">INSERT</span><span class="p">,</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">,</span><span class="w"> </span><span class="k">DELETE</span><span class="w">    </span><span class="k">ON</span><span class="w"> </span><span class="n">app_db</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;app_write&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 2. 把角色授给用户
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;reporter&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;pwd&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="s1">&#39;app_read&#39;</span><span class="w">              </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;reporter&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="s1">&#39;app_read&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;app_write&#39;</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;tom&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 3. 登陆即激活（否则用户每次连进来要 SET ROLE 一下，应用层一般不会做）
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;reporter&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>用户侧查看/切换：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">CURRENT_ROLE</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="s1">&#39;app_read&#39;</span><span class="p">;</span><span class="w">   </span><span class="c1">-- 只激活某个
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="k">ALL</span><span class="p">;</span><span class="w">          </span><span class="c1">-- 激活全部已授角色
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="k">NONE</span><span class="p">;</span><span class="w">         </span><span class="c1">-- 全部停用
</span></span></span></code></pre></div><p>几条容易踩的坑：</p>
<ul>
<li>角色与用户共享 <code>mysql.user</code> 命名空间——<code>CREATE ROLE 'x'</code> 之后 <code>CREATE USER 'x'@'%'</code> 会冲突。</li>
<li>角色默认 <strong><code>ACCOUNT LOCK</code></strong>，不能直接登陆，符合「角色不是账号」的语义。</li>
<li>想全实例自动激活，可设服务器变量：<code>SET PERSIST activate_all_roles_on_login = ON;</code></li>
</ul>
<h2 id="远程访问">远程访问</h2>
<p>开启 MySQL 远程访问 = 用户 host 字段允许远端 + 服务器 <code>bind-address</code> 不锁本机。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;root&#39;</span><span class="o">@</span><span class="s1">&#39;192.168.1.100&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;pwd&#39;</span><span class="p">;</span><span class="w">      </span><span class="c1">-- 仅特定 IP
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">&#39;root&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">&#39;pwd&#39;</span><span class="p">;</span><span class="w">                  </span><span class="c1">-- 任意 IP
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="k">PRIVILEGES</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="o">*</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">&#39;root&#39;</span><span class="o">@</span><span class="s1">&#39;%&#39;</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="k">GRANT</span><span class="w"> </span><span class="k">OPTION</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>Ubuntu/Debian 上还要改服务端配置：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># 默认 127.0.0.1（只听本机），改为 0.0.0.0 监听全部网卡后重启 mysql</span>
</span></span><span class="line"><span class="cl"><span class="na">bind-address</span> <span class="o">=</span> <span class="s">0.0.0.0</span>
</span></span></code></pre></div><blockquote>
<p>生产环境千万别配 <code>'root'@'%'</code> —— 给应用账号用最小权限，给运维账号绑定 VPN/堡垒机 IP。</p>
</blockquote>
<h2 id="host-匹配规则">host 匹配规则</h2>
<p>查看当前会话的账号：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">CURRENT_USER</span><span class="p">();</span><span class="w">
</span></span></span></code></pre></div><p><code>user</code> / <code>host</code> 字段的匹配模式：</p>
<table>
	<thead>
			<tr>
					<th>HOST</th>
					<th>User</th>
					<th>匹配的连接</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>'thomas.loc.gov'</code></td>
					<td><code>'fred'</code></td>
					<td>fred，从 thomas.loc.gov</td>
			</tr>
			<tr>
					<td><code>'thomas.loc.gov'</code></td>
					<td><code>''</code></td>
					<td>任何用户，从 thomas.loc.gov</td>
			</tr>
			<tr>
					<td><code>'%'</code></td>
					<td><code>'fred'</code></td>
					<td>fred，任意主机</td>
			</tr>
			<tr>
					<td><code>'%'</code></td>
					<td><code>''</code></td>
					<td>任何用户，任意主机</td>
			</tr>
			<tr>
					<td><code>'%.loc.gov'</code></td>
					<td><code>'fred'</code></td>
					<td>fred，loc.gov 任意子域</td>
			</tr>
			<tr>
					<td><code>'x.y.%'</code></td>
					<td><code>'fred'</code></td>
					<td>fred，<code>x.y.*</code> 主机</td>
			</tr>
			<tr>
					<td><code>'144.155.166.177'</code></td>
					<td><code>'fred'</code></td>
					<td>fred，指定 IP</td>
			</tr>
			<tr>
					<td><code>'144.155.166.%'</code></td>
					<td><code>'fred'</code></td>
					<td>fred，整个 C 类子网</td>
			</tr>
	</tbody>
</table>
<p>⚠️ <strong>IP 通配符仅对纯 IP 生效</strong>。MySQL 会拒绝匹配「以数字 + 点」起始的主机名——防止有人把主机命名成 <code>144.155.166.evil.com</code> 来骗过 <code>144.155.166.%</code> 规则。<code>1.2.foo.com</code> 这种主机名永远不会命中 host 列。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/access-control.html">MySQL 8.0 Reference — Access Control and Account Management</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/roles.html">MySQL 8.0 Reference — Using Roles</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html#mysql-nutshell-removals">What&rsquo;s Removed in MySQL 8.0</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/password-management.html#failed-login-tracking">Password Management — Failed-Login Tracking and Temporary Locking</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>让 Git 线图说话：pull rebase &#43; merge no-ff</title>
      <link>https://zyf.im/2017/03/17/git-clean-commit-graph/</link>
      <pubDate>Fri, 17 Mar 2017 16:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/03/17/git-clean-commit-graph/</guid>
      <description>&lt;p&gt;&lt;code&gt;git log --graph&lt;/code&gt; 中清晰的提交线图方便 code review 和回退定位。两条互补的策略：&lt;code&gt;git pull --rebase&lt;/code&gt; 把无意义的分叉抹平，&lt;code&gt;git merge --no-ff&lt;/code&gt; 把一组相关提交刻意保留成一个&amp;quot;鼓包&amp;quot;。看起来矛盾，目的一致 —— 让线图表达意图，而不是过程。&lt;/p&gt;
&lt;h2 id=&#34;pull-rebase抹掉无意义的分叉&#34;&gt;pull &amp;ndash;rebase：抹掉无意义的分叉&lt;/h2&gt;
&lt;h3 id=&#34;默认-pull-出了什么&#34;&gt;默认 pull 出了什么&lt;/h3&gt;
&lt;p&gt;本地和远端各自有了新提交，pull 时若不是 fast-forward，Git 默认 &lt;code&gt;merge&lt;/code&gt;，会多出一个&lt;strong&gt;没有任何代码意义&lt;/strong&gt;的&amp;quot;Merge branch &amp;lsquo;main&amp;rsquo; of &amp;hellip;&amp;ldquo;提交：&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-01&#34; loading=&#34;lazy&#34; src=&#34;https://user-images.githubusercontent.com/9289792/80202129-c1cdfb00-8657-11ea-814e-49f8618f301c.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;假设 pull 前：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                 A---B---C  origin/main
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                /
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;           D---E---F---G  main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;git pull&lt;/code&gt;（默认 merge）后多出 H：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                 A---B---C
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                /         \
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;           D---E---F---G---H  main, origin/main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;git pull --rebase&lt;/code&gt; 后线图变平，F G 被基于 C 重放成 F&amp;rsquo; G&amp;rsquo;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;           D---E---A---B---C---F&amp;#39;---G&amp;#39;  main, origin/main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;写进配置不要每次记得加&#34;&gt;写进配置，不要每次记得加&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git config --global pull.rebase &lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;       &lt;span class=&#34;c1&#34;&gt;# pull 默认 rebase&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git config --global pull.ff only           &lt;span class=&#34;c1&#34;&gt;# 不能 fast-forward 就拒绝（避免误产生 merge commit）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git config --global rebase.autostash &lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;# rebase 前自动 stash 未提交修改&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Git 2.27+ 起，未显式配置 &lt;code&gt;pull.rebase&lt;/code&gt; 时每次 pull 会警告，等于在催你做一次明确选择。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><code>git log --graph</code> 中清晰的提交线图方便 code review 和回退定位。两条互补的策略：<code>git pull --rebase</code> 把无意义的分叉抹平，<code>git merge --no-ff</code> 把一组相关提交刻意保留成一个&quot;鼓包&quot;。看起来矛盾，目的一致 —— 让线图表达意图，而不是过程。</p>
<h2 id="pull-rebase抹掉无意义的分叉">pull &ndash;rebase：抹掉无意义的分叉</h2>
<h3 id="默认-pull-出了什么">默认 pull 出了什么</h3>
<p>本地和远端各自有了新提交，pull 时若不是 fast-forward，Git 默认 <code>merge</code>，会多出一个<strong>没有任何代码意义</strong>的&quot;Merge branch &lsquo;main&rsquo; of &hellip;&ldquo;提交：</p>
<p><img alt="170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-01" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202129-c1cdfb00-8657-11ea-814e-49f8618f301c.jpg"></p>
<p>假设 pull 前：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">                 A---B---C  origin/main
</span></span><span class="line"><span class="cl">                /
</span></span><span class="line"><span class="cl">           D---E---F---G  main
</span></span></code></pre></div><p><code>git pull</code>（默认 merge）后多出 H：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">                 A---B---C
</span></span><span class="line"><span class="cl">                /         \
</span></span><span class="line"><span class="cl">           D---E---F---G---H  main, origin/main
</span></span></code></pre></div><p><code>git pull --rebase</code> 后线图变平，F G 被基于 C 重放成 F&rsquo; G&rsquo;：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">           D---E---A---B---C---F&#39;---G&#39;  main, origin/main
</span></span></code></pre></div><h3 id="写进配置不要每次记得加">写进配置，不要每次记得加</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global pull.rebase <span class="nb">true</span>       <span class="c1"># pull 默认 rebase</span>
</span></span><span class="line"><span class="cl">git config --global pull.ff only           <span class="c1"># 不能 fast-forward 就拒绝（避免误产生 merge commit）</span>
</span></span><span class="line"><span class="cl">git config --global rebase.autostash <span class="nb">true</span>  <span class="c1"># rebase 前自动 stash 未提交修改</span>
</span></span></code></pre></div><p>Git 2.27+ 起，未显式配置 <code>pull.rebase</code> 时每次 pull 会警告，等于在催你做一次明确选择。</p>
<h3 id="什么时候别用-rebase">什么时候<strong>别</strong>用 rebase</h3>
<ul>
<li>冲突预期很多 —— rebase 每个被重放的 commit 都可能触发冲突，merge 只触发一次。</li>
<li>已 push 出去的本地分支 —— rebase 会改写 SHA，强推会把基于它工作的同事埋掉。</li>
<li>想保留&quot;这一组改动是同一时间合入&quot;的语境 —— 那是 <code>--no-ff</code> 的活。</li>
</ul>
<h2 id="merge-no-ff把一组提交保留成鼓包">merge &ndash;no-ff：把一组提交保留成&quot;鼓包&rdquo;</h2>
<p><code>git merge --no-ff &lt;branch&gt;</code> 在可以 fast-forward 时也强制生成一个 merge commit，把整条 feature 分支保留为可识别的形状。</p>
<h3 id="先看-ff-与-no-ff-在线图上的差别">先看 ff 与 no-ff 在线图上的差别</h3>
<p>合并前（feature 从 A 拉出，main 期间没动 —— ff 的前提）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">A ─ main
</span></span><span class="line"><span class="cl"> \
</span></span><span class="line"><span class="cl">  B ─ C ─ feature
</span></span></code></pre></div><p><code>git merge feature</code>（默认，能 ff 就 ff）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">A ─ B ─ C ─ main, feature       ← main 指针滑到 C，无新 commit，feature 拓扑消失
</span></span></code></pre></div><p><code>git merge --no-ff feature</code>（强制不 ff）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">A ─────────── M ─ main          ← M 是新 merge commit，两个 parent: A、C
</span></span><span class="line"><span class="cl"> \           /                     git log --graph 能看到 feature 这一段&#34;鼓包&#34;
</span></span><span class="line"><span class="cl">  B ─ C ─────
</span></span></code></pre></div><p><img alt="170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-02" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202132-c397be80-8657-11ea-8135-781a36fc64e5.jpg"></p>
<blockquote>
<p><strong><code>--no-ff</code> 只在 ff 可行时才有意义</strong>。main 在 feature 期间也有新提交（拓扑已经分叉）时，Git 无论如何都会生成 merge commit，加不加 <code>--no-ff</code> 一样。</p>
</blockquote>
<h3 id="鼓包带来的两个隐性价值">鼓包带来的两个隐性价值</h3>
<ol>
<li><strong>语义边界</strong>。merge commit 的 message（默认 <code>Merge branch 'feature/xxx'</code>）天然标注&quot;以下这串提交都属于同一个特性&quot;。code review 时一眼分组；<code>git log --merges</code> 直接列出所有 feature 合并点做 release notes；<code>git bisect</code> 区分&quot;哪个特性引入了 bug&quot;也更精确。</li>
<li><strong>整体回滚</strong>。<code>git revert -m 1 &lt;merge-sha&gt;</code> 一条命令撤回整个特性，<code>-m 1</code> 指定保留第一个 parent（main 侧），feature 侧的所有改动整体反转。比一笔笔 <code>git revert</code> 简单一个量级。</li>
</ol>
<p>强制全局 no-ff（团队约定后再开）：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global merge.ff <span class="nb">false</span>       <span class="c1"># 自己手动 merge 时强制不 ff</span>
</span></span><span class="line"><span class="cl">git config --global merge.ff only        <span class="c1"># 反过来：只允许 ff，禁止生成 merge commit（线性派）</span>
</span></span></code></pre></div><h3 id="你们项目可能本来就是-no-ff">你们项目可能本来就是 no-ff</h3>
<p>走 PR / MR 流程的团队，服务端的 merge 策略<strong>比本地 <code>merge.ff</code> 更优先</strong>。常见平台默认值：</p>
<table>
	<thead>
			<tr>
					<th>平台 / 选项</th>
					<th>等价行为</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>GitLab Merge method = Merge commit（默认）</td>
					<td><code>--no-ff</code></td>
			</tr>
			<tr>
					<td>GitLab Merge method = Fast-forward merge</td>
					<td>ff（要求先 rebase）</td>
			</tr>
			<tr>
					<td>GitHub PR = Create a merge commit（默认）</td>
					<td><code>--no-ff</code></td>
			</tr>
			<tr>
					<td>GitHub PR = Rebase and merge</td>
					<td>rebase + ff</td>
			</tr>
			<tr>
					<td>GitHub / GitLab Squash</td>
					<td>整 PR 压成 1 commit + ff</td>
			</tr>
	</tbody>
</table>
<p>判断你们用哪种：<code>git log --graph --oneline main</code> 看一眼。一堆 <code>Merge branch ...</code> 鼓包 = no-ff；纯一条直线 = ff（多半还 squash 了）。</p>
<h3 id="合并前先-rebase让线图更干净">合并前先 rebase，让线图更干净</h3>
<p>将 feature 合入 dev 前的标准动作：先把 feature 重排到最新 dev 之后，再 <code>--no-ff</code> merge，分叉点会上移，鼓包紧凑：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git fetch origin
</span></span><span class="line"><span class="cl">git switch feature
</span></span><span class="line"><span class="cl">git rebase origin/dev          <span class="c1"># 把 feature 重放到最新 dev 之后</span>
</span></span><span class="line"><span class="cl">git switch dev <span class="o">&amp;&amp;</span> git pull
</span></span><span class="line"><span class="cl">git merge --no-ff feature      <span class="c1"># 鼓包合入</span>
</span></span></code></pre></div><p>判断 feature 是否落后于 dev：<code>git log feature..dev</code> 列出&quot;dev 有而 feature 没有&quot;的提交，<strong>有输出 = feature 落后</strong>，需要先 rebase；无输出可以直接合。</p>
<p><img alt="170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-03" loading="lazy" src="https://user-images.githubusercontent.com/9289792/80202134-c4305500-8657-11ea-8c4b-52f858f669ec.jpg"></p>
<h3 id="--rebasemergesrebase-时保留-merge-鼓包"><code>--rebase=merges</code>：rebase 时保留 merge 鼓包</h3>
<p>普通 rebase 会把 merge commit 抹平。如果一段历史里有重要的 <code>--no-ff</code> 鼓包要保留，用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rebase --rebase-merges origin/main   <span class="c1"># 旧名 --preserve-merges 已废弃</span>
</span></span></code></pre></div><h2 id="平台-squash-merge-改变了什么">平台 squash merge 改变了什么</h2>
<p>GitHub / GitLab / Bitbucket 的 &ldquo;Squash and merge&rdquo; 把整个 PR 压成一条 commit 合入主干。结果是：</p>
<ul>
<li>主干线图天生干净，本地的 <code>--rebase</code> / <code>--no-ff</code> 都变得次要。</li>
<li>但本地分支 push 后回到 GitHub 看到的还是原始 commit，PR 描述应当承担起&quot;语义边界&quot;的责任 —— 这部分原本是 merge commit 在干的事。</li>
<li>整体回滚也变成了 <code>git revert &lt;squashed-sha&gt;</code>，一条命令同样能撤。</li>
</ul>
<p><strong>结论</strong>：用 PR squash merge 的团队，本地策略可以放宽到只关心 <code>pull.rebase = true</code>；自托管直接 push 到主干的团队，<code>--no-ff</code> + <code>git rebase --rebase-merges</code> 的组合仍然是首选。</p>
<h2 id="决策表">决策表</h2>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>命令</th>
					<th>目的</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>日常 pull</td>
					<td><code>git pull --rebase</code></td>
					<td>抹平无意义分叉</td>
			</tr>
			<tr>
					<td>合入 feature 分支</td>
					<td><code>git merge --no-ff feature</code></td>
					<td>保留语义鼓包</td>
			</tr>
			<tr>
					<td>合并前同步基</td>
					<td><code>git rebase origin/dev</code></td>
					<td>让鼓包紧凑</td>
			</tr>
			<tr>
					<td>rebase 含 merge 的历史</td>
					<td><code>git rebase --rebase-merges</code></td>
					<td>保留已有鼓包</td>
			</tr>
			<tr>
					<td>团队走 PR squash</td>
					<td>只配 <code>pull.rebase=true</code></td>
					<td>平台已处理线图</td>
			</tr>
	</tbody>
</table>
<h2 id="references">References</h2>
<ul>
<li><a href="https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---rebase-mergesrebase-cousinsno-rebase-cousins">git-rebase &ndash;rebase-merges | Git Docs</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>Git 改写提交历史</title>
      <link>https://zyf.im/2017/03/13/git-rewrite-history/</link>
      <pubDate>Mon, 13 Mar 2017 17:00:00 +0000</pubDate>
      <guid>https://zyf.im/2017/03/13/git-rewrite-history/</guid>
      <description>&lt;p&gt;改写历史的核心动作只有三个：&lt;code&gt;commit --amend&lt;/code&gt;（改最近一次）、&lt;code&gt;rebase -i&lt;/code&gt;（改任意一段）、&lt;code&gt;filter-repo&lt;/code&gt;（批改全历史）。日常代码评审里&amp;quot;补一行到三次前的 commit&amp;quot;，配合 &lt;code&gt;--fixup&lt;/code&gt; / &lt;code&gt;--autosquash&lt;/code&gt; 不必再手动开 rebase 菜单。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;改写历史会重算 SHA，&lt;strong&gt;只在自己未推送、或确认没人基于该分支工作时做&lt;/strong&gt;。已 push 到 protected branch 才发现要改的，先看末尾&amp;quot;安全网&amp;quot;一节再动手。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;改最近一次提交&#34;&gt;改最近一次提交&lt;/h2&gt;
&lt;h3 id=&#34;改提交说明&#34;&gt;改提交说明&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git commit --amend              &lt;span class=&#34;c1&#34;&gt;# 进编辑器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git commit --amend -m &lt;span class=&#34;s2&#34;&gt;&amp;#34;msg&amp;#34;&lt;/span&gt;     &lt;span class=&#34;c1&#34;&gt;# 一行写完&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git commit --amend --no-edit    &lt;span class=&#34;c1&#34;&gt;# 只追加文件，不改说明&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;追加--替换文件&#34;&gt;追加 / 替换文件&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git add fixed.cpp
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git commit --amend --no-edit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;从最近一次提交中移除某个文件&#34;&gt;从最近一次提交中移除某个文件&lt;/h3&gt;
&lt;p&gt;不要用 &lt;code&gt;git reset HEAD~1&lt;/code&gt; —— 那会把整个提交炸开，其余文件的暂存状态也丢了。正确做法是只把目标文件在 index 里还原到上一版，再 amend：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git restore --source&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;HEAD~ --staged -- path/to/file   &lt;span class=&#34;c1&#34;&gt;# index 回到上一版&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git commit --amend --no-edit                          &lt;span class=&#34;c1&#34;&gt;# 重新打包&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果还想丢掉工作区里的修改，再加 &lt;code&gt;--worktree&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git restore --source&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;HEAD~ --staged --worktree -- path/to/file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;改一段提交交互式-rebase&#34;&gt;改一段提交：交互式 rebase&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git rebase -i HEAD~3        &lt;span class=&#34;c1&#34;&gt;# 改最近 3 次&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git rebase -i --root        &lt;span class=&#34;c1&#34;&gt;# 改到根提交（含第一次 commit）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;打开的 todo 列表里，&lt;strong&gt;最早的提交在最上面&lt;/strong&gt;（和 &lt;code&gt;git log&lt;/code&gt; 相反），改第一列动词即可：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>改写历史的核心动作只有三个：<code>commit --amend</code>（改最近一次）、<code>rebase -i</code>（改任意一段）、<code>filter-repo</code>（批改全历史）。日常代码评审里&quot;补一行到三次前的 commit&quot;，配合 <code>--fixup</code> / <code>--autosquash</code> 不必再手动开 rebase 菜单。</p>
<blockquote>
<p>改写历史会重算 SHA，<strong>只在自己未推送、或确认没人基于该分支工作时做</strong>。已 push 到 protected branch 才发现要改的，先看末尾&quot;安全网&quot;一节再动手。</p>
</blockquote>
<h2 id="改最近一次提交">改最近一次提交</h2>
<h3 id="改提交说明">改提交说明</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git commit --amend              <span class="c1"># 进编辑器</span>
</span></span><span class="line"><span class="cl">git commit --amend -m <span class="s2">&#34;msg&#34;</span>     <span class="c1"># 一行写完</span>
</span></span><span class="line"><span class="cl">git commit --amend --no-edit    <span class="c1"># 只追加文件，不改说明</span>
</span></span></code></pre></div><h3 id="追加--替换文件">追加 / 替换文件</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add fixed.cpp
</span></span><span class="line"><span class="cl">git commit --amend --no-edit
</span></span></code></pre></div><h3 id="从最近一次提交中移除某个文件">从最近一次提交中移除某个文件</h3>
<p>不要用 <code>git reset HEAD~1</code> —— 那会把整个提交炸开，其余文件的暂存状态也丢了。正确做法是只把目标文件在 index 里还原到上一版，再 amend：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git restore --source<span class="o">=</span>HEAD~ --staged -- path/to/file   <span class="c1"># index 回到上一版</span>
</span></span><span class="line"><span class="cl">git commit --amend --no-edit                          <span class="c1"># 重新打包</span>
</span></span></code></pre></div><p>如果还想丢掉工作区里的修改，再加 <code>--worktree</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git restore --source<span class="o">=</span>HEAD~ --staged --worktree -- path/to/file
</span></span></code></pre></div><h2 id="改一段提交交互式-rebase">改一段提交：交互式 rebase</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git rebase -i HEAD~3        <span class="c1"># 改最近 3 次</span>
</span></span><span class="line"><span class="cl">git rebase -i --root        <span class="c1"># 改到根提交（含第一次 commit）</span>
</span></span></code></pre></div><p>打开的 todo 列表里，<strong>最早的提交在最上面</strong>（和 <code>git log</code> 相反），改第一列动词即可：</p>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>pick</code> (<code>p</code>)</td>
					<td>保留</td>
			</tr>
			<tr>
					<td><code>reword</code> (<code>r</code>)</td>
					<td>保留 commit，只改说明</td>
			</tr>
			<tr>
					<td><code>edit</code> (<code>e</code>)</td>
					<td>停在这里，让你 amend 或拆分</td>
			</tr>
			<tr>
					<td><code>squash</code> (<code>s</code>)</td>
					<td>并入上一行，合并 message</td>
			</tr>
			<tr>
					<td><code>fixup</code> (<code>f</code>)</td>
					<td>并入上一行，丢弃本条 message</td>
			</tr>
			<tr>
					<td><code>drop</code> (<code>d</code>)</td>
					<td>删除（直接删行也行）</td>
			</tr>
			<tr>
					<td><code>exec</code> (<code>x</code>)</td>
					<td>在该提交点执行 shell</td>
			</tr>
	</tbody>
</table>
<p>调整顺序：直接挪行。</p>
<h3 id="拆分一个提交">拆分一个提交</h3>
<p>todo 改为：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">edit  bb199a0  Update the version
</span></span></code></pre></div><p>rebase 暂停时执行：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reset HEAD^           <span class="c1"># 把这次提交的改动还原回工作区</span>
</span></span><span class="line"><span class="cl">git add file1 <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;Update version: bump&#34;</span>
</span></span><span class="line"><span class="cl">git add file2 <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;Update version: changelog&#34;</span>
</span></span><span class="line"><span class="cl">git rebase --continue
</span></span></code></pre></div><h2 id="fixup--autosquash无菜单-rebase">fixup + autosquash：无菜单 rebase</h2>
<p>刚 review 完，发现 3 次前的 commit 漏写了校验逻辑。传统做法是打开 rebase 菜单挑出对应行手动 <code>edit</code>；<code>--fixup</code> 把这一步自动化：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add validator.go
</span></span><span class="line"><span class="cl">git commit --fixup<span class="o">=</span>&lt;old-sha&gt;           <span class="c1"># 生成 &#34;fixup! &lt;old subject&gt;&#34;</span>
</span></span><span class="line"><span class="cl">git rebase -i --autosquash &lt;old-sha&gt;^  <span class="c1"># 自动把 fixup! 行排到 old commit 后，并标 fixup</span>
</span></span></code></pre></div><p>写进配置就不用每次加 <code>--autosquash</code>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global rebase.autosquash <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git config --global commit.verbose <span class="nb">true</span>   <span class="c1"># 顺带：让 commit 编辑器里看到 diff</span>
</span></span></code></pre></div><h2 id="改作者--邮箱">改作者 / 邮箱</h2>
<h3 id="最新一笔amend">最新一笔：amend</h3>
<p>漏配 <code>user.name</code> 就提交了？最新一笔用 amend 修：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Jiang Xin&#34;</span>
</span></span><span class="line"><span class="cl">git commit --amend --allow-empty --reset-author
</span></span></code></pre></div><ul>
<li><code>--allow-empty</code>：允许空白提交（修补后无文件变化时需要）。</li>
<li><code>--reset-author</code>：同时重置 Author 与 AuthorDate；不加只会改 Committer。</li>
</ul>
<blockquote>
<p>Git 不做提交者身份认证，&ldquo;谁都能写任意 name/email&rdquo; 是分布式特性。权限控制交给 GitLab / GitHub 等服务端。</p>
</blockquote>
<h3 id="全历史先-mailmap必要时-filter-repo">全历史：先 mailmap，必要时 filter-repo</h3>
<p>amend 只管最新一笔。历史上一千个用旧邮箱提的 commit 它够不着 —— 两种正解，<strong>先试 mailmap</strong>。</p>
<h4 id="用法-1mailmap不动历史只改显示">用法 1：mailmap，不动历史只改显示</h4>
<p>仓库根目录建 <code>.mailmap</code>，告诉 Git &ldquo;看到旧身份就当新身份显示&rdquo;：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># 前是新身份，后是旧身份，空格分隔
</span></span><span class="line"><span class="cl">New Name &lt;new@gmail.com&gt;  Old Name &lt;old@qq.com&gt;   # 名 + 邮箱完整匹配
</span></span><span class="line"><span class="cl">New Name &lt;new@gmail.com&gt;  &lt;old@qq.com&gt;            # 只按旧邮箱匹配，旧名任意
</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="cl">git log --use-mailmap
</span></span><span class="line"><span class="cl">git shortlog -sn                <span class="c1"># 默认就 honor mailmap</span>
</span></span></code></pre></div><h4 id="用法-2filter-repo真改写历史">用法 2：filter-repo，真改写历史</h4>
<p>需要 commit 字段本身也变（审计、合规、对外开源前清洗），上 <a href="https://til.zyf.im/posts/260514-git-filter-repo-purge-secrets/"><code>git filter-repo</code></a>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git filter-repo --mailmap .mailmap
</span></span></code></pre></div><blockquote>
<p>早年的 <code>git filter-branch</code> 官方已弃用 —— 每 commit 起一个 shell 子进程，大仓库小时级别，且默认保留 <code>refs/original/</code> 导致旧对象&quot;清理完又复活&quot;。filter-repo 是 Python 单进程流式，快 10–100 倍。</p>
</blockquote>
<h4 id="两者对比">两者对比</h4>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>用法 1：mailmap</th>
					<th>用法 2：filter-repo</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>改的是</td>
					<td><strong>显示</strong>，原 commit 不动</td>
					<td><strong>commit 字段本身</strong></td>
			</tr>
			<tr>
					<td>commit SHA</td>
					<td>不变</td>
					<td>全变</td>
			</tr>
			<tr>
					<td>协作者成本</td>
					<td>零（pull 即可）</td>
					<td>必须重 clone 或硬 reset</td>
			</tr>
			<tr>
					<td>撤销</td>
					<td>删 <code>.mailmap</code> 即可</td>
					<td>不可逆（除非备份）</td>
			</tr>
			<tr>
					<td>第三方工具识别</td>
					<td>GitHub 认；审计工具未必</td>
					<td>永远认</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p><code>.mailmap</code> 正好就是 <code>filter-repo --mailmap</code> 吃的输入 —— 先写 mailmap 跑用法 1，确认效果再决定要不要上用法 2。一份配置覆盖两种场景。</p>
</blockquote>
<h4 id="实操辅助">实操辅助</h4>
<p>查看当前所有作者：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git log --format<span class="o">=</span><span class="s1">&#39;%aN &lt;%aE&gt;&#39;</span> <span class="p">|</span> sort -u
</span></span></code></pre></div><p>filter-repo 改完一定要 force push，并通知所有 collaborator 重新 clone —— 他们本地的旧 SHA 在远程已不存在，再 push 会把旧链子带回来：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git push --force-with-lease --tags    <span class="c1"># 比 -f 安全：远程被别人推过会拒绝</span>
</span></span></code></pre></div><h2 id="安全网">安全网</h2>
<ul>
<li><strong>改坏了能找回</strong>：<code>git reflog</code> 列出 HEAD 最近 90 天的每次跳转，<code>git reset --hard HEAD@{3}</code> 回到任意一步。reflog 是本地的，没 push 也照样能用。</li>
<li><strong>强推首选 <code>--force-with-lease</code></strong>：上游被别人 push 过时会拒绝覆盖，避免顺手抹掉同事的提交。</li>
<li><strong>保护分支兜底</strong>：GitHub / GitLab 默认提供 protected branch；本地也可用 <code>pre-push</code> hook 拒绝任何对 <code>main</code> 的非 fast-forward 推送。</li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History">Git Tools - Rewriting History | Pro Git</a></li>
<li><a href="https://til.zyf.im/posts/260514-git-filter-repo-purge-secrets/">git filter-repo 清除历史敏感文件 | TIL.ZYF.IM</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>回顾 2016</title>
      <link>https://zyf.im/2016/12/31/review-2016/</link>
      <pubDate>Sat, 31 Dec 2016 22:00:00 +0000</pubDate>
      <guid>https://zyf.im/2016/12/31/review-2016/</guid>
      <description>&lt;p&gt;2016 肯定是很特别的一年。年初离开了老师的队伍，来北京找实习。年中和 318 告别，大学毕业了。年末换房学做饭，开始另种生活。对于 2017 更多了对自己的期待。&lt;/p&gt;
&lt;!-- more --&gt;
&lt;iframe frameborder=&#34;no&#34; border=&#34;0&#34; marginwidth=&#34;0&#34; marginheight=&#34;0&#34; width=260 height=52 src=&#34;//music.163.com/outchain/player?type=2&amp;id=31445772&amp;auto=0&amp;height=32&#34;&gt;&lt;/iframe&gt;
&lt;p&gt;2016 是在保定的雾霾中开始。结束了大为，决定离开老师的队伍，来北京实习，这对我来说是一个不大不小的决定。老师带着，跟学长一起走到这里，这里有了很多宝贵的东西，有很多回忆，很多故事可以讲。选择离开，主要还是想看看在这之外的是什么样子。&lt;/p&gt;
&lt;p&gt;朋友、老师、爸妈都为我的实习操过心，不过推荐的地方在各种奇怪的理由下都没有成。自己开始在拉勾上投简历，一次次来返在京石铁路上。面试一直是不顺的。完全没有想到对实习生有如此高的要求，很多技术问题正中下怀。心里是有落差的，在我当时的想法里，那些在工程中是用不易用到的，但也感到自己技术知识的浅薄。面试了俩三家后也发现了套路，回答不好的、答不上的问题，回去查查准备准备，下回就能用上。&lt;/p&gt;
&lt;p&gt;没什么难度的大学毕业。大学里更多的收获是跟着老师、学长在课堂外搞事，是和宿舍的、好朋友在一起的时间。真的好幸运啊，遇到 318 的舍友们。没想到和你们每一个人都超级聊的来。大四的时候人就渐渐不齐，但只要宿舍在，不到最后毕业真的不知道要分别了。我们相互影响着成长，至少除去了我很多很 low 的思想。我们相互练着酒量、每天演着戏、日常说着骚话。&lt;/p&gt;
&lt;p&gt;德育答辩上一个同学说：我常翘课、考试挂科，但我大学四年过的很坦荡，我会因为和舍友游戏上的一个好玩的套路高兴很久。我收获朋友。我没什么后悔的。
这很戳中我的泪点。没什么后悔的，也是对我大学的注脚。愿你们成家的幸福，工作的有成。&lt;/p&gt;
&lt;p&gt;从上学到工作，过渡的很平稳。工作中有一些挑战，但总之是还应对的来。可没有什么出众的地方，我不想过的太平凡，愿有一技之长，独挡一面。我常说自己：菜啊、傻啊、什么的，我确实不想用这些来词来逃避问题，我想尽快的甩掉这些。我也愿自己可以更有底气和勇气说出，现在不能说出的话，愿更决绝的对大事小情做决定。&lt;/p&gt;
&lt;p&gt;年末随着公司搬家，自己也搬了。炒菜做饭成了每天另一部分。也许才是真正生活二字的开始。&lt;/p&gt;
&lt;p&gt;突然想到学长提的一个问题：你们有谁把技术当做以后安身立命的本事？
现在对于我来说，这问题的答案比任何时候都更加清晰。&lt;/p&gt;
&lt;p&gt;最后，还是怀揣着 2014 末的敬请期待，让一切皆有可能，开始 2017。&lt;/p&gt;
&lt;p&gt;—— 送给 木林木丶&lt;/p&gt;
&lt;p&gt;&amp;ndash; EOF &amp;ndash;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>2016 肯定是很特别的一年。年初离开了老师的队伍，来北京找实习。年中和 318 告别，大学毕业了。年末换房学做饭，开始另种生活。对于 2017 更多了对自己的期待。</p>
<!-- more -->
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=260 height=52 src="//music.163.com/outchain/player?type=2&id=31445772&auto=0&height=32"></iframe>
<p>2016 是在保定的雾霾中开始。结束了大为，决定离开老师的队伍，来北京实习，这对我来说是一个不大不小的决定。老师带着，跟学长一起走到这里，这里有了很多宝贵的东西，有很多回忆，很多故事可以讲。选择离开，主要还是想看看在这之外的是什么样子。</p>
<p>朋友、老师、爸妈都为我的实习操过心，不过推荐的地方在各种奇怪的理由下都没有成。自己开始在拉勾上投简历，一次次来返在京石铁路上。面试一直是不顺的。完全没有想到对实习生有如此高的要求，很多技术问题正中下怀。心里是有落差的，在我当时的想法里，那些在工程中是用不易用到的，但也感到自己技术知识的浅薄。面试了俩三家后也发现了套路，回答不好的、答不上的问题，回去查查准备准备，下回就能用上。</p>
<p>没什么难度的大学毕业。大学里更多的收获是跟着老师、学长在课堂外搞事，是和宿舍的、好朋友在一起的时间。真的好幸运啊，遇到 318 的舍友们。没想到和你们每一个人都超级聊的来。大四的时候人就渐渐不齐，但只要宿舍在，不到最后毕业真的不知道要分别了。我们相互影响着成长，至少除去了我很多很 low 的思想。我们相互练着酒量、每天演着戏、日常说着骚话。</p>
<p>德育答辩上一个同学说：我常翘课、考试挂科，但我大学四年过的很坦荡，我会因为和舍友游戏上的一个好玩的套路高兴很久。我收获朋友。我没什么后悔的。
这很戳中我的泪点。没什么后悔的，也是对我大学的注脚。愿你们成家的幸福，工作的有成。</p>
<p>从上学到工作，过渡的很平稳。工作中有一些挑战，但总之是还应对的来。可没有什么出众的地方，我不想过的太平凡，愿有一技之长，独挡一面。我常说自己：菜啊、傻啊、什么的，我确实不想用这些来词来逃避问题，我想尽快的甩掉这些。我也愿自己可以更有底气和勇气说出，现在不能说出的话，愿更决绝的对大事小情做决定。</p>
<p>年末随着公司搬家，自己也搬了。炒菜做饭成了每天另一部分。也许才是真正生活二字的开始。</p>
<p>突然想到学长提的一个问题：你们有谁把技术当做以后安身立命的本事？
现在对于我来说，这问题的答案比任何时候都更加清晰。</p>
<p>最后，还是怀揣着 2014 末的敬请期待，让一切皆有可能，开始 2017。</p>
<p>—— 送给 木林木丶</p>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>刚刚毕业的两个月小结</title>
      <link>https://zyf.im/2016/08/31/2016-08-report/</link>
      <pubDate>Wed, 31 Aug 2016 23:00:00 +0000</pubDate>
      <guid>https://zyf.im/2016/08/31/2016-08-report/</guid>
      <description>&lt;p&gt;走出校园已经两个月了，因为之前的暑期也没怎么在家待过，大一在中康、大二在腾骏、大三在大为，大四毕业也就是现在，所以也没有什么特别的感觉。可以说，这两个月也做了些事情，学了些东西的。&lt;/p&gt;
&lt;!-- more --&gt;
&lt;p&gt;从学校毕业，最直接的影响就是自己更加专注于计算机知识，不用再为学业担心。确定了先走技术的道路，也让自己不那么迷茫做什么。工作规律，自己开始读读书，才觉的读书是件有意思的事。也逼着自己常常写点东西，主要是觉的：写东西的时候自己会主动的思考，文笔练着练着也就能进步吧。&lt;/p&gt;
&lt;p&gt;（一）&lt;/p&gt;
&lt;p&gt;DMV 是毕业后自己第一个项目，全栈开发。初版两周上线，后有花了一周多的时间编写了答题记录的功能。其中的收获是：项目框架的搭建和前端入门知识。原来的项目搭建都不是自己做的，也一直觉的是一件很难的事情。也许就是因为没有做过才觉的难，做过之后也觉的不过如此。项目的页面不多，但都是自己一点点写的，HTML JS 原来也就是自己改改，没有完整写过，这次算是一次不错的锻炼。写前端的时候才发现现在的前端真的是日新月异 AngularJS React 好多好多自己都没有听过的东西，也是从 Mengqi 那里了解到了很多。可惜的是自己没有实际的用上这些，如有机会一定尝试。&lt;/p&gt;
&lt;p&gt;DMV 项目流量平平，因为有 Domain 类项目的工作，DMV 的维护就暂时放置了，还是心有不舍的。期间和 John 聊过一次关于产品的事基本总结为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;产品项目是会有失败的，有流量的项目才有维护的意义。&lt;/li&gt;
&lt;li&gt;大型项目只有成功与失败之分，没有中间项，失败将代价很大。&lt;/li&gt;
&lt;li&gt;不一定要留住用户。&lt;/li&gt;
&lt;li&gt;更小的成本更高的流量，高流量后就可以做很多的事。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（二）&lt;/p&gt;
&lt;p&gt;Domain 信息类项目，使用的架构和 DMV 一致，逻辑更加简单。自己主要做了数据整理，收获是关于 SQL 命令。SQL 就可以直接处理很多的事情。使用了 RMI 做数据同步，用反射写了一段程序，是一次有突破的尝试。
一次看到自己年初的简历，笑了。自己是真敢写，能写个 Hello World 就敢标成了解。从会用到原理，是接下要走的路。&lt;/p&gt;
&lt;p&gt;（三）&lt;/p&gt;
&lt;p&gt;最近在看设计模式和算法的书，一直在做 LeetCode 的题。设计模式没用过，逮到能用机会绝不错过。算法健脑，刚刚开始接触觉的挺有意思，对数据结构也是种了解。&lt;/p&gt;
&lt;p&gt;（四）&lt;/p&gt;
&lt;p&gt;再次参加舍友的婚礼，又个结婚了。大一奶了一口，现在成真。祝福 Lu&amp;amp;Feifei。&lt;/p&gt;
&lt;p&gt;（五）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;来说说程序员那无处安放的创造力&lt;/p&gt;
&lt;p&gt;有了锤子想找钉子是很正常的原始冲动，但我们必须认识到，创造力对于程序员这个职业来讲，是锦上添花的东西。如果你没有强大的工程能力，那么创造力也不过是无本之木。所以扎扎实实的把工程基础打好，这是最根本的。&lt;/p&gt;
&lt;p&gt;在此基础上，我比较推荐程序员采用内外两条线来培养自己。在公司内的项目上采取相对保守的策略，尽力把稳定性做到最好，培养出自己卓越的工程能力；然后在公司外的开源项目和自己的独立项目上，采用一些新的技术、实践一些新的想法、充分发挥自己的创造力，梦想还是要有的，对吧。&lt;/p&gt;
&lt;p&gt;这样做最明显的好处是，你可以了解到新技术和激进方案的优缺点，从而在进行方案选型时，有更多的依据；还有一个职业发展上的好处：如果不是主负责人，公司的项目往往不能代表你的能力；但独立项目却可以作为一个非常好的能力证明出现在你的简历里边。&lt;/p&gt;
&lt;p&gt;你可以是一个身怀绝技的手艺人，在自己家里你尝试各种手法各种风格的个人作品；但当你参与颐和园这种级别的工程时，好好的把自己负责的石头雕成总设计师要求的样子就好 —— 毕竟这个时代一个人已经很难负责整个项目了。这就是我所理解的程序员的工匠精神。&lt;/p&gt;
&lt;p&gt;摘自：&lt;a href=&#34;http://mp.weixin.qq.com/s?__biz=MzI5OTI5Njg2Mg==&amp;amp;mid=2247483667&amp;amp;idx=1&amp;amp;sn=d6e5953c7a7835148e3822b919b82416#rd&#34;&gt;程序员到底是一个什么职业？&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（尾）&lt;/p&gt;
&lt;p&gt;现在自己能听见进去一些原来听不去的话了。我不认为自己是被同化了，也许是心中少了些恶意。&lt;/p&gt;
&lt;p&gt;状态不错，继续前进。&lt;/p&gt;
&lt;p&gt;&amp;ndash; EOF &amp;ndash;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>走出校园已经两个月了，因为之前的暑期也没怎么在家待过，大一在中康、大二在腾骏、大三在大为，大四毕业也就是现在，所以也没有什么特别的感觉。可以说，这两个月也做了些事情，学了些东西的。</p>
<!-- more -->
<p>从学校毕业，最直接的影响就是自己更加专注于计算机知识，不用再为学业担心。确定了先走技术的道路，也让自己不那么迷茫做什么。工作规律，自己开始读读书，才觉的读书是件有意思的事。也逼着自己常常写点东西，主要是觉的：写东西的时候自己会主动的思考，文笔练着练着也就能进步吧。</p>
<p>（一）</p>
<p>DMV 是毕业后自己第一个项目，全栈开发。初版两周上线，后有花了一周多的时间编写了答题记录的功能。其中的收获是：项目框架的搭建和前端入门知识。原来的项目搭建都不是自己做的，也一直觉的是一件很难的事情。也许就是因为没有做过才觉的难，做过之后也觉的不过如此。项目的页面不多，但都是自己一点点写的，HTML JS 原来也就是自己改改，没有完整写过，这次算是一次不错的锻炼。写前端的时候才发现现在的前端真的是日新月异 AngularJS React 好多好多自己都没有听过的东西，也是从 Mengqi 那里了解到了很多。可惜的是自己没有实际的用上这些，如有机会一定尝试。</p>
<p>DMV 项目流量平平，因为有 Domain 类项目的工作，DMV 的维护就暂时放置了，还是心有不舍的。期间和 John 聊过一次关于产品的事基本总结为：</p>
<ul>
<li>产品项目是会有失败的，有流量的项目才有维护的意义。</li>
<li>大型项目只有成功与失败之分，没有中间项，失败将代价很大。</li>
<li>不一定要留住用户。</li>
<li>更小的成本更高的流量，高流量后就可以做很多的事。</li>
</ul>
<p>（二）</p>
<p>Domain 信息类项目，使用的架构和 DMV 一致，逻辑更加简单。自己主要做了数据整理，收获是关于 SQL 命令。SQL 就可以直接处理很多的事情。使用了 RMI 做数据同步，用反射写了一段程序，是一次有突破的尝试。
一次看到自己年初的简历，笑了。自己是真敢写，能写个 Hello World 就敢标成了解。从会用到原理，是接下要走的路。</p>
<p>（三）</p>
<p>最近在看设计模式和算法的书，一直在做 LeetCode 的题。设计模式没用过，逮到能用机会绝不错过。算法健脑，刚刚开始接触觉的挺有意思，对数据结构也是种了解。</p>
<p>（四）</p>
<p>再次参加舍友的婚礼，又个结婚了。大一奶了一口，现在成真。祝福 Lu&amp;Feifei。</p>
<p>（五）</p>
<blockquote>
<p>来说说程序员那无处安放的创造力</p>
<p>有了锤子想找钉子是很正常的原始冲动，但我们必须认识到，创造力对于程序员这个职业来讲，是锦上添花的东西。如果你没有强大的工程能力，那么创造力也不过是无本之木。所以扎扎实实的把工程基础打好，这是最根本的。</p>
<p>在此基础上，我比较推荐程序员采用内外两条线来培养自己。在公司内的项目上采取相对保守的策略，尽力把稳定性做到最好，培养出自己卓越的工程能力；然后在公司外的开源项目和自己的独立项目上，采用一些新的技术、实践一些新的想法、充分发挥自己的创造力，梦想还是要有的，对吧。</p>
<p>这样做最明显的好处是，你可以了解到新技术和激进方案的优缺点，从而在进行方案选型时，有更多的依据；还有一个职业发展上的好处：如果不是主负责人，公司的项目往往不能代表你的能力；但独立项目却可以作为一个非常好的能力证明出现在你的简历里边。</p>
<p>你可以是一个身怀绝技的手艺人，在自己家里你尝试各种手法各种风格的个人作品；但当你参与颐和园这种级别的工程时，好好的把自己负责的石头雕成总设计师要求的样子就好 —— 毕竟这个时代一个人已经很难负责整个项目了。这就是我所理解的程序员的工匠精神。</p>
<p>摘自：<a href="http://mp.weixin.qq.com/s?__biz=MzI5OTI5Njg2Mg==&amp;mid=2247483667&amp;idx=1&amp;sn=d6e5953c7a7835148e3822b919b82416#rd">程序员到底是一个什么职业？</a></p>
</blockquote>
<p>（尾）</p>
<p>现在自己能听见进去一些原来听不去的话了。我不认为自己是被同化了，也许是心中少了些恶意。</p>
<p>状态不错，继续前进。</p>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>【此生为完成】随笔</title>
      <link>https://zyf.im/2016/08/22/being-alive-is-a-gift-reading-notes/</link>
      <pubDate>Mon, 22 Aug 2016 23:00:00 +0000</pubDate>
      <guid>https://zyf.im/2016/08/22/being-alive-is-a-gift-reading-notes/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;我们要用多大的代价，才能认清活着的意义？
于娟，这个风华正茂的女子，拥有留洋经历和博士学位的复旦大学青年教师，在与晚期癌症抗争一年又四个月后，终于撒手人寰。她带走的家人的思念和不舍，给我们留下坚强的力量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;《此生未完成》这本书是上周末聚会时 Zhen 提到的，上周京东买书就带上了。书里前半探讨作者在生死临界时对生活、工作、名利、家人、朋友的一些看法和思考。这对于刚刚毕业的我来说，确实有些是无法真切体会到的，毕竟经历少。但是在书的字里行间中，仍然可以看到一个（不知道用什么词）的女子。&lt;/p&gt;
&lt;!-- more --&gt;
&lt;h2 id=&#34;一&#34;&gt;（一）&lt;/h2&gt;
&lt;p&gt;疾病能让人有怎样的疼痛？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我还是没有哭，不是因为坚强，而是因为痛的想不起来哭，那个时候，只要能用尽全力顶着。如果稍微分神，我就会痛的晕厥。我不想家人看到我的痛苦。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;说来惭愧的一个对比，自己在喝多时有过这样的经历：自己抱着垃圾桶使劲顶着一股劲，要是有人来给自己说话，就会分神似的使劲的呕吐。可这蚀骨之痛要比这疼痛多少倍呢？&lt;/p&gt;
&lt;h2 id=&#34;二&#34;&gt;（二）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;重要的是当他得知我生病消息后的第一反应，眼神表情乃至电话语气网络留言里端倪尽出，你会觉得世间很多人情世故是那么的让你淡然一笑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在遇到大事的时候方显出真正的朋友和所谓的朋友，扬尘散土，洗沙留金。能遇到“大事”的机会不多，遇到的“大事”还能全身而退的机会就更少了。所以觉的自己应该及时去明辨所有的朋友，将真心给最真的，对于其他的就随他去吧。我不可能讨所有人喜欢，也用不着。&lt;/p&gt;
&lt;h2 id=&#34;三&#34;&gt;（三）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;三十岁之前的努力更多是因为自己有着太多的欲望和执著，从没有“只要活着就好”的简单。名利权情，没有一样是不辛苦的，却没有一样可以带走。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是一个作者很重要的一个观点。在现在的我看来，“只要活着就好”的活着，是一种精神上富有的活着，是充满智慧的活着。
就说现在的自己，什么都不曾拥有，或许不用拥有过的名、利，但是其他方面什么都没有，自己一切都是空的，看到好就会有强烈的“欲望”，如果再强烈一点就会有“执著”。在文字中我感受到了作者在精神方面的强大与富有，或许在这之后才能感受到在名、利之外更重要的东西。其实，自己真的很难想通，到底拥有什么自己就可以“苟”了，自己到底想要什么。了解自己真正想要什么真是一幸事。
作者提到的有时间，多多陪陪家人父母、有机会就尽孝，自己是很赞同的。&lt;/p&gt;
&lt;h2 id=&#34;四&#34;&gt;（四）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;外公反映土豆每次经过，都趴在玻璃上看玻璃窗里的圣诞树、圣诞彩球和姜饼屋。土豆眼神里有点想，但却乖乖看着，并不讨。外公回想说：“那个眼神我想起了卖火柴的小女孩，有时候孩子听话，你却反而会心酸。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这文字碰触心灵最柔软的地方。&lt;/p&gt;
&lt;h2 id=&#34;五&#34;&gt;（五）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;你的妈妈不是懦夫，所以你的人生里，遇到珍贵的人与关键的事，都要积极争取，可以失败，但是不能放弃。我想做一个让儿子骄傲的妈妈，只此一点，无论任何地步，我都不会选择自己走，哪怕，万劫不复的痛。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;六&#34;&gt;（六）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多人不会明白，我们两个的谈笑深处埋藏着多少不能言表的无声叹息。上一次见面，我和梅两个事多么风华正茂，像展翅云霄的鹰隼，挥着翅膀相约下次的冲天。这次的相逢，是灰头土脸被命运按在尘土里依然微笑的土鸡之间的问候。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;七&#34;&gt;（七）&lt;/h2&gt;
&lt;p&gt;“为啥是我得癌症”的非学术报告，是书的精华文章。从饮食习惯、睡眠习惯、突击作业、环境问题，分析了对身体不利的因素。突击作业其实就是工作习惯。&lt;/p&gt;
&lt;p&gt;在饮食上常吃不寻常的动物、暴饮暴食、嗜荤如命。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们要相信聪明的祖先，几千年的智慧沉淀，才最终锁定了我们现在的食材，并在远古对他们进行豢养。如果孔雀比鸡好吃，那么现在的就是孔雀，孔雀就是鸡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在睡眠习惯上：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;二十三时至次日三时，是肝脏活动能力最强的时段，也是肝脏最佳的排毒时期，如果肝脏功能得不到休息，会引起肝脏血流相对不足，已受损的肝细胞难以修复并加剧恶化。所以“长期熬夜等于慢性自杀”的说法并不夸张。因此医生建议人们从二十三时左右开始上床睡觉，次日一至三时进入深睡眠状态，好好地养足肝血。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;突击作业：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一辆平时就跌跌撞撞一直不维修的破车，一踩油门就彻夜地疯跑疯开半个月。一年搞个五六次，就是钢筋铁打的汽车，开个二十几年也到报废了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;环境问题，空气污染、水污染和食品安全危机还有那可怕的甲醛家具。&lt;/p&gt;
&lt;h2 id=&#34;八&#34;&gt;（八）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;不去想控制大局小局，不想去多管闲事淡事，我不在有对手，不再有敌人，我也不在关心谁比谁强，课题也好，任务也罢，暂且放着。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也许只有正在想明白的人，才懂的世间的一切，隔岸看花、风淡云轻。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>我们要用多大的代价，才能认清活着的意义？
于娟，这个风华正茂的女子，拥有留洋经历和博士学位的复旦大学青年教师，在与晚期癌症抗争一年又四个月后，终于撒手人寰。她带走的家人的思念和不舍，给我们留下坚强的力量。</p>
</blockquote>
<p>《此生未完成》这本书是上周末聚会时 Zhen 提到的，上周京东买书就带上了。书里前半探讨作者在生死临界时对生活、工作、名利、家人、朋友的一些看法和思考。这对于刚刚毕业的我来说，确实有些是无法真切体会到的，毕竟经历少。但是在书的字里行间中，仍然可以看到一个（不知道用什么词）的女子。</p>
<!-- more -->
<h2 id="一">（一）</h2>
<p>疾病能让人有怎样的疼痛？</p>
<blockquote>
<p>我还是没有哭，不是因为坚强，而是因为痛的想不起来哭，那个时候，只要能用尽全力顶着。如果稍微分神，我就会痛的晕厥。我不想家人看到我的痛苦。</p>
</blockquote>
<p>说来惭愧的一个对比，自己在喝多时有过这样的经历：自己抱着垃圾桶使劲顶着一股劲，要是有人来给自己说话，就会分神似的使劲的呕吐。可这蚀骨之痛要比这疼痛多少倍呢？</p>
<h2 id="二">（二）</h2>
<blockquote>
<p>重要的是当他得知我生病消息后的第一反应，眼神表情乃至电话语气网络留言里端倪尽出，你会觉得世间很多人情世故是那么的让你淡然一笑。</p>
</blockquote>
<p>在遇到大事的时候方显出真正的朋友和所谓的朋友，扬尘散土，洗沙留金。能遇到“大事”的机会不多，遇到的“大事”还能全身而退的机会就更少了。所以觉的自己应该及时去明辨所有的朋友，将真心给最真的，对于其他的就随他去吧。我不可能讨所有人喜欢，也用不着。</p>
<h2 id="三">（三）</h2>
<blockquote>
<p>三十岁之前的努力更多是因为自己有着太多的欲望和执著，从没有“只要活着就好”的简单。名利权情，没有一样是不辛苦的，却没有一样可以带走。</p>
</blockquote>
<p>这是一个作者很重要的一个观点。在现在的我看来，“只要活着就好”的活着，是一种精神上富有的活着，是充满智慧的活着。
就说现在的自己，什么都不曾拥有，或许不用拥有过的名、利，但是其他方面什么都没有，自己一切都是空的，看到好就会有强烈的“欲望”，如果再强烈一点就会有“执著”。在文字中我感受到了作者在精神方面的强大与富有，或许在这之后才能感受到在名、利之外更重要的东西。其实，自己真的很难想通，到底拥有什么自己就可以“苟”了，自己到底想要什么。了解自己真正想要什么真是一幸事。
作者提到的有时间，多多陪陪家人父母、有机会就尽孝，自己是很赞同的。</p>
<h2 id="四">（四）</h2>
<blockquote>
<p>外公反映土豆每次经过，都趴在玻璃上看玻璃窗里的圣诞树、圣诞彩球和姜饼屋。土豆眼神里有点想，但却乖乖看着，并不讨。外公回想说：“那个眼神我想起了卖火柴的小女孩，有时候孩子听话，你却反而会心酸。”</p>
</blockquote>
<p>这文字碰触心灵最柔软的地方。</p>
<h2 id="五">（五）</h2>
<blockquote>
<p>你的妈妈不是懦夫，所以你的人生里，遇到珍贵的人与关键的事，都要积极争取，可以失败，但是不能放弃。我想做一个让儿子骄傲的妈妈，只此一点，无论任何地步，我都不会选择自己走，哪怕，万劫不复的痛。</p>
</blockquote>
<h2 id="六">（六）</h2>
<blockquote>
<p>更多人不会明白，我们两个的谈笑深处埋藏着多少不能言表的无声叹息。上一次见面，我和梅两个事多么风华正茂，像展翅云霄的鹰隼，挥着翅膀相约下次的冲天。这次的相逢，是灰头土脸被命运按在尘土里依然微笑的土鸡之间的问候。</p>
</blockquote>
<h2 id="七">（七）</h2>
<p>“为啥是我得癌症”的非学术报告，是书的精华文章。从饮食习惯、睡眠习惯、突击作业、环境问题，分析了对身体不利的因素。突击作业其实就是工作习惯。</p>
<p>在饮食上常吃不寻常的动物、暴饮暴食、嗜荤如命。</p>
<blockquote>
<p>我们要相信聪明的祖先，几千年的智慧沉淀，才最终锁定了我们现在的食材，并在远古对他们进行豢养。如果孔雀比鸡好吃，那么现在的就是孔雀，孔雀就是鸡。</p>
</blockquote>
<p>在睡眠习惯上：</p>
<blockquote>
<p>二十三时至次日三时，是肝脏活动能力最强的时段，也是肝脏最佳的排毒时期，如果肝脏功能得不到休息，会引起肝脏血流相对不足，已受损的肝细胞难以修复并加剧恶化。所以“长期熬夜等于慢性自杀”的说法并不夸张。因此医生建议人们从二十三时左右开始上床睡觉，次日一至三时进入深睡眠状态，好好地养足肝血。</p>
</blockquote>
<p>突击作业：</p>
<blockquote>
<p>一辆平时就跌跌撞撞一直不维修的破车，一踩油门就彻夜地疯跑疯开半个月。一年搞个五六次，就是钢筋铁打的汽车，开个二十几年也到报废了。</p>
</blockquote>
<p>环境问题，空气污染、水污染和食品安全危机还有那可怕的甲醛家具。</p>
<h2 id="八">（八）</h2>
<blockquote>
<p>不去想控制大局小局，不想去多管闲事淡事，我不在有对手，不再有敌人，我也不在关心谁比谁强，课题也好，任务也罢，暂且放着。</p>
</blockquote>
<p>也许只有正在想明白的人，才懂的世间的一切，隔岸看花、风淡云轻。</p>
]]></content:encoded>
    </item>
    <item>
      <title>【蛤蟆的油】随笔</title>
      <link>https://zyf.im/2016/08/17/something-like-an-autobiography-reading-notes/</link>
      <pubDate>Wed, 17 Aug 2016 23:00:00 +0000</pubDate>
      <guid>https://zyf.im/2016/08/17/something-like-an-autobiography-reading-notes/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;日本民间流传着这样一个故事：在深山里，有一种特别的蛤蟆，它和同类相比不仅外表更丑，而且还多长了几条腿。人们转到它后，将其放在镜前或者玻璃箱内，蛤蟆一看到自己丑陋不堪的真面目，不禁吓出一身油。这种油，也是民间用来治疗烧伤烫伤的珍贵药材。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第一次写人文类书籍的笔记。&lt;/p&gt;
&lt;p&gt;故事是从黑泽明一岁多光着身子洗澡开始回忆的，然后按时间，一个个小故事讲述自己。&lt;/p&gt;
&lt;p&gt;（一）&lt;/p&gt;
&lt;p&gt;二年级是他得到了“糖酥”这个绰号，因为有人揪他的头发，往他西装上抹鼻涕，让他哭了好几次。&lt;/p&gt;
&lt;p&gt;想到了自己大概三四年级的一件事：一次在公交车站等车被人嘲笑嘴巴大，是陌生的同龄人。自己当时被蒙掉了，也不知道说什么或者做点什么，傻傻的，后来车来了，上车回家了。我记得我是在车上委屈的哭了。十几年来这件事我一直都记得，总是把这事归为黑历史、很怂的事，也从来没和别人讲过。我当时为什么没有还击？是被大人教育的打架不是好孩子？还是自己的胆小？还是正中下怀？细思这些都有，是太小没有自己的判断力，也缺少一份对自己信心与肯定。&lt;/p&gt;
&lt;p&gt;要能再来我一定会把那小子打到哭。&lt;/p&gt;
&lt;!-- more --&gt;
&lt;p&gt;（二）&lt;/p&gt;
&lt;p&gt;小黑遇到了“崇尚自由、以鲜活的感性及创造精神从事教育的老师”，立川老师对智力发育缓慢、性格乖僻的他多方庇护，使他第一次有了自信。立川老师还为了使植草（小黑好友）尽快地开出灿烂的花，把植草移栽到了副班长这个盆里，而且放在向阳之处。&lt;/p&gt;
&lt;p&gt;遇到一个能指导你成长的同学、朋友、老师、哥哥姐姐真的是一件很幸运的事情。我觉的他们在有智慧的人群里，不是瞎折腾或者很世俗。&lt;/p&gt;
&lt;p&gt;（三）&lt;/p&gt;
&lt;p&gt;给客人吃的鱼，鱼头朝左，鱼腹朝着客人。给剖腹者上的鱼，大概是鱼头朝右，鱼背朝着本人。或许是因为，如果让剖腹者看到剖开的鱼腹，未免太残酷了。&lt;/p&gt;
&lt;p&gt;（四）&lt;/p&gt;
&lt;p&gt;小黑在学剑道时，道场的主人被汽车撞了，那是汽车本来是稀罕之物，却又让这稀罕之物撞伤，简直像挨马踢一样可笑。而对道场主人的尊敬烟消云散。自己在高野佐三郎的道场在学交叉坎时，被弹回来撞到墙上，眼前一阵发黑，两眼直冒金星，对自己剑道的自豪化为乌有。&lt;/p&gt;
&lt;p&gt;人世并不像想象的那么简单。人外有人，天外有天。自己不免是井底之蛙，总是管中窥豹。很多像这样的道理，我都听过，我也认同，可在没有遇到一件具体的事情的时候，总是看不到认不清自己的浅薄。多多经历，多多思考。&lt;/p&gt;
&lt;p&gt;（五）&lt;/p&gt;
&lt;p&gt;在体育课上，体育老师教跳高采取比赛的方法，撞掉竿就被淘汰下去。黑泽明刚一起跑，同学们就哄堂大笑。他们准是觉得我会头一个把横杆撞下来。出乎意料的是我轻松越过了横杆。横杆逐渐上移，然而，挑战的人中间总有我。看热闹的人们寂然无声了。不知道什么原因，居然出现了奇迹：只剩我一个人在挑战了。一个个无不呆呆地看着我。而且，只剩下我一个人之后，仍然几次跳过了横杆。也许是天使哀怜我体操课总得零分，给我的背插上了翅膀。
最后一句，写的真美。&lt;/p&gt;
&lt;p&gt;（六）&lt;/p&gt;
&lt;p&gt;人是可笑的，过分受惊时，头脑的一部分会脱离现实，想入非非，看起来显得十分沉着。当大地震时，抱着电线杆忍受着强烈的摇晃时，仍然想到了这些，而且非常佩服日式建筑的优越性。然而这绝不意味着我遇事沉着冷静。&lt;/p&gt;
&lt;p&gt;（七）&lt;/p&gt;
&lt;p&gt;没有经历过的人无法想象对人类来说何谓正真的黑暗，这黑暗又是多么的可怕。这恐怖夺走了人的正气。无论朝哪里望，什么都看不见，这是最使人感到孤立无援的地方，它使人内心深处产生了惊慌和不安，也使人处于名副其实的疑心生暗鬼状态。&lt;/p&gt;
&lt;p&gt;好像有点像我国的“吃盐防辐射”，都是巧妙地利用黑暗对人的威胁制造的阴谋？还是易受蛊惑的人心是社会发展的过程中要的经历？&lt;/p&gt;
&lt;p&gt;（八）&lt;/p&gt;
&lt;p&gt;结束这趟可怕的远足，当天晚上，我以为一定难以入睡，还会大做噩梦，但头刚一沾枕就到了第二天早晨。哥哥说：“面对可怕的事物闭眼不敢看，就会觉得它可怕；什么都不在乎，哪里还有什么可怕的呢？”。现在看来，那趟远足，对哥哥来说可能也是可怕的。正因为可怕，所以必须征服它。这次远足也是一次征服恐怖的远征。&lt;/p&gt;
&lt;p&gt;可是面对了自己可怕的东西后，还是怕的原因是什么呢？是还没有正面面对？&lt;/p&gt;
&lt;p&gt;（九）&lt;/p&gt;
&lt;p&gt;高山仰止&lt;/p&gt;
&lt;p&gt;对有气质、有修养或有崇高品德之人的崇敬、仰慕之情。&lt;/p&gt;
&lt;p&gt;（十）&lt;/p&gt;
&lt;p&gt;人能把卫星送进宇宙，在精神层面却不会向上看，而是像野狗一样，只注意脚下，徘徊不已。&lt;/p&gt;
&lt;p&gt;（十一）&lt;/p&gt;
&lt;p&gt;后娘用艾苦我身/我为后娘买大艾/为讨她欢心。继母为什么虐待前房孩子？如果说出于憎恨丈夫的前妻而虐待其子，这是没有道理的。认为这完全出于愚昧。愚昧是人的疯狂病症之一，以虐待没有反抗能力的孩子或者小动物为乐的人，纯粹是疯子。然而这类疯子并不认为这是犯罪，却认为是理所当然，所以难以对付。我能解开绑她的带子，然而无法把她从捆绑她的境遇中救出。对这个孩子来说，人们的同情是毫无意义的。那种温情反倒给她招来更多的麻烦。&lt;/p&gt;
&lt;p&gt;想到了打劫的故事，说：被劫持的人只要交的钱比上一人交的钱多一倍，就可以走。人争先恐后的交赎金，不再有人反抗。&lt;/p&gt;
&lt;p&gt;（十二）&lt;/p&gt;
&lt;p&gt;本来是红的，却不老实说它是红的，等到能坦率说出来的时候，已经到了晚年。很多人年轻时表现欲过强，这样反倒迷失了自己。我也毫不例外，拼命的考技巧作画，那画的俗气使我对自己心生憎恶，逐渐丧失了对自己才能有的信心，把绘画看作痛苦了。为了买油彩和画布还得干些不愿干的杂活儿赚钱才行。&lt;/p&gt;
&lt;p&gt;少搞些套路，玩花活，踏踏实实才是真。别瞎折腾，没有用。&lt;/p&gt;
&lt;p&gt;（十三）&lt;/p&gt;
&lt;p&gt;产生这种急于就业的焦虑和降格以求的心情，主要是以为哥哥突然去世，我要继他之后负起长子的责任。&lt;/p&gt;
&lt;p&gt;在年初有类似的经历，最后一个学期，都找工作去了，也要毕业要长大了，急着确定个工作。其实现在也着急着，想快有所作为。前几日，看到好友暗叶的朋友圈发：年轻人想法没多，都是还得慢慢来。我初看是不赞同。黑泽明的父亲也告诫他说：“不要着急，也没有着急的必要”，“要等下去，前面的道路自然会打开的”。这些话我确实还不太理解为什么，得慢慢的体会。&lt;/p&gt;
&lt;p&gt;（十四）&lt;/p&gt;
&lt;p&gt;凡事想用人的，必须先培养人。培养出人，激发出人的才能，这才能用。&lt;/p&gt;
&lt;p&gt;纷扰的现在，还有多少人或公司愿意培养人？应该说人培养应该是必须的。&lt;/p&gt;
&lt;p&gt;（十五）&lt;/p&gt;
&lt;p&gt;山本先生对于追随他的副导演，绝不干改变他们个性的事，而是一心一意的着力于发掘他们的个性；而且丝毫不让我们有从师学习的拘束心情，而是让我们充分的发展自己。
山本先生为了培养副导演，不惜牺牲自己的作品。
“那是卖香荷包的招牌。随随便便说话可不行！不知道的事情就该说不知道！”&lt;/p&gt;
&lt;p&gt;（十六）&lt;/p&gt;
&lt;p&gt;提出批评不难。但是，提出批评的人能够按照自己的批评意见亲自把剧本改好，却不是普通人能做到的事。&lt;/p&gt;
&lt;p&gt;太有同感了，常常有人在旁边指手画脚，却说不出什么切实可行的办法建议。&lt;/p&gt;
&lt;p&gt;（十七）&lt;/p&gt;
&lt;p&gt;人往往习惯于认为价值与辛苦成正比。这在电影界剪辑上是最要不得的。人们说电影是时间的艺术，所以，没有用的时间就应删去。&lt;/p&gt;
&lt;p&gt;这观点在互联网行业也行的通，做好的功能不合适了，说废也就废掉了。&lt;/p&gt;
&lt;p&gt;（十八）&lt;/p&gt;
&lt;p&gt;我常常把群众演员的名字忘掉，所以只好按他们的衣服颜色招呼。结果被山本先生训了几句：“黑泽明，那可不行。人都有个名嘛！”。名不见经传的演员听到山本先生如此亲切的招呼他，无不感到。难道能说山本先生有些滑头吗？我看应该说他善于用人。&lt;/p&gt;
&lt;p&gt;自己还不是什么重要人物，但是记住别人的名字应该是件重要的事情。&lt;/p&gt;
&lt;p&gt;（十九）&lt;/p&gt;
&lt;p&gt;关于演员的关键问题主要有以下几点：第一，人很难了解自己，不能客观的观察自己的说话方式和行为举止。第二，凡事有意识的动作，首先注意的不是动作本身，而是意识。第三，教给演员怎么做，同时必须告诉他为什么怎么做，而且让他充分理解、心悦诚服。&lt;/p&gt;
&lt;p&gt;做事情的时候应该反向考虑第三点，为什么要怎么做事情？目的是什么？&lt;/p&gt;
&lt;p&gt;（二十）&lt;/p&gt;
&lt;p&gt;电影评论家中冲有这样的人，看到电影中一些自己认为是缺点的地方就如获至宝，大发谬论，但是电影导演也乐于此道可就错了。&lt;/p&gt;
&lt;p&gt;现在这样的人越来越多，哗众取宠。&lt;/p&gt;
&lt;p&gt;（二十一）&lt;/p&gt;
&lt;p&gt;他们以为一部卖座影片的续集也必然会成功，于是不想再开拓新的领域，总想旧梦重温。重拍的影片绝不如前作，这虽然是已经证明的事实，但他们还要重蹈覆辙，这才是地地道道的愚蠢之举。&lt;/p&gt;
&lt;p&gt;学习新东西，找新路子。&lt;/p&gt;
&lt;p&gt;（二十二）&lt;/p&gt;
&lt;p&gt;我为了抵抗人的苦恼，戴上一幅强者的面具；而植草却为了沉溺与人的苦恼，戴上了一幅弱者的面具。事实不过如此。但这只是表面的不同，就本质来说。我们都是弱者。我不是特别的人。我既不是特别强的人，也不是得天独厚的有特殊才能的人。我不过是个不愿示弱于人，不愿输给别人，因而不懈努力的人。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>日本民间流传着这样一个故事：在深山里，有一种特别的蛤蟆，它和同类相比不仅外表更丑，而且还多长了几条腿。人们转到它后，将其放在镜前或者玻璃箱内，蛤蟆一看到自己丑陋不堪的真面目，不禁吓出一身油。这种油，也是民间用来治疗烧伤烫伤的珍贵药材。</p>
</blockquote>
<p>第一次写人文类书籍的笔记。</p>
<p>故事是从黑泽明一岁多光着身子洗澡开始回忆的，然后按时间，一个个小故事讲述自己。</p>
<p>（一）</p>
<p>二年级是他得到了“糖酥”这个绰号，因为有人揪他的头发，往他西装上抹鼻涕，让他哭了好几次。</p>
<p>想到了自己大概三四年级的一件事：一次在公交车站等车被人嘲笑嘴巴大，是陌生的同龄人。自己当时被蒙掉了，也不知道说什么或者做点什么，傻傻的，后来车来了，上车回家了。我记得我是在车上委屈的哭了。十几年来这件事我一直都记得，总是把这事归为黑历史、很怂的事，也从来没和别人讲过。我当时为什么没有还击？是被大人教育的打架不是好孩子？还是自己的胆小？还是正中下怀？细思这些都有，是太小没有自己的判断力，也缺少一份对自己信心与肯定。</p>
<p>要能再来我一定会把那小子打到哭。</p>
<!-- more -->
<p>（二）</p>
<p>小黑遇到了“崇尚自由、以鲜活的感性及创造精神从事教育的老师”，立川老师对智力发育缓慢、性格乖僻的他多方庇护，使他第一次有了自信。立川老师还为了使植草（小黑好友）尽快地开出灿烂的花，把植草移栽到了副班长这个盆里，而且放在向阳之处。</p>
<p>遇到一个能指导你成长的同学、朋友、老师、哥哥姐姐真的是一件很幸运的事情。我觉的他们在有智慧的人群里，不是瞎折腾或者很世俗。</p>
<p>（三）</p>
<p>给客人吃的鱼，鱼头朝左，鱼腹朝着客人。给剖腹者上的鱼，大概是鱼头朝右，鱼背朝着本人。或许是因为，如果让剖腹者看到剖开的鱼腹，未免太残酷了。</p>
<p>（四）</p>
<p>小黑在学剑道时，道场的主人被汽车撞了，那是汽车本来是稀罕之物，却又让这稀罕之物撞伤，简直像挨马踢一样可笑。而对道场主人的尊敬烟消云散。自己在高野佐三郎的道场在学交叉坎时，被弹回来撞到墙上，眼前一阵发黑，两眼直冒金星，对自己剑道的自豪化为乌有。</p>
<p>人世并不像想象的那么简单。人外有人，天外有天。自己不免是井底之蛙，总是管中窥豹。很多像这样的道理，我都听过，我也认同，可在没有遇到一件具体的事情的时候，总是看不到认不清自己的浅薄。多多经历，多多思考。</p>
<p>（五）</p>
<p>在体育课上，体育老师教跳高采取比赛的方法，撞掉竿就被淘汰下去。黑泽明刚一起跑，同学们就哄堂大笑。他们准是觉得我会头一个把横杆撞下来。出乎意料的是我轻松越过了横杆。横杆逐渐上移，然而，挑战的人中间总有我。看热闹的人们寂然无声了。不知道什么原因，居然出现了奇迹：只剩我一个人在挑战了。一个个无不呆呆地看着我。而且，只剩下我一个人之后，仍然几次跳过了横杆。也许是天使哀怜我体操课总得零分，给我的背插上了翅膀。
最后一句，写的真美。</p>
<p>（六）</p>
<p>人是可笑的，过分受惊时，头脑的一部分会脱离现实，想入非非，看起来显得十分沉着。当大地震时，抱着电线杆忍受着强烈的摇晃时，仍然想到了这些，而且非常佩服日式建筑的优越性。然而这绝不意味着我遇事沉着冷静。</p>
<p>（七）</p>
<p>没有经历过的人无法想象对人类来说何谓正真的黑暗，这黑暗又是多么的可怕。这恐怖夺走了人的正气。无论朝哪里望，什么都看不见，这是最使人感到孤立无援的地方，它使人内心深处产生了惊慌和不安，也使人处于名副其实的疑心生暗鬼状态。</p>
<p>好像有点像我国的“吃盐防辐射”，都是巧妙地利用黑暗对人的威胁制造的阴谋？还是易受蛊惑的人心是社会发展的过程中要的经历？</p>
<p>（八）</p>
<p>结束这趟可怕的远足，当天晚上，我以为一定难以入睡，还会大做噩梦，但头刚一沾枕就到了第二天早晨。哥哥说：“面对可怕的事物闭眼不敢看，就会觉得它可怕；什么都不在乎，哪里还有什么可怕的呢？”。现在看来，那趟远足，对哥哥来说可能也是可怕的。正因为可怕，所以必须征服它。这次远足也是一次征服恐怖的远征。</p>
<p>可是面对了自己可怕的东西后，还是怕的原因是什么呢？是还没有正面面对？</p>
<p>（九）</p>
<p>高山仰止</p>
<p>对有气质、有修养或有崇高品德之人的崇敬、仰慕之情。</p>
<p>（十）</p>
<p>人能把卫星送进宇宙，在精神层面却不会向上看，而是像野狗一样，只注意脚下，徘徊不已。</p>
<p>（十一）</p>
<p>后娘用艾苦我身/我为后娘买大艾/为讨她欢心。继母为什么虐待前房孩子？如果说出于憎恨丈夫的前妻而虐待其子，这是没有道理的。认为这完全出于愚昧。愚昧是人的疯狂病症之一，以虐待没有反抗能力的孩子或者小动物为乐的人，纯粹是疯子。然而这类疯子并不认为这是犯罪，却认为是理所当然，所以难以对付。我能解开绑她的带子，然而无法把她从捆绑她的境遇中救出。对这个孩子来说，人们的同情是毫无意义的。那种温情反倒给她招来更多的麻烦。</p>
<p>想到了打劫的故事，说：被劫持的人只要交的钱比上一人交的钱多一倍，就可以走。人争先恐后的交赎金，不再有人反抗。</p>
<p>（十二）</p>
<p>本来是红的，却不老实说它是红的，等到能坦率说出来的时候，已经到了晚年。很多人年轻时表现欲过强，这样反倒迷失了自己。我也毫不例外，拼命的考技巧作画，那画的俗气使我对自己心生憎恶，逐渐丧失了对自己才能有的信心，把绘画看作痛苦了。为了买油彩和画布还得干些不愿干的杂活儿赚钱才行。</p>
<p>少搞些套路，玩花活，踏踏实实才是真。别瞎折腾，没有用。</p>
<p>（十三）</p>
<p>产生这种急于就业的焦虑和降格以求的心情，主要是以为哥哥突然去世，我要继他之后负起长子的责任。</p>
<p>在年初有类似的经历，最后一个学期，都找工作去了，也要毕业要长大了，急着确定个工作。其实现在也着急着，想快有所作为。前几日，看到好友暗叶的朋友圈发：年轻人想法没多，都是还得慢慢来。我初看是不赞同。黑泽明的父亲也告诫他说：“不要着急，也没有着急的必要”，“要等下去，前面的道路自然会打开的”。这些话我确实还不太理解为什么，得慢慢的体会。</p>
<p>（十四）</p>
<p>凡事想用人的，必须先培养人。培养出人，激发出人的才能，这才能用。</p>
<p>纷扰的现在，还有多少人或公司愿意培养人？应该说人培养应该是必须的。</p>
<p>（十五）</p>
<p>山本先生对于追随他的副导演，绝不干改变他们个性的事，而是一心一意的着力于发掘他们的个性；而且丝毫不让我们有从师学习的拘束心情，而是让我们充分的发展自己。
山本先生为了培养副导演，不惜牺牲自己的作品。
“那是卖香荷包的招牌。随随便便说话可不行！不知道的事情就该说不知道！”</p>
<p>（十六）</p>
<p>提出批评不难。但是，提出批评的人能够按照自己的批评意见亲自把剧本改好，却不是普通人能做到的事。</p>
<p>太有同感了，常常有人在旁边指手画脚，却说不出什么切实可行的办法建议。</p>
<p>（十七）</p>
<p>人往往习惯于认为价值与辛苦成正比。这在电影界剪辑上是最要不得的。人们说电影是时间的艺术，所以，没有用的时间就应删去。</p>
<p>这观点在互联网行业也行的通，做好的功能不合适了，说废也就废掉了。</p>
<p>（十八）</p>
<p>我常常把群众演员的名字忘掉，所以只好按他们的衣服颜色招呼。结果被山本先生训了几句：“黑泽明，那可不行。人都有个名嘛！”。名不见经传的演员听到山本先生如此亲切的招呼他，无不感到。难道能说山本先生有些滑头吗？我看应该说他善于用人。</p>
<p>自己还不是什么重要人物，但是记住别人的名字应该是件重要的事情。</p>
<p>（十九）</p>
<p>关于演员的关键问题主要有以下几点：第一，人很难了解自己，不能客观的观察自己的说话方式和行为举止。第二，凡事有意识的动作，首先注意的不是动作本身，而是意识。第三，教给演员怎么做，同时必须告诉他为什么怎么做，而且让他充分理解、心悦诚服。</p>
<p>做事情的时候应该反向考虑第三点，为什么要怎么做事情？目的是什么？</p>
<p>（二十）</p>
<p>电影评论家中冲有这样的人，看到电影中一些自己认为是缺点的地方就如获至宝，大发谬论，但是电影导演也乐于此道可就错了。</p>
<p>现在这样的人越来越多，哗众取宠。</p>
<p>（二十一）</p>
<p>他们以为一部卖座影片的续集也必然会成功，于是不想再开拓新的领域，总想旧梦重温。重拍的影片绝不如前作，这虽然是已经证明的事实，但他们还要重蹈覆辙，这才是地地道道的愚蠢之举。</p>
<p>学习新东西，找新路子。</p>
<p>（二十二）</p>
<p>我为了抵抗人的苦恼，戴上一幅强者的面具；而植草却为了沉溺与人的苦恼，戴上了一幅弱者的面具。事实不过如此。但这只是表面的不同，就本质来说。我们都是弱者。我不是特别的人。我既不是特别强的人，也不是得天独厚的有特殊才能的人。我不过是个不愿示弱于人，不愿输给别人，因而不懈努力的人。</p>
<p>在原来看到这样的句子是很不屑的，觉的成功的人，说什么都行。现在信了，真的人都本是普通人，只是有的人一直在进步成长就现在不再普通了。</p>
<p>（二十三）</p>
<p>有一天，我在电车里看到这种杂志的广告，简直目瞪口呆。那上面用大字标题写着：是谁夺走了 XX 的贞操？乍看起来，这似乎在为 XX 女士鸣不平，实际则是把 XX 当作玩物恣意戏耍。我认为，这不是言论自由，而是言论暴力。</p>
<p>没想到在那么早，就提到了“言论暴力”的话题，社交平台的评论区常常有的血雨腥风。</p>
<p>（二十四）</p>
<p>本民族人为什么对于本民族的存在毫无自信呢？为什么对异域的东西那么尊重，对于自己的东西就那么轻视呢？</p>
<p>黑泽明提到是：可悲的国民性。自己没太理解。</p>
<blockquote>
<p>人是很难如实的谈论自己的。人总是本能的美化自己。人不会老老实实的说自己是怎样一个人，常常是假托别人才能老老实实的谈论自己。
因为，在没有什么能比作品更好的说明作者了。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>写一些人文的日志</title>
      <link>https://zyf.im/2016/07/11/write-some-humanities-diary/</link>
      <pubDate>Mon, 11 Jul 2016 09:00:00 +0000</pubDate>
      <guid>https://zyf.im/2016/07/11/write-some-humanities-diary/</guid>
      <description>&lt;p&gt;还是越来越觉的思想重要。每周给自己博客的计划是写三篇，稍稍改动下：其中一篇要是关于思想上的。&lt;/p&gt;
&lt;p&gt;周末的时候和北京的朋友们：龙、梦、思，小聚了下。问起毕业一个月感觉有啥变化，都说没啥。是，刚刚才一个月。可我心里却有些小慌张，总想搞个大新闻，让自己不那么平凡。是眼高手低吗？&lt;/p&gt;
&lt;p&gt;现在每天都会跑跑步，已经坚持了 6 天了也习惯了，挺赞的。也总是去了解新鲜的东西技术，并且尝试运用它们，才有点原来学长们不喜欢用用过的东西做事情的劲。关于英语的学习，现在找到一个法：阅读英文的技术教程或者技术文档。这些东西肯定和计算机有关，也写的比较通俗，感觉挺有收获的。&lt;/p&gt;
&lt;p&gt;最近状态还不错，就是晚上想看会书，可眼睛挺累的。职业生涯的第一个月，经济有些困难。想参与个开源项目。现在手头项目就自己一个人在编，有点孤独，也不知道自己设计的和写的哪里好哪里不好。还有就是在 GitHub 或者 StackOverflow 有人给自己点赞，感觉挺激动的。&lt;/p&gt;
&lt;p&gt;因为公司都是 Google 的服务，出门很方便，GitHub 也用的越来越多。时不时总能搜到、看到很多工程师写的博客，有教程、有分享，还有我最喜欢看的：他们的思想。我觉的他们现在都是比我棒的，我想知道他们是怎么想的、怎么解决问题、怎么看待问题、看待工作生活。&lt;/p&gt;
&lt;p&gt;最后毒奶一口自己：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;与那些名校的同学的差距不是毕业后的薪水，也不是学业水平的差距，而是一种思维方式与做事标准。
对自己的标准会不由自主的降低以适应这个环境，减少自身与环境的冲突，在一个低标准下，自觉“满意”的度过每一天。
 
为什么名企喜欢要名校的学生？其实名企要的不是多么高的 GPA 成绩，而是一种内在的精神状态。他们的辛苦不叫辛苦，也不为百万年薪。辛苦是他们获得自我实现的途径，自我是实现使他无穷快乐。这世界就是一拨人在昼夜不停的高速运转，另一拨人起床发现世界变了。
 
我们大部分人的工作和生活状态是怎样的呢？
上班稍微努力点就开始讲究公平，自己不得志就开始抱怨公司和领导，下班后看几页书就觉得自己特别上进，辛苦上几天就觉得自己要赶紧去享受一下生活了，加几天班就担心自己会过劳死。
遇到些鸡毛蒜皮的小事儿就郁郁寡欢，仿佛遇到了天大的人生难题磨磨唧唧解决不清。看见牛逼的人也会心生羡慕，但总也突破不了努力却总不得要领魔咒。
 
越能干，越努力；越有钱，越上进
而造成这一切的差异，最主要的来自人的精神内核。
 
你要用牛人的标准要求自己，不断的走到牛人当中去，拉近和牛人之间的距离。当你觉得自己能够成为他们中的一员的时候，你才能成为了真正的牛人。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;成功？我才刚上路呢。&lt;/p&gt;
&lt;p&gt;&amp;ndash; EOF &amp;ndash;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>还是越来越觉的思想重要。每周给自己博客的计划是写三篇，稍稍改动下：其中一篇要是关于思想上的。</p>
<p>周末的时候和北京的朋友们：龙、梦、思，小聚了下。问起毕业一个月感觉有啥变化，都说没啥。是，刚刚才一个月。可我心里却有些小慌张，总想搞个大新闻，让自己不那么平凡。是眼高手低吗？</p>
<p>现在每天都会跑跑步，已经坚持了 6 天了也习惯了，挺赞的。也总是去了解新鲜的东西技术，并且尝试运用它们，才有点原来学长们不喜欢用用过的东西做事情的劲。关于英语的学习，现在找到一个法：阅读英文的技术教程或者技术文档。这些东西肯定和计算机有关，也写的比较通俗，感觉挺有收获的。</p>
<p>最近状态还不错，就是晚上想看会书，可眼睛挺累的。职业生涯的第一个月，经济有些困难。想参与个开源项目。现在手头项目就自己一个人在编，有点孤独，也不知道自己设计的和写的哪里好哪里不好。还有就是在 GitHub 或者 StackOverflow 有人给自己点赞，感觉挺激动的。</p>
<p>因为公司都是 Google 的服务，出门很方便，GitHub 也用的越来越多。时不时总能搜到、看到很多工程师写的博客，有教程、有分享，还有我最喜欢看的：他们的思想。我觉的他们现在都是比我棒的，我想知道他们是怎么想的、怎么解决问题、怎么看待问题、看待工作生活。</p>
<p>最后毒奶一口自己：</p>
<blockquote>
<p>与那些名校的同学的差距不是毕业后的薪水，也不是学业水平的差距，而是一种思维方式与做事标准。
对自己的标准会不由自主的降低以适应这个环境，减少自身与环境的冲突，在一个低标准下，自觉“满意”的度过每一天。
 
为什么名企喜欢要名校的学生？其实名企要的不是多么高的 GPA 成绩，而是一种内在的精神状态。他们的辛苦不叫辛苦，也不为百万年薪。辛苦是他们获得自我实现的途径，自我是实现使他无穷快乐。这世界就是一拨人在昼夜不停的高速运转，另一拨人起床发现世界变了。
 
我们大部分人的工作和生活状态是怎样的呢？
上班稍微努力点就开始讲究公平，自己不得志就开始抱怨公司和领导，下班后看几页书就觉得自己特别上进，辛苦上几天就觉得自己要赶紧去享受一下生活了，加几天班就担心自己会过劳死。
遇到些鸡毛蒜皮的小事儿就郁郁寡欢，仿佛遇到了天大的人生难题磨磨唧唧解决不清。看见牛逼的人也会心生羡慕，但总也突破不了努力却总不得要领魔咒。
 
越能干，越努力；越有钱，越上进
而造成这一切的差异，最主要的来自人的精神内核。
 
你要用牛人的标准要求自己，不断的走到牛人当中去，拉近和牛人之间的距离。当你觉得自己能够成为他们中的一员的时候，你才能成为了真正的牛人。</p>
</blockquote>
<p>成功？我才刚上路呢。</p>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
    <item>
      <title>使用 GitHub 和 Hexo 搭建个人独立博客</title>
      <link>https://zyf.im/2016/06/24/hexo-github-blog/</link>
      <pubDate>Fri, 24 Jun 2016 18:30:00 +0000</pubDate>
      <guid>https://zyf.im/2016/06/24/hexo-github-blog/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026 重读&lt;/strong&gt;：这是十年前的旧文，方案上保留原貌，Hexo 今天仍在维护，这条路线依然走得通；但如果是现在从零开始，可能会考虑 &lt;strong&gt;Hugo / Astro / Eleventy&lt;/strong&gt;——我自己也把博客从 Hexo 迁到了 Hugo。评论系统 Disqus 因广告与隐私问题不再推荐，今天的同类选项是 &lt;strong&gt;Giscus&lt;/strong&gt;（基于 GitHub Discussions）。部署环节如今基本由 &lt;strong&gt;GitHub Actions&lt;/strong&gt; 接管，不再需要手动把 &lt;code&gt;public/&lt;/code&gt; 推上去。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;WordPress&lt;/code&gt; 这类博客系统功能强大，但对只想随手写点东西的人来说有点重。而且 &lt;code&gt;WordPress&lt;/code&gt; 需要部署，服务器本身就是负担：国内服务器要备案、费用不低，国外服务器访问速度受影响。&lt;/p&gt;
&lt;p&gt;近来接触到一种新的博客系统 Hexo，它的不同之处是：&lt;strong&gt;把博客的编写和页面渲染都放到线下完成&lt;/strong&gt;。在本地编写博文的 Markdown 文件，使用 Hexo 生成博客网站的全部静态 HTML，然后把生成的文件上传到服务器就行了。&lt;/p&gt;
&lt;p&gt;那么 WordPress 里的评论等动态功能怎么办呢？放心，第三方服务商早已为我们考虑过。例如 &lt;a href=&#34;https://disqus.com/&#34;&gt;Disqus&lt;/a&gt; 就是一家第三方社会化评论系统，主要为站长提供评论托管服务。&lt;/p&gt;
&lt;h2 id=&#34;了解-hexo&#34;&gt;了解 Hexo&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A fast, simple &amp;amp; powerful blog framework&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://hexo.io/&#34;&gt;Hexo&lt;/a&gt; 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown（或其他渲染引擎）解析文章，在几秒内，即可利用靓丽的主题生成静态网页，&lt;a href=&#34;https://hexo.io/zh-cn/docs/setup.html&#34;&gt;Hexo setup 官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;安装-hexo&#34;&gt;安装 Hexo&lt;/h2&gt;
&lt;p&gt;所有必备的应用程序安装完成后，即可使用 npm 安装 Hexo：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;npm install -g hexo-cli
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;建站&#34;&gt;建站&lt;/h2&gt;
&lt;p&gt;安装 Hexo 完成后，请执行下列命令，Hexo 将会在指定文件夹中新建所需要的文件：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;hexo init &amp;lt;folder&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; &amp;lt;folder&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;npm install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;新建完成后，指定文件夹的目录如下：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>2026 重读</strong>：这是十年前的旧文，方案上保留原貌，Hexo 今天仍在维护，这条路线依然走得通；但如果是现在从零开始，可能会考虑 <strong>Hugo / Astro / Eleventy</strong>——我自己也把博客从 Hexo 迁到了 Hugo。评论系统 Disqus 因广告与隐私问题不再推荐，今天的同类选项是 <strong>Giscus</strong>（基于 GitHub Discussions）。部署环节如今基本由 <strong>GitHub Actions</strong> 接管，不再需要手动把 <code>public/</code> 推上去。</p>
</blockquote>
<hr>
<p><code>WordPress</code> 这类博客系统功能强大，但对只想随手写点东西的人来说有点重。而且 <code>WordPress</code> 需要部署，服务器本身就是负担：国内服务器要备案、费用不低，国外服务器访问速度受影响。</p>
<p>近来接触到一种新的博客系统 Hexo，它的不同之处是：<strong>把博客的编写和页面渲染都放到线下完成</strong>。在本地编写博文的 Markdown 文件，使用 Hexo 生成博客网站的全部静态 HTML，然后把生成的文件上传到服务器就行了。</p>
<p>那么 WordPress 里的评论等动态功能怎么办呢？放心，第三方服务商早已为我们考虑过。例如 <a href="https://disqus.com/">Disqus</a> 就是一家第三方社会化评论系统，主要为站长提供评论托管服务。</p>
<h2 id="了解-hexo">了解 Hexo</h2>
<blockquote>
<p>A fast, simple &amp; powerful blog framework</p>
</blockquote>
<p><a href="https://hexo.io/">Hexo</a> 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown（或其他渲染引擎）解析文章，在几秒内，即可利用靓丽的主题生成静态网页，<a href="https://hexo.io/zh-cn/docs/setup.html">Hexo setup 官方文档</a>。</p>
<h2 id="安装-hexo">安装 Hexo</h2>
<p>所有必备的应用程序安装完成后，即可使用 npm 安装 Hexo：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install -g hexo-cli
</span></span></code></pre></div><h2 id="建站">建站</h2>
<p>安装 Hexo 完成后，请执行下列命令，Hexo 将会在指定文件夹中新建所需要的文件：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hexo init &lt;folder&gt;
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> &lt;folder&gt;
</span></span><span class="line"><span class="cl">npm install
</span></span></code></pre></div><p>新建完成后，指定文件夹的目录如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">├── _config.yml
</span></span><span class="line"><span class="cl">├── package.json
</span></span><span class="line"><span class="cl">├── scaffolds
</span></span><span class="line"><span class="cl">├── source
</span></span><span class="line"><span class="cl">| ├── _drafts
</span></span><span class="line"><span class="cl">| └── _posts
</span></span><span class="line"><span class="cl">└── themes
</span></span></code></pre></div><h3 id="新建一篇文章">新建一篇文章</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hexo new <span class="o">[</span>layout<span class="o">]</span> &lt;title&gt;
</span></span></code></pre></div><p>如果没有设置 layout 的话，默认使用 <code>_config.yml</code> 中的 default_layout 参数代替。如果标题包含空格的话，请使用引号括起来。</p>
<h3 id="生成静态文件">生成静态文件</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hexo generate
</span></span><span class="line"><span class="cl"><span class="c1"># 可以简写为</span>
</span></span><span class="line"><span class="cl">hexo g
</span></span></code></pre></div><h3 id="启动服务器">启动服务器</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hexo server
</span></span><span class="line"><span class="cl"><span class="c1"># 可以简写为</span>
</span></span><span class="line"><span class="cl">hexo s
</span></span></code></pre></div><p>网站会在 <a href="http://localhost:4000">http://localhost:4000</a> 下启动。在服务器启动期间，Hexo 会监视文件变动并自动更新，您无须重启服务器。</p>
<h2 id="部署静态网页到-github">部署静态网页到 GitHub</h2>
<h3 id="注册设置-github">注册设置 GitHub</h3>
<ol>
<li>登录 GitHub，注册自定义用户名如：<code>imzyf</code></li>
<li>在主页右下角创建 New repository，name 必须和用户名一致如：<code>imzyf.github.io</code></li>
<li>等待 3 分钟左右，之后即可访问静态主页如：<code>https://imzyf.github.io</code></li>
</ol>
<h3 id="同步内容至-github">同步内容至 GitHub</h3>
<ol>
<li>在 Hexo 目录下 <code>git clone git@github.com:imzyf/imzyf.github.io.git</code></li>
<li>将 <code>public</code> 文件下的所有文件拷贝到 <code>imzyf.github.io</code> 下</li>
<li><code>git add .</code> 增加当前子目录下所有更改过的文件至 index</li>
<li><code>git commit -m 'xxx'</code> 提交到本地</li>
<li><code>git push origin master</code> 将当前分支 push 到远程 master 分支<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li>最后访问主页 <code>https://imzyf.github.io</code> 观察效果</li>
</ol>
<h2 id="绑定个人域名">绑定个人域名</h2>
<p>在 GitHub 项目页面，Settings → Pages，Source 选择部署分支，Custom domain 填写自己的域名，并勾选 Enforce HTTPS。<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p>在你的域名服务商那里，将填写的域名解析到 <code>&lt;username&gt;.github.io</code>，例如 <code>imzyf.github.io</code>。</p>
<p>&ndash; EOF &ndash;</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>GitHub 自 2020 年起新建仓库的默认分支改为 <code>main</code>，老仓库的 <code>master</code> 仍可继续使用，命令本身没有变。&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>GitHub Pages 的设置界面已经多次改版，当前路径是 Settings → Pages；HTTPS 在 2018 年之后基本默认启用，证书由 GitHub 自动签发。&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>试着写东西</title>
      <link>https://zyf.im/2016/03/18/try-to-write-something/</link>
      <pubDate>Fri, 18 Mar 2016 17:41:00 +0000</pubDate>
      <guid>https://zyf.im/2016/03/18/try-to-write-something/</guid>
      <description>&lt;p&gt;现在是大四下学期，从学校出来实习。在面试一家公司时，面试官看到我的邮箱（168#yifans.com）是自己的域名后说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;问：&amp;ldquo;有自己的网站吗？&amp;rdquo;
&amp;ldquo;没有。&amp;rdquo;
问：&amp;ldquo;域名都买了不自己搭个网站？&amp;rdquo;
&amp;ldquo;……&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实这也不是自己要搭博客的根本原因。&lt;/p&gt;
&lt;p&gt;在大学里，学了些、做了些东西，可思考的不多。现在越发觉得一个人的强大在于思想。看书、写些东西，我想是提高一个人思想的方法吧。文字写下来了也方便交流。&lt;/p&gt;
&lt;p&gt;出来了换了一个环境，遇到新的人、新的事，可以换个角度看人看事。原来我理解的技术博客，就是解决问题后的笔记本，有问题从中找，而且觉得很多网上的技术博客里的内容都是来回转来转去，很多解决方案都是过时的，没什么价值。可最近在浏览博客园、CSDN 时发现了很多精华——前人分享自己所理解的知识、技术中走的弯路，阅读后很受益。&lt;/p&gt;
&lt;p&gt;想想自己，很多事、很多情况下就是这样：没见多少就下结论，管中窥豹，too young。&lt;/p&gt;
&lt;p&gt;其中有这样的一个签名：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我不能保证写的每个地方都是对的，但是至少能保证不复制、不黏贴，保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后，希望都能看到自己对于技术、对于生活的态度。
我相信乔布斯说的，只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力，我可以挑灯夜战、不眠不休；面对困难，我愿意迎难而上、永不退缩。
其实我想说的是，我只是一个程序员，这就是我现在纯粹人生的全部。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再认同不过了。&lt;/p&gt;
&lt;h2 id=&#34;references&#34;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://zhuanlan.zhihu.com/p/19743861&#34;&gt;为什么你要写博客？ - 知乎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.cnblogs.com/xrq730/p/8446246.html&#34;&gt;详解 MySQL 数据类型 | 五月的仓颉&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;ndash; EOF &amp;ndash;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>现在是大四下学期，从学校出来实习。在面试一家公司时，面试官看到我的邮箱（168#yifans.com）是自己的域名后说：</p>
<blockquote>
<p>问：&ldquo;有自己的网站吗？&rdquo;
&ldquo;没有。&rdquo;
问：&ldquo;域名都买了不自己搭个网站？&rdquo;
&ldquo;……&rdquo;</p>
</blockquote>
<p>其实这也不是自己要搭博客的根本原因。</p>
<p>在大学里，学了些、做了些东西，可思考的不多。现在越发觉得一个人的强大在于思想。看书、写些东西，我想是提高一个人思想的方法吧。文字写下来了也方便交流。</p>
<p>出来了换了一个环境，遇到新的人、新的事，可以换个角度看人看事。原来我理解的技术博客，就是解决问题后的笔记本，有问题从中找，而且觉得很多网上的技术博客里的内容都是来回转来转去，很多解决方案都是过时的，没什么价值。可最近在浏览博客园、CSDN 时发现了很多精华——前人分享自己所理解的知识、技术中走的弯路，阅读后很受益。</p>
<p>想想自己，很多事、很多情况下就是这样：没见多少就下结论，管中窥豹，too young。</p>
<p>其中有这样的一个签名：</p>
<blockquote>
<p>我不能保证写的每个地方都是对的，但是至少能保证不复制、不黏贴，保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后，希望都能看到自己对于技术、对于生活的态度。
我相信乔布斯说的，只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力，我可以挑灯夜战、不眠不休；面对困难，我愿意迎难而上、永不退缩。
其实我想说的是，我只是一个程序员，这就是我现在纯粹人生的全部。</p>
</blockquote>
<p>再认同不过了。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/19743861">为什么你要写博客？ - 知乎</a></li>
<li><a href="https://www.cnblogs.com/xrq730/p/8446246.html">详解 MySQL 数据类型 | 五月的仓颉</a></li>
</ul>
<p>&ndash; EOF &ndash;</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
