<?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>运维 on On My Way</title>
    <link>https://tuanzi-wow.cn/tags/%E8%BF%90%E7%BB%B4/</link>
    <description>Recent content in 运维 on On My Way</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 09 Sep 2026 19:25:05 +0800</lastBuildDate>
    <atom:link href="https://tuanzi-wow.cn/tags/%E8%BF%90%E7%BB%B4/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>复盘：在 1.6 GiB 的境内服务器上，把博客做成分钟级自愈系统</title>
      <link>https://tuanzi-wow.cn/engineering/selfhosted-blog-postmortem/</link>
      <pubDate>Sun, 06 Sep 2026 00:00:00 +0800</pubDate>
      <guid>https://tuanzi-wow.cn/engineering/selfhosted-blog-postmortem/</guid>
      <description>零付费约束下的完整决策链：为什么不用 Pages 和 Runner、构建为什么放境内、FTS5 为什么要 jieba 预分词、原子发布如何用 8 个真实缺陷喂出来。</description>
      <content:encoded><![CDATA[<p>这台博客跑在一台最便宜的阿里云 ECS 上：2 vCPU、<strong>实际可用内存 1.6 GiB</strong>、固定 3 Mbps 带宽，约束是「除服务器本身外新增付费 0 元」。这篇文章复盘整个工程链路的四个关键决策，以及它们各自是怎么被现实打脸又修正的。</p>
<p>最终形态的实测数据：push 后 <strong>2 秒</strong>完成构建发布与增量索引，站内搜索 p95 <strong>6.6 ms</strong>，检索服务常驻内存 <strong>122 MiB</strong>，Lighthouse 四项 100。这些数字后面是 8 个记录在案的缺陷（D32~D39）和三次架构级改判。</p>
<h2 id="一为什么不用-github-pages--vercel">一、为什么不用 GitHub Pages / Vercel</h2>
<p>第一个被否掉的是最省事的方案。理由按权重排：</p>
<ol>
<li><strong>境内访问不稳</strong>。读者在国内，GitHub Pages 的可达性看运气；这篇文章的读者大概率也用不了 Vercel 的默认域名。</li>
<li><strong>检索与统计只能挂第三方</strong>。静态托管意味着搜索要么引第三方服务（境外依赖 + 付费 + 隐私），要么不做。自托管后，搜索是 60 行 SQL 的事。</li>
<li><strong>「自有域名 + 自有数据」</strong>。备案域名指向自己的服务器，数据在自己手里——这本身就是练习运维的机会。</li>
</ol>
<h2 id="二为什么构建放服务器本地pull-式而不是境外-runner-推过来">二、为什么构建放服务器本地（pull 式），而不是境外 Runner 推过来</h2>
<p>直觉方案是 GitHub Actions：push 触发构建，构建完 SSH 推产物到服务器。实测否决：<strong>境外 Runner 到境内的 SSH 连接间歇丢包</strong>，2 分钟的更新 SLA 根本承诺不了。这是一次典型的「把不稳定面摊到整条链路上」的错误——CI 推送意味着每次发布都要赌一次跨境传输。</p>
<p>改成 <strong>pull 式</strong>之后，链路变成：push → （Gitee 中转）→ 服务器每 60 秒轮询发现新提交 → 服务器本地 <code>git pull</code> + <code>hugo</code> 构建 → 原子切版。构建从「不可控的境外网络任务」变成「服务器本地 2 秒的确定性操作」，不稳定的国际链路被压缩到一次 git fetch——甚至后来干脆把拉取源切到 Gitee，连这一次赌运气的 fetch 都消掉了。</p>
<p>这个演进的触发点值得单独说：GitHub 的 HTTPS 对境内 ECS 会间歇性 SYN 黑洞（connect 挂满 127 秒，恰好是 TCP SYN 重传上限），一度让每分钟一次的轮询变成每分钟一封告警邮件。最后的解法不是继续调告警阈值，而是<strong>让服务器和 GitHub 解耦</strong>：本地 push 同时推 GitHub 和 Gitee，服务器只跟境内的 Gitee 说话。GitHub 退化成纯展示副本——它挂了只影响简历仓库的新鲜度，不影响任何真实功能。</p>
<h2 id="三fts5-为什么必须-jieba-预分词">三、FTS5 为什么必须 jieba 预分词</h2>
<p>SQLite FTS5 的默认分词器 <code>unicode61</code> 按 Unicode 词边界切词——对英文完美，对中文灾难：<strong>「标准库概览」整句是一个 token</strong>。意味着搜「标准库」永远搜不到「标准库概览」里的内容，除非用户输入整句。</p>
<p>解法是写入索引前先 jieba 分词，把「标准库 概览」以空格分隔喂给 FTS5。两个实现细节：</p>
<ul>
<li><strong>查询侧用 <code>cut_for_search</code></strong>（更细粒度），索引侧用精确模式——查询 token 是索引 token 的超集，配合 OR 兜底，命中率优先。</li>
<li><strong>摘要和锚点不交给 FTS5</strong>：FTS5 的 <code>snippet()</code> 只能作用于索引列（已分词的文本），展示出来全是断词空格。所以索引列之外另存一张 <code>sections</code> 表（每节的原始文本 + Goldmark 自动生成的锚点 id），命中后由应用层挑最佳小节、拼 <code>&lt;mark&gt;</code> 高亮和「跳转到该节」的锚点。搜索结果的「命中正确段落并跳转」就是这么来的。</li>
</ul>
<p>实测：站内搜索 p95 6.6 ms（含 jieba 查询分词），增量索引（一篇新文章）2 秒内可搜。</p>
<h2 id="四原子发布与失败保护8-个真实缺陷喂出来的">四、原子发布与失败保护：8 个真实缺陷喂出来的</h2>
<p>发布链路的核心不变量只有一句：<strong>先建新版本、验证通过才切软链；任何一步失败，线上继续服务旧内容</strong>。<code>deploy.sh</code> 的顺序是固定的：<code>flock</code> → 拉取 → hugo 构建到全新 release 目录 → 产物健全性检查 → <code>ln + mv -Tf</code> 原子切软链 → nginx reload → 增量索引 → 清理旧版。</p>
<p>这套设计的价值不是纸面文章——上线头一天它就被真实触发了三次：</p>
<table>
	<thead>
			<tr>
					<th>缺陷</th>
					<th>触发</th>
					<th>失败保护的表现</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>D39</td>
					<td>测量脚本用 PowerShell 拼探针页 front matter，日期值被解析器拆到下一行，TOML 空值</td>
					<td>hugo 构建失败 → <strong>未切版</strong>，线上无恙；告警邮件即时送达</td>
			</tr>
			<tr>
					<td>D38</td>
					<td>备份凭据文件粘贴断行，快照根本没推上远端</td>
					<td>巡检新增「备份新鲜度」维度，5 分钟内报「备份失败」</td>
			</tr>
			<tr>
					<td>D32</td>
					<td>nginx 用 reload 后旧 worker 还持旧配置，自检撞上 404</td>
					<td>改为「配置有变化才 restart」，顺带幂等</td>
			</tr>
	</tbody>
</table>
<p>除此之外还有五个：告警无去抖变成风暴（D35）、去抖的「成功即清零」在边缘抖动下失效（D36）、安装脚本对拉起的 oneshot 只展示不查终态（D34）、logrotate 判据把 <code>grep -q</code> 的方向用反（D33）、探测 URL 漏了 Hugo 的分区前缀导致三轮「探的都是 404」（D41）。</p>
<p>这些缺陷的共同模式很有意思：<strong>没有一个是「代码写错逻辑」，全是「判据与被测对象错位」</strong>——探的 URL 不是真实 URL、查的返回码不代表真实状态、比的字符串被编码破坏、认定成功时其实只完成了一半。对应的防回归也被提炼成几条硬规则写进了缺陷档的总纲：判据来自产物扫描不来自意图；装载类判据查终态不查动作返回码；计数断言必须能让脚本停下；去抖必须覆盖恢复侧；生成结构化文本后必须回读关键字段。</p>
<h2 id="五为什么不用-docker--wordpress--第三方评论">五、为什么不用 Docker / WordPress / 第三方评论</h2>
<ul>
<li><strong>Docker</strong>：1.6 GiB 上容器常驻开销大于收益，systemd 单元 + <code>MemoryMax</code> 是更便宜的资源边界。</li>
<li><strong>WordPress/Halo</strong>：内容进数据库，「Git 是唯一内容源」就死了——push 更新、回滚、异地备份全部退化。</li>
<li><strong>第三方统计/评论</strong>：境外脚本拖慢页面还要隐私政策；<code>navigator.sendBeacon</code> + 一张 SQLite 表，456 KB。</li>
</ul>
<h2 id="收个尾">收个尾</h2>
<p>这套系统的完整决策链、平台约束（备案拦截、镜像黑洞、内存预算）和运维手册都在仓库里。如果说有什么元教训：<strong>约束不是敌人</strong>。1.6 GiB 内存逼出了标准库 http.server 和 SQLite；0 元付费逼出了 Gitee 中转和自建统计；国际链路的黑洞逼出了本地构建和原子发布。每一层被砍掉的「标准方案」，最后都变成了更便宜、更快、更可解释的自己方案。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
