<?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>发布与恢复 · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/series/release-and-recovery/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/series/release-and-recovery/index.xml" rel="self" type="application/rss+xml"/><item><title>小系统的优势</title><link>https://hk.xbcatm.com/zh/notes/the-advantage-of-small-systems/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/the-advantage-of-small-systems/</guid><description>当系统的重要行为能装进一个思维模型时，它更容易被信任。</description><content:encoded>&lt;p&gt;小不等于未完成。它意味着每个组件职责清晰、依赖经过斟酌，并且从请求到结果之间始终可见。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/small-system.detail.zh.svg" alt="DNS 查询向客户端返回地址，不承载 HTTP。请求进入预期的 TLS 边缘与一个已知输出目录。文件判断分流到必要内容或由站点处理的真实 404。职责字段需要明确 DNS、边缘与发布负责人；文件来源和恢复要求仍需记录。独立备份与恢复演练应对单点故障。" width="720" height="3030"&gt;&lt;figcaption&gt;明确每个请求边界、相关证据与负责人。组件更少只会减少需要理解的路径；恢复承诺仍靠备份与恢复演练支撑。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="可见性是一种特性"&gt;可见性是一种特性&lt;/h2&gt;
&lt;p&gt;当运维人员能说出每个边界时，他们也能指出缺少哪份证据。日志变得更有用，告警更具体，例行维护风险更低。&lt;/p&gt;
&lt;p&gt;对静态网站而言，重要的边界通常很容易列出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS 把主机名指向预期的边缘。&lt;/li&gt;
&lt;li&gt;TLS 在预期的代理处终结。&lt;/li&gt;
&lt;li&gt;代理只服务一个已知的输出目录。&lt;/li&gt;
&lt;li&gt;输出只包含站点需要的文件。&lt;/li&gt;
&lt;li&gt;404 响应由站点本身处理，而不是意外的上游。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这些写下来，比加上一块回答不了任何上述问题的仪表盘更有价值。&lt;/p&gt;
&lt;h2 id="复杂性要用实力取得位置"&gt;复杂性要用实力取得位置&lt;/h2&gt;
&lt;p&gt;当新的一层消除的不确定性多于它引入的，它才值得。缓存重要时，给资源加指纹的构建步骤会有用；内容需要事务时，数据库才有用。但对一个小而多为静态的站点，这两者都不必然是改进。&lt;/p&gt;
&lt;p&gt;对客户端 JavaScript 也适用同一规则。如果页面确实需要客户端脚本，应选择满足需求的最小实现，并有意识地审查内容安全策略。对本站而言，这首先需要重新评估已接受的零脚本架构及其验证契约，并不是当前策略的例外。如果页面是一份文档，纯 HTML 往往更容易检视、更有韧性。&lt;/p&gt;
&lt;h2 id="让所有权明确"&gt;让所有权明确&lt;/h2&gt;
&lt;p&gt;每个文件都应有存在的理由与明确的更新方式。vendored 主题应记录其上游版本与许可。生成目录应标记为生成物。资源下载应说明它包含什么、何时更新、校验和如何产生。&lt;/p&gt;
&lt;p&gt;这些简短声明能防止未来的运维者把偶然产生的制品当作受支持的接口。&lt;/p&gt;
&lt;h2 id="为下一次变更留出空间"&gt;为下一次变更留出空间&lt;/h2&gt;
&lt;p&gt;最好的小系统不是行数最少的那个，而是下一次变更看得见去处的那一个。笔记站可以从 Markdown、静态生成器与一个 Web 服务器起步；如果以后需要搜索或评论，可以独立评估这个新需求，而不是一开始就假设它存在。&lt;/p&gt;
&lt;p&gt;规模小并不自动带来容错。一个边缘、磁盘或运维者仍可能成为单点故障。明确可接受的恢复时间与数据损失，保留独立备份并演练恢复。组件更少的好处是需要理解的故障路径更少，而不是不会停机。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/getting-started/directory-structure/"&gt;Hugo directory structure&lt;/a&gt;，内容与资源的目录约定，已核验 2026-07-27。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt; 指令&lt;/a&gt;，设计中引用的静态文件服务默认值，已核验 2026-07-27。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>静态服务器应有一个明确的根目录</title><link>https://hk.xbcatm.com/zh/notes/a-static-server-should-have-one-obvious-root/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-static-server-should-have-one-obvious-root/</guid><description>让 Caddy 文件服务、生成制品与回滚路径保持一致。</description><content:encoded>&lt;p&gt;静态部署最有用的属性是：运维人员不用读应用代码，就能回答&amp;quot;这条响应是哪个目录提供的？&amp;quot;。Caddy 的 &lt;code&gt;root&lt;/code&gt; 与 &lt;code&gt;file_server&lt;/code&gt; 指令直接提供了这个边界。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/caddy-root.detail.zh.svg" alt="完整目录先核验，再设为 Caddy 根目录；新目录通过冒烟测试前保留旧的已知良好目录。虚线发布树包含 HTML、文本与资源。指向树外的符号链接可能暴露服务用户可读取的目标。私有文件留在树外；边缘检查独立核验状态码、内容类型、重定向、证书名称与缺失路由行为。" width="720" height="3010"&gt;&lt;figcaption&gt;明确提供响应的目录。检查符号链接及读取和目录穿越权限，再验证边缘的真实响应。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="让根目录明确"&gt;让根目录明确&lt;/h2&gt;
&lt;p&gt;一个最小的站点块可以贴近制品布局：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;www.example.test {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; root * /srv/example-site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; encode zstd gzip
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; header {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; X-Content-Type-Options nosniff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; X-Frame-Options DENY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Referrer-Policy strict-origin-when-cross-origin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; file_server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;主机名与路径都是占位值；&lt;code&gt;www.example.test&lt;/code&gt; 不是可签发公开可信证书的生产名称。生产环境应使用自己控制且可解析的主机名，测试环境的本地 TLS 信任则另行安排。发布命令应发布一个完整目录、核验它，然后才把这个目录设为配置里的根。在新目录通过冒烟测试之前，保留旧的已知良好目录。&lt;/p&gt;
&lt;h2 id="检查边缘而不只是文件树"&gt;检查边缘，而不只是文件树&lt;/h2&gt;
&lt;p&gt;请求一个 HTML 页面、一个可下载的文本文件和一个缺失路径。把内容类型与状态码与本地制品比较。即使 404 页看起来很精致，若返回 &lt;code&gt;200&lt;/code&gt; 仍是部署缺陷。&lt;/p&gt;
&lt;p&gt;当 Caddy 管理 HTTPS 时，证书签发与 HTTP 到 HTTPS 的重定向都属于边缘行为。请从裸的 HTTP 监听器测试重定向，并从公共主机名读取证书名称。不要只凭进程健康就推断成功。&lt;/p&gt;
&lt;h2 id="根目录是发布边界不是沙箱"&gt;根目录是发布边界，不是沙箱&lt;/h2&gt;
&lt;p&gt;把版本控制元数据、备份、凭据与私有配置留在发布树之外。Caddy 会跟随根目录中的符号链接，因此链接可能暴露根目录外的文件。既要检查制品内容，也要检查服务用户的读取与目录穿越权限。此外，仅把 &lt;code&gt;404.html&lt;/code&gt; 放进目录并不会配置自定义错误处理；应核验缺失路由的状态码与预期错误页行为。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt; 指令&lt;/a&gt;，静态文件服务与根目录选择，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/header"&gt;Caddy &lt;code&gt;header&lt;/code&gt; 指令&lt;/a&gt;，响应头配置，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/automatic-https"&gt;Caddy Automatic HTTPS&lt;/a&gt;，证书签发与 HTTPS 重定向，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options"&gt;MDN: &lt;code&gt;X-Frame-Options&lt;/code&gt;&lt;/a&gt;，防止被嵌套的选项，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7034.html"&gt;RFC 7034 — X-Frame-Options&lt;/a&gt;，DENY 嵌入策略，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6761.html"&gt;RFC 6761 — Special-Use Domain Names&lt;/a&gt;，保留的 &lt;code&gt;.test&lt;/code&gt; 名称，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>静态网站的发布检查清单</title><link>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</guid><description>每个发布都值得一组小、快、针对性强、且会为真实原因失败的检查。</description><content:encoded>&lt;p&gt;静态网站发布很简单，但简单并不免除发布检查的需要。有用的检查足够小，能在每次发布运行，也足够具体，能为真实的理由而失败。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-checklist.detail.zh.svg" alt="四个编号阶段分别构建固定生成器版本的干净制品，要求 index.html、404.html、robots.txt、sitemap.xml 非空并复核链接，在待发布和公共路由测试确切制品，最后记录提交、生成器版本和下载校验和。Hugo 源码预览位于独立分支，会重新构建；HEAD 检查元数据，GET 支持正文比较。" width="720" height="2570"&gt;&lt;figcaption&gt;检查对象应是生成制品。干净输出不等于可复现；占位符搜索只是复核线索，真正的链接门禁应基于解析器。发布前服务确切制品，发布后比较两种语言的公共 GET 正文，并保存提交、生成器版本和下载校验和。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="构建干净的制品"&gt;构建干净的制品&lt;/h2&gt;
&lt;p&gt;使用固定版本的生成器，构建到一次性的输出目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo --cleanDestinationDir --gc --minify --destination public
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--gc&lt;/code&gt; 回收未使用的构建缓存资源，不会清理陈旧的已发布页面。&lt;code&gt;--cleanDestinationDir&lt;/code&gt; 会移除目标目录中的陈旧文件，因此只能用于一次性制品目录，不能指向源码或其他数据所在的目录。把打印出的版本与项目固定版本比较；打印版本本身不会强制版本一致。记录构建输入并验证生成的制品。干净的目标目录不能证明逐字节可复现：时间戳与其他非确定性输入需要单独控制和比较。不要在构建之后手改生成的 HTML。&lt;/p&gt;
&lt;h2 id="检查制品而不只是源码"&gt;检查制品，而不只是源码&lt;/h2&gt;
&lt;p&gt;检视生成的树，确认期望的入口点存在。把下面的断言块作为独立 shell 脚本运行；任何必需文件缺失或为空时，都应在打印清单之前停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/index.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/404.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后搜索意外的占位符、本地路径与仅开发用的主机：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rg -l &lt;span class="s1"&gt;&amp;#39;localhost|127\.0\.0\.1|TODO|example\.com|/Users/&amp;#39;&lt;/span&gt; public --glob &lt;span class="s1"&gt;&amp;#39;*.html&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这只是复核线索，不是自动发布门禁：本清单这类示例本来就会命中，翻译页中也一样。这项仅检查 HTML 的扫描不会覆盖 XML 订阅或 JSON 搜索输出。&lt;code&gt;rg&lt;/code&gt; 无匹配时退出 1，发生错误时退出 2。逐个复核列出的文件，区分围栏示例与有效链接或属性，并用基于解析器的链接检查作为真正门禁。公开链接中的私有路径属于发布错误。&lt;/p&gt;
&lt;h2 id="验证公共路由"&gt;验证公共路由&lt;/h2&gt;
&lt;p&gt;让隔离的类生产服务器直接指向这份制品，才能进行更可靠的发布前路由检查。Hugo 开发服务器会重新构建内容；下面是独立的源码预览，不能证明它服务的就是已经检查过的 &lt;code&gt;public/&lt;/code&gt; 字节。第一条命令在一个终端运行，其余请求在另一个终端运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo server --bind 127.0.0.1 --port &lt;span class="m"&gt;1313&lt;/span&gt; --baseURL http://127.0.0.1:1313/ &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --disableFastRender --disableLiveReload --renderToMemory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/notes/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/resources/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/missing-page/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;HEAD 检查不比较正文。对待发布服务器再运行 GET，发布后将公共正文与制品比较，并检查两种语言。确认缺失路由在生产服务器上返回 404 状态。一个外观精美但状态 200 的错误页，仍然会破坏监控与缓存。&lt;/p&gt;
&lt;h2 id="记录结果"&gt;记录结果&lt;/h2&gt;
&lt;p&gt;在发布记录中保留源码提交、生成器版本与每个可下载资源的校验和。这样无需依赖记忆，就能把线上文件与产生它的源码进行比较。&lt;/p&gt;
&lt;p&gt;这份检查清单的可下载版本在&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;资源&lt;/a&gt;一节中。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo command reference&lt;/a&gt;，干净生产构建的生成器标志，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，HTTP 验证与响应头检查选项，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，可下载的配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo_server/"&gt;Hugo server command reference&lt;/a&gt;，开发服务器重新构建、内存渲染与实时刷新控制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>通过切换完整目录发布静态网站</title><link>https://hk.xbcatm.com/zh/notes/publish-a-static-site-by-switching-complete-directories/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/publish-a-static-site-by-switching-complete-directories/</guid><description>在活动树旁边预置一份完整制品，并只在验证后替换目录名。</description><content:encoded>&lt;p&gt;直接把新文件复制到活动的静态目录上，会造成一段访问者可能从一个版本拿到 HTML、从另一个版本拿到资源的时期。更安全的模式是：在活动树旁预置一棵完整的树，验证它，再改变目录名。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/atomic-switch.detail.zh.svg" alt="由单一发布者完成同文件系统与预置树核验后，live 存放旧版，stage 存放新版。第一次把 live 移到 rollback，live 缺失而 stage 与 rollback 保留。第二次把 stage 移到 live。成功时 live 为新版、rollback 为旧版；失败时 live 仍缺失，需要显式尝试恢复。恢复失败必须检查路径；发布后还须验证公共响应。" width="720" height="1890"&gt;&lt;figcaption&gt;两次移动之间 live 缺失，崩溃可能延长空窗。恢复需要尝试并验证。浏览器各次请求彼此独立，旧资源 URL 必须继续可访问。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="把所有发布目录放在同一文件系统上"&gt;把所有发布目录放在同一文件系统上&lt;/h2&gt;
&lt;p&gt;把活动树、预置树与回滚树放在同一文件系统上。以下片段假设使用同一个 Bash 会话、GNU Coreutils，且只有一个发布进程独占这些目录名。任何命令或复制管道任一侧失败，都应停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -P /srv/www
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在该父目录下创建唯一命名的中转目录。不要把未解析的变量或宽泛通配符当成删除目标来复用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /srv/www/site.new.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="填充并验证预置目录"&gt;填充并验证预置目录&lt;/h2&gt;
&lt;p&gt;把生成制品复制进空目录，然后检查代表性文件。最后一条命令只显示数量；应与已核验制品的期望数量或清单比较，不能把任意数字都当作通过：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -C public -cf - . &lt;span class="p"&gt;|&lt;/span&gt; tar -C &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -xf -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;/index.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;/404.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -type f &lt;span class="p"&gt;|&lt;/span&gt; wc -l
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当工具支持目录参数时，对预置路径运行链接与校验和检查。还要确认服务用户能穿过目录并读取文件：&lt;code&gt;mktemp -d&lt;/code&gt; 初始权限较严，解包后的制品必须具备预期权限。未通过验证的预置目录绝不能获得活动目录的名字。&lt;/p&gt;
&lt;h2 id="用显式回滚路径切换名字"&gt;用显式回滚路径切换名字&lt;/h2&gt;
&lt;p&gt;在第一次移动前解析每个路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;live&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date -u +%Y%m%dT%H%M%SZ&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;rollback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site.old-&lt;span class="nv"&gt;$stamp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; ! -e &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -P &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;live_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat -c %d -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat -c %d -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;/index.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Restore failed; inspect %s and %s before retrying.\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是两步切换，不是一次原子替换：两次移动之间活动路径不存在，崩溃后可能一直缺失，直到恢复。GNU &lt;code&gt;mv -T&lt;/code&gt; 可防止已有目录被悄悄当作容器、把发布树移进其中，但不能协调并发发布进程。文件系统检查结果不一致时不要继续。平台特定的原子交换或符号链接替换能消除名字缺失窗口，但必须结合实际服务器测试。&lt;/p&gt;
&lt;h2 id="验证公共边缘"&gt;验证公共边缘&lt;/h2&gt;
&lt;p&gt;切换之后，请求首页、一个深层路由、一个带指纹的资源与一个缺失路径。把公共字节与预置制品比较。在检查完成与观察窗口结束之前，保留回滚目录。&lt;/p&gt;
&lt;p&gt;即使目录名切换是原子的，浏览器请求 HTML 与资源仍不是同一笔事务。让旧指纹资源继续可访问，才能服务缓存中或传输中的 HTML；仅保留一个不对外服务的回滚目录并不能做到这一点。文件系统重命名也不自动保证崩溃后的持久性，这需要平台相应的持久化步骤。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html"&gt;POSIX &lt;code&gt;rename()&lt;/code&gt;&lt;/a&gt;，名字替换语义，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/rename.2.html"&gt;Linux &lt;code&gt;rename(2)&lt;/code&gt;&lt;/a&gt;，文件系统与替换行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/tar/manual/tar.html"&gt;GNU tar manual&lt;/a&gt;，归档创建与解包，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/Target-directory.html"&gt;GNU Coreutils: 目标目录&lt;/a&gt;，&lt;code&gt;-T&lt;/code&gt; 与目录嵌套风险，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/stat-invocation.html"&gt;GNU Coreutils: stat&lt;/a&gt;，文件系统设备号检查，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布清单应点名每一个已发布的字节</title><link>https://hk.xbcatm.com/zh/notes/a-release-manifest-should-name-every-published-byte/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-manifest-should-name-every-published-byte/</guid><description>生成确定性的文件清单，并在传输前后比较。</description><content:encoded>&lt;p&gt;静态制品是一个目录，而不是单个可执行文件。只记录源码提交或首页校验和会留下缺口：缺失的图片、陈旧别名或变化的下载可能悄悄留下来。传输前，应冻结制品并枚举其中每个常规文件。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-manifest.detail.zh.svg" alt="冻结的常规文件制品被传输到目标端，两端分别生成位于制品树外的有序清单。目标清单中多出示意的 old.js 记录；两份清单汇入完整记录比较。另一条分支对源端清单做哈希以生成发布标识。逐项校验不能发现额外文件，字节一致也不能证明作者身份。" width="720" height="1990"&gt;&lt;figcaption&gt;测量前停止写入、拒绝符号链接和特殊文件，并将清单保存在制品树外。逐项校验发现缺失或变化的字节；重新生成并比较完整清单还可发现多余文件。对可信源端清单做哈希可得到紧凑标识，真实性与元数据仍需独立控制。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="生成稳定的列表"&gt;生成稳定的列表&lt;/h2&gt;
&lt;p&gt;从包含 &lt;code&gt;public/&lt;/code&gt; 的目录，在 Bash 中使用 GNU findutils 与 coreutils 运行以下命令。清单应保存在被测目录之外。发布只能包含目录和常规文件：先检查并拒绝符号链接或特殊文件，因为它们不在该清单的契约内。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;nonregular&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find . ! -type d ! -type f -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$nonregular&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 -r sha256sum --
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;先排序文件名，再计算哈希。工具之间用 NUL 分隔路径，可保留空格、制表符与内嵌换行；GNU &lt;code&gt;sha256sum&lt;/code&gt; 会在普通可校验输出中转义特殊文件名。不要用 &lt;code&gt;awk '{print $2}'&lt;/code&gt; 解析该输出：文件名中可以合法包含空白。单独赋值会保留 &lt;code&gt;find&lt;/code&gt; 失败时的退出状态；只检查其输出，可能把扫描失败误认成空结果。扫描或管道一旦失败，就应把清单视为不完整，不得发布。测量期间停止写入；这些命令不会创建文件系统快照。&lt;/p&gt;
&lt;h2 id="传输后验证"&gt;传输后验证&lt;/h2&gt;
&lt;p&gt;在目标端的同一个可丢弃 Bash 会话中，按顺序运行后续片段，使路径变量与 &lt;code&gt;set&lt;/code&gt; 选项保持生效。将清单与制品路径设为实际位置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site.new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$artifact&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&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="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;observed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.observed.XXXXXX&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -f &amp;#34;$observed&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$artifact&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;nonregular&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find . ! -type d ! -type f -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$nonregular&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 -r sha256sum --
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$observed&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;diff -u &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$observed&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&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;h2 id="为发布记录增加聚合摘要"&gt;为发布记录增加聚合摘要&lt;/h2&gt;
&lt;p&gt;对清单本身做哈希，得到紧凑的发布标识：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&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;h2 id="不要把完整性等同于真实性"&gt;不要把完整性等同于真实性&lt;/h2&gt;
&lt;p&gt;SHA-256 摘要一致，是字节一致的强实用证据，并不能证明作者身份。攻击者若能同时替换制品与清单，也能让检查通过。签名与可信分发是独立控制；只应校验通过预期可信渠道取得的清单。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/sha2-utilities.html"&gt;GNU Coreutils: sha2 utilities&lt;/a&gt;，校验和生成与验证；通过下列上游手册源码核对，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/coreutils/coreutils/master/doc/coreutils.texi"&gt;GNU Coreutils 手册源码&lt;/a&gt;，非 tagged 校验和的转义、&lt;code&gt;--check&lt;/code&gt; 与 NUL 分隔排序，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man1/sha256sum.1.html"&gt;sha256sum(1)&lt;/a&gt;，命令行为与输出格式，已核验 2026-07-16。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Simple-Command-Expansion.html"&gt;GNU Bash: Simple Command Expansion&lt;/a&gt;，纯赋值命令保留命令替换的退出状态，已核验 2026-10-18。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>用两条命令读取公共边缘</title><link>https://hk.xbcatm.com/zh/notes/read-the-public-edge-in-two-commands/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/read-the-public-edge-in-two-commands/</guid><description>读取响应头与证书日期，同时区分到期、身份与字节一致性的验证。</description><content:encoded>&lt;p&gt;部署完成意味着公共响应符合发布契约，而不是最后一条上传命令退出码为零。&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;操作套件&lt;/a&gt;里的两个只读工具能提供有用证据，但不能证明整个制品都已正确发布。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/field-output.detail.zh.svg" alt="源文中的示意片段展示 HTTP_STATUS 200 与 FINAL_URL，以及完整的 2026 年证书到期时间和 30 天到期检查结果。注释区分命令成功与 HTTP 状态，以及到期与 notBefore、主机名和信任检查；正文比较与 SAN 身份策略另行验证。" width="720" height="1970"&gt;&lt;figcaption&gt;这些片段仅为示意。分别判断 HTTP 状态与到期结果；主机名、信任与正文一致性需要对应的独立检查。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="检查响应头"&gt;检查响应头&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check-http-headers.sh&lt;/code&gt; 发出 GET、丢弃正文、跟随重定向，打印每一跳的响应头以及最终状态和 URL：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;check-http-headers.sh https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以下输出只演示要检查的字段，并非刚刚采集的生产观察结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept-ranges: bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-type: text/html; charset=utf-8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 11:20:54 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;permissions-policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;referrer-policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;strict-transport-security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-content-type-options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-frame-options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP_STATUS 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FINAL_URL https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核对预期状态、最终主机与路径，以及最终响应上的策略。工具没有使用 curl 的 &lt;code&gt;--fail&lt;/code&gt;：HTTP 404 或 500 仍可能使 shell 命令成功结束。必须判断它打印的状态。响应头不一致需要调查服务器、代理或缓存配置；单凭这种不一致，不能证明活动输出目录选错了。&lt;/p&gt;
&lt;h2 id="检查证书日期"&gt;检查证书日期&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check-tls-expiry.sh HOST [DAYS]&lt;/code&gt; 提取使用 SNI 时服务器呈现的证书，并检查距离到期是否至少还有指定天数对应的秒数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;check-tls-expiry.sh www.xbcatm.com &lt;span class="m"&gt;30&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="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;subject=CN=www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;issuer=C=US, O=Let&amp;#39;s Encrypt, CN=YE1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notBefore=Aug 21 08:37:13 2026 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notAfter=Nov 19 08:37:12 2026 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;certificate remains valid for at least 30 day(s)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;工具打印主体、签发者、日期与 SHA-256 指纹，不打印剩余天数。成功消息只描述到期检查：脚本没有强制验证主机名或信任链，&lt;code&gt;-checkend&lt;/code&gt; 也不验证 &lt;code&gt;notBefore&lt;/code&gt;。主体 CN 不足以证明主机身份；服务身份应与证书的主体备用名称匹配。有效的通配符也可能合法覆盖该主机。&lt;/p&gt;
&lt;p&gt;需要验证这些属性时，另行运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -verify_hostname www.xbcatm.com -verify_return_error &amp;lt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这使用已配置的信任库，并在 OpenSSL 匹配规则下的证书验证出错时失败。在 OpenSSL 3.0 中，没有合适的 DNS 主题备用名称时，主机名验证可能回退到主题 CN。因此，命令成功本身不能证明符合 RFC 9525 仅使用 SAN 的身份策略：还要检查 SAN 扩展，并单独验证目标客户端的策略。TLS 工具本身没有网络截止时间，应在任务层设置超时。证书检查与自动续期配置仍是两项独立检查。&lt;/p&gt;
&lt;h2 id="在发布前后使用两者"&gt;在发布前后使用两者&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;发布前：保存预期策略、最终 URL、验证器与证书日期，并注明 UTC 采集时间。&lt;/li&gt;
&lt;li&gt;部署已经验证的制品。&lt;/li&gt;
&lt;li&gt;发布后：重复探测，并下载代表性正文与该制品比较。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;响应头与证书日期不能证明字节一致。&lt;code&gt;Last-Modified&lt;/code&gt; 可能不变，也可能在复制中被重写。&lt;a href="https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/"&gt;发布冒烟测试&lt;/a&gt;保存正文并直接比较；完整清单可将检查扩展到单个页面之外。浏览器行为、不同地区边缘的差异，以及只在负载下出现的故障，需要另行测试。记录实际检查过的路径与观察点。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，GET、重定向、响应头输出与 HTTP 失败行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;，SNI、显式主机名检查与验证错误时失败，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-verification-options/"&gt;OpenSSL verification options&lt;/a&gt;，主机名匹配与主题 CN 回退，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-x509/"&gt;OpenSSL &lt;code&gt;x509&lt;/code&gt;&lt;/a&gt;，证书显示与只检查到期的 &lt;code&gt;-checkend&lt;/code&gt; 行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9525.html"&gt;RFC 9525 — Service Identity in TLS&lt;/a&gt;，主体备用名称与通配符身份匹配，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/automatic-https"&gt;Caddy Automatic HTTPS&lt;/a&gt;，自动证书管理与续期配置，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP"&gt;MDN: Content Security Policy guide&lt;/a&gt;，所示策略的背景说明，已核验 2026-07-13。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，响应元数据与验证器不能证明完整制品匹配，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布在公共边缘一致之前不算完成</title><link>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</guid><description>把生成制品与访问者实际能收到的响应进行比较。</description><content:encoded>&lt;p&gt;静态发布消除了应用运行时的复杂度，但不会消除生成目录与公共响应之间的边界。发布检查应同时覆盖两边。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/diagrams/release-path.svg" alt="一条小的静态发布路径" /&gt;
&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-edge.detail.zh.svg" alt="先在使用预期配置的待发布服务器上检查确切制品，切换目录后在公共边缘重复检查。将 index.html、下载资源和 404.html 与解码后的 GET 正文配对。字节比较与预期状态、类型、缓存、安全头分别检查。重复中英文样本，并在新目录通过检查前保留上一个已知良好目录。" width="720" height="2200"&gt;&lt;figcaption&gt;切换前后使用同一份制品。下载行对应文中的 public/downloads/check-http-headers.sh。cmp 比较前会解码受支持的内容编码；服务器转换需有明确比较规则。状态和响应头另行检查，并保留前一目录供主动回滚。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="从干净的制品开始"&gt;从干净的制品开始&lt;/h2&gt;
&lt;p&gt;使用固定生成器构建到干净的目录。记录源码提交、生成器版本、输出文件列表，以及用户会下载的文件的校验和。一次干净的构建能捕获复制式部署可能会意外保留的陈旧文件。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文件列表是证据，不是装饰。如果某条路由消失，diff 应能说明是该来源被移除，还是生成器未能产出它。&lt;/p&gt;
&lt;h2 id="演练三类响应"&gt;演练三类响应&lt;/h2&gt;
&lt;p&gt;发布前，用预期服务器配置测试待发布制品。切换目录后，在公共边缘重复检查；探测仍在线的旧站不能验证新发布。请求一个 HTML 页面、一个静态资源与一个缺失路径。检查状态码、内容类型、缓存策略与安全头：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D page.headers -o page.body https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D asset.headers -o asset.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/downloads/check-http-headers.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -sS -D missing.headers -o missing.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/does-not-exist
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/index.html page.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/downloads/check-http-headers.sh asset.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/404.html missing.body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把每个片段作为独立 shell 脚本运行，或放在可丢弃的 shell 会话中执行。&lt;code&gt;set -eu&lt;/code&gt; 让这个顺序脚本在构建、传输或比较失败后停止，避免后面的成功命令掩盖先前失败。缺失路径的请求有意不用 &lt;code&gt;-f&lt;/code&gt;：404 是预期结果，仍应被捕获。检查各响应头文件的最终状态：依次为 &lt;code&gt;200&lt;/code&gt;、&lt;code&gt;200&lt;/code&gt;、&lt;code&gt;404&lt;/code&gt;；字节比较不会自动完成状态码检查。这里不跟随重定向。&lt;code&gt;--compressed&lt;/code&gt; 在比较前解码受支持的内容编码；若服务器有意转换 HTML，需要另行说明比较规则。&lt;code&gt;cmp&lt;/code&gt; 相等时不输出，有差异时返回非零。在 &lt;code&gt;/zh/&lt;/code&gt; 及其错误页上重复检查，不能直接假设两种语言一致。&lt;/p&gt;
&lt;h2 id="让回滚保持简单"&gt;让回滚保持简单&lt;/h2&gt;
&lt;p&gt;在新目录通过检查之前，不要删除上一个目录。回滚应选择一个已知良好目录，而不是在事故中凭记忆重新构造一个。&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;发布检查清单&lt;/a&gt;与&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;HTTP 响应头检查&lt;/a&gt;都刻意做得足够小，能在干净的 shell 里运行。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，状态与表示语义，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，响应检查选项，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>DNS 变更需要一个感知缓存的回滚窗口</title><link>https://hk.xbcatm.com/zh/notes/dns-changes-need-a-cache-aware-rollback-window/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/dns-changes-need-a-cache-aware-rollback-window/</guid><description>在变更前临时降低 TTL，并不会抹掉已用旧值缓存的应答。</description><content:encoded>&lt;p&gt;改一条地址记录很容易，预测每个解析器何时停止使用旧应答却很难。生存时间（TTL）值随 DNS 响应传播，控制通常情况下的缓存新鲜度。在迁移那一刻降低 TTL，并不能改写更早被缓存下来的应答。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/dns-ttl.detail.zh.svg" alt="至少提前一个旧 TTL 降低 TTL。两个示意递归缓存，在权威切换后不同时间内仍保留旧应答。迁移与观察期间，新旧端点都服务预期主机名。权威证据、递归缓存状态、A 与 AAAA 记录、CNAME 链及 SOA 决定的否定 TTL 应分别检查。过期应答、应用缓存与长连接，使 TTL 无法成为统一下线期限。" width="720" height="3260"&gt;&lt;figcaption&gt;TTL 随每个应答传播。切换前做好准备，让两侧持续有效，并结合旧端点流量与余量决定下线和回滚。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="在事件前降低-ttl"&gt;在事件前降低 TTL&lt;/h2&gt;
&lt;p&gt;如果既有记录是一天的 TTL，至少要在计划切换前一个旧 TTL 时就把它降低。这样在权威记录仍指向旧端点时，缓存有时间去取较短的值。确切间隔应写进变更计划，而不是在迁移中即兴执行。&lt;/p&gt;
&lt;p&gt;单独记录权威服务器的应答。下面两条探针比较系统配置的解析器与公共递归解析器，并不天然属于权威查询：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig +noall +answer www.example.test A
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig @1.1.1.1 +noall +answer www.example.test A
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;要取得权威证据，应向该区域实际的名称服务器发起非递归查询，并检查 &lt;code&gt;AA&lt;/code&gt; 标志、状态、答案与权威部分。递归解析器的剩余 TTL 反映其当前缓存状态，不同解析器可以合理地不同。还应检查服务使用的 AAAA 与 CNAME 链，不能只检查 A。&lt;/p&gt;
&lt;h2 id="让两侧都保持有效"&gt;让两侧都保持有效&lt;/h2&gt;
&lt;p&gt;切换期间，保持旧端点与新端点都能为预期主机名提供服务。TLS 名称、重定向与应用配置在两侧必须一致。权威记录改变后立刻移除旧端点，会给仍持有先前应答的客户端造成本可避免的停机。&lt;/p&gt;
&lt;p&gt;否定应答也会被缓存。一个最近返回 &lt;code&gt;NXDOMAIN&lt;/code&gt; 的名字，其出现可能很慢才可见，因为根据区域的权威数据，否定响应可能仍留在递归缓存里。&lt;/p&gt;
&lt;h2 id="观察后恢复-ttl"&gt;观察后恢复 TTL&lt;/h2&gt;
&lt;p&gt;一旦权威与具有代表性的递归应答都指向新端点，且旧缓存窗口已过，就恢复正常 TTL。在变更记录中保留旧值、新值、权威应答与回滚条件。&lt;/p&gt;
&lt;p&gt;TTL 不是所有客户端统一的下线期限。解析器在解析失败时可能提供过期数据，长连接与应用缓存也可能超过 DNS 新鲜期。应观察旧端点流量并预留余量；把 DNS 改回去同样有缓存窗口。对否定应答，应检查由 SOA 决定的否定 TTL，不能假设新 A 记录的 TTL 适用。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc1034.html"&gt;RFC 1034 — Domain Names: Concepts and Facilities&lt;/a&gt;，缓存模型，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc1035.html"&gt;RFC 1035 — Domain Names: Implementation and Specification&lt;/a&gt;，TTL 字段，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc2308.html"&gt;RFC 2308 — Negative Caching of DNS Queries&lt;/a&gt;，否定缓存模型与保留的错误 TTL，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8767.html"&gt;RFC 8767 — 提供过期数据&lt;/a&gt;，DNS 解析失败时提供过期应答，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bind9.readthedocs.io/en/v9.20.23/manpages.html#dig-dns-lookup-utility"&gt;BIND 9 &lt;code&gt;dig&lt;/code&gt; 手册&lt;/a&gt;，服务器选择与递归控制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>重启策略需要一个速率限制</title><link>https://hk.xbcatm.com/zh/notes/restart-policies-need-a-rate-limit/</link><pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/restart-policies-need-a-rate-limit/</guid><description>让 systemd 恢复瞬时故障，同时不掩盖永久性的配置错误。</description><content:encoded>&lt;p&gt;对长驻服务来说，自动重启很有用，但无界的循环会把一个清晰的失败变成一个嘈杂的失败。单元文件应同时描述正常进程生命周期与可接受的恢复量。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/restart-loop.detail.zh.svg" alt="程序执行、应用就绪和响应健康是三项独立检查。图中示例配置允许 60 秒内启动五次，快速失败后延迟五秒。再次尝试达到限制时，自动重启停止。较慢失败可能始终低于速率限制。单纯等到间隔结束不会排定新尝试；修复原因后应有意识地启动，或先清除计数器再启动。保存第一次失败日志，并核验就绪状态和健康响应。" width="720" height="3410"&gt;&lt;figcaption&gt;按配置窗口统计启动次数。启动速率限制可以停止快速反复重启，但不能证明健康，也不限制生命周期重试总数。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="捕获启动失败"&gt;捕获启动失败&lt;/h2&gt;
&lt;p&gt;对长驻进程，&lt;code&gt;Type=exec&lt;/code&gt; 能在启动时暴露配置程序执行失败，而不是把一次 fork 当成功。把它与显式的 &lt;code&gt;ExecStart&lt;/code&gt; 以及能完成工作所需最小权限的 &lt;code&gt;User&lt;/code&gt; 搭配使用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example foreground site service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;60s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;StartLimitBurst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/site/bin/server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Type=exec&lt;/code&gt; 确认程序已执行，并不确认应用已就绪。程序应保持前台运行；应用支持显式就绪通知时可用 &lt;code&gt;Type=notify&lt;/code&gt;。&lt;code&gt;Restart=on-failure&lt;/code&gt; 是恢复策略，不是健康检查。如果进程在提供错误响应的同时保持存活，systemd 不会知道站点坏了；外部冒烟测试仍有其职责。&lt;/p&gt;
&lt;h2 id="限制循环"&gt;限制循环&lt;/h2&gt;
&lt;p&gt;Systemd 通过 &lt;code&gt;StartLimitIntervalSec=&lt;/code&gt; 与 &lt;code&gt;StartLimitBurst=&lt;/code&gt; 应用启动速率限制。这两个指令放在 &lt;code&gt;[Unit]&lt;/code&gt;，&lt;code&gt;RestartSec&lt;/code&gt; 放在 &lt;code&gt;[Service]&lt;/code&gt;。示例允许 60 秒内启动五次；快速失败之间只有五秒重启延迟，因此可能达到限制。更长延迟可能让循环一直低于该速率：这并不是整个生命周期的重试次数预算。应按预期故障模式选择限制。当达到限制时，日志里应包含区分&amp;quot;重启成功&amp;quot;与&amp;quot;重复失败后放弃&amp;quot;所需的证据。&lt;/p&gt;
&lt;p&gt;在获准使用的测试主机上安装或修改单元后，先验证文件、重载管理器并检查有效配置，再重启。下列命令会改变服务状态，可能中断流量：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd-analyze verify /etc/systemd/system/example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl cat example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl show example.service -p Restart -p RestartUSec &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -p StartLimitIntervalUSec -p StartLimitBurst
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl restart example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status --no-pager example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u example.service -n &lt;span class="m"&gt;80&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不要用重启策略掩盖缺失文件、无效权限或糟糕的环境。先修复第一次失败，再测试恢复路径。&lt;/p&gt;
&lt;p&gt;启动限制停止自动重启后，单纯等到间隔结束不会自动排定下一次尝试。修复原因后，可等待间隔结束再启动，或有意识地用 &lt;code&gt;systemctl reset-failed example.service&lt;/code&gt; 清除计数器后启动。在隔离服务中测试可执行文件缺失与快速崩溃，并保存第一次失败日志，不要只留下最后的“达到启动限制”消息。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html"&gt;systemd.service&lt;/a&gt;，服务类型、重启与启动速率限制器，已核验 2026-08-03。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.unit.5.html"&gt;systemd.unit&lt;/a&gt;，通用单元段与 drop-in 语义，已核验 2026-08-03。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.service.xml"&gt;systemd.service upstream manual (v257)&lt;/a&gt;，Type=exec/oneshot、就绪、激活超时与重启语义，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.unit.xml"&gt;systemd.unit upstream manual (v257)&lt;/a&gt;，启动限制所属配置段、速率窗口与 reset-failed 行为，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>维护定时器应让其最近结果可见</title><link>https://hk.xbcatm.com/zh/notes/a-maintenance-timer-should-leave-its-last-result-visible/</link><pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-maintenance-timer-should-leave-its-last-result-visible/</guid><description>用一个 oneshot 服务、一个定时器、有界运行时间与日志证据搭配使用。</description><content:encoded>&lt;p&gt;cron 式语法并不是维护任务契约的全部。命令需要一个清晰的服务身份、一个有界的运行时间，以及能区分&amp;quot;未调度&amp;quot;与&amp;quot;运行但失败&amp;quot;的证据。一个 systemd 定时器与 oneshot 服务能把这些关注点分开。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/maintenance-timer.detail.zh.svg" alt="每小时调度、随机延迟、oneshot 激活与结果证据是时间线上的独立阶段。错过的日历事件合并为一次补跑，仍受随机延迟限制。两分钟激活超时还需考虑停止处理与工具自身的有界请求。成功可以回到非活动状态；必须对应 Result、退出状态和新的日志时间戳。服务仍活动时，重复定时器不会再启动实例。" width="720" height="3270"&gt;&lt;figcaption&gt;定时器触发只证明发生了调度事件。用服务结果、退出状态和新的时间戳判断本次运行。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="把工作定义成服务"&gt;把工作定义成服务&lt;/h2&gt;
&lt;p&gt;使用显式用户与绝对命令路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/site-check.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Read-only public site check&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sitecheck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/check-public-site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;TimeoutStartSec&lt;/code&gt; 限制 oneshot 的激活过程，其中包括命令运行时间；oneshot 默认没有启动超时。超时处理之后还可能需要停止超时，且不能立即结束不可中断的内核 I/O，因此工具本身也需要有界的网络请求。当契约失败时，helper 应返回非零退出码，并且不得修改它所检查的服务。&lt;/p&gt;
&lt;h2 id="调度该服务"&gt;调度该服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/site-check.timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;每小时运行公共站点检查&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Timer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;OnCalendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;site-check.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若至少错过一次日历事件，&lt;code&gt;Persistent=true&lt;/code&gt; 会在定时器重新激活时触发一次补跑；它不会逐小时重放所有错过的运行。补跑仍受随机延迟限制。&lt;code&gt;RandomizedDelaySec&lt;/code&gt; 把一个窗口内的例行工作分散开；当任务必须在精确时刻运行时它并不合适。&lt;/p&gt;
&lt;h2 id="启用前先验证"&gt;启用前先验证&lt;/h2&gt;
&lt;p&gt;创建两个单元文件后，先验证并重载管理器，再手动启动服务。这些是在目标测试主机上的管理操作，不是只读检查：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd-analyze verify /etc/systemd/system/site-check.service &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/systemd/system/site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd-analyze calendar hourly
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl start site-check.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status site-check.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u site-check.service -n &lt;span class="m"&gt;50&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl show site-check.service -p Result -p ExecMainCode -p ExecMainStatus
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只有当服务在预期的用户、环境、路径与网络访问下成功时才启用定时器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl list-timers site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="对服务结果告警"&gt;对服务结果告警&lt;/h2&gt;
&lt;p&gt;定时器触发并不等于成功。要监控 oneshot 服务的退出状态或日志结果。保持 stdout 简洁，调试需要的详细制品写入一个有界的路径。&lt;/p&gt;
&lt;p&gt;成功的 oneshot 通常回到 &lt;code&gt;inactive (dead)&lt;/code&gt;；这本身不是失败，&lt;code&gt;systemctl status&lt;/code&gt; 对非活动服务可能返回非零。应将 &lt;code&gt;Result&lt;/code&gt;、退出状态与新的日志时间戳对应，避免把旧运行的成功误认成本次结果。不要为了显示绿色而添加 &lt;code&gt;RemainAfterExit=yes&lt;/code&gt;：服务仍处于活动状态时，重复定时器不会启动一个新实例。日历时间默认使用主机时区，而且常规定时器精度会在随机延迟之外增加调度余量。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.timer.5.html"&gt;systemd.timer(5)&lt;/a&gt;，日历定时器、持久化与延迟行为，已核验 2026-08-04。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/systemd.time.7.html"&gt;systemd.time(7)&lt;/a&gt;，日历表达式语法，已核验 2026-08-04。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html"&gt;systemd.service&lt;/a&gt;，oneshot 服务语义，已核验 2026-08-04。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.service.xml"&gt;systemd.service upstream manual (v257)&lt;/a&gt;，Type=exec/oneshot、就绪、激活超时与重启语义，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.timer.xml"&gt;systemd.timer upstream manual (v257)&lt;/a&gt;，持久化会合并错过的运行；活动单元、随机延迟与精度，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>备份只有在演练过恢复之后才有用</title><link>https://hk.xbcatm.com/zh/notes/backups-are-only-useful-after-a-restore-rehearsal/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/backups-are-only-useful-after-a-restore-rehearsal/</guid><description>一份备份计划应证明重要文件能被找到、验证并恢复。</description><content:encoded>&lt;p&gt;&amp;ldquo;我们有备份&amp;quot;还不是一种恢复能力。一份有用的备份有已知范围、已知新鲜度、一个验证步骤，以及一份确实有人运行过的恢复流程。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/backup-restore.detail.zh.svg" alt="源码仓库提供权威构建输入，发布制品提供最近发布版本的恢复字节。取回归档和校验和，解包前核验，再以非特权用户将可信归档恢复到空隔离目标。记录校验和、权限、依赖、恢复目标与耗时。校验和不证明来源可信，两个非空页面也不等于完整验收。" width="720" height="2400"&gt;&lt;figcaption&gt;恢复演练从可识别的输入，以及取回并核验的存储副本开始。在可丢弃的隔离目标中恢复，再记录文件、权限、依赖与恢复耗时。校验和或归档存在本身，都不能证明可恢复。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="定义恢复单元"&gt;定义恢复单元&lt;/h2&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;li&gt;重要下载的校验和；&lt;/li&gt;
&lt;li&gt;保留期限与存储位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要把凭据、私钥、Cookie 或访问令牌放进网站备份。它们应属于独立的机密管理流程。&lt;/p&gt;
&lt;h2 id="存储前先验证"&gt;存储前先验证&lt;/h2&gt;
&lt;p&gt;打不开的归档不是备份。一个最小的验证循环如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -czf site-2026-06-03.tar.gz public/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -tzf site-2026-06-03.tar.gz &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum site-2026-06-03.tar.gz &amp;gt; site-2026-06-03.tar.gz.sha256
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;生成校验和只是记录基线，并未回读实际存储的副本。先从备份存储取回归档与校验和，再在解包前核验。校验和匹配能发现意外变化，但攻击者若可同时替换两个文件，它便不能证明归档来源可信。能列出成员也不等于备份了正确的文件。&lt;/p&gt;
&lt;h2 id="在某个可丢弃的地方恢复"&gt;在某个可丢弃的地方恢复&lt;/h2&gt;
&lt;p&gt;使用可信归档，以非特权用户身份解包到空临时目录或隔离主机，再检查入口点与一个有代表性的资源。不可信归档需要更强的隔离，因为路径与链接会影响解包行为。不要通过覆盖唯一活动副本来测试。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum -c site-2026-06-03.tar.gz.sha256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;restore_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -xzf site-2026-06-03.tar.gz -C &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$restore_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$restore_dir&lt;/span&gt;&lt;span class="s2"&gt;/public/index.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$restore_dir&lt;/span&gt;&lt;span class="s2"&gt;/public/404.html&amp;#34;&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;h2 id="诚实地度量新鲜度"&gt;诚实地度量新鲜度&lt;/h2&gt;
&lt;p&gt;备份日程只有在相对可能丢失的工作量时才有意义。如果源码一个月才变一次，每日快照可能不必要；如果某个资源每小时更新，一次月度归档又不够。&lt;/p&gt;
&lt;p&gt;写下目标恢复点与恢复时间，再选择能满足这些目标的存储与检查。当最后一次经过验证的恢复备份已经陈旧时，不要宣称&amp;quot;连续保护&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;至少保留一份位于活动主机故障与删除范围之外的可恢复副本。记录恢复耗时、恢复文件的校验和、权限与人工依赖；两个非空 HTML 文件只是冒烟测试，不是完整的恢复验收。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/tar/manual/tar.html"&gt;GNU tar manual&lt;/a&gt;，归档创建与验证行为，已核验 2026-06-03。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/sha2-utilities.html"&gt;GNU Coreutils: SHA-2 工具&lt;/a&gt;，生成校验和与 &lt;code&gt;--check&lt;/code&gt; 核验，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>可靠的默认行为胜过聪明的恢复</title><link>https://hk.xbcatm.com/zh/notes/reliable-defaults-beat-clever-recovery/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/reliable-defaults-beat-clever-recovery/</guid><description>一条可预测的首要路径比复杂的兜底更能预防事故。</description><content:encoded>&lt;p&gt;小服务往往在正常路径被充分理解之前，就先累积了恢复逻辑。每一次重试、备用端点与兼容分支，都增加了必须被观察和测试的一个状态。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/reliable-path.detail.zh.svg" alt="常见路径使用固定工具构建，检查完整目录，发布并验证公共响应，保留上一版目录。配置、运行时与文档一致指向一个选中发布。故障后先判断是否瞬时且可安全重复，仅符合条件的操作可使用次数与耗时预算重试，否则停止并修正。权限错误不进入重试；响应丢失时首次操作可能已成功。恢复显式回到保留的已知良好目录并记录结果。" width="720" height="3620"&gt;&lt;figcaption&gt;让常见路径可见。先判断并限制重试，再恢复到保留的已知良好状态，避免选择半成品副本。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="让常见路径变得无聊"&gt;让常见路径变得无聊&lt;/h2&gt;
&lt;p&gt;一个有用的默认行为是明确的、易于检视的、且可安全重复的。对静态站点，可以简单到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建自固定的工具链。&lt;/li&gt;
&lt;li&gt;检查生成的 HTML、资源与完整的待发布目录。&lt;/li&gt;
&lt;li&gt;发布该目录，再把公共响应与它核对。&lt;/li&gt;
&lt;li&gt;保留上一个目录以便回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这段序列不如多阶段部署系统炫目，但它的状态可见。当某处失败时，运维者能回答：问题在源码、构建、复制还是 Web 服务器。&lt;/p&gt;
&lt;h2 id="只对可限定范围的操作重试"&gt;只对可限定范围的操作重试&lt;/h2&gt;
&lt;p&gt;当故障可能是瞬时的、且重复操作不会改变其预期效果时，重试才有用。幂等不等于每次响应或附带日志都相同；响应丢失后，第一次尝试可能早已成功。从源站下载可能可安全重试；数据库迁移则未必。把每个错误都看作可重试的，会掩盖需要关注的边界。&lt;/p&gt;
&lt;p&gt;当重试适当时，记录尝试次数与最终原因。类似&amp;quot;发布失败：校验和不匹配&amp;quot;的日志条目，比一串静默重试后跟一个笼统超时更有可操作性。&lt;/p&gt;
&lt;h2 id="偏好单一权威状态"&gt;偏好单一权威状态&lt;/h2&gt;
&lt;p&gt;配置、运行时状态与文档应一致地说明哪个目录是活动的、哪条命令产生它。如果一个发布可以被不同的人复制到三条不同路径，恢复就变成了猜谜。&lt;/p&gt;
&lt;p&gt;对静态部署，一份简短的发布记录就够了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;release: 2026-07-20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;source: git commit &amp;lt;commit-id&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;builder: Hugo 0.165.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;output: public/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;verification: links + headers + checksums
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;占位符 commit ID 应按设计在发布时解析，而不是在文档里杜撰。&lt;/p&gt;
&lt;h2 id="恢复应回到已知状态"&gt;恢复应回到已知状态&lt;/h2&gt;
&lt;p&gt;当恢复把服务带回最后一个已知良好目录，而不是在几个半成品替代品之间选择时，恢复最容易。保留上一次输出直到新输出通过检查，再按保留策略移除旧副本。&lt;/p&gt;
&lt;p&gt;目标不是消除失败，而是把失败缩小到能解释、把恢复缩小到能信任。&lt;/p&gt;
&lt;p&gt;同时限制重试次数与总耗时。多个客户端可能一起重试时，使用带随机抖动的退避，并避免每一层都重试。权限错误与确定性的无效输入需要修正，而不是更长的重试循环。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，9.2.2 节关于幂等方法与重试，已核验 2026-07-20。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/"&gt;AWS Builders’ Library: Timeouts, retries, and backoff with jitter&lt;/a&gt;，重试放大、有界重试预算、退避与随机抖动，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>事故复盘应留下一份事实记录</title><link>https://hk.xbcatm.com/zh/notes/a-postmortem-should-leave-a-fact-record/</link><pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-postmortem-should-leave-a-fact-record/</guid><description>围绕捕获到的输出来组织事故记录，让复盘线程停留在证据而非记忆上。</description><content:encoded>&lt;p&gt;一份复盘在它的说法能被重新检查时才赢得信任。记录每项观察的来源、采集时间与时区、命令或查询，以及适用时的退出状态。日志、指标与明确注明来源的人工观察也属于证据；把已确认事实与假设分开，保存脱敏后的证据，不要保存秘密。下面的四个阶段让任何写下来的复盘在日后都可复核。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/postmortem.detail.zh.svg" alt="竖直事故时间线包含四项：检测连接采集结果，界定范围连接已测边界，修复连接配置差异与预期结果，验证连接重复检查的证据。每项都保留来源、采集时间和上下文。虚线不确定项区域与带负责人、到期日和验收检查的预防动作分开。" width="720" height="2860"&gt;&lt;figcaption&gt;每个阶段都要连接真实证据。修复后的采样成功仍是有范围的观察；原因、不确定项和有负责人的预防动作仍需明确记录。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="检测症状而且要带证据"&gt;检测：症状，而且要带证据&lt;/h2&gt;
&lt;p&gt;从最小的可验证症状与展示它的命令开始。保存真实采集结果。以下只是格式示例，并不代表刚刚执行过探测：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ check-http-headers.sh https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: ... script-src &amp;#39;none&amp;#39; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 11:20:54 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果症状是某个响应头变了，那么展示旧策略与新策略的输出应并排放在一起。如果是失败，请包含退出状态与错误行。&lt;/p&gt;
&lt;h2 id="界定范围影响多广以及你如何知道"&gt;界定范围：影响多广，以及你如何知道&lt;/h2&gt;
&lt;p&gt;用证据而不是形容词陈述影响范围。先界定能够检视的边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些主机与路径出现症状？记录每次探测的 URL 与观察时间；一次成功请求不能代表所有边缘或访问者的状态。&lt;/li&gt;
&lt;li&gt;缓存与源站是否不同？使用相同请求头，比较两条路径的实际响应、缓存元数据与验证器。&lt;code&gt;Vary&lt;/code&gt; 描述选择输入，不能证明响应来自缓存。&lt;/li&gt;
&lt;li&gt;什么因素拖延恢复？DNS 变更需记录旧 TTL 与解析器观察结果；HTTP 缓存需另记新鲜度与重新验证策略。DNS TTL 不是通用的回滚期限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="修复变更及其预期效果"&gt;修复：变更及其预期效果&lt;/h2&gt;
&lt;p&gt;记录实际配置差异与部署标识，让修复动作可理解，并在安全时可重放。下面的响应头片段是预期观察结果，不是 shell 命令，也不是完整策略。优先选择恢复契约的最小变更，并说明修复后运行应显示什么：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Content-Security-Policy: script-src &amp;#39;none&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="验证事后运行"&gt;验证：事后运行&lt;/h2&gt;
&lt;p&gt;变更后重复检测命令，并演示观察到的输出现在与预期契约一致。没有最后这一节的复盘只是一个故事；有了它，它就是一份允许自己未来被再检查的记录。&lt;/p&gt;
&lt;p&gt;最后记录促成事故的因素、仍不确定的事项，以及带负责人、到期日和验收检查的预防动作。一次探测成功只确认该时刻的采样路径；它本身不能确定原因，也不能防止复发。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，响应元数据与条件请求，已核验 2026-07-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;制品 HTTP 响应头检查&lt;/a&gt;，贯穿本记录使用的只读检查。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/sre-book/postmortem-culture/"&gt;Google SRE: Postmortem Culture&lt;/a&gt;，影响、促成因素、证据复核与预防动作，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;，缓存选择与新鲜度不同于 DNS 缓存寿命，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布一个静态站点并证明边缘一致</title><link>https://hk.xbcatm.com/zh/tutorials/ship-a-static-release/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/tutorials/ship-a-static-release/</guid><description>构建并暂存静态产物，切换发布指针，验证公共响应样本，并明确回滚与证据边界。</description><content:encoded>&lt;p&gt;一次发布，只有在所需公共边缘检查与选定产物一致时才算完成，而不是上传退出码为零就完成。本教程串起&lt;strong&gt;构建&lt;/strong&gt;、&lt;strong&gt;校验&lt;/strong&gt;、&lt;strong&gt;发布&lt;/strong&gt;与&lt;strong&gt;验证&lt;/strong&gt;，同时区分本地检查和部署变更。发布会改变服务器；结尾的探针才是只读网络观察。&lt;/p&gt;
&lt;p&gt;命令假定使用 Bash、GNU find/sort/sha256sum、rsync、Python 3 与项目固定版本的 Hugo。构建步骤从仓库根运行。接触线上主机前，先在一次性目录演练&lt;a href="https://hk.xbcatm.com/zh/decisions/adr-complete-directory-release/"&gt;目录切换示例&lt;/a&gt;。明确实际服务根、部署负责人、上一版目标与回滚过程。下面的 &lt;code&gt;/srv&lt;/code&gt; 布局要求部署已配置完成，不是仓库会自动安装的东西。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-release.detail.zh.svg" alt="构建并验证一份准确产物，最后一次构建后生成清单，核验全新的空暂存目录。选择前失败停止，旧版不变；选择时替换发布指针。之后对公共响应与产物文件进行采样核对，失败时新版仍被选中，直到显式回滚到所记录上一版并重新探测。原子命名空间替换不能保证跨请求一致或断电持久性。磁盘检查、交付字节、TLS 身份、到期、DNS 缓存变化与独立备份各有证据要求。" width="720" height="4720"&gt;&lt;figcaption&gt;指针切换改变失败边界：之前停止可保留旧版，之后需要显式回滚并重验。磁盘检查与边缘样本证明不同事实。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-从已记录输入构建一份产物"&gt;1. 从已记录输入构建一份产物&lt;/h2&gt;
&lt;p&gt;从干净源码版本开始，运行项目构建并记录已安装生成器版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git status --short
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git rev-parse HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;短状态为空可支持干净工作树的判断，但命令本身不会强制执行。在称其为某个提交的发布前，应检查所有变更。&lt;code&gt;make build&lt;/code&gt; 将短提交号和当前 UTC 构建时间写入 &lt;code&gt;data/buildinfo.toml&lt;/code&gt;，再生成 &lt;code&gt;public/&lt;/code&gt;。页面中的标记说明记录了哪些输入，不能证明有未提交修改的工作树只含那个提交。&lt;/p&gt;
&lt;p&gt;固定 Hugo 与 vendored 主题可减少漂移，但构建时间字段使不同时间的普通构建不一定逐字节相同。应构建选定发布一次、验证后传输这份准确产物，而不是在服务器另行重建。&lt;/p&gt;
&lt;h2 id="2-检查完整离线契约"&gt;2. 检查完整离线契约&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;make check&lt;/code&gt; 检查当前产物的生成视觉资源、离线包新鲜度、内部链接、无脚本输出、来源链接及下载校验和。完整仓库关卡则使用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make check-upstream
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整关卡还运行回归测试、双语文章检查和审阅清单新鲜度校验，再重新构建并检查输出。因此要在最后一次构建后才生成发布清单。构建成功及审阅记录未过期，不能证明每篇文章技术正确，也不证明公共服务器使用了预期头部。页数与耗时会变化，应保留实际输出，不与文章里复制来的计数作比较。&lt;/p&gt;
&lt;h2 id="3-用清单标识产物字节"&gt;3. 用清单标识产物字节&lt;/h2&gt;
&lt;p&gt;计算哈希前，确认 &lt;code&gt;public/&lt;/code&gt; 只包含目录与常规文件，并停止所有写入，直到暂存完成。下面的命令不会创建快照，也不会检查每种特殊文件类型。清单放在 &lt;code&gt;public/&lt;/code&gt; 外，避免把自身纳入哈希。NUL 分隔文件名可保留空格及其他 shell 敏感字符：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 sha256sum
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;GNU 校验行的摘要在文件名前。&lt;code&gt;sha256sum -c&lt;/code&gt; 成功说明已列出的文件匹配，不证明没有额外文件。全新的空发布目录可避免目标端残留旧文件，但不能发现计算哈希后在源端新增的额外文件。要证明常规文件清单完整一致，应按相同路径、排序与哈希规则重算暂存清单，在选择指针前比较完整列表。另行检查符号链接：&lt;code&gt;find -type f&lt;/code&gt; 不包含它们，而意外链接可能暴露预期根外的文件。将清单及其自身摘要留在发布记录中；校验和证明相对于清单的一致性，不能证明不可信清单的真实性。&lt;/p&gt;
&lt;h2 id="4-暂存并选择完整发布"&gt;4. 暂存并选择完整发布&lt;/h2&gt;
&lt;p&gt;假定 Caddy 已服务 &lt;code&gt;/srv/www/example-site/current&lt;/code&gt;，它是指向 &lt;code&gt;releases/&lt;/code&gt; 内部的符号链接，所有权与读取权限正确。&lt;code&gt;releases&lt;/code&gt; 父目录须事先存在。发布者必须串行执行，并选择唯一发布 ID。把此块作为独立 Bash 脚本，在部署机器上包含已验证 &lt;code&gt;public/&lt;/code&gt; 与 &lt;code&gt;release.sha256&lt;/code&gt; 的目录中运行。运行前按部署策略检查产物的所有权与读取权限。此块自行设置清单路径和遇错停止策略；前置条件、复制或校验和失败时，必须在选择指针前停止。它拒绝暂存产物内的符号链接；这是会修改部署的步骤。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/example-site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;release_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20261015T120000Z
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/releases/&lt;/span&gt;&lt;span class="nv"&gt;$release_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/current&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;readlink &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/current&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# Fail if this release already exists.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -a public/ &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -type l -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$links&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Refusing symlinks in the staged artifact&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Ownership and readable permissions must match the deployment policy.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 - &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;PYTHON&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;from pathlib import Path
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;import os, sys, uuid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;base = Path(sys.argv[1])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;release = base / &amp;#34;releases&amp;#34; / sys.argv[2]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;current = base / &amp;#34;current&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if not current.is_symlink() or not release.is_dir():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; raise SystemExit(&amp;#34;Expected current symlink and a complete release directory&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pending = base / (&amp;#34;.current-&amp;#34; + uuid.uuid4().hex)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pending.symlink_to(os.path.relpath(release, base), target_is_directory=True)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;os.replace(pending, current)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;print(&amp;#34;selected:&amp;#34;, os.readlink(current))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PYTHON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Previous target: %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;临时链接与 &lt;code&gt;current&lt;/code&gt; 共用父目录，因此替换通过同文件系统的一次 rename 完成。普通 &lt;code&gt;mv new existing-directory&lt;/code&gt; 不会交换两个非空目录。不要用此片段转换已有的真实目录服务根，应单独规划迁移。在预发布环境验证真实服务器的根解析及文件系统行为。&lt;/p&gt;
&lt;p&gt;旧目标仍在磁盘上，不等于切换后旧资源 URL 仍可访问。应在新服务命名空间或稳定资源库中保留旧版指纹资源字节。浏览器多个请求可能跨越两个发布，缓存也可能持有旧 HTML。原子命名空间替换不是跨请求快照隔离，也不是断电持久性保证。&lt;/p&gt;
&lt;h2 id="5-验证实际交付字节与策略"&gt;5. 验证实际交付字节与策略&lt;/h2&gt;
&lt;p&gt;切换后，用固定表示选择比较公共响应样本与相应产物文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -D served.headers -o served.html -w &lt;span class="s1"&gt;&amp;#39;%{http_code}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/index.html served.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh public/downloads/check-http-headers.sh &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从暂存时使用的同一产物目录运行此块。头部辅助脚本使用该可信产物内的副本；此步骤无需源码工作树。&lt;code&gt;cmp&lt;/code&gt; 成功证明这个首页样本逐字节匹配。遇到意外重定向、返回 200 的代理错误页、CDN 转换或编码响应时，需要调查。检查 &lt;code&gt;served.headers&lt;/code&gt; 中的 &lt;code&gt;Content-Encoding&lt;/code&gt;、缓存年龄、CSP 与内容类型。头部脚本打印响应及最终状态，不会强制所有预期条件。&lt;/p&gt;
&lt;p&gt;对中文内容、嵌套文章、CSS、SVG、下载项、重定向及错误路由重复适用检查。记录 URL 到文件的映射，包括目录索引。示例的逐项校验只证明所列文件的一致性，不证明暂存清单完整，也不证明所有公共缓存节点的交付结果。完整边缘论断需要更广泛抓取和表示感知检查；少数探针始终只是样本。&lt;/p&gt;
&lt;p&gt;切换后的检查失败时，新版仍被选中，直到你采取行动。用新临时链接指向已记录的 &lt;code&gt;previous&lt;/code&gt; 值，再次原子替换 &lt;code&gt;current&lt;/code&gt;，随后重做探针。不要只移走新树，诊断期间也不要删除证据或上一版。&lt;/p&gt;
&lt;h2 id="6-分别检查-tls-身份和到期窗口"&gt;6. 分别检查 TLS 身份和到期窗口&lt;/h2&gt;
&lt;p&gt;从仓库根运行到期辅助脚本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh static/downloads/check-tls-expiry.sh www.xbcatm.com &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它提取证书、打印日期、检查剩余到期间隔，却不是完整 TLS 身份测试：当前实现没有让主机名或信任验证失败成为致命错误。应按照&lt;a href="https://hk.xbcatm.com/zh/tutorials/audit-a-public-edge/"&gt;公共边缘信任教程&lt;/a&gt;执行显式验证握手与 SAN 检查。不能从“至少还有 30 天有效期”推断信任链或吊销检查成功。&lt;/p&gt;
&lt;p&gt;临近过期是另一项发布风险。保留实际 &lt;code&gt;notAfter&lt;/code&gt;、信任结果与客户端环境，不要把历史签发者或日期复制到发布日志。&lt;/p&gt;
&lt;h2 id="7-dns-变化时纳入缓存窗口"&gt;7. DNS 变化时纳入缓存窗口&lt;/h2&gt;
&lt;p&gt;纯内容发布不一定改变 DNS。若主机或边缘变化，应在缓存过期期间继续维护旧目标服务。变更时才降低 TTL，不会缩短已经按旧 TTL 缓存的条目。解析器 serve-stale、负缓存和客户端缓存会让窗口更复杂；TTL 不是保证所有客户端都完成迁移的统一截止点。CAA 变化影响签发策略，不负责把流量路由到新内容。&lt;/p&gt;
&lt;h2 id="8-保留演练过的恢复路径"&gt;8. 保留演练过的恢复路径&lt;/h2&gt;
&lt;p&gt;上一版目录有利于快速回滚，但不是独立备份。磁盘故障、误清理或部署账户被攻破，都可能影响所有本地发布。应保留源码、构建输入、配置及适当的独立产物备份，并在隔离环境演练恢复。一次成功演练证明的是那个时刻的那条恢复路径，不会永久保证之后的变更。&lt;/p&gt;
&lt;h2 id="9-精确记录结果"&gt;9. 精确记录结果&lt;/h2&gt;
&lt;p&gt;保留源码版本与干净工作树状态、构建元数据和工具版本、清单摘要、选中与上一版目标、探针时间、HTTP/TLS 观察及任何回滚。说明覆盖的是样本、全部映射 URL，还是多个观测点。关卡失败时，应记录失败与恢复过程，而不是“它工作过”。&lt;/p&gt;
&lt;p&gt;切换前失败可以让旧版保持不变；切换后恢复则是需要单独验证的显式操作。区分这两点，才能让发布路径可审计，而不假装每次失败都会自动恢复旧树。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;rsync 手册&lt;/a&gt;，归档复制与源路径尾斜线语义，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8767.html"&gt;RFC 8767：Serving Stale Data&lt;/a&gt;，TTL 过期后的 DNS serve-stale 行为，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://manpages.debian.org/trixie/coreutils/sha256sum.1.en.html"&gt;GNU sha256sum 手册，Debian 软件包&lt;/a&gt;，校验和输出及验证选项，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.python.org/3/library/os.html#os.replace"&gt;Python &lt;code&gt;os.replace&lt;/code&gt;&lt;/a&gt;，原子替换与文件系统限制，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt;&lt;/a&gt;，根与符号链接处理，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 手册&lt;/a&gt;，状态捕获、响应头及内容解码，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;，显式证书验证，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://reproducible-builds.org/docs/definition/"&gt;Reproducible Builds：定义&lt;/a&gt;，产物身份与可复现性的区别，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>