<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Mirror Works</title>
    <link>https://blog.mirror-works.net/</link>
    <description>关于半导体工艺、TCAD 仿真与工程实践的笔记</description>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 24 Sep 2026 23:08:43 +0800</lastBuildDate>
    <atom:link href="https://blog.mirror-works.net/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>工艺节点的数字，早就不代表尺寸了</title>
      <link>https://blog.mirror-works.net/posts/node-naming-broken/</link>
      <guid isPermaLink="true">https://blog.mirror-works.net/posts/node-naming-broken/</guid>
      <pubDate>Thu, 24 Sep 2026 08:00:00 +0800</pubDate>
      <description>从 7nm 开始，&#34;nm&#34; 就成了一枚注册商标而不是一把尺子。把各家节点的实际物理尺寸摊开对比，会发现数字和现实之间隔着一整套营销语言。</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>如果你把台积电 5nm、三星 5nm、Intel 7 三个&rdquo;同代&rdquo;节点的 TEM 截面并排放，第一反应通常不是&rdquo;原来如此&rdquo;，而是&rdquo;这仨真的是同一代吗&rdquo;。</p>
<p>答案是：它们确实不同代，因为<strong>&ldquo;nm&rdquo; 这个单位已经不表示任何物理长度了</strong>。</p>
<h2 id="一曾经的诚实年代">一、曾经的诚实年代</h2>
<p>在 0.35µm 到 28nm 这段时期，工艺节点的名字基本对得上一个真实可测的量——<strong>接触栅间距（Contacted Gate Pitch, CGP）</strong>或其半值。</p>
<table>
<thead>
<tr>
<th>节点</th>
<th>栅极长度 Lg</th>
<th>接触栅间距 CGP</th>
<th>命名依据</th>
</tr>
</thead>
<tbody>
<tr>
<td>180nm</td>
<td>~140nm</td>
<td>~460nm</td>
<td>半间距</td>
</tr>
<tr>
<td>65nm</td>
<td>~35nm</td>
<td>~220nm</td>
<td>半间距</td>
</tr>
<tr>
<td>28nm</td>
<td>~25nm</td>
<td>~117nm</td>
<td>半间距</td>
</tr>
<tr>
<td>16/14nm</td>
<td>~20nm</td>
<td>~90nm</td>
<td>逻辑上已开始脱钩</td>
</tr>
</tbody>
</table>
<p>注意 28nm 那一行的 Lg 已经是 25nm 了——<strong>栅长比节点名还小</strong>。也就是说从 28nm 起，&rdquo;节点名 ≈ 某个特征尺寸&rdquo;这个等式就已经不成立了，只是当时大家还勉强能找到口径。</p>
<h2 id="二分水岭1614nm-与-finfet">二、分水岭：16/14nm 与 FinFET</h2>
<p>FinFET 的引入彻底打碎了对应关系。原因很朴素：</p>
<ul>
<li>平面器件时代，性能主要由沟道长度和栅氧厚度决定，微缩路径是<strong>一维的</strong>——把东西做小。</li>
<li>FinFET 之后，出现了一组互相耦合的、<strong>不可能同时优化</strong>的尺寸：鳍宽 $W_{fin}$、鳍高 $H_{fin}$、鳍间距 $FinPitch$、栅极跨过的鳍数量。</li>
</ul>
<p>于是器件性能变成：</p>
<p>$$
I_{eff} \propto \frac{W_{eff}}{L_g}\cdot \mu_{eff} \cdot C_{ox} \cdot (V_{GS}-V_{TH})
$$</p>
<p>其中 $W_{eff} = N_{fin}\cdot(2H_{fin} + W_{fin})$。</p>
<p>关键就在这个式子里：<strong>想让电流上升，不必缩小 $L_g$，只要把 $H_{fin}$ 拉高就行。</strong> 高度属于&rdquo;第三维&rdquo;，它不占平面面积。厂商第一次拿到了一个<strong>不靠微缩也能提升性能</strong>的自由度。</p>
<p>从那一天起，节点名就只是&rdquo;代际标签&rdquo;了。</p>
<h2 id="三各家口径对照">三、各家口径对照</h2>
<p>把 2020 年后的节点摊开看会更清楚：</p>
<table>
<thead>
<tr>
<th>厂商标称</th>
<th>接触栅间距</th>
<th>金属间距</th>
<th>实际沟道</th>
<th>器件结构</th>
</tr>
</thead>
<tbody>
<tr>
<td>TSMC N5</td>
<td>~48nm</td>
<td>~28nm</td>
<td>~16nm</td>
<td>FinFET</td>
</tr>
<tr>
<td>Samsung 5LPE</td>
<td>~50nm</td>
<td>~30nm</td>
<td>~17nm</td>
<td>FinFET</td>
</tr>
<tr>
<td>Intel 7</td>
<td>~54nm</td>
<td>~36nm</td>
<td>—</td>
<td>FinFET</td>
</tr>
<tr>
<td>Intel 4</td>
<td>~50nm</td>
<td>~30nm</td>
<td>—</td>
<td>FinFET/EUV</td>
</tr>
<tr>
<td>TSMC N3</td>
<td>~45nm</td>
<td>~23nm</td>
<td>~12nm</td>
<td>FinFET</td>
</tr>
<tr>
<td>Samsung 3GAE</td>
<td>~46nm</td>
<td>~24nm</td>
<td>~12nm</td>
<td><strong>GAA</strong></td>
</tr>
</tbody>
</table>
<p>几个结论：</p>
<ol>
<li><strong>Intel 的节点号系统性偏保守</strong>——Intel 7 的几何尺寸其实贴近别人家的 5nm。这是 Intel 在 2021 年主动改口径（把 10nm 改叫 Intel 7）的结果，属于&rdquo;重新校准而非缩水&rdquo;。</li>
<li><strong>三星 3GAE 与台积电 N3 尺寸接近，但结构不同</strong>——一个是环绕栅，一个还是 FinFET。</li>
<li><strong>标称数字的差值已经远大于几何尺寸的差值。</strong> 从 N5 到 N3，数字降了 40%，CGP 只缩了约 6%。</li>
</ol>
<p><img alt="不同节点几何尺寸的实际差距远小于数字差距" src="" /></p>
<h2 id="四为什么这套话术能持续">四、为什么这套话术能持续</h2>
<p>因为<strong>替换指标的成本太高</strong>。</p>
<p>业界试过替代方案：</p>
<ul>
<li><strong>Intel 的&rdquo;每瓦性能代际提升&rdquo;</strong> —— 太抽象，客户听不懂。</li>
<li><strong>晶体管密度（MTr/mm²）</strong> —— 看似客观，但统计口径可以差出 30%（是否计入 SRAM？算不算 dummy 结构？）。</li>
<li><strong>IRDS 的&rdquo;节点后缀体系&rdquo;</strong> —— 学术界认可，市场部不用。</li>
</ul>
<p>最终胜出的还是数字，因为它<strong>可以排序，且天然暗示&rdquo;越小越好&rdquo;</strong>。营销的语言总是战胜工程的语言。</p>
<blockquote>
<p>判断一个节点的真实先进程度，别去看它叫什么，去看三件事：<strong>CGP 与金属间距的实际值、器件结构、以及 SRAM bitcell 的面积。</strong></p>
</blockquote>
<h2 id="五一个实用的判断清单">五、一个实用的判断清单</h2>
<p>下次再看到&rdquo;N nm 节点&rdquo;的宣传，按这个顺序问：</p>
<ul>
<li>[ ] <strong>SRAM bitcell 面积是多少 mm²？</strong> 这是最难注水、也最能反映真实光刻能力的指标</li>
<li>[ ] <strong>器件是 FinFET 还是 GAA？</strong> 同尺寸下 GAA 通常有 10~20% 的驱动电流优势</li>
<li>[ ] <strong>金属层数有多少？</strong> 层数暴涨通常是布线能力跟不上的信号</li>
<li>[ ] <strong>供电方式：正面还是背面（BSPDN）？</strong> 这是真正的代际差异，比数字大小重要得多</li>
</ul>
<p>至于那个数字本身——把它当成<strong>商标</strong>读，而不是当成尺子用。<sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup></p>
<div class="footnote">
<hr />
<ol>
<li id="fn:1">
<p>本文的尺寸数据综合自公开的 ISSCC/IEDM 论文与第三方逆向分析，不同来源存在 ±5% 的口径差异，趋势判断不受影响。&#160;<a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text">&#8617;</a></p>
</li>
</ol>
</div>]]></content:encoded>
<category>半导体工艺</category><category>微缩</category><category>行业观察</category>    </item>
    <item>
      <title>用 SEMulator3D 做虚拟工艺验证：一条完整的结构到电性链路</title>
      <link>https://blog.mirror-works.net/posts/semulator3d-virtual-process/</link>
      <guid isPermaLink="true">https://blog.mirror-works.net/posts/semulator3d-virtual-process/</guid>
      <pubDate>Fri, 18 Sep 2026 08:00:00 +0800</pubDate>
      <description>工艺仿真最常见的失败不是模型不对，而是把&#34;几何结构走通&#34;当成了验证完成。这篇记录一条可复现的链路：从刻蚀配方到结构、从结构到器件、再从器件回到工艺窗口。</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<h2 id="为什么单独把-semulator3d-拎出来讲">为什么单独把 SEMulator3D 拎出来讲</h2>
<p>TCAD 的教科书链路通常是 <strong>工艺 → 结构 → 器件 → 电路</strong>。但实际工作中，前两步之间的那道缝才是最容易出事的地方。</p>
<p>原因在于：<strong>刻蚀和沉积是非保形的，而大多数器件仿真器默认你给的是一份&rdquo;理想几何&rdquo;。</strong> SEMulator3D 这类基于体素（voxel）的工艺仿真器，价值就在于它老老实实模拟了&rdquo;材料怎么被一点点拿走/堆上去&rdquo;。</p>
<p>代价是：它输出的是<strong>体素结构</strong>，而器件仿真器想要的是<strong>连续介质网格</strong>。这条接口的质量，几乎决定了整个仿真的可信度。</p>
<h2 id="一先把工艺配方拆成可仿真的步骤">一、先把工艺配方拆成可仿真的步骤</h2>
<p>假设要验证一个 3nm 级别的 GAA 纳米片释放（nanosheet release）工艺。真实配方可能是几十步，先做<strong>约束检查</strong>：</p>
<div class="highlight"><pre><span></span><code><span class="c1"># process_recipe.yaml —— 精简到只保留影响结构的步骤</span>
<span class="nt">steps</span><span class="p">:</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">P01</span>
<span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">deposit</span>
<span class="w">    </span><span class="nt">material</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SiGe</span>
<span class="w">    </span><span class="nt">thickness_nm</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">8</span>
<span class="w">    </span><span class="nt">conformality</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0.95</span><span class="w">      </span><span class="c1"># 关键：1.0 才是完全保形</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">P02</span>
<span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">deposit</span>
<span class="w">    </span><span class="nt">material</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Si</span>
<span class="w">    </span><span class="nt">thickness_nm</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">5</span>
<span class="w">    </span><span class="nt">conformality</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0.98</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">P03</span>
<span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">repeat</span><span class="w">             </span><span class="c1"># 交替叠层 x3</span>
<span class="w">    </span><span class="nt">times</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3</span>
<span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="nv">P01</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="nv">P02</span><span class="p p-Indicator">]</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">P04</span>
<span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">etch</span>
<span class="w">    </span><span class="nt">material</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SiGe</span>
<span class="w">    </span><span class="nt">selectivity</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt">Si</span><span class="p">:</span><span class="w"> </span><span class="nv">100</span><span class="p p-Indicator">,</span><span class="nt"> SiGe</span><span class="p">:</span><span class="w"> </span><span class="nv">1</span><span class="p p-Indicator">}</span><span class="w">   </span><span class="c1"># 对 SiGe 的刻蚀速率比 Si 快 ~100x</span>
<span class="w">    </span><span class="nt">isotropy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">0.8</span><span class="w">            </span><span class="c1"># 0=完全各向异性, 1=完全各向同性</span>
<span class="w">    </span><span class="nt">over_etch_pct</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">15</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">P05</span>
<span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">anneal</span>
<span class="w">    </span><span class="nt">purpose</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SiGe_relaxation</span>
<span class="w">    </span><span class="nt">temp_C</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1050</span>
<span class="w">    </span><span class="nt">time_ms</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2</span>
</code></pre></div>

<p>这几行里藏着三个<strong>必须显式建模、否则结果必然失真</strong>的参数：</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>物理含义</th>
<th>忽略后的典型偏差</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>conformality</code></td>
<td>沉积在侧壁/底部的厚度比</td>
<td>结构尺寸偏差 5~15%</td>
</tr>
<tr>
<td><code>selectivity</code></td>
<td>不同材料的刻蚀速率比</td>
<td>要么释放不干净，要么 Si 被咬掉</td>
</tr>
<tr>
<td><code>isotropy</code></td>
<td>刻蚀方向性</td>
<td>侧向掏空量差 2~3 倍</td>
</tr>
</tbody>
</table>
<h2 id="二从体素到网格这一步最容易被糊过去">二、从体素到网格：这一步最容易被糊过去</h2>
<p>体素结构导出后，网格化时你会遇到一个<strong>无法回避的取舍</strong>：</p>
<div class="highlight"><pre><span></span><code><span class="c1"># 伪代码：体素 → 器件网格的典型处理</span>
<span class="n">structure</span> <span class="o">=</span> <span class="n">load_voxel</span><span class="p">(</span><span class="s2">"release.structure"</span><span class="p">)</span>

<span class="c1"># 方案 A：直接栅格化成六面体网格（快，但界面呈阶梯状）</span>
<span class="n">mesh_a</span> <span class="o">=</span> <span class="n">voxel_to_hex</span><span class="p">(</span><span class="n">structure</span><span class="p">,</span> <span class="n">pitch_nm</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>

<span class="c1"># 方案 B：表面重建后生成贴体网格（慢，界面光滑）</span>
<span class="n">mesh_b</span> <span class="o">=</span> <span class="n">surface_reconstruct</span><span class="p">(</span><span class="n">structure</span><span class="p">)</span><span class="o">.</span><span class="n">then</span><span class="p">(</span><span class="n">tetrahedral_mesh</span><span class="p">,</span> <span class="n">min_size_nm</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>

<span class="c1"># 方案 C：折中——界面附近细化，体区粗化</span>
<span class="n">mesh_c</span> <span class="o">=</span> <span class="n">voxel_to_hex</span><span class="p">(</span><span class="n">structure</span><span class="p">,</span> <span class="n">pitch_nm</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
<span class="n">mesh_c</span><span class="o">.</span><span class="n">refine_near</span><span class="p">(</span><span class="n">interface</span><span class="p">,</span> <span class="n">min_size_nm</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>
</code></pre></div>

<p><strong>判断标准很具体</strong>：如果沟道界面处的网格像楼梯，那么栅氧电场会被系统性算错，最终 $V_{TH}$ 的误差可能达到几十毫伏——这个量级已经足以让你对工艺窗口做出错误结论。</p>
<p>一条经验法则：</p>
<blockquote>
<p>界面处的网格尺寸不应大于<strong>栅氧厚度的 1/3</strong>。对 1nm 等效氧化层，就是 0.33nm 量级——这正是为什么这类仿真的收敛总是很难调。</p>
</blockquote>
<h2 id="三器件仿真与电性提取">三、器件仿真与电性提取</h2>
<p>拿到贴体网格后，接一个 drifts-diffusion 求解器：</p>
<div class="highlight"><pre><span></span><code><span class="c1"># 用 Sentaurus Device 做 DD 仿真（示意）</span>
sdevice<span class="w"> </span>release_mesh.tdr<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-physics<span class="w"> </span><span class="s2">"Hydrodynamic"</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-bias<span class="w"> </span><span class="s2">"Vg 0..1.0 step 0.02, Vd 0.05"</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-output<span class="w"> </span>IdVg.dat,<span class="w"> </span>IdVd.dat
</code></pre></div>

<p>从中提出你真正关心的量：</p>
<ul>
<li><strong>$V_{TH}$（恒定电流法）</strong>：$I_D = 100\text{nA}\times W/L$</li>
<li><strong>SS（亚阈斜率）</strong>：$\text{SS}=\left(\frac{d\log_{10}I_D}{dV_G}\right)^{-1}$，室温理论下限 60 mV/dec</li>
<li><strong>DIBL</strong>：$\frac{V_{TH}(V_D{=}0.05)-V_{TH}(V_D{=}V_{DD})}{\Delta V_D}$</li>
<li><strong>$I_{on}/I_{off}$ 比</strong></li>
</ul>
<h2 id="四真正的目的回到工艺窗口">四、真正的目的：回到工艺窗口</h2>
<p>电性只是中间产物。<strong>仿真链条的价值在于反向给出工艺容差。</strong></p>
<p>做法是扫关键工艺参数的组合，看电性何时越界：</p>
<table>
<thead>
<tr>
<th>over-etch</th>
<th>isotropy</th>
<th>释放完整度</th>
<th>SS (mV/dec)</th>
<th>判定</th>
</tr>
</thead>
<tbody>
<tr>
<td>5%</td>
<td>0.6</td>
<td>部分残留</td>
<td>78</td>
<td>✗ 漏电</td>
</tr>
<tr>
<td>10%</td>
<td>0.7</td>
<td>完整</td>
<td>66</td>
<td>✓</td>
</tr>
<tr>
<td>15%</td>
<td>0.8</td>
<td>完整</td>
<td>65</td>
<td>✓</td>
</tr>
<tr>
<td>20%</td>
<td>0.9</td>
<td>Si 被咬蚀</td>
<td>—</td>
<td>✗ 结构失效</td>
</tr>
<tr>
<td>25%</td>
<td>0.9</td>
<td>纳米片断裂</td>
<td>—</td>
<td>✗ 报废</td>
</tr>
</tbody>
</table>
<p>这样你就得到了一条<strong>工艺窗口</strong>：<code>over-etch ∈ [10%, 15%]</code> 且 <code>isotropy ≤ 0.8</code>。</p>
<p>这才是仿真该交付的东西——不是一个漂亮的 $I_d$-$V_g$ 曲线，而是<strong>&ldquo;哪些工艺参数可以波动，波动到哪里就会失效&rdquo;</strong>。</p>
<h2 id="五几个踩过的坑">五、几个踩过的坑</h2>
<ul>
<li><strong>别用理想几何代替工艺仿真去&rdquo;验证&rdquo;工艺。</strong> 如果结构是你手画的，那你验证的是你的画工。</li>
<li><strong>选择性刻蚀的 selectivity 值必须来自实测或文献，不能猜。</strong> 它是指数级敏感的。</li>
<li><strong>体素分辨率不是越高越好。</strong> 分辨率翻倍，仿真时间涨 8 倍，而 0.5nm 以下通常在拟合数值噪声。</li>
<li><strong>先做二维截面验证，再做三维。</strong> 三维不收敛时，你很难判断是物理错了还是网格错了。</li>
</ul>
<hr />
<p>工具只是链路的一环。<strong>真正决定仿真可信度的，是你有没有认真对待&rdquo;体素到网格&rdquo;这一步所引入的误差。</strong></p>]]></content:encoded>
<category>TCAD</category><category>SEMulator3D</category><category>工艺仿真</category><category>方法学</category>    </item>
    <item>
      <title>把静态博客跑在一台 2 核 4G 的云服务器上</title>
      <link>https://blog.mirror-works.net/posts/static-blog-on-ecs/</link>
      <guid isPermaLink="true">https://blog.mirror-works.net/posts/static-blog-on-ecs/</guid>
      <pubDate>Wed, 09 Sep 2026 08:00:00 +0800</pubDate>
      <description>不用 Netlify、不用 Vercel、不用 Docker，只用一台最低配的云服务器和 nginx——顺带把 HTTPS、gzip、缓存策略和安全响应头一次配齐。</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<h2 id="为什么不用托管平台">为什么不用托管平台</h2>
<p>托管平台的静态站部署体验确实好：推代码、自动构建、全球 CDN。但它有三个隐性成本：</p>
<ol>
<li><strong>域名和流量被绑定</strong>——想换平台就要动 DNS。</li>
<li><strong>构建分钟数有额度</strong>——博客这种低频更新场景，额度基本用不完，但一旦要跑重构建（比如图片处理），就开始算钱。</li>
<li><strong>调试不透明</strong>——重定向、缓存、响应头出问题的时候，你只能猜。</li>
</ol>
<p>而一台最低配的云服务器（2 核 4G，一年几十块）能承载的静态站，量级远超个人博客的需求。<strong>nginx 处理静态文件的能力，是被严重低估的。</strong></p>
<h2 id="一生成器三个依赖就够了">一、生成器：三个依赖就够了</h2>
<p>核心逻辑其实很短：Markdown → HTML → 写文件。需要的库只有三个：</p>
<div class="highlight"><pre><span></span><code>pip<span class="w"> </span>install<span class="w"> </span>markdown<span class="w"> </span>pygments<span class="w"> </span>jinja2
</code></pre></div>

<table>
<thead>
<tr>
<th>库</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>markdown</code></td>
<td>解析 Markdown，产出 HTML</td>
</tr>
<tr>
<td><code>pygments</code></td>
<td>代码块语法高亮</td>
</tr>
<tr>
<td><code>jinja2</code></td>
<td>套用页面模板</td>
</tr>
</tbody>
</table>
<p>构建脚本的核心就三步：</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">markdown</span><span class="o">,</span><span class="w"> </span><span class="nn">yaml</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">jinja2</span><span class="w"> </span><span class="kn">import</span> <span class="n">Environment</span><span class="p">,</span> <span class="n">FileSystemLoader</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">pathlib</span><span class="w"> </span><span class="kn">import</span> <span class="n">Path</span>

<span class="c1"># 1. 读文章 + 解析 frontmatter</span>
<span class="k">def</span><span class="w"> </span><span class="nf">load_post</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
    <span class="n">raw</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">"utf-8"</span><span class="p">)</span>
    <span class="n">meta</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="n">split_frontmatter</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>      <span class="c1"># --- YAML --- 分隔</span>
    <span class="n">md</span> <span class="o">=</span> <span class="n">markdown</span><span class="o">.</span><span class="n">Markdown</span><span class="p">(</span><span class="n">extensions</span><span class="o">=</span><span class="p">[</span><span class="s2">"extra"</span><span class="p">,</span> <span class="s2">"toc"</span><span class="p">,</span> <span class="s2">"codehilite"</span><span class="p">,</span> <span class="s2">"tables"</span><span class="p">])</span>
    <span class="k">return</span> <span class="p">{</span><span class="o">**</span><span class="n">meta</span><span class="p">,</span> <span class="s2">"html"</span><span class="p">:</span> <span class="n">md</span><span class="o">.</span><span class="n">convert</span><span class="p">(</span><span class="n">body</span><span class="p">),</span> <span class="s2">"toc"</span><span class="p">:</span> <span class="n">md</span><span class="o">.</span><span class="n">toc</span><span class="p">}</span>

<span class="c1"># 2. 套模板</span>
<span class="n">env</span> <span class="o">=</span> <span class="n">Environment</span><span class="p">(</span><span class="n">loader</span><span class="o">=</span><span class="n">FileSystemLoader</span><span class="p">(</span><span class="s2">"templates"</span><span class="p">),</span> <span class="n">autoescape</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">page</span> <span class="o">=</span> <span class="n">env</span><span class="o">.</span><span class="n">get_template</span><span class="p">(</span><span class="s2">"post.html"</span><span class="p">)</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">post</span><span class="o">=</span><span class="n">load_post</span><span class="p">(</span><span class="n">p</span><span class="p">))</span>

<span class="c1"># 3. 写盘</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">"dist/posts"</span><span class="p">)</span> <span class="o">/</span> <span class="n">slug</span> <span class="o">/</span> <span class="s2">"index.html"</span>
<span class="n">out</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">out</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">"utf-8"</span><span class="p">)</span>
</code></pre></div>

<p>关键设计取舍：<strong>输出目录结构用 <code>posts/&lt;slug&gt;/index.html</code> 而不是 <code>posts/&lt;slug&gt;.html</code></strong>。这样 URL 里没有 <code>.html</code> 后缀，将来想换生成器也不用改任何链接。</p>
<h2 id="二nginx-配置短但每一行都有用">二、nginx 配置：短，但每一行都有用</h2>
<div class="highlight"><pre><span></span><code><span class="k">server</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kn">listen</span><span class="w">      </span><span class="mi">80</span><span class="p">;</span>
<span class="w">    </span><span class="kn">listen</span><span class="w">      </span><span class="s">[::]:80</span><span class="p">;</span>
<span class="w">    </span><span class="kn">server_name</span><span class="w"> </span><span class="s">mirror-works.net</span><span class="w"> </span><span class="s">www.mirror-works.net</span><span class="p">;</span>

<span class="w">    </span><span class="kn">root</span><span class="w">  </span><span class="s">/home/admin/blog/dist</span><span class="p">;</span>
<span class="w">    </span><span class="kn">index</span><span class="w"> </span><span class="s">index.html</span><span class="p">;</span>

<span class="w">    </span><span class="c1"># 无后缀 URL → 找同名目录</span>
<span class="w">    </span><span class="kn">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="kn">try_files</span><span class="w"> </span><span class="nv">$uri</span><span class="w"> </span><span class="nv">$uri/</span><span class="w"> </span><span class="p">=</span><span class="mi">404</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="kn">error_page</span><span class="w"> </span><span class="mi">404</span><span class="w"> </span><span class="s">/404.html</span><span class="p">;</span>

<span class="w">    </span><span class="c1"># 静态资源长缓存：文件名带指纹才敢这么干，</span>
<span class="w">    </span><span class="c1"># 否则改样式用户看不到</span>
<span class="w">    </span><span class="kn">location</span><span class="w"> </span><span class="p">~</span><span class="sr">*</span><span class="w"> </span><span class="s">\.(css|js|svg|png|jpg|webp|woff2)</span>$<span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="kn">expires</span><span class="w"> </span><span class="s">7d</span><span class="p">;</span>
<span class="w">        </span><span class="kn">add_header</span><span class="w"> </span><span class="s">Cache-Control</span><span class="w"> </span><span class="s">"public,</span><span class="w"> </span><span class="s">max-age=604800"</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="kn">gzip</span><span class="w">            </span><span class="no">on</span><span class="p">;</span>
<span class="w">    </span><span class="kn">gzip_min_length</span><span class="w"> </span><span class="mi">512</span><span class="p">;</span>
<span class="w">    </span><span class="kn">gzip_types</span><span class="w">      </span><span class="s">text/plain</span><span class="w"> </span><span class="s">text/css</span><span class="w"> </span><span class="s">application/javascript</span>
<span class="w">                    </span><span class="s">application/json</span><span class="w"> </span><span class="s">text/xml</span><span class="w"> </span><span class="s">application/xml</span><span class="w"> </span><span class="s">image/svg+xml</span><span class="p">;</span>

<span class="w">    </span><span class="kn">add_header</span><span class="w"> </span><span class="s">X-Content-Type-Options</span><span class="w">   </span><span class="s">nosniff</span><span class="w">      </span><span class="s">always</span><span class="p">;</span>
<span class="w">    </span><span class="kn">add_header</span><span class="w"> </span><span class="s">X-Frame-Options</span><span class="w">          </span><span class="s">SAMEORIGIN</span><span class="w">   </span><span class="s">always</span><span class="p">;</span>
<span class="w">    </span><span class="kn">add_header</span><span class="w"> </span><span class="s">Referrer-Policy</span><span class="w">          </span><span class="s">strict-origin-when-cross-origin</span><span class="w"> </span><span class="s">always</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>四个容易踩的点：</p>
<ul>
<li><strong><code>try_files</code> 的顺序</strong>必须是 <code>$uri</code> → <code>$uri/</code>，反过来目录会优先，性能更差。</li>
<li><strong><code>gzip_types</code> 不会自动包含 text/html</strong>——nginx 默认已经为 <code>text/html</code> 开了 gzip，重复写反而会覆盖掉默认值。</li>
<li><strong>缓存时间要和构建策略匹配。</strong> 如果 CSS 文件名里没有内容哈希（hash），<code>max-age=604800</code> 会让你改完样式刷新十次都看不到效果。稳妥做法是把缓存降到 1 小时，或者加 <code>?v=&lt;构建时间戳&gt;</code>。</li>
<li><strong><code>add_header</code> 在 <code>location</code> 里会覆盖外层同名头</strong>，所以要么都写 <code>always</code>，要么都放同一层。</li>
</ul>
<h2 id="三https交给-cloudflare">三、HTTPS：交给 Cloudflare</h2>
<p>自己签 Let&rsquo;s Encrypt 证书需要 80/443 都通、还要配 <code>certbot</code> 的自动续期。如果域名本来就托管在 Cloudflare，直接开代理模式（小云朵变橙）更省事：</p>
<ul>
<li>源站只需要暴露 80 端口</li>
<li>TLS 在边缘终结，证书自动续</li>
<li>顺手拿到 DDoS 防护和 CDN 缓存</li>
</ul>
<p>唯一的注意点：<strong>Cloudflare 的 SSL 模式必须从 <code>Flexible</code> 改成 <code>Full</code> 或 <code>Full (strict)</code></strong>，否则源站到边缘这一段是明文，而且会触发重定向循环。</p>
<div class="highlight"><pre><span></span><code><span class="c1"># 验证 HTTPS 与响应头</span>
curl<span class="w"> </span>-sI<span class="w"> </span>https://mirror-works.net<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-iE<span class="w"> </span><span class="s2">"http/|server|cf-cache-status|strict-transport"</span>
</code></pre></div>

<h2 id="四自动化一条-cron-解决发布">四、自动化：一条 cron 解决发布</h2>
<p>写文章和发布彻底解耦——文章就是仓库里的 <code>.md</code> 文件，发布是构建：</p>
<div class="highlight"><pre><span></span><code><span class="c1"># 每 10 分钟检查一次是否有新 commit，有就重建</span>
*/10<span class="w"> </span>*<span class="w"> </span>*<span class="w"> </span>*<span class="w"> </span>*<span class="w"> </span><span class="nb">cd</span><span class="w"> </span>/home/admin/blog<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>git<span class="w"> </span>pull<span class="w"> </span>-q<span class="w"> </span>--ff-only<span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>python3<span class="w"> </span>build.py<span class="w"> </span>&gt;&gt;<span class="w"> </span>/var/log/blog-build.log<span class="w"> </span><span class="m">2</span>&gt;<span class="p">&amp;</span><span class="m">1</span>
</code></pre></div>

<p>如果想要更快的反馈，加个 <code>inotifywait</code> 监听 <code>posts/</code> 目录，文件一变就重建：</p>
<div class="highlight"><pre><span></span><code>inotifywait<span class="w"> </span>-m<span class="w"> </span>-e<span class="w"> </span>close_write<span class="w"> </span>-e<span class="w"> </span>moved_to<span class="w"> </span>posts/<span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="nb">read</span><span class="w"> </span>_<span class="w"> </span>_<span class="w"> </span>file<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">  </span><span class="k">case</span><span class="w"> </span><span class="s2">"</span><span class="nv">$file</span><span class="s2">"</span><span class="w"> </span><span class="k">in</span><span class="w"> </span>*.md<span class="o">)</span><span class="w"> </span>python3<span class="w"> </span>build.py<span class="w"> </span><span class="p">;;</span><span class="w"> </span><span class="k">esac</span>
<span class="k">done</span>
</code></pre></div>

<h2 id="五这套方案的真实边界">五、这套方案的真实边界</h2>
<p>诚实地讲，它不适合：</p>
<ul>
<li><strong>日均百万 PV 的站点</strong>——没有分布式缓存，单机的连接数有上限</li>
<li><strong>需要动态能力的场景</strong>——评论、搜索服务端渲染、用户系统，这些得另外搭</li>
<li><strong>需要全球低延迟</strong>——单一地域的服务器在跨洲访问时延迟会明显</li>
</ul>
<p>但对一个个人技术博客来说，它的性价比高得离谱：<strong>没有构建额度、没有平台锁定、没有隐藏账单，出问题时你手上有一整个 shell 可以排查。</strong></p>
<blockquote>
<p>可控性不是性能指标，但它是你在半夜三点出问题时唯一在乎的东西。</p>
</blockquote>]]></content:encoded>
<category>Nginx</category><category>运维</category><category>静态站点</category><category>工程实践</category>    </item>
  </channel>
</rss>