<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>小生说大声讲</title><link href="https://www.chenxiaosheng.com/" rel="alternate"/><link href="https://www.chenxiaosheng.com/feeds/all.atom.xml" rel="self"/><id>https://www.chenxiaosheng.com/</id><updated>2026-08-15T14:20:00+08:00</updated><entry><title>用 Hammerspoon 搞定远程锁屏与 SMB 动态挂载</title><link href="https://www.chenxiaosheng.com/posts/2026-08-15/hammerspoon-screen-sharing-and-smb-automount.html" rel="alternate"/><published>2026-08-15T14:20:00+08:00</published><updated>2026-08-15T14:20:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2026-08-15:/posts/2026-08-15/hammerspoon-screen-sharing-and-smb-automount.html</id><summary type="html">&lt;p&gt;结合 M4 Mac mini 与 M5 MacBook Air 的双机协作场景，使用 Hammerspoon 的数十行 Lua 代码解决远程屏幕共享防锁屏与 40 Gbps / Tailscale 多通道 SMB 动态安全挂载两个痛点。&lt;/p&gt;</summary><content type="html">&lt;h2&gt;01 我的双 Mac 工作流与两个真实痛点&lt;/h2&gt;
&lt;p&gt;我的日常工作流由两台 Mac 组成：
* 一台 &lt;strong&gt;M4 Mac mini&lt;/strong&gt;：放在工位充当算力中心，常驻跑一些耗时较长的 AI 任务与重型脚本，外挂了一块 2 TB 的高速 SSD；
* 一台 &lt;strong&gt;M5 MacBook Air&lt;/strong&gt;：主力移动设备，用来写方案、做设计、做决策，经常带去会议室或外出办公。&lt;/p&gt;
&lt;p&gt;在工位时，两台设备通过一条 40 Gbps 的高速 USB-C 线直连（建立高带宽的 Direct IP 连接）；外出时，则通过 Tailscale 组网连接。&lt;/p&gt;
&lt;p&gt;&lt;img alt="双 Mac 硬件架构与双通道网络拓扑" src="/static/hammerspoon-screen-sharing-and-smb-automount/workflow.jpg"&gt;&lt;/p&gt;
&lt;p&gt;看似美好的双机流，在实际使用中却撞上了两个极度影响体验的痛点。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;痛点一：Mac mini 远程屏幕共享——「人在不锁，人走即锁」&lt;/h3&gt;
&lt;p&gt;为了管理 Mac mini 上跑的 AI 任务，我经常在 MacBook Air 上通过 macOS 原生的 Screen Sharing（屏幕共享）连过去查看。&lt;/p&gt;
&lt;p&gt;这时就出现了两难：
1. &lt;strong&gt;保留系统默认闲置锁屏&lt;/strong&gt;：远程查看着训练日志或思考方案，稍微停顿几分钟，远程桌面就黑屏锁定，必须重新键入密码解锁。一天下来要输几十次密码，体验非常割裂。
2. &lt;strong&gt;彻底关闭锁屏密码&lt;/strong&gt;：极其危险。万一哪天有人给 Mac mini 插上便携显示器，或者直接在物理机上操作，就能毫无阻拦地直通桌面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心诉求&lt;/strong&gt;：只要我的屏幕共享连接着，就阻止锁屏；只要我一断开连接，立即恢复锁定。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;痛点二：大文件跨机共享——告别 Git 冗余，动态挂载外挂盘&lt;/h3&gt;
&lt;p&gt;两台电脑之间必然存在数据同步需求：
* 最初我习惯用 Git 做同步：Mac mini 这边跑完 &lt;code&gt;git commit&lt;/code&gt;，MacBook Air 那边 &lt;code&gt;git pull&lt;/code&gt;。
* 但很快遇到瓶颈：很多 AI 中间数据、方案设计素材体积非常大，且往往不到适合作为版本提交（commit）的时机。如果硬塞进 Git，会迅速吃光两台 Mac 宝贵的内置硬盘空间。&lt;/p&gt;
&lt;p&gt;顺理成章的解法是：&lt;strong&gt;把 Mac mini 外挂的 2 TB 硬盘通过 macOS 自带的 SMB 共享出来&lt;/strong&gt;，MacBook Air 直接挂载使用。&lt;/p&gt;
&lt;p&gt;但这个方案带来了新的难题：
1. &lt;strong&gt;多网络自动适应&lt;/strong&gt;：在工位插着 USB-C 线时，我希望优先走 40 Gbps 极速直连通道（&lt;code&gt;169.254.169.99&lt;/code&gt;）；带着 MacBook Air 移动办公时，又希望它自动切换走 Tailscale 的网络通道（&lt;code&gt;mini&lt;/code&gt; 主机名）。
2. &lt;strong&gt;安全与自动化的矛盾&lt;/strong&gt;：如果用常规 Shell 脚本执行 &lt;code&gt;mount_smbfs&lt;/code&gt; 命令，macOS 要求必须开启「Windows 文件共享」。开启时系统会明确警告「密码将以不安全的方式存储」。而 Finder（Cmd + K）之所以能免密秒进，是因为使用了两台 Mac 绑定的 Apple ID 凭据认证。
3. &lt;strong&gt;断网卡死与弹窗骚扰&lt;/strong&gt;：当网络未连通时，如果盲目执行挂载，macOS 会在后台阻塞等待 TCP 超时达数分钟，最后弹出烦人的系统错误对话框。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;02 解决方案：Hammerspoon 小工具解决大问题&lt;/h2&gt;
&lt;p&gt;以前遇到这种需求，通常得写一堆复杂的 Shell 脚本配 launchd 守护进程，或者安装好几个臃肿的常驻软件。&lt;/p&gt;
&lt;p&gt;直到用上了 &lt;strong&gt;Hammerspoon&lt;/strong&gt;。它的定位是 macOS 系统的 Lua 胶水层，体积极小，能直接调用系统底层的电源管理、网络事件和 AppleScript 接口。用几十行 Lua 配置，就把两个难题彻底理顺了。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;场景 1：精准控制 Mac mini 锁屏策略&lt;/h3&gt;
&lt;p&gt;在 Mac mini 端的 Hammerspoon 中，通过监听 5900 端口（macOS 屏幕共享的 VNC 端口）的连接状态，动态调用 &lt;code&gt;hs.caffeinate&lt;/code&gt; 电源管理接口：&lt;/p&gt;
&lt;p&gt;&lt;img alt="Mac mini 智能锁屏自动化流转逻辑" src="/static/hammerspoon-screen-sharing-and-smb-automount/smart_lock.jpg"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;检测到 5900 端口有 &lt;code&gt;ESTABLISHED&lt;/code&gt; 连接&lt;/strong&gt;：激活 &lt;code&gt;displayIdle&lt;/code&gt; 阻止屏幕变暗和锁屏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测到连接断开&lt;/strong&gt;：释放 &lt;code&gt;displayIdle&lt;/code&gt;，交还系统默认的锁屏策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ~/.hammerspoon/init.lua (运行在 Mac mini 上)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkVNC&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;netstat -an | grep &amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.5900 .*ESTABLISHED&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caffeinate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;displayIdle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
            &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Hammerspoon&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;informativeText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Screen Sharing 已连接，防锁屏已启用&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;}):&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="kr"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caffeinate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;displayIdle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
            &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Hammerspoon&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;informativeText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Screen Sharing 已断开，防锁屏已关闭&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;}):&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;span class="kr"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- 每 3 秒检测一次&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doEvery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkVNC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这样既免去了远程操作时频繁输密码的烦恼，又保证了断开连接后即使物理接上显示器也是锁屏状态。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;场景 2：USB-C 直连 / Tailscale 多通道智能挂载&lt;/h3&gt;
&lt;p&gt;在 MacBook Air 端的 Hammerspoon 中，实现了以下全自动逻辑：&lt;/p&gt;
&lt;p&gt;&lt;img alt="SMB 动态双通道自动挂载与 445 端口预检逻辑" src="/static/hammerspoon-screen-sharing-and-smb-automount/smb_automount.jpg"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;445 端口 1 秒静默预检&lt;/strong&gt;：在发起挂载前，先用 &lt;code&gt;nc -z -w 1&lt;/code&gt; 测试目标的 445（SMB）端口。如果网络不通，1 秒内静默跳过，彻底避免系统弹窗与数分钟的线程阻塞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复用原生 Apple ID 认证&lt;/strong&gt;：通过 &lt;code&gt;hs.osascript&lt;/code&gt; 调用 AppleScript 的 &lt;code&gt;mount volume&lt;/code&gt; 命令。不仅免去在脚本中硬编码密码，还完全不需要开启不安全的「Windows 文件共享」，保持最高安全级别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级与静默 Fallback&lt;/strong&gt;：优先挂载 USB-C 高速直连通道（&lt;code&gt;169.254.169.99&lt;/code&gt;）；若不通则静默回退到 Tailscale 节点（&lt;code&gt;mini&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核事件监听&lt;/strong&gt;：通过 &lt;code&gt;hs.network.reachability&lt;/code&gt; 监听网络状态，插上网线或网络连通瞬间即刻自动触发挂载。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ~/.hammerspoon/init.lua (运行在 MacBook Air 上)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;servers&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;169.254.169.99&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;mini&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;-- 优先 USB-C 直连，Fallback 到 Tailscale&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;shareName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;TiPlus&amp;quot;&lt;/span&gt;                     &lt;span class="c1"&gt;-- Mac mini 外挂的 2 TB 共享名&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;mountTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;

&lt;span class="c1"&gt;-- 检查是否已挂载（兼容 /Volumes/TiPlus 及 TiPlus-1 等路径）&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isMounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dir_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/Volumes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
        &lt;span class="kr"&gt;for&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="kr"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dir_obj&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
            &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;TiPlus&amp;quot;&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
                &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/Volumes/&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
            &lt;span class="kr"&gt;end&lt;/span&gt;
        &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="kr"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- 静默预检：1 秒内检测 445 端口，拦截系统报错弹窗&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isPortOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;string.format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nc -z -w 1 %s 445 &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- 挂载核心函数&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mountTiPlus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manualAlert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isMounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mounted&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[TiPlus] 已经挂载于: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;currentPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;manualAlert&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TiPlus 已经挂载于 &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;currentPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
        &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="kr"&gt;end&lt;/span&gt;

    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;manualAlert&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;正在尝试挂载 TiPlus...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;-- 依次尝试高速直连 IP 与 Tailscale 域名&lt;/span&gt;
    &lt;span class="kr"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt; &lt;span class="kr"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[TiPlus] 正在预检服务器 &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; 端口...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isPortOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverHost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
            &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;shareURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;smb://&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;shareName&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[TiPlus] 端口连通，执行安全挂载: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;shareURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;osascript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applescript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mount volume &amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;shareURL&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[TiPlus] 挂载成功，当前通道: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;manualAlert&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TiPlus 挂载成功（&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;）&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
                &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="kr"&gt;end&lt;/span&gt;
        &lt;span class="kr"&gt;else&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[TiPlus] 节点 &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; 离线，静默跳过&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="kr"&gt;end&lt;/span&gt;

    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[TiPlus] 当前所有网络通道均不可达&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;manualAlert&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TiPlus 挂载失败，网络未连通&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="kr"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- 1. 启动或重载配置时主动检测一次&lt;/span&gt;
&lt;span class="n"&gt;mountTiPlus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. 网络事件监听：无论哪条通道恢复连通，自动尝试挂载&lt;/span&gt;
&lt;span class="kr"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serverHost&lt;/span&gt; &lt;span class="kr"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;reachability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reachability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forHostName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverHost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reachability&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;reachability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;setCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;isReachable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reachability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reachable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isReachable&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
                &lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mountTimer&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt; &lt;span class="n"&gt;mountTimer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kr"&gt;end&lt;/span&gt;
                &lt;span class="n"&gt;mountTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="n"&gt;mountTiPlus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="kr"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kr"&gt;end&lt;/span&gt;
        &lt;span class="kr"&gt;end&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kr"&gt;end&lt;/span&gt;
&lt;span class="kr"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. 快捷键手动挂载 (Option + Command + M)&lt;/span&gt;
&lt;span class="n"&gt;hs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;alt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;cmd&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;M&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mountTiPlus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;03 总结与体会&lt;/h2&gt;
&lt;p&gt;在配置好之后，整个工作流的体验变得极其顺畅：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;远程控制 Mac mini&lt;/strong&gt;：屏幕共享连上即用，中间查资料再也不会频繁被锁屏打断；断开连接后自动锁定，安全无虞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据共享&lt;/strong&gt;：插上 40 Gbps 的 USB-C 线，极速共享盘瞬间挂载到位；拔掉线出门，切到 Tailscale 依然能在后台无感访问，不需要在命令行敲密码，更不需要降级安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hammerspoon 确实是个非常轻量又方便的小工具，用几十行代码就能把日常使用中很具体的痛点给顺畅解决掉，推荐给有类似需求的朋友。&lt;/p&gt;</content><category term="misc"/><category term="mac"/><category term="hammerspoon"/><category term="lua"/><category term="automation"/><category term="smb"/><category term="screensharing"/></entry><entry><title>上下文、压缩与缓存：大模型使用者必须搞懂的三个概念</title><link href="https://www.chenxiaosheng.com/posts/2026-08-02/llm-context-compaction-caching.html" rel="alternate"/><published>2026-08-02T22:50:00+08:00</published><updated>2026-08-02T22:50:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-08-02:/posts/2026-08-02/llm-context-compaction-caching.html</id><summary type="html">&lt;p&gt;100 万上下文到底指什么？什么时候该压缩、Claude Code 与 Codex 各自何时自动触发？缓存为什么会失效、失效了到底损失什么？本文以 Anthropic 与 OpenAI 官方文档为准，用 7 张图把这三个概念串成一条因果链，并给出可直接照做的日常实践。&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;面向日常重度使用 Claude Code / Codex 等 AI 编程工具的工程师。所有涉及 Claude 与 Codex 的技术细节均以官方文档为准，文末附完整出处。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;0. 先给一个心智模型&lt;/h2&gt;
&lt;p&gt;这三个概念不是并列关系，而是一条因果链：&lt;/p&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart LR
    A["&amp;lt;b&amp;gt;上下文窗口&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;模型单次能看到的全部内容&amp;lt;br/&amp;gt;（有硬上限）"]
    B["&amp;lt;b&amp;gt;压缩 Compaction&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;窗口快满了怎么办&amp;lt;br/&amp;gt;把历史换成摘要"]
    C["&amp;lt;b&amp;gt;缓存 Prompt Caching&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;每轮都重发全部历史&amp;lt;br/&amp;gt;不想每次都重新计费"]

    A -- "容量不够 → 必须取舍" --&amp;gt; B
    A -- "每轮重发 → 重复计算" --&amp;gt; C
    B -- "压缩改写了前缀&amp;lt;br/&amp;gt;必然打断缓存" --&amp;gt; C

    style A fill:#2d6cdf,stroke:#1b4a9c,color:#ffffff
    style B fill:#c0392b,stroke:#7d2519,color:#ffffff
    style C fill:#1e8449,stroke:#125230,color:#ffffff&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一句话概括三者的取舍差异，这是全文最重要的一句：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;触发时发生什么&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;th&gt;会不会丢信息&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;缓存失效&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;重新计算整个前缀&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;变贵 + 变慢&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不会&lt;/strong&gt;，模型看到的内容完全一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;上下文压缩&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;历史被摘要替换&lt;/td&gt;
&lt;td&gt;一次额外的模型调用&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;会&lt;/strong&gt;，细节永久丢失&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;很多人把两者混为一谈。&lt;strong&gt;缓存失效只是钱和延迟的问题，压缩才是真正会伤害任务质量的操作。&lt;/strong&gt; 后面所有的实践建议都从这条区别推导而来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 上下文（Context Window）&lt;/h2&gt;
&lt;h3&gt;1.1 官方定义&lt;/h3&gt;
&lt;p&gt;Anthropic 官方文档的定义是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The 'context window' refers to all the text a language model can reference when generating a response, including the response itself."&lt;/p&gt;
&lt;p&gt;—— 上下文窗口指的是模型生成回复时能够引用的全部文本，&lt;strong&gt;包括回复本身&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有两个要点常被误解：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，它不是模型的知识，是模型的"工作记忆"。&lt;/strong&gt; 官方特别强调它 "is different from the large corpus of data the language model was trained on"（区别于训练语料）。训练知识是模型权重里固化的东西；上下文窗口是这一次请求临时塞进去的东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，输出也占窗口。&lt;/strong&gt; "100 万上下文"不是"你可以输入 100 万 token，模型再额外输出"，而是&lt;strong&gt;输入 + 输出总共不超过 100 万&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1.2 所以"100 万上下文"到底指什么&lt;/h3&gt;
&lt;p&gt;指的是&lt;strong&gt;单次 API 请求中，输入与输出 token 数之和的上限是 1,000,000&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体到什么东西会占用它，官方列举得非常清楚：&lt;/p&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TB
    subgraph W["一次请求的上下文窗口"]
        direction TB
        subgraph IN["输入侧（Input）"]
            direction TB
            I1["System Prompt&amp;lt;br/&amp;gt;系统提示词"]
            I2["Tool Definitions&amp;lt;br/&amp;gt;工具定义 / MCP schema"]
            I3["历史消息&amp;lt;br/&amp;gt;user / assistant 全部轮次"]
            I4["Tool Results&amp;lt;br/&amp;gt;工具返回：文件内容、命令输出、搜索结果"]
            I5["图片 / PDF / 文档"]
        end
        subgraph OUT["输出侧（Output）"]
            direction TB
            O1["Extended Thinking&amp;lt;br/&amp;gt;模型的思考过程"]
            O2["文本回复"]
            O3["Tool Use 请求"]
        end
        IN --&amp;gt; OUT
    end

    style IN fill:#eaf2fd,stroke:#2d6cdf,color:#1a1a1a
    style OUT fill:#fdf3e7,stroke:#e67e22,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原文表述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Everything in the request counts toward the context window: the system prompt, every message in &lt;code&gt;messages&lt;/code&gt; (including tool results, images, and documents), and your tool definitions. The output Claude generates for the turn, including its extended thinking, counts too."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个反直觉但很重要的补充：&lt;strong&gt;缓存过的内容照样占窗口&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Cached prompt prefixes still occupy the context window: prompt caching changes what you pay for those tokens, not whether they count."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;缓存省的是钱和时间，不是空间。&lt;/p&gt;
&lt;h3&gt;1.3 关键推论：模型是无状态的，每轮都在重发全部历史&lt;/h3&gt;
&lt;p&gt;这是理解后面一切的基础。Claude Code 官方文档说得最直白：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The model doesn't remember anything between requests, so Claude Code re-sends the full context: the system prompt, your project context, every prior message and tool result, and your new message."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，你在终端里看到的"连续对话"是个幻觉。实际发生的是：&lt;/p&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TB
    subgraph R1["第 1 轮请求"]
        direction LR
        a1["系统提示&amp;lt;br/&amp;gt;+工具"] --&amp;gt; b1["用户 1"]
    end
    subgraph R2["第 2 轮请求"]
        direction LR
        a2["系统提示&amp;lt;br/&amp;gt;+工具"] --&amp;gt; b2["用户 1"] --&amp;gt; c2["助手 1"] --&amp;gt; d2["用户 2"]
    end
    subgraph R3["第 3 轮请求"]
        direction LR
        a3["系统提示&amp;lt;br/&amp;gt;+工具"] --&amp;gt; b3["用户 1"] --&amp;gt; c3["助手 1"] --&amp;gt; d3["用户 2"] --&amp;gt; e3["助手 2"] --&amp;gt; f3["用户 3"]
    end

    R1 --&amp;gt; R2 --&amp;gt; R3

    style a1 fill:#d5e8d4,color:#1a1a1a
    style a2 fill:#d5e8d4,color:#1a1a1a
    style a3 fill:#d5e8d4,color:#1a1a1a
    style f3 fill:#ffe6cc,color:#1a1a1a
    style d2 fill:#ffe6cc,color:#1a1a1a
    style b1 fill:#ffe6cc,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绿色部分每轮完全相同，橙色是每轮新增的部分。&lt;strong&gt;正因为绝大部分内容重复，缓存才有意义；也正因为历史只增不减，窗口才会满，才需要压缩。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1.4 Context Rot：为什么"塞满"是个坏主意&lt;/h3&gt;
&lt;p&gt;官方明确警告：更大的窗口不等于更好的效果。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"more context isn't automatically better. As token count grows, accuracy and recall degrade, a phenomenon known as &lt;em&gt;context rot&lt;/em&gt;. This makes curating what's in context just as important as how much space is available."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;上下文腐烂（context rot）&lt;/strong&gt;：随着 token 数增长，模型的准确率和召回率会下降。所以"精心筛选放什么进去"和"有多大空间"同等重要。&lt;/p&gt;
&lt;p&gt;这直接影响你该怎么用工具：一个跑了 3 小时、塞满了 50 个文件内容的 session，即使还没到窗口上限，效果也已经在衰减了。主动 &lt;code&gt;/clear&lt;/code&gt; 往往比死撑到自动压缩更好。&lt;/p&gt;
&lt;h3&gt;1.5 当前主流模型的窗口大小&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Claude 系列&lt;/strong&gt;（官方文档）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;上下文窗口&lt;/th&gt;
&lt;th&gt;单次最大输出&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5 / Mythos 5&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5 / 4.8 / 4.7 / 4.6&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5 / Sonnet 4.6&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.5 及其他更早模型&lt;/td&gt;
&lt;td&gt;200K&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1M 是&lt;strong&gt;默认值&lt;/strong&gt;，不需要 beta header：&lt;em&gt;"For every model with a 1M-token context window, 1M is the default: you don't need a beta header."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;1M 按&lt;strong&gt;标准价格&lt;/strong&gt;计费，超过 200K 没有溢价：&lt;em&gt;"A 900k-token request is billed at the same per-token rate as a 9k-token request."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;单次请求最多 600 张图片或 PDF 页（200K 窗口的模型是 100 张）。&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Claude 4.7 及之后的模型换了新分词器，同样的文本会多出约 30% 的 token&lt;/strong&gt;（&lt;em&gt;"This tokenizer produces approximately 30% more tokens for the same text"&lt;/em&gt;）。所以拿 4.6 的 token 数经验去估 4.7+ 会偏低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;OpenAI GPT-5.6 Sol&lt;/strong&gt;（官方模型页）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;数值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;上下文窗口&lt;/td&gt;
&lt;td&gt;1,050,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单次最大输出&lt;/td&gt;
&lt;td&gt;128,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长上下文计费门槛&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;超过 272K 输入 token 的请求，整个请求按 2× 输入价、1.5× 输出价计费&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原文：&lt;em&gt;"Prompts with &amp;gt;272K input tokens are priced at 2x input and 1.5x output for the full request."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这个 272K 门槛非常关键，也是 Codex 社区里各种"为什么是 272K"讨论的根源——&lt;strong&gt;它是一条计费红线，不是能力上限&lt;/strong&gt;。特别注意措辞是 "for the full request"：一旦越线，&lt;strong&gt;整个请求&lt;/strong&gt;都按 2 倍输入价计费，而不只是超出的那部分 token。所以在接近 272K 时主动压缩，省下的不是一点边际成本，而是整轮请求的一半。&lt;/p&gt;
&lt;h3&gt;1.6 窗口满了会发生什么（API 层面）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入本身就超了&lt;/strong&gt;：所有模型都返回 400 &lt;code&gt;invalid_request_error&lt;/code&gt;（"prompt is too long"）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输入 + &lt;code&gt;max_tokens&lt;/code&gt; 超了&lt;/strong&gt;：Claude 4.5 及更新的模型会接受请求，生成到上限时以 &lt;code&gt;stop_reason: "model_context_window_exceeded"&lt;/code&gt; 停止；更早的模型直接报校验错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.7 一个容易被忽略的机制：Context Awareness&lt;/h3&gt;
&lt;p&gt;Claude Sonnet 5 / Sonnet 4.6 / Sonnet 4.5 / Haiku 4.5 具备&lt;strong&gt;上下文感知&lt;/strong&gt;能力——API 会自动往系统提示里注入模型的 token 预算：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;budget:token_budget&amp;gt;&lt;/span&gt;200000&lt;span class="nt"&gt;&amp;lt;/budget:token_budget&amp;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&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;system_warning&amp;gt;&lt;/span&gt;Token&lt;span class="w"&gt; &lt;/span&gt;usage:&lt;span class="w"&gt; &lt;/span&gt;35000/200000;&lt;span class="w"&gt; &lt;/span&gt;165000&lt;span class="w"&gt; &lt;/span&gt;remaining&lt;span class="nt"&gt;&amp;lt;/system_warning&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这让模型能"知道自己还剩多少余量"，从而主动规划长任务节奏，而不是盲目往下做。这些标签是 API 自动注入的，你不需要也不应该自己发送。&lt;/p&gt;
&lt;p&gt;Opus 4.7 及之后的 Opus、Fable 5、Mythos 5 &lt;strong&gt;不接收&lt;/strong&gt;这些注入标签，改用 beta 的 task budgets 机制。&lt;/p&gt;
&lt;h3&gt;1.8 Extended Thinking 与窗口的关系&lt;/h3&gt;
&lt;p&gt;思考（thinking）token 的计费与占用规则值得单独说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;thinking token 属于 &lt;code&gt;max_tokens&lt;/code&gt; 的一部分，&lt;strong&gt;按输出 token 计费&lt;/strong&gt;，计入速率限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;历史思考块是否保留，取决于模型&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;Opus 4.5+、Sonnet 4.6+、Fable 5、Mythos 5：&lt;strong&gt;默认保留&lt;/strong&gt;，作为后续请求的输入 token 计费。&lt;/li&gt;
&lt;li&gt;更早的 Opus/Sonnet 与所有 Haiku：API 自动剥离历史思考块（你传回去它也会自动删），为对话内容腾出空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;唯一必须回传 thinking 块的场景&lt;/strong&gt;：工具调用循环中，提交 &lt;code&gt;tool_result&lt;/code&gt; 时必须原样带上对应的完整 thinking 块（含签名）。API 用密码学签名校验，改动就会报错。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 压缩（Compaction）&lt;/h2&gt;
&lt;h3&gt;2.1 什么情况下应该压缩&lt;/h3&gt;
&lt;p&gt;先区分三种"腾空间"的手段，它们代价完全不同：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;做了什么&lt;/th&gt;
&lt;th&gt;信息损失&lt;/th&gt;
&lt;th&gt;缓存影响&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;压缩（Compaction）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;历史 → 一段摘要&lt;/td&gt;
&lt;td&gt;大，细节永久丢失&lt;/td&gt;
&lt;td&gt;打断（对话层前缀完全变了）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;上下文编辑（Context Editing）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;只清除旧的工具结果 / 思考块&lt;/td&gt;
&lt;td&gt;中，仅丢工具输出&lt;/td&gt;
&lt;td&gt;打断（清除位置之后失效）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;清空（/clear、开新 session）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全部丢弃&lt;/td&gt;
&lt;td&gt;全部（但你是主动的）&lt;/td&gt;
&lt;td&gt;重建，但系统提示层可能仍命中&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;该压缩的时机（按优先级）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;✅ &lt;strong&gt;任务与任务之间的自然断点&lt;/strong&gt; —— 最佳时机。你已经不需要上一个任务的细节了，压缩几乎零损失。&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;快到窗口上限，但当前任务还没做完&lt;/strong&gt; —— 不得不压，让它压。&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;任务做到一半、正在调试细节时&lt;/strong&gt; —— 最糟。摘要会丢掉你刚建立起来的关键上下文。&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;换新任务时&lt;/strong&gt; —— 这时应该 &lt;code&gt;/clear&lt;/code&gt; 而不是 &lt;code&gt;/compact&lt;/code&gt;。压缩会把无关的旧任务摘要一直背在身上，既占 token 又干扰模型。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Claude Code 官方给的建议正是这一条：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"run &lt;code&gt;/compact&lt;/code&gt; at a natural break in your work, such as between tasks, instead of waiting for auto-compaction to trigger mid-task."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.2 压缩的通用流程&lt;/h3&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TD
    A["对话持续增长"] --&amp;gt; B{"input tokens&amp;lt;br/&amp;gt;达到触发阈值？"}
    B -- 否 --&amp;gt; A
    B -- 是 --&amp;gt; C["额外发起一次模型调用&amp;lt;br/&amp;gt;让模型总结当前全部历史"]
    C --&amp;gt; D["生成摘要&amp;lt;br/&amp;gt;（Claude 包在 &amp;amp;lt;summary&amp;amp;gt;&amp;amp;lt;/summary&amp;amp;gt; 中）"]
    D --&amp;gt; E["用摘要替换掉阈值之前的全部内容"]
    E --&amp;gt; F["带着摘要继续任务"]
    F --&amp;gt; A

    style C fill:#fdebd0,stroke:#e67e22,color:#1a1a1a
    style E fill:#fadbd8,stroke:#c0392b,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;strong&gt;C 这一步是一次真实的、额外的模型调用&lt;/strong&gt;——它要消耗 token、计费、占用速率限制。这是压缩的隐性成本。&lt;/p&gt;
&lt;h3&gt;2.3 Claude：官方 API 的服务端压缩&lt;/h3&gt;
&lt;p&gt;Anthropic 提供了&lt;strong&gt;服务端压缩&lt;/strong&gt;（beta），是官方推荐的长会话上下文管理方案：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Server-side compaction is the recommended strategy for managing context in long-running conversations and agentic workflows."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;配置方式：&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;context_management&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;edits&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;compact_20260112&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;trigger&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;input_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;需要 beta header：&lt;code&gt;anthropic-beta: compact-2026-01-12&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键参数与规则：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;默认触发阈值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input_tokens = 150,000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;阈值下限&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;必须 ≥ 50,000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;触发类型&lt;/td&gt;
&lt;td&gt;目前仅支持 &lt;code&gt;input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pause_after_compaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为 &lt;code&gt;true&lt;/code&gt; 时压缩后返回 &lt;code&gt;stop_reason: "compaction"&lt;/code&gt;，让你有机会插入内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instructions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自定义摘要提示词，&lt;strong&gt;完全替换&lt;/strong&gt;默认提示词（不是追加）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;支持模型&lt;/td&gt;
&lt;td&gt;Fable 5、Mythos 5、Mythos Preview、Opus 5、Opus 4.8–4.6、Sonnet 5、Sonnet 4.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;执行后发生什么：&lt;/strong&gt; 响应里会出现一个 &lt;code&gt;compaction&lt;/code&gt; 块。后续请求中，&lt;em&gt;"the API automatically drops all content blocks prior to the &lt;code&gt;compaction&lt;/code&gt; block and continues from the summary."&lt;/em&gt;——API 自动丢弃压缩块之前的所有内容。你必须把这个 compaction 块原样传回去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计费方式（重要）：&lt;/strong&gt; 压缩会多出一次采样迭代，&lt;code&gt;usage&lt;/code&gt; 里用 &lt;code&gt;iterations&lt;/code&gt; 数组分开记账：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;usage&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;input_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;output_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;iterations&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;compaction&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;input_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;output_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;input_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;output_tokens&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;⚠️ &lt;strong&gt;顶层的 &lt;code&gt;input_tokens&lt;/code&gt; / &lt;code&gt;output_tokens&lt;/code&gt; 只反映非压缩迭代&lt;/strong&gt;。要算真实总消耗，必须把 &lt;code&gt;iterations&lt;/code&gt; 全部加起来。只看顶层字段会严重低估账单。&lt;/p&gt;
&lt;h3&gt;2.4 Claude：更精细的替代方案——Context Editing&lt;/h3&gt;
&lt;p&gt;如果压缩太"重"（一次性丢掉所有细节），可以用 &lt;strong&gt;context editing&lt;/strong&gt;（beta header：&lt;code&gt;context-management-2025-06-27&lt;/code&gt;）：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工具结果清除&lt;/strong&gt; &lt;code&gt;clear_tool_uses_20250919&lt;/code&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trigger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100,000 input tokens&lt;/td&gt;
&lt;td&gt;何时开始清除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 tool uses&lt;/td&gt;
&lt;td&gt;保留最近几组工具调用/结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clear_at_least&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;至少清除多少 token，否则不动手&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exclude_tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;永不清除哪些工具的结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clear_tool_inputs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否连工具入参一起清&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;它在&lt;strong&gt;服务端、模型看到 prompt 之前&lt;/strong&gt;执行，客户端保留完整历史，无需同步。被清除的结果会替换成占位文本，让模型知道"这里原本有东西，被清掉了"。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考块清除&lt;/strong&gt; &lt;code&gt;clear_thinking_20251015&lt;/code&gt;：用 &lt;code&gt;keep: {"type": "thinking_turns", "value": N}&lt;/code&gt; 保留最近 N 轮，或 &lt;code&gt;keep: "all"&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;⚠️ 两者同时使用时，&lt;code&gt;clear_thinking_20251015&lt;/code&gt; &lt;strong&gt;必须排在前面&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;与缓存的关系（官方明确）：&lt;/strong&gt;
- 清除工具结果&lt;strong&gt;会使缓存前缀失效&lt;/strong&gt;，每次清除都产生 cache write 成本。所以官方建议用 &lt;code&gt;clear_at_least&lt;/code&gt; 确保"清得够多，值回缓存重建的钱"。
- 思考块&lt;strong&gt;保留时缓存命中、被清时缓存失效&lt;/strong&gt;。要最大化缓存命中率就设 &lt;code&gt;keep: "all"&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;2.5 Claude Code：自动压缩的实际行为&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;触发时机&lt;/strong&gt;：接近窗口上限时自动触发。官方对 Sonnet 5 给出了具体数字：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Sessions auto-compact before the window fills, at about &lt;strong&gt;967K tokens&lt;/strong&gt; by default; set &lt;code&gt;CLAUDE_CODE_AUTO_COMPACT_WINDOW&lt;/code&gt; to choose a different threshold."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;即约 96.7% 的位置（1M 窗口）。&lt;code&gt;/context&lt;/code&gt; 命令能看到实时占用明细以及预留的 autocompact buffer。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;压缩顺序&lt;/strong&gt;：不是一上来就摘要，而是分两步：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It clears older tool outputs first, then summarizes the conversation if needed."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;先清旧的工具输出，不够再总结对话。这个设计很合理——工具输出（文件内容、命令回显）通常是最占空间又最容易重新获取的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;压缩后什么活下来了&lt;/strong&gt;（官方表格，这张表非常实用）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;机制&lt;/th&gt;
&lt;th&gt;压缩后的状态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;系统提示、输出风格&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不变&lt;/strong&gt;（本来就不在消息历史里）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;项目根 &lt;code&gt;CLAUDE.md&lt;/code&gt;、无作用域 rules&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;从磁盘重新注入&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto memory (&lt;code&gt;MEMORY.md&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;从磁盘重新注入&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;带 &lt;code&gt;paths:&lt;/code&gt; frontmatter 的 rules&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;丢失&lt;/strong&gt;，直到再次读到匹配文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子目录里的嵌套 &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;丢失&lt;/strong&gt;，直到再次读到该目录下的文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;已调用的 Skill 正文&lt;/td&gt;
&lt;td&gt;重新注入，但&lt;strong&gt;每个 skill 上限 5,000 token、总计 25,000 token，超出时最旧的先丢&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks&lt;/td&gt;
&lt;td&gt;不受影响（hooks 是代码，不占上下文）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这张表能直接指导你的工程实践：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;必须跨压缩存活的规则，绝不要用 &lt;code&gt;paths:&lt;/code&gt; frontmatter，放进项目根 &lt;code&gt;CLAUDE.md&lt;/code&gt;。&lt;/strong&gt; 官方原话：&lt;em&gt;"If a rule must persist across compaction, drop the &lt;code&gt;paths:&lt;/code&gt; frontmatter or move it to the project-root CLAUDE.md."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skill 的关键指令放在 &lt;code&gt;SKILL.md&lt;/code&gt; 最前面。&lt;/strong&gt; 因为截断保留的是开头：&lt;em&gt;"Truncation keeps the start of the file, so put the most important instructions near the top."&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;其他细节：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/compact focus on the auth bug fix&lt;/code&gt; —— 可以带指令，让摘要保留你指定的重点，而不是让模型猜。&lt;/li&gt;
&lt;li&gt;也可以在 &lt;code&gt;CLAUDE.md&lt;/code&gt; 里加一个 "Compact Instructions" 章节，长期生效。&lt;/li&gt;
&lt;li&gt;v2.1.198 起，摘要请求会继承 session 的 extended thinking 配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩抖动保护&lt;/strong&gt;：如果单个文件或工具输出大到"每次压缩完立刻又填满"，Claude Code 会在尝试几次后停止自动压缩并报错，而不是无限循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.6 Codex：压缩机制与配置&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;手动压缩&lt;/strong&gt; &lt;code&gt;/compact&lt;/code&gt;，官方描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Summarize the visible chat to free tokens. Use after long runs so Codex retains key points without blowing the context window."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;清空&lt;/strong&gt; &lt;code&gt;/clear&lt;/code&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Reset the visible UI and chat context together when you want a fresh start."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;自动压缩的配置项&lt;/strong&gt;（&lt;code&gt;config.toml&lt;/code&gt;，官方配置参考）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置项&lt;/th&gt;
&lt;th&gt;官方描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model_auto_compact_token_limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Token threshold that triggers automatic history compaction (unset uses model defaults)."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model_auto_compact_token_limit_scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;total&lt;/code&gt;（默认）或 &lt;code&gt;body_after_prefix&lt;/code&gt;——"Controls whether the auto-compaction threshold counts the full active context (&lt;code&gt;total&lt;/code&gt;, the default) or only growth after the carried compaction-window prefix (&lt;code&gt;body_after_prefix&lt;/code&gt;)."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model_context_window&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Context window tokens available to the active model."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compact_prompt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;内联覆盖压缩提示词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;experimental_compact_prompt_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从文件加载压缩提示词（实验特性）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;model_auto_compact_token_limit_scope&lt;/code&gt; 这个参数值得多说一句：&lt;code&gt;total&lt;/code&gt; 是拿&lt;strong&gt;整个活动上下文&lt;/strong&gt;去比阈值；&lt;code&gt;body_after_prefix&lt;/code&gt; 只算&lt;strong&gt;压缩窗口前缀之后新增的部分&lt;/strong&gt;。后者能避免"摘要本身越滚越大导致压缩越来越频繁"的问题，长会话下更稳。&lt;/p&gt;
&lt;p&gt;Codex TUI 会显示 &lt;code&gt;100% context left&lt;/code&gt; 之类的剩余量指示器，可以直接观察。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：社区广泛流传的"阈值会被静默钳制到窗口 90%"、"压缩后保留 20,000 token 的最近用户消息"等说法来自第三方逆向分析，&lt;strong&gt;未见于 OpenAI 官方文档&lt;/strong&gt;，版本间也可能变化，此处不作为事实引用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Codex 背后的 API 能力&lt;/strong&gt;：OpenAI 在 Responses API 层提供两种压缩方式——
- &lt;strong&gt;服务端压缩&lt;/strong&gt;：请求里带 &lt;code&gt;context_management&lt;/code&gt; + &lt;code&gt;compact_threshold&lt;/code&gt;，流式响应过程中一旦越线就触发压缩，在同一个流里发出 compaction item 再继续推理。
- &lt;strong&gt;独立端点&lt;/strong&gt; &lt;code&gt;/responses/compact&lt;/code&gt;：完全无状态、ZDR 友好。你把整个上下文窗口发过去，它返回一个压缩后的窗口。&lt;/p&gt;
&lt;p&gt;两者返回的都是一个&lt;strong&gt;加密的 compaction item&lt;/strong&gt;——&lt;em&gt;"It is opaque and not intended to be human-interpretable."&lt;/em&gt; 这是与 Anthropic 的显著差异：Claude 的摘要是明文可读的 &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;，OpenAI 的是加密不透明对象。&lt;/p&gt;
&lt;h3&gt;2.7 压缩对本次 session / 任务的影响&lt;/h3&gt;
&lt;p&gt;这是提问里最实际的一问，分四个层面回答：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 信息层面：细节永久丢失，且丢失是不可逆的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;摘要是有损的。压缩前你说过的"注意这个接口的超时要设 3s 不是 30s"，如果模型没判断它重要，就没了。&lt;strong&gt;Claude Code 官方的原话是 "detailed instructions from early in the conversation may be lost"。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;→ &lt;strong&gt;对策&lt;/strong&gt;：持久化的约束写进 &lt;code&gt;CLAUDE.md&lt;/code&gt;（Codex 写进 &lt;code&gt;AGENTS.md&lt;/code&gt;），别指望对话历史。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 连续性层面：模型会"忘记"自己刚才在干什么的细节&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;摘要的目的是"提供连续性以便在新的上下文中继续推进"，但它保留的是&lt;strong&gt;状态和下一步&lt;/strong&gt;，不是过程。已经排查过、排除掉的错误路径最容易在摘要中丢失，导致模型压缩后又走一遍老路。&lt;/p&gt;
&lt;p&gt;→ &lt;strong&gt;对策&lt;/strong&gt;：压缩前把关键结论落到文件（笔记、TODO、代码注释），文件是跨压缩最可靠的记忆。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 成本层面：压缩本身要花钱，而且可能很贵&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;压缩要额外跑一次全历史的总结。在 Claude Code 里有一个巨大的成本差异：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"While the cache is warm, that request reads your prefix from the cache, so a mid-session &lt;code&gt;/compact&lt;/code&gt; costs a fraction of what the context size suggests... After a break longer than the cache lifetime, there is no cache left to read, so the summarization request reprocesses the full history as uncached input. &lt;strong&gt;This is why &lt;code&gt;/compact&lt;/code&gt; costs the most when you resume an old session.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;缓存热的时候压缩很便宜；隔夜回来再压缩最贵。&lt;/strong&gt; 这条直接推出一个实践：想压缩就趁热压，别等第二天。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 缓存层面：压缩必然打断对话层缓存&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"By design, this invalidates the conversation layer, since the next request has a new, shorter history that doesn't share a prefix with the old one."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但好消息是：压缩&lt;strong&gt;之后&lt;/strong&gt;那一轮反而不慢——因为新的对话层只有一小段摘要，重建缓存的成本很低。慢和贵的是压缩这一步本身。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⑤ 质量层面：这是唯一的正面影响&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;别忘了压缩也有好处。官方指出：&lt;em&gt;"as a conversation grows, response quality degrades, so compaction replaces older content with a concise summary."&lt;/em&gt; 在 context rot 严重时，压缩掉一堆无关历史反而会&lt;strong&gt;提升&lt;/strong&gt;回答质量。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;一个比压缩更好的选择：&lt;code&gt;/rewind&lt;/code&gt;&lt;/strong&gt;
如果你只是走错了路想退回去，用 &lt;code&gt;/rewind&lt;/code&gt; 而不是 &lt;code&gt;/compact&lt;/code&gt;。
官方原话：&lt;em&gt;"Rewinding truncates back to a prefix that is already cached, rather than building a new one as compaction does."&lt;/em&gt;
回退到的前缀&lt;strong&gt;本来就在缓存里&lt;/strong&gt;，几乎零成本，而且不丢信息——只是把错误的分支砍掉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;3. 缓存（Prompt Caching）&lt;/h2&gt;
&lt;h3&gt;3.1 什么是缓存&lt;/h3&gt;
&lt;p&gt;回到 1.3 节的结论：每一轮请求，绝大部分内容和上一轮完全相同。缓存就是让服务端&lt;strong&gt;复用已经算过的部分&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Claude Code 文档的描述最清晰：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The API caches by matching the start of each request, called the &lt;strong&gt;prefix&lt;/strong&gt;, against content it recently processed. On a normal turn, the prefix is the entire previous request and only the latest exchange is new. &lt;strong&gt;The match is exact, so a change anywhere in the prefix recomputes everything after it. There is no per-file or per-segment caching.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;三个必须记住的性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;只匹配前缀&lt;/strong&gt;，不是任意片段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精确匹配&lt;/strong&gt;，改一个字符就不算命中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;改动点之后的全部内容都要重算&lt;/strong&gt;——所以越靠前的改动越致命。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TB
    subgraph OK["✅ 命中：只在末尾追加"]
        direction LR
        K1["系统提示"] --&amp;gt; K2["历史 1-5"] --&amp;gt; K3["新消息"]
    end
    subgraph BAD["❌ 未命中：改了靠前的内容"]
        direction LR
        B1["系统提示&amp;lt;br/&amp;gt;（变了！）"] --&amp;gt; B2["历史 1-5"] --&amp;gt; B3["新消息"]
    end

    style K1 fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a
    style K2 fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a
    style K3 fill:#ffe6cc,stroke:#e67e22,color:#1a1a1a
    style B1 fill:#f8cecc,stroke:#c0392b,color:#1a1a1a
    style B2 fill:#f8cecc,stroke:#c0392b,color:#1a1a1a
    style B3 fill:#f8cecc,stroke:#c0392b,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绿色 = 缓存读取（便宜），橙色 = 新写入，红色 = 全部重算。&lt;/p&gt;
&lt;h3&gt;3.2 缓存命中率&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;命中率&lt;/strong&gt;指的是本轮输入 token 中，从缓存读取的比例。Anthropic 在响应的 &lt;code&gt;usage&lt;/code&gt; 里给了两个字段：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache_read_input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;本轮从缓存读取的 token，按标准输入价的 &lt;strong&gt;10%&lt;/strong&gt; 计费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache_creation_input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;本轮写入缓存的 token，按标准输入价的 &lt;strong&gt;1.25×&lt;/strong&gt;（5 分钟）或 &lt;strong&gt;2×&lt;/strong&gt;（1 小时）计费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;⚠️ &lt;strong&gt;只是最后一个缓存断点之后的 token&lt;/strong&gt;，不是全部输入&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;总输入 = &lt;code&gt;cache_read&lt;/code&gt; + &lt;code&gt;cache_creation&lt;/code&gt; + &lt;code&gt;input_tokens&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;判断标准很简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"A high read-to-creation ratio means caching is working well. &lt;strong&gt;If creation stays high turn after turn, something is changing in your prefix.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果 creation 一直居高不下，说明你的前缀里有东西每轮都在变，需要排查。&lt;/p&gt;
&lt;p&gt;OpenAI 对应的字段是 &lt;code&gt;cached_tokens&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;3.3 计费影响：缓存到底省多少&lt;/h3&gt;
&lt;p&gt;Anthropic 的倍率（官方定价页）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;倍率&lt;/th&gt;
&lt;th&gt;有效期&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 分钟缓存写入&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.25×&lt;/strong&gt; 基础输入价&lt;/td&gt;
&lt;td&gt;5 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 小时缓存写入&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2×&lt;/strong&gt; 基础输入价&lt;/td&gt;
&lt;td&gt;1 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存读取（命中）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1×&lt;/strong&gt; 基础输入价&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;官方给出的回本计算：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"A cache hit costs 10% of the standard input price, which means caching &lt;strong&gt;pays off after just one cache read&lt;/strong&gt; for the 5-minute duration (1.25x write), or &lt;strong&gt;after two cache reads&lt;/strong&gt; for the 1-hour duration (2x write)."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;举个实感数字&lt;/strong&gt;。假设你在 Claude Code 里用 Opus 5（$5/MTok 输入），会话累积了 300K token 的上下文，接下来还要来回 20 轮：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;计算&lt;/th&gt;
&lt;th&gt;成本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;无缓存&lt;/td&gt;
&lt;td&gt;20 轮 × 300K × $5/M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$30.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存全部命中&lt;/td&gt;
&lt;td&gt;1 次写入 300K × $6.25/M + 19 次读取 × 300K × $0.50/M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.73&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中途切了一次模型&lt;/td&gt;
&lt;td&gt;上述 + 再写一次 300K × $6.25/M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6.60&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结论：&lt;strong&gt;缓存能把长会话成本压到约六分之一；而中途切一次模型多花的 $1.88，约等于 12 轮正常缓存命中对话的成本&lt;/strong&gt;（每轮命中 300K × $0.50/M = $0.15）。这就是"随手切个模型试试"的真实代价。&lt;/p&gt;
&lt;p&gt;各家的定价（Anthropic 官方）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;基础输入&lt;/th&gt;
&lt;th&gt;5m 写入&lt;/th&gt;
&lt;th&gt;1h 写入&lt;/th&gt;
&lt;th&gt;命中&lt;/th&gt;
&lt;th&gt;输出&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5 / Mythos 5&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$12.50&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5 / 4.8 / 4.7 / 4.6&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$6.25&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5（8/31 前优惠价）&lt;/td&gt;
&lt;td&gt;$2&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 4.6 / 4.5&lt;/td&gt;
&lt;td&gt;$3&lt;/td&gt;
&lt;td&gt;$3.75&lt;/td&gt;
&lt;td&gt;$6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;td&gt;$2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;单位：美元 / 百万 token&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;3.4 计费之外：缓存失效还影响延迟&lt;/h3&gt;
&lt;p&gt;提问里问"缓存失效的影响，理解主要是计费？"——&lt;strong&gt;不完全是&lt;/strong&gt;。官方描述失效的后果时，钱和速度是并列的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"some actions invalidate the cache and make the next response &lt;strong&gt;slower and more expensive&lt;/strong&gt; while it rebuilds."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;重新处理 300K token 的首 token 延迟（TTFT）会非常明显。在交互式使用中，这个"卡一下"的体感往往比账单更让人难受。&lt;/p&gt;
&lt;p&gt;但要再次强调 &lt;strong&gt;缓存失效不影响正确性&lt;/strong&gt;——模型看到的内容一模一样，只是重新算了一遍。这和压缩有本质区别。&lt;/p&gt;
&lt;h3&gt;3.5 什么情况下缓存会失效&lt;/h3&gt;
&lt;h4&gt;（A）API 层面的失效规则&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;层级结构&lt;/strong&gt;：缓存按 &lt;code&gt;tools&lt;/code&gt; → &lt;code&gt;system&lt;/code&gt; → &lt;code&gt;messages&lt;/code&gt; 的顺序建立。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Changes at each level invalidate that level and all subsequent levels."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TB
    T["&amp;lt;b&amp;gt;Tools&amp;lt;/b&amp;gt; 工具定义"] --&amp;gt; S["&amp;lt;b&amp;gt;System&amp;lt;/b&amp;gt; 系统提示"] --&amp;gt; M["&amp;lt;b&amp;gt;Messages&amp;lt;/b&amp;gt; 对话消息"]

    T -. "改动 → 全部失效" .-&amp;gt; X1["💥 tools + system + messages"]
    S -. "改动 → 向下失效" .-&amp;gt; X2["💥 system + messages"]
    M -. "改动 → 只影响自己" .-&amp;gt; X3["💥 messages"]

    style T fill:#f8cecc,stroke:#c0392b,color:#1a1a1a
    style S fill:#fdebd0,stroke:#e67e22,color:#1a1a1a
    style M fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整失效对照表&lt;/strong&gt;（Anthropic 官方，✓ = 该层缓存仍有效，✘ = 失效）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变化项&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Messages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;工具定义（名称/描述/参数）改动&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开关 web search&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开关 citations&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换 speed 设置&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改 &lt;code&gt;tool_choice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;增删图片（任意位置）&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改 thinking 参数&lt;/td&gt;
&lt;td&gt;视模型&lt;/td&gt;
&lt;td&gt;视模型&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改 &lt;code&gt;output_config.effort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;视模型&lt;/td&gt;
&lt;td&gt;视模型&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;其他会导致未命中的情况：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;切换模型&lt;/strong&gt; —— 每个模型有独立缓存，内容一样也不通用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超过 TTL 未使用&lt;/strong&gt; —— 5 分钟（默认）或 1 小时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低于最小可缓存长度&lt;/strong&gt; —— 达不到就静默不缓存，&lt;strong&gt;不报错&lt;/strong&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;"Any requests to cache fewer than this number of tokens will be processed without caching, and &lt;strong&gt;no error is returned&lt;/strong&gt;."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;最小可缓存 token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5、Fable 5、Mythos 5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;512&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 4.8、Sonnet 5、Sonnet 4.6/4.5、Opus 4.1/4、Sonnet 4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,024&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mythos Preview、Opus 4.7、Haiku 3.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,048&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 4.6、Opus 4.5、Haiku 4.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,096&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;跨组织 / 跨 workspace&lt;/strong&gt; —— 缓存在组织之间完全隔离，部分平台还按 workspace 隔离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;断点放错位置&lt;/strong&gt;（最常见的工程错误）——把 &lt;code&gt;cache_control&lt;/code&gt; 放在每次都变的块上（比如带时间戳的块）。官方给了明确解释：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;"The lookback does not find stable content behind your breakpoint and cache it. &lt;strong&gt;It finds entries that prior requests already wrote, and writes happen only at breakpoints.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;正确做法：断点放在&lt;strong&gt;最后一个不变的块&lt;/strong&gt;上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具 JSON key 顺序不稳定&lt;/strong&gt; —— 某些语言序列化时 key 顺序随机，会静默破坏缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;其他限制&lt;/strong&gt;：最多 4 个显式缓存断点；回溯窗口最多 20 个块。&lt;/p&gt;
&lt;h4&gt;（B）Claude Code 层面的失效规则&lt;/h4&gt;
&lt;p&gt;Claude Code 把请求按"变化频率"分了三层，尽量让稳定的东西排前面：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;th&gt;何时变化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;System prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;核心指令、工具定义、输出风格&lt;/td&gt;
&lt;td&gt;工具集变化，或 Claude Code 升级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;、auto memory、无作用域 rules&lt;/td&gt;
&lt;td&gt;session 启动，或 &lt;code&gt;/clear&lt;/code&gt;、&lt;code&gt;/compact&lt;/code&gt; 之后&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conversation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;你的消息、Claude 的回复、工具结果&lt;/td&gt;
&lt;td&gt;每一轮&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;另外两个&lt;strong&gt;不在 prompt 文本里、但同样属于缓存 key&lt;/strong&gt; 的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模型&lt;/strong&gt;：每个模型独立缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effort level&lt;/strong&gt;：同一模型的不同 effort 也是独立缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;❌ 会打断缓存的操作（官方完整清单）：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;切换模型&lt;/strong&gt; &lt;code&gt;/model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;整个历史重算。&lt;strong&gt;&lt;code&gt;opusplan&lt;/code&gt; 每次进出 plan mode 都是一次模型切换&lt;/strong&gt;；Fable 5/Opus 5 的自动模型 fallback 也算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;改 effort level&lt;/strong&gt; &lt;code&gt;/effort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;会话中改动时 Claude Code 会先弹确认框&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;打开 fast mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;增加了一个属于缓存 key 的请求头。&lt;strong&gt;每次会话只付一次&lt;/strong&gt;，之后开关都不再打断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;连接/断开 MCP server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ 仅当工具定义被加载进前缀时。&lt;strong&gt;默认的 deferred tools 模式下不受影响&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;启用/禁用插件&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仅当插件提供 MCP server 时。Skills / commands / agents / hooks / LSP / themes &lt;strong&gt;都不打断&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;deny 整个工具&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;如 &lt;code&gt;Bash&lt;/code&gt;、&lt;code&gt;WebFetch&lt;/code&gt; 这种裸工具名的 deny 规则。带作用域的如 &lt;code&gt;Bash(rm *)&lt;/code&gt; &lt;strong&gt;不打断&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;执行压缩&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;见 2.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;升级 Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;系统提示/工具定义变了。⚠️ &lt;strong&gt;升级后 resume 长会话可能是你发出过的最贵的一次请求&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;✅ 不会打断缓存的操作：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;为什么安全&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;编辑仓库里的文件&lt;/td&gt;
&lt;td&gt;文件内容只在被读取时进入上下文；改文件只会追加一条 &lt;code&gt;&amp;lt;system-reminder&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;会话中编辑 &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不打断缓存，&lt;strong&gt;但改动也不生效&lt;/strong&gt;——要等 &lt;code&gt;/clear&lt;/code&gt;、&lt;code&gt;/compact&lt;/code&gt; 或重启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改输出风格&lt;/td&gt;
&lt;td&gt;同上，不打断也不生效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换权限模式&lt;/td&gt;
&lt;td&gt;不改系统提示（&lt;code&gt;opusplan&lt;/code&gt; 的 plan mode 除外）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调用 skills / commands&lt;/td&gt;
&lt;td&gt;作为 user message 追加在末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/recap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;把摘要作为命令输出追加，而非替换历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;/rewind&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;回退到的前缀本来就在缓存里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;启动 subagent&lt;/td&gt;
&lt;td&gt;子代理有自己独立的上下文和缓存，父会话前缀不受影响&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 官方建议一句话总结：
&lt;em&gt;"Pick your model and effort level at the top of a session, then save &lt;code&gt;/compact&lt;/code&gt; for natural breaks between tasks. The fewer changes you make mid-task, the higher your cache hit rate."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;（C）Claude Code 的缓存生命周期（TTL）&lt;/h4&gt;
&lt;p&gt;&lt;svg viewBox="0 0 760 235" width="100%" style="max-width:760px;height:auto;display:block;margin:1.2rem auto" role="img" aria-label="缓存 TTL 生命周期时间线" font-family="-apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif"&gt;
  &lt;title&gt;缓存 TTL 生命周期：连续请求保持缓存热，闲置超过 TTL 后冷启动&lt;/title&gt;
  &lt;rect x="48" y="62" width="322" height="92" rx="6" fill="#2e7d32" opacity="0.10"/&gt;
  &lt;rect x="370" y="62" width="218" height="92" rx="6" fill="#c0392b" opacity="0.09"/&gt;
  &lt;text x="209" y="84" text-anchor="middle" fill="#38914a" font-size="12.5" font-weight="600"&gt;缓存热 · 每次命中都重置计时器&lt;/text&gt;
  &lt;text x="479" y="84" text-anchor="middle" fill="#c0392b" font-size="12.5" font-weight="600"&gt;闲置超过 TTL&lt;/text&gt;
  &lt;line x1="30" y1="118" x2="692" y2="118" stroke="currentColor" stroke-width="1.5" opacity="0.5"/&gt;
  &lt;path d="M692 118 l-9 -4.5 v9 z" fill="currentColor" opacity="0.5"/&gt;
  &lt;text x="700" y="122" font-size="11" fill="currentColor" opacity="0.6"&gt;时间&lt;/text&gt;
  &lt;line x1="322" y1="101" x2="608" y2="101" stroke="#c0392b" stroke-width="1.2" stroke-dasharray="4 3" opacity="0.75"/&gt;
  &lt;g fill="#2e7d32"&gt;&lt;circle cx="85" cy="118" r="7"/&gt;&lt;circle cx="160" cy="118" r="7"/&gt;&lt;circle cx="235" cy="118" r="7"/&gt;&lt;circle cx="310" cy="118" r="7"/&gt;&lt;/g&gt;
  &lt;g fill="#ffffff" font-size="9.5" font-weight="700" text-anchor="middle"&gt;&lt;text x="85" y="121.5"&gt;✓&lt;/text&gt;&lt;text x="160" y="121.5"&gt;✓&lt;/text&gt;&lt;text x="235" y="121.5"&gt;✓&lt;/text&gt;&lt;text x="310" y="121.5"&gt;✓&lt;/text&gt;&lt;/g&gt;
  &lt;text x="197" y="146" text-anchor="middle" fill="currentColor" opacity="0.75" font-size="11.5"&gt;连续对话：整个前缀按 0.1× 输入价读取&lt;/text&gt;
  &lt;circle cx="620" cy="118" r="8" fill="#c0392b"/&gt;
  &lt;text x="620" y="122" text-anchor="middle" fill="#ffffff" font-size="10" font-weight="700"&gt;✕&lt;/text&gt;
  &lt;text x="620" y="146" text-anchor="middle" fill="#c0392b" font-size="11.5" font-weight="600"&gt;冷启动&lt;/text&gt;
  &lt;text x="620" y="161" text-anchor="middle" fill="currentColor" opacity="0.7" font-size="10.5"&gt;全量重算，慢且贵&lt;/text&gt;
  &lt;rect x="48" y="178" width="630" height="42" rx="6" fill="currentColor" opacity="0.06"/&gt;
  &lt;text x="64" y="197" font-size="11.5" fill="currentColor" opacity="0.85"&gt;TTL：订阅计划自动 1 小时；API key / 第三方默认 5 分钟&lt;/text&gt;
  &lt;text x="64" y="212" font-size="11.5" fill="currentColor" opacity="0.85"&gt;Subagent 恒为 5 分钟；超额动用 credits 时自动降回 5 分钟&lt;/text&gt;
&lt;/svg&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude 订阅计划&lt;/strong&gt;：Claude Code &lt;strong&gt;自动请求 1 小时 TTL&lt;/strong&gt;，休息一小时内回来仍能命中。&lt;/li&gt;
&lt;li&gt;但如果超出套餐额度、开始消耗 usage credits，会&lt;strong&gt;自动降回 5 分钟 TTL&lt;/strong&gt;（因为此时 1 小时写入更贵）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API key / Bedrock / Google Cloud / Foundry&lt;/strong&gt;：默认 5 分钟，设 &lt;code&gt;ENABLE_PROMPT_CACHING_1H=1&lt;/code&gt; 开启 1 小时。&lt;/li&gt;
&lt;li&gt;调试用：&lt;code&gt;FORCE_PROMPT_CACHING_5M=1&lt;/code&gt; 强制 5 分钟。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（D）Claude Code 的缓存作用域（容易踩坑）&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;"In Claude Code, the cache is effectively scoped to &lt;strong&gt;one machine and directory&lt;/strong&gt;."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;系统提示里嵌入了工作目录、平台、shell、OS 版本、auto-memory 路径，所以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;不同目录的两个 session 互不命中&lt;/strong&gt; —— &lt;strong&gt;包括同一个仓库的不同 git worktree&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;✅ 同一目录下&lt;strong&gt;并行&lt;/strong&gt;的多个 session &lt;strong&gt;会共享缓存&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;顺序&lt;/strong&gt;启动的 session 只有在启动时的 git 状态快照一致时才共享（系统提示还捕获了分支和最近提交）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Subagent 与 Fork 的差别很重要：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;缓存行为&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subagent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全新的系统提示和工具集，&lt;strong&gt;从零建缓存&lt;/strong&gt;；即使订阅计划也只用 5 分钟 TTL。父会话前缀不受影响&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;完整继承父会话的系统提示、工具、历史，&lt;strong&gt;第一次请求就命中父会话的缓存&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这解释了为什么"用 subagent 做大量文件调研"是划算的：它把大读取隔离在自己的窗口里，父会话的上下文和缓存都保持干净。&lt;/p&gt;
&lt;h3&gt;3.6 OpenAI / Codex 的缓存管理&lt;/h3&gt;
&lt;p&gt;OpenAI 的缓存是&lt;strong&gt;全自动&lt;/strong&gt;的，没有 &lt;code&gt;cache_control&lt;/code&gt; 这种标注机制。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;规则&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;启用方式&lt;/td&gt;
&lt;td&gt;自动，无需改代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最小前缀&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,024 token&lt;/strong&gt;（"Caching is available for prefixes containing at least 1,024 tokens"）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存内容&lt;/td&gt;
&lt;td&gt;system / user / assistant 消息、user 消息里的图片、&lt;code&gt;messages&lt;/code&gt; 数组与 &lt;code&gt;tools&lt;/code&gt; 列表、结构化输出 schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保留时长（GPT-5.6+）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;"for at least 30 minutes, but OpenAI may retain it longer"&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保留时长（更早模型）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;"generally remain active for 5 to 10 minutes of inactivity, up to a maximum of one hour"&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;观测字段&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cached_tokens&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写入成本（GPT-5.6+）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.25×&lt;/strong&gt; 未缓存输入价&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由优化&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prompt_cache_key&lt;/code&gt;——共享长前缀的请求用同一个 key，&lt;strong&gt;建议每个 key 保持约 15 请求/分钟&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;未命中的原因&lt;/strong&gt;：前缀不精确匹配，或隐式断点里包含了变化的元素（时间戳、工具调用历史、用户输入）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;官方最佳实践&lt;/strong&gt;与 Anthropic 完全一致：静态/重复内容放开头，变化的、用户相关的信息放末尾。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;两家的核心差异对照&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Anthropic&lt;/th&gt;
&lt;th&gt;OpenAI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;缓存控制&lt;/td&gt;
&lt;td&gt;显式 &lt;code&gt;cache_control&lt;/code&gt;（也支持自动模式）&lt;/td&gt;
&lt;td&gt;全自动，无手动控制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最小长度&lt;/td&gt;
&lt;td&gt;512 ~ 4,096（按模型）&lt;/td&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL&lt;/td&gt;
&lt;td&gt;5 分钟 / 1 小时（可选，价格不同）&lt;/td&gt;
&lt;td&gt;GPT-5.6+ 至少 30 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写入溢价&lt;/td&gt;
&lt;td&gt;1.25×（5m）/ 2×（1h）&lt;/td&gt;
&lt;td&gt;1.25×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;命中折扣&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1×&lt;/strong&gt;（省 90%）&lt;/td&gt;
&lt;td&gt;显著折扣&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由控制&lt;/td&gt;
&lt;td&gt;无需（按前缀哈希）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prompt_cache_key&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;压缩摘要&lt;/td&gt;
&lt;td&gt;明文 &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;加密不透明对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. 三者的相互作用&lt;/h2&gt;
&lt;p&gt;现在把三个概念串起来。一个长会话的真实 token 曲线是这样的：&lt;/p&gt;
&lt;p&gt;&lt;svg viewBox="0 0 760 310" width="100%" style="max-width:760px;height:auto;display:block;margin:1.2rem auto" role="img" aria-label="长会话上下文占用的锯齿曲线" font-family="-apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif"&gt;
  &lt;title&gt;长会话 token 曲线：每次压缩形成一个锯齿&lt;/title&gt;
  &lt;text x="62" y="30" font-size="11.5" fill="currentColor" opacity="0.8"&gt;每个谷底 = 一次额外的模型调用 + 一次信息损失 + 一次对话层缓存重建&lt;/text&gt;
  &lt;line x1="62" y1="45" x2="62" y2="250" stroke="currentColor" stroke-width="1.3" opacity="0.45"/&gt;
  &lt;line x1="62" y1="250" x2="712" y2="250" stroke="currentColor" stroke-width="1.3" opacity="0.45"/&gt;
  &lt;path d="M712 250 l-9 -4.5 v9 z" fill="currentColor" opacity="0.45"/&gt;
  &lt;line x1="62" y1="72" x2="700" y2="72" stroke="#c0392b" stroke-width="1.4" stroke-dasharray="6 4" opacity="0.85"/&gt;
  &lt;text x="700" y="65" text-anchor="end" fill="#c0392b" font-size="11.5" font-weight="600"&gt;auto-compact 阈值（Sonnet 5 ≈ 967K）&lt;/text&gt;
  &lt;polyline points="62,240 230,72 230,222 400,72 400,222 570,72 570,222 690,150" fill="none" stroke="#2d6cdf" stroke-width="2.5" stroke-linejoin="round"/&gt;
  &lt;g fill="#c0392b"&gt;&lt;circle cx="230" cy="222" r="4.5"/&gt;&lt;circle cx="400" cy="222" r="4.5"/&gt;&lt;circle cx="570" cy="222" r="4.5"/&gt;&lt;/g&gt;
  &lt;g stroke="currentColor" opacity="0.35" stroke-width="1"&gt;&lt;line x1="230" y1="250" x2="230" y2="257"/&gt;&lt;line x1="400" y1="250" x2="400" y2="257"/&gt;&lt;line x1="570" y1="250" x2="570" y2="257"/&gt;&lt;/g&gt;
  &lt;g fill="#c0392b" font-size="11" font-weight="600" text-anchor="middle"&gt;&lt;text x="230" y="270"&gt;压缩&lt;/text&gt;&lt;text x="400" y="270"&gt;压缩&lt;/text&gt;&lt;text x="570" y="270"&gt;压缩&lt;/text&gt;&lt;/g&gt;
  &lt;g fill="currentColor" opacity="0.7" font-size="11" text-anchor="end"&gt;&lt;text x="56" y="76"&gt;967K&lt;/text&gt;&lt;text x="56" y="226"&gt;≈40K&lt;/text&gt;&lt;text x="56" y="254"&gt;0&lt;/text&gt;&lt;/g&gt;
  &lt;text x="712" y="270" text-anchor="end" fill="currentColor" opacity="0.65" font-size="11"&gt;会话时间 →&lt;/text&gt;
  &lt;g fill="#2e7d32" opacity="0.20"&gt;&lt;rect x="62" y="284" width="164" height="9" rx="3"/&gt;&lt;rect x="234" y="284" width="162" height="9" rx="3"/&gt;&lt;rect x="404" y="284" width="162" height="9" rx="3"/&gt;&lt;rect x="574" y="284" width="116" height="9" rx="3"/&gt;&lt;/g&gt;
  &lt;text x="376" y="306" text-anchor="middle" fill="currentColor" opacity="0.7" font-size="10.5"&gt;每一段绿条 = 一个缓存热的平台期，优化目标就是让它尽可能长&lt;/text&gt;
&lt;/svg&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个锯齿的谷底 = 一次压缩 = 一次昂贵的额外调用 + 一次信息损失 + 一次对话层缓存重建。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;优化的目标因此非常明确：&lt;strong&gt;让锯齿尽可能少，让每段"缓存热"的平台期尽可能长。&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TD
    Start["长会话进行中"] --&amp;gt; Q1{"要换任务了吗？"}
    Q1 -- 是 --&amp;gt; Clear["&amp;lt;b&amp;gt;/clear&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;最干净：不背历史包袱&amp;lt;br/&amp;gt;也避免 context rot"]
    Q1 -- 否 --&amp;gt; Q2{"走错路了想退回？"}
    Q2 -- 是 --&amp;gt; Rewind["&amp;lt;b&amp;gt;/rewind&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;✅ 前缀已在缓存&amp;lt;br/&amp;gt;✅ 不丢信息"]
    Q2 -- 否 --&amp;gt; Q3{"要做大量文件调研？"}
    Q3 -- 是 --&amp;gt; Sub["&amp;lt;b&amp;gt;subagent&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;大读取隔离在子窗口&amp;lt;br/&amp;gt;父会话上下文与缓存不受影响"]
    Q3 -- 否 --&amp;gt; Q4{"接近窗口上限？"}
    Q4 -- 否 --&amp;gt; Go["继续，什么都别动&amp;lt;br/&amp;gt;保持缓存热"]
    Q4 -- 是 --&amp;gt; Q5{"当前任务做完了？"}
    Q5 -- 是 --&amp;gt; Compact["&amp;lt;b&amp;gt;/compact focus on ...&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;趁缓存热压，最便宜"]
    Q5 -- 否 --&amp;gt; Wait["先把手上这一步收尾&amp;lt;br/&amp;gt;结论落到文件&amp;lt;br/&amp;gt;再压缩"]

    style Clear fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a
    style Rewind fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a
    style Sub fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a
    style Go fill:#d5e8d4,stroke:#2e7d32,color:#1a1a1a
    style Compact fill:#fdebd0,stroke:#e67e22,color:#1a1a1a
    style Wait fill:#fdebd0,stroke:#e67e22,color:#1a1a1a&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5. 日常实践：怎么用好一个 session&lt;/h2&gt;
&lt;h3&gt;5.1 会话开始时（决定了后面 80% 的效率）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;一开始就把模型和 effort 定下来，中途别换。&lt;/strong&gt;
   这是投入产出比最高的一条。中途切模型 = 整个历史重算。官方的观察很有意思：为了省钱切到"更便宜"的模型，重建缓存的开销可能比继续用原模型还贵。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;要用 fast mode 就在会话开头开。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"turning it on at the start of a session costs less than turning it on deep into a long one."
   开头开：几乎无成本。第 200K token 时开：整个历史按 fast mode 价重算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;该配的 MCP server、插件一次配好。&lt;/strong&gt;
   在 deferred tools 默认模式下 MCP 变动是安全的，但如果你的环境走 LLM gateway 或用了 &lt;code&gt;alwaysLoad&lt;/code&gt;，工具定义就在前缀里，中途变动 = 全量重算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;稳定的约束写进 &lt;code&gt;CLAUDE.md&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt;，不要写在对话里。&lt;/strong&gt;
   这是唯一能跨压缩、跨 &lt;code&gt;/clear&lt;/code&gt; 存活的地方。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 会话进行中&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;保持追加，避免修改。&lt;/strong&gt;
   这是所有缓存优化的第一性原理。Claude Code 的很多设计——plan mode 做成可调用的工具而不是切换工具集、MCP 工具延迟加载——都是为了这一条。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;大规模调研交给 subagent。&lt;/strong&gt;
   一次 &lt;code&gt;grep&lt;/code&gt; 全仓库 + 读 20 个文件，可能就是 50K token。放进 subagent，父会话只收到一段摘要。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;走错路用 &lt;code&gt;/rewind&lt;/code&gt;，不要用 &lt;code&gt;/compact&lt;/code&gt;。&lt;/strong&gt;
   前者回到已缓存的前缀且不丢信息，后者两样都占。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;会话中改 &lt;code&gt;CLAUDE.md&lt;/code&gt; 是无效的。&lt;/strong&gt;
   官方明确：改了不生效，Claude 还在用 session 启动时加载的版本。要生效必须 &lt;code&gt;/clear&lt;/code&gt;、&lt;code&gt;/compact&lt;/code&gt; 或重启。&lt;strong&gt;这是很多人反复踩的坑&lt;/strong&gt;——改完配置发现 Claude 完全没变化，以为是模型不听话。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定期 &lt;code&gt;/context&lt;/code&gt; 看占用。&lt;/strong&gt;
   看看是不是某个 MCP server 或某次巨大的文件读取吃掉了大部分空间。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.3 关于休息和恢复&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;知道你的 TTL 是多少。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;订阅计划：1 小时。&lt;strong&gt;午休一小时回来，缓存大概率还在。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;API key：默认 5 分钟。&lt;strong&gt;接杯水回来可能就冷了&lt;/strong&gt;，除非设了 &lt;code&gt;ENABLE_PROMPT_CACHING_1H=1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;要长时间离开前，先决定回来怎么办。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;短暂离开（TTL 内）：什么都不用做。&lt;/li&gt;
&lt;li&gt;长时间离开且任务未完成：&lt;strong&gt;离开前趁缓存热执行 &lt;code&gt;/compact&lt;/code&gt;&lt;/strong&gt;。等第二天回来再压缩，是最贵的一种压缩方式。&lt;/li&gt;
&lt;li&gt;长时间离开且任务已完成：直接关掉。下次 &lt;code&gt;/clear&lt;/code&gt; 开新的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;升级 Claude Code 之后不要立刻 resume 长会话。&lt;/strong&gt;
    官方警告：&lt;em&gt;"the first turn back into a long session can be the most expensive request you send."&lt;/em&gt; 升级后更适合开新会话。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.4 关于目录（容易忽略的一条）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;固定在同一个目录工作。&lt;/strong&gt;
    缓存实际上是按机器 + 目录隔离的。频繁在 worktree 之间跳，等于每次都是冷启动。同一目录并行开多个 session 反而是共享缓存的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.5 健康度自检&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;症状&lt;/th&gt;
&lt;th&gt;可能原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;每轮都慢，&lt;code&gt;cache_creation&lt;/code&gt; 持续很高&lt;/td&gt;
&lt;td&gt;前缀里有东西每轮在变&lt;/td&gt;
&lt;td&gt;查 MCP server 是否在反复重连；查是否有动态工具更新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;刚 resume 就特别慢特别贵&lt;/td&gt;
&lt;td&gt;缓存已过期，或升级过 Claude Code&lt;/td&gt;
&lt;td&gt;正常现象；考虑改用 &lt;code&gt;/clear&lt;/code&gt; 开新会话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动压缩报错停止&lt;/td&gt;
&lt;td&gt;单个文件/工具输出过大导致"压缩抖动"&lt;/td&gt;
&lt;td&gt;别再整读那个大文件；改用 grep 定位或交给 subagent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改了 &lt;code&gt;CLAUDE.md&lt;/code&gt; 但没生效&lt;/td&gt;
&lt;td&gt;会话中编辑不生效&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt; 或重启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;压缩后模型"忘了"之前的约定&lt;/td&gt;
&lt;td&gt;规则用了 &lt;code&gt;paths:&lt;/code&gt; frontmatter，或在嵌套 &lt;code&gt;CLAUDE.md&lt;/code&gt; 里&lt;/td&gt;
&lt;td&gt;移到项目根 &lt;code&gt;CLAUDE.md&lt;/code&gt;，去掉 &lt;code&gt;paths:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill 指令压缩后失效&lt;/td&gt;
&lt;td&gt;超出 5K/skill 或 25K 总量被截断&lt;/td&gt;
&lt;td&gt;关键指令挪到 &lt;code&gt;SKILL.md&lt;/code&gt; 开头&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;6. 速查表&lt;/h2&gt;
&lt;h3&gt;压缩阈值&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;阈值&lt;/th&gt;
&lt;th&gt;可调方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API 服务端压缩&lt;/td&gt;
&lt;td&gt;默认 150,000 input tokens（下限 50,000）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trigger.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic 工具结果清除&lt;/td&gt;
&lt;td&gt;默认 100,000 input tokens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trigger.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code（Sonnet 5, 1M 窗口）&lt;/td&gt;
&lt;td&gt;约 967K tokens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLAUDE_CODE_AUTO_COMPACT_WINDOW&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;未公开默认值（"unset uses model defaults"）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;model_auto_compact_token_limit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;缓存 TTL&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API 默认&lt;/td&gt;
&lt;td&gt;5 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API 可选&lt;/td&gt;
&lt;td&gt;1 小时（写入 2×）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code + 订阅计划&lt;/td&gt;
&lt;td&gt;1 小时（自动）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code + 订阅超额用 credits&lt;/td&gt;
&lt;td&gt;自动降到 5 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code + API key / 第三方&lt;/td&gt;
&lt;td&gt;5 分钟（&lt;code&gt;ENABLE_PROMPT_CACHING_1H=1&lt;/code&gt; 可改）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code subagent&lt;/td&gt;
&lt;td&gt;5 分钟（即使订阅计划）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI GPT-5.6+&lt;/td&gt;
&lt;td&gt;至少 30 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI 更早模型&lt;/td&gt;
&lt;td&gt;闲置 5–10 分钟，最长 1 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;相关环境变量（Claude Code）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLAUDE_CODE_AUTO_COMPACT_WINDOW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自定义自动压缩阈值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENABLE_PROMPT_CACHING_1H=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在 API key 场景启用 1 小时 TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FORCE_PROMPT_CACHING_5M=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;强制 5 分钟 TTL（调试用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DISABLE_PROMPT_CACHING=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;完全关闭缓存（仅调试）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLAUDE_CODE_DISABLE_1M_CONTEXT=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁用 1M 上下文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DISABLE_AUTOUPDATER=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制升级时机，避免意外的缓存重建&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;一句话记忆&lt;/h3&gt;
&lt;blockquote&gt;
&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;strong&gt;只影响钱和延迟、不影响正确性&lt;/strong&gt;。
&lt;strong&gt;最优策略&lt;/strong&gt; = 开头定好模型，中途只追加不修改，大活交给 subagent，任务之间 &lt;code&gt;/clear&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Anthropic 官方&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-windows"&gt;Context windows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching"&gt;Prompt caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/compaction"&gt;Compaction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-editing"&gt;Context editing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/about-claude/pricing"&gt;Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents"&gt;Effective context engineering for AI agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 官方&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/context-window"&gt;Explore the context window&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/prompt-caching"&gt;How Claude Code uses prompt caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/how-claude-code-works"&gt;How Claude Code works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/model-config"&gt;Model configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/blog/lessons-from-building-claude-code-prompt-caching-is-everything"&gt;Lessons from building Claude Code: Prompt caching is everything&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;OpenAI 官方&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching"&gt;Prompt caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/compaction"&gt;Compaction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.chatgpt.com/docs/config-file/config-reference"&gt;Codex configuration reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.chatgpt.com/docs/developer-commands"&gt;Developer commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/models/gpt-5.6"&gt;GPT-5.6 model card&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="misc"/><category term="ai"/><category term="llm"/><category term="claude"/><category term="codex"/><category term="context"/><category term="cache"/><category term="agent"/></entry><entry><title>Skill、Subagent 与 Agent 究竟是什么？从一个月度总结实战谈 AI 原生架构</title><link href="https://www.chenxiaosheng.com/posts/2026-07-22/demystifying-skill-subagent-and-agent.html" rel="alternate"/><published>2026-07-22T11:45:00+08:00</published><updated>2026-07-22T11:45:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2026-07-22:/posts/2026-07-22/demystifying-skill-subagent-and-agent.html</id><summary type="html">&lt;p&gt;本文通过一个真实的“仓库月度自动统计与总结报告”落地需求，深入剖析 Skill、Subagent 和 Agent 三者的本质区别、协作模式与持久化原理，帮助读者建立对 AI 原生架构最直观透彻的认识。&lt;/p&gt;</summary><content type="html">&lt;p&gt;在大模型（LLM）和 AI 智能体（Agent）快速演进的今天，我们在使用类似 Claude Code、Google Antigravity、AutoGPT 等 AI 编程助手或 Agent 框架时，经常会听到几个核心词汇：&lt;strong&gt;Agent&lt;/strong&gt;、&lt;strong&gt;Subagent&lt;/strong&gt; 和 &lt;strong&gt;Skill&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;很多开发者往往会感到困惑：
- &lt;em&gt;“Skill 和 Agent 到底有什么区别？不都是给 AI 用的工具吗？”&lt;/em&gt;
- &lt;em&gt;“既然主 Agent 已经能干活了，为什么还需要 Subagent？”&lt;/em&gt;
- &lt;em&gt;“Subagent 到底是保存在内存里的一个虚无概念，还是一个真正的代码实体？”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;为了彻底说清楚这三者的概念与工程落地，本文将结合一个真实的实战需求——&lt;strong&gt;“每月自动统计代码仓库变化并生成结构化月报”&lt;/strong&gt;，手把手拆解这套 AI 原生架构。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一、 形象比喻：企业团队的运作模型&lt;/h2&gt;
&lt;p&gt;要搞懂这三者的关系，最直观的方式就是对比一个&lt;strong&gt;现代企业项目团队&lt;/strong&gt;的日常分工：&lt;/p&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph 企业团队模型
        Manager["主 Agent (项目负责人 / PM)"]
        Worker["Subagent (专职分析员工)"]
        SOP["Skill (SOP 操作手册 &amp;amp; 工具脚本)"]

        Manager --&amp;gt;|派发任务| Worker
        Worker --&amp;gt;|读取标准并执行工具| SOP
        Worker --&amp;gt;|交付成果报告| Manager
    end&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;维度&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Agent (主智能体)&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Subagent (子智能体)&lt;/th&gt;
&lt;th style="text-align: left;"&gt;Skill (技能/工具包)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;现实比喻&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;项目负责人 / PM&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;专职员工 / 专项专员&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;SOP 操作手册 / 员工指南 / 工具箱&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;本质属性&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;全局控制中枢，负责响应用户输入与任务调度。&lt;/td&gt;
&lt;td style="text-align: left;"&gt;动态派生的独立 LLM 线程/会话，专注解决单一复杂子任务。&lt;/td&gt;
&lt;td style="text-align: left;"&gt;静态的指令集、规范文件（如 Markdown SOP）及可执行脚本。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;上下文 (Context)&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;包含与用户对话的全部主历史。&lt;/td&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;完全隔离&lt;/strong&gt;的独立上下文空间，不受主对话噪音干扰。&lt;/td&gt;
&lt;td style="text-align: left;"&gt;没有上下文，它是被读取的文件与调用的工具。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;&lt;strong&gt;生命周期&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left;"&gt;贯穿整个 Chat Session 过程。&lt;/td&gt;
&lt;td style="text-align: left;"&gt;被唤醒时启动，完成特定任务后提交结果并结束。&lt;/td&gt;
&lt;td style="text-align: left;"&gt;长期静态保存在项目 Git 仓库中。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;核心一句话：&lt;/strong&gt;
&lt;strong&gt;Skill 告诉 Agent“标准和工具是什么”；Subagent 是真正“带上工具、在独立房间里闭门干活的员工”。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;二、 真实需求场景：每月仓库变化统计与月报生成&lt;/h2&gt;
&lt;p&gt;假设我们在一个项目中需要增加一个自动化能力：&lt;strong&gt;每月自动统计本仓库的代码提交、文档新增、模块变动，并形成一份有深度、有分类总结的月度 Markdown 报告&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;为什么单纯靠“主 Agent + 提示词”很笨重？&lt;/h3&gt;
&lt;p&gt;如果直接让主 Agent 在当前对话窗口里去干这件事情：
1. &lt;strong&gt;上下文严重污染&lt;/strong&gt;：主 Agent 需要读取近 30 天几十上百条提交记录和几百个文件的变更 diff。这些巨量日志会瞬间塞满主对话窗口的 Context Window。
2. &lt;strong&gt;效率与成本低下&lt;/strong&gt;：海量的中间数据会使后续每句聊天的 Token 消耗急剧增加，甚至导致主 Agent “头晕遗忘”之前的上下文。&lt;/p&gt;
&lt;p&gt;因此，完美的解法就是：&lt;strong&gt;Skill + Subagent 协同架构&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;三、 架构设计与落地实现&lt;/h2&gt;
&lt;p&gt;整个系统的运作流程如下图所示：&lt;/p&gt;
&lt;pre class="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant User as "用户 / 定时任务 (Cron)"
    participant MainAgent as "主 Agent (Main Agent)"
    participant SubAgent as "专员 Subagent (repo_monthly_reporter)"
    participant SkillDir as "静态 Skill (skills/repo-monthly-report)"
    participant Report as "输出成果 (outputs/reports/)"

    User-&amp;gt;&amp;gt;MainAgent: 触发“生成上月仓库报告”
    MainAgent-&amp;gt;&amp;gt;SubAgent: 派发子任务 (invoke_subagent)
    Note over SubAgent: 启动独立 Context 空间
    SubAgent-&amp;gt;&amp;gt;SkillDir: 1. 读取 SKILL.md 指南 SOP
    SubAgent-&amp;gt;&amp;gt;SkillDir: 2. 执行 extract_git_metrics.py 脚本
    SkillDir--&amp;gt;&amp;gt;SubAgent: 返回 JSON 格式硬指标数据
    Note over SubAgent: 3. 对 Git Log 进行语义归纳与分类
    SubAgent-&amp;gt;&amp;gt;Report: 4. 生成 monthly-YYYY-MM.md 报告
    SubAgent--&amp;gt;&amp;gt;MainAgent: 汇报完成情况与文件路径
    MainAgent--&amp;gt;&amp;gt;User: 最终答复与成果呈现&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 第一步：打造 Skill（静态 SOP 与工具箱）&lt;/h3&gt;
&lt;p&gt;在仓库中建立 &lt;code&gt;skills/repo-monthly-report/&lt;/code&gt; 目录：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SKILL.md&lt;/code&gt; (标准操作流程指南)&lt;/strong&gt;：
   定义了报告的四大归纳维度（如 AI 技能库、投资模型、学术论文、项目清理）、报告保存路径规范（&lt;code&gt;outputs/reports/monthly-YYYY-MM.md&lt;/code&gt;）以及标准的 Markdown 结构模板。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scripts/extract_git_metrics.py&lt;/code&gt; (数据提取脚本)&lt;/strong&gt;：
   用 Python 自动化提取过去指定天数的提交总数、贡献者、增删代码行数及按业务模块分类的文件列表。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# extract_git_metrics.py 核心片段&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;categorize_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;根据文件路径进行业务模块分类&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;articles/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;学术论文/文章 (articles/)&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;invest/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;投资/金融模型 (invest/)&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;skills/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;AI Agent Skills (skills/)&amp;quot;&lt;/span&gt;
    &lt;span class="c1"&gt;# ...其他分类逻辑&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;2. 第二步：定义并派生 Subagent（动态分析员工）&lt;/h3&gt;
&lt;p&gt;主 Agent 通过声明并派生一个名为 &lt;code&gt;repo_monthly_reporter&lt;/code&gt; 的子智能体：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;repo_monthly_reporter&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Repo Monthly Reporting Specialist&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;system_prompt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;你是一个专业的仓库月报生成子智能体。严格遵守 skills/repo-monthly-report/SKILL.md 规范，运行 Python 统计脚本，对业务模块进行深度语义归纳，生成月报保存至 outputs/reports/。&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;随后主 Agent 执行 &lt;code&gt;invoke_subagent&lt;/code&gt; 派发任务：
&lt;strong&gt;Subagent 启动后，在自己独立的 Context 空间内加载 Skill，运行 Python 脚本提取了 18 个变更文件与 -45,831 行数据，并完成了深度语义总结。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3. 第三步：生成结构化月度报告&lt;/h3&gt;
&lt;p&gt;最终生成的月报 &lt;code&gt;outputs/reports/monthly-2026-07.md&lt;/code&gt; 包含了极其清晰的业务图景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;总体概览&lt;/strong&gt;：代码新增 +621 行，清理 -45,831 行大文本与废弃文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三大核心亮点&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;上线 &lt;code&gt;pdf-watermark&lt;/code&gt; 水印 Agent 技能包；&lt;/li&gt;
&lt;li&gt;新增《价值平均定投法》金融知识文档与配套 Excel 工具；&lt;/li&gt;
&lt;li&gt;彻底清理老旧爬虫数据集与临时产物。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;四、 核心追问：Subagent 究竟保存在哪里？&lt;/h2&gt;
&lt;p&gt;在实际工程中，很多人会好奇：&lt;strong&gt;Subagent 到底是一个保存在硬盘上的实体，还是仅仅一串临时提示词？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是：&lt;strong&gt;它既有运行态的实体，也有静态配置的储存&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 运行态实体 (Runtime Entity)&lt;/h3&gt;
&lt;p&gt;当 Subagent 被派生时，系统真正为其建立了独立的进程与存储路径。
例如本次运行中，Subagent 的完整推理过程、思维链以及中间工具调用历史，真实地保存在本地磁盘上：
&lt;code&gt;~/.gemini/antigravity/brain/&amp;lt;subagent-conversation-id&amp;gt;/.system_generated/logs/transcript.jsonl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这也正是为什么它的运行不会占用或污染主 Agent 的 Context 窗口。&lt;/p&gt;
&lt;h3&gt;2. 配置与定义持久化 (Persistence)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动态注册（会话级 Session Scope）&lt;/strong&gt;：通过 &lt;code&gt;define_subagent&lt;/code&gt; 工具在运行内存中注册，仅对当前 Chat Session 有效，重启后注册表中不保留。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态配置文件（项目级 Project Scope）&lt;/strong&gt;：将 Subagent 的 Prompt 和角色写入项目的配置文件（如 &lt;code&gt;AGENTS.md&lt;/code&gt; 或 &lt;code&gt;.claude/agents/&lt;/code&gt;），这样每次打开客户端或任何人拉取仓库代码，系统均会自动识别并加载该 Subagent。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[仓库根目录]
├── skills/                      &amp;lt;-- 静态 Skill 目录 (跟随 Git 永久持久化)
│   └── repo-monthly-report/
│       ├── SKILL.md
│       └── scripts/extract_git_metrics.py
├── AGENTS.md                    &amp;lt;-- 静态 Subagent 配置文件 (项目级持久化)
└── outputs/reports/             &amp;lt;-- 成果输出路径
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;五、 工程总结：何时用 Skill？何时用 Subagent？&lt;/h2&gt;
&lt;p&gt;在设计 AI 原生系统或进行 Agentic Workflow 开发时，可以遵循以下&lt;strong&gt;三分法原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写成 Skill&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;凡是&lt;strong&gt;静态的操作流程 (SOP)&lt;/strong&gt;、代码规范、文档模板、纯 Python/Shell 工具脚本，通通写成 Skill 放在 &lt;code&gt;skills/&lt;/code&gt; 目录下。它是纯粹的知识与工具沉淀。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义 Subagent&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;凡是需要&lt;strong&gt;大量上下文消耗&lt;/strong&gt;（如读大量日志、审阅成百上千行代码）、&lt;strong&gt;并发处理&lt;/strong&gt;、或&lt;strong&gt;特定专业角色设定&lt;/strong&gt;（如专业 Code Review 员、安全审计员、月报撰写专员）的任务，定义为 Subagent。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主 Agent 负责调度&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;主 Agent 保持轻量，专注于倾听用户的复杂指令，做顶层任务拆解，并发派给对应的 Subagent 去完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这套架构，我们不仅实现了一个自动化的仓库月报工具，更彻底搞懂了 AI 原生开发中最核心的三大基石理念。&lt;/p&gt;</content><category term="misc"/><category term="ai"/><category term="agent"/><category term="subagent"/><category term="skill"/><category term="architecture"/></entry><entry><title>CPU、GPU、TPU 到底谁更“聪明”？从逻辑运算到光互连，一篇看懂 AI Infra</title><link href="https://www.chenxiaosheng.com/posts/2026-07-19/cpu-gpu-tpu-ai-infra.html" rel="alternate"/><published>2026-07-19T21:00:00+08:00</published><updated>2026-07-19T21:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-07-19:/posts/2026-07-19/cpu-gpu-tpu-ai-infra.html</id><summary type="html">&lt;p&gt;朋友问了两个问题：GPU 逻辑运算是不是不如 CPU？CPO 出来后光模块是不是就没未来了？两个问题都把系统问题压成了单点对决。本文从 CPU、GPU、TPU 的设计出发点讲起，经过内存墙、互连和软件栈，最后回到光模块，把这条线走完。不背产品参数，只讲“为什么这样设计”--参数每年都换，设计逻辑十年不变。&lt;/p&gt;</summary><content type="html">&lt;p&gt;朋友最近问了我两个问题。一个是：“GPU 的逻辑能力不强，逻辑运算应该是 CPU 更强吧？”另一个是：“一体封装出来以后，光模块就没有未来了，只剩光纤？”&lt;/p&gt;
&lt;p&gt;两个问题都踩在真实的技术趋势上，也都把一个系统问题压成了“谁强谁弱、谁取代谁”的判断题。第一句对了一半，而错的那一半，恰好是理解 GPU 的钥匙；第二句基本不成立，但它错的方式很有代表性——把“产品形态的变化”当成了“功能的消失”。&lt;/p&gt;
&lt;p&gt;要把这两个问题说清楚，得从 CPU、GPU、TPU 各自的设计出发点讲起，再经过内存墙、互连和软件栈，最后回到光模块。这篇文章就把这条线走完。不背产品参数，只讲“为什么这样设计”——参数每年都换，设计逻辑十年不变。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一、先把三兄弟放在一张表里&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;TPU（本文特指 Google TPU）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;第一目标&lt;/td&gt;
&lt;td&gt;单线程、少量线程的低延迟&lt;/td&gt;
&lt;td&gt;大规模并行吞吐&lt;/td&gt;
&lt;td&gt;矩阵计算的吞吐与能效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型硬件&lt;/td&gt;
&lt;td&gt;少量复杂核心、大缓存、分支预测、乱序执行&lt;/td&gt;
&lt;td&gt;上百个 SM、Warp/SIMT、CUDA Core、Tensor Core&lt;/td&gt;
&lt;td&gt;少量 TensorCore（MXU 脉动阵列 + 向量 + 标量单元）、SparseCore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;怎么对付延迟&lt;/td&gt;
&lt;td&gt;预测、推测、乱序，尽量不等&lt;/td&gt;
&lt;td&gt;海量线程轮转，谁的数据到了就跑谁&lt;/td&gt;
&lt;td&gt;编译器提前排好数据搬运和执行节拍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存思路&lt;/td&gt;
&lt;td&gt;多级硬件缓存隐藏不规则访存&lt;/td&gt;
&lt;td&gt;寄存器、共享内存、L2、HBM 分层复用&lt;/td&gt;
&lt;td&gt;软件管理的 VMEM 暂存 + HBM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最舒服的任务&lt;/td&gt;
&lt;td&gt;分支多、串行依赖、不规则、任务小&lt;/td&gt;
&lt;td&gt;大批量、数据并行、矩阵/向量密集&lt;/td&gt;
&lt;td&gt;形状稳定、矩阵占比高、可编译成规则数据流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常见软件&lt;/td&gt;
&lt;td&gt;C/C++、Java、数据库、操作系统&lt;/td&gt;
&lt;td&gt;CUDA、cuDNN、PyTorch、Triton、NCCL&lt;/td&gt;
&lt;td&gt;JAX/PyTorch、XLA、Pallas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型角色&lt;/td&gt;
&lt;td&gt;主机、调度、预处理、业务控制&lt;/td&gt;
&lt;td&gt;通用加速：训练、推理、渲染、HPC&lt;/td&gt;
&lt;td&gt;Google 体系内的大规模训练与推理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img alt="CPU、GPU、TPU 的架构与设计取舍" src="/static/cpu-gpu-tpu-ai-infra/cpu-gpu-tpu-architecture.png"&gt;&lt;/p&gt;
&lt;p&gt;这张图值得记住的不是“CPU 核少、GPU 核多”，而是底下那行小字：芯片面积和功耗是死的，每换一种花钱方式，都要牺牲另一种能力的密度。三种芯片不是三个段位，是三份不同的预算分配方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;二、三种芯片，三种花钱方式&lt;/h2&gt;
&lt;h3&gt;CPU：为“不可预测”买单&lt;/h3&gt;
&lt;p&gt;通用代码的第一特征是不可预测：满地的 &lt;code&gt;if/else&lt;/code&gt; 和函数调用，指针追着哈希表、树、图到处跳，中断和系统调用随时插队，前一条指令的结果决定下一条干什么。&lt;/p&gt;
&lt;p&gt;现代 CPU 的应对，是把大量晶体管花在“猜”上。分支预测器每个周期都在猜下一条指令从哪取，主流设计的命中率能做到 95% 以上——但猜错一次，十几级流水线里的工作全部作废；乱序引擎把不依赖当前结果的后续指令提前执行；推测执行沿着猜测的路径先跑起来，错了再回滚；多级缓存负责把不规则的访存伪装成低延迟。这些机制没有一个在“算”，它们都在让等待消失。&lt;/p&gt;
&lt;p&gt;所以 CPU 的核心数是几十到上百，而每个核心都很“重”。它的目标是延迟：一个请求进来，尽快出去。&lt;/p&gt;
&lt;h3&gt;GPU：用人海掩盖等待&lt;/h3&gt;
&lt;p&gt;GPU 的出发点相反。图形渲染要对几百万像素做同一套运算，AI 的矩阵计算也一样：一条计算规则，作用于海量数据。这种任务不需要每个线程都配备分支预测和乱序引擎，它需要的是数量。&lt;/p&gt;
&lt;p&gt;一张 H100 上有 132 个 SM（流式多处理器），合计一万六千多条 FP32 通道。硬件把线程按 32 个一组编成 Warp 调度：一个 Warp 在等内存，调度器就切去跑另一个 Warp。CPU 靠预测隐藏延迟，GPU 靠“手头永远有别的活”隐藏延迟。它优化的不是单个任务多快结束，而是单位时间完成的总量。&lt;/p&gt;
&lt;p&gt;另外，现代 GPU 早就不是“一堆小 ALU”。SM 里同时住着做通用算术和逻辑运算的 CUDA Core、专做矩阵乘加的 Tensor Core、访存和特殊函数单元；存储从寄存器、共享内存、L2 一路分层到 HBM；再往外还有 NVLink/NVSwitch 的多卡互连。这个清单后面会反复用到。&lt;/p&gt;
&lt;h3&gt;TPU：为“确定性”重排一切&lt;/h3&gt;
&lt;p&gt;TPU 是从工作负载倒推出来的机器。Google 在 2015 年部署第一代 TPU 时的判断是：推理负载的主体是矩阵乘加，而矩阵乘加的循环边界、访存模式在运行前就全部已知——那么 CPU 和 GPU 为“不确定性”准备的机制，缓存、乱序、推测、复杂调度，统统是可以省掉的开销。2017 年那篇著名的 TPU 论文把这个立场写得毫不客气，几乎逐条否定了通用处理器的经典设计。&lt;/p&gt;
&lt;p&gt;省下来的面积去了哪里？一个 256×256 的脉动阵列：65,536 个 8-bit 乘加单元排成网格，权重驻留在阵列中，激活值按时钟节拍从一侧流入，部分和在相邻单元间接力，结果从另一侧流出。每个单元不取指令、不做调度，唯一的工作是乘加和传递，所以同样的面积能塞进远多于通用设计的运算单元。这颗 92 TOPS（INT8）的芯片上没有硬件管理的缓存，只有 24 MiB 由软件显式管理的片上缓冲。&lt;/p&gt;
&lt;p&gt;今天的 TPU 复杂了很多，骨架没变。TensorCore（Google 文档里的用词，和 NVIDIA 的 Tensor Core 不是一回事）由三部分组成：MXU 脉动阵列做矩阵乘加，向量单元处理激活函数、Softmax、归一化这些逐元素运算，标量单元管控制流和地址计算。VMEM 是编译器显式管理的片上 SRAM，站在 GPU 里 L1/L2 的位置上；SparseCore 专门处理嵌入查表这类 MXU 极不擅长的稀疏访存；芯片之间用 ICI 高速互连。数据什么时候从 HBM 搬进 VMEM、以什么布局喂给 MXU，全部由 XLA 编译器在编译期排定。&lt;/p&gt;
&lt;p&gt;顺带澄清一个名词：TPU 特指 Google 的产品线；行业里更宽泛的叫法是 NPU，各家 NPU 内部可能是脉动阵列、向量引擎或别的数据流结构，差异很大，不能都按 TPU 来理解。&lt;/p&gt;
&lt;h3&gt;边界正在互相渗透&lt;/h3&gt;
&lt;p&gt;真实产品从不严格站队。CPU 这边加了向量和矩阵扩展（AVX-512、Intel AMX、Arm SVE/SME）；GPU 的 Tensor Core 一代比一代大、一代比一代独立；TPU 反过来在补向量灵活性和稀疏处理。最直白的信号是 CUDA 13 正式加入了 Tile 编程模型：程序员按“数据块”描述计算，线程怎么排交给编译器，块内单一控制流、没有分支发散的概念——这套叙事，十年前是 TPU 的专利。&lt;/p&gt;
&lt;p&gt;所以三类芯片的差别从来不是“能不能做”，而是做某类任务时，有多大比例的硅面积在产出有效工作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;三、AI 负载长什么样&lt;/h2&gt;
&lt;h3&gt;主体是矩阵乘法&lt;/h3&gt;
&lt;p&gt;以 Transformer 为例，计算时间大头花在 Q/K/V 投影、Attention 内部的两次矩阵乘、输出投影、FFN 层的矩阵乘，以及训练时对应的反向传播上。矩阵乘法的特点是：乘加操作海量重复，循环边界和访存模式提前可知——这正是 Tensor Core 和 MXU 的主场。&lt;/p&gt;
&lt;p&gt;但模型里还有 Softmax、LayerNorm、激活函数、采样、稀疏路由、Embedding 查表这些非矩阵部分，它们会落到 GPU 的 CUDA Core、TPU 的向量单元或 SparseCore，有些干脆留在 CPU 上。一轮训练或推理，从来不是某一种计算单元的独角戏。&lt;/p&gt;
&lt;h3&gt;精度是第二根杠杆&lt;/h3&gt;
&lt;p&gt;AI 对单个数值的精度要求远低于科学计算。训练主流是 BF16，FP8 正在铺开；推理进一步用到 INT8、FP8 甚至 FP4。位宽砍半的收益是三重的：同样的内存放下双倍参数，同样的带宽搬运双倍数据，同样的面积摆下更多低位宽运算单元。&lt;/p&gt;
&lt;p&gt;当然“位数越低越好”是误解。低精度需要缩放、校准和足够的累加精度兜底。现在小位宽的主流玩法是微缩放（microscaling，如 MXFP8、NVFP4）：数值用 4/8 bit 存，每一小组数配一个缩放因子，硬件在计算时把缩放乘回去——存储和带宽按小格式付费，精度靠缩放因子找补。&lt;/p&gt;
&lt;h3&gt;三种执行模型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;执行模型&lt;/th&gt;
&lt;th&gt;直观理解&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SIMD&lt;/td&gt;
&lt;td&gt;一条指令驱动多条数据通道&lt;/td&gt;
&lt;td&gt;控制开销低，适合规则向量计算&lt;/td&gt;
&lt;td&gt;通道间难以各行其是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIMT&lt;/td&gt;
&lt;td&gt;写起来像独立线程，硬件按 Warp 成组锁步执行&lt;/td&gt;
&lt;td&gt;编程灵活，通用并行能力强&lt;/td&gt;
&lt;td&gt;同 Warp 分支不一致会发散&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;脉动阵列&lt;/td&gt;
&lt;td&gt;数据按节拍流过二维乘加网格&lt;/td&gt;
&lt;td&gt;矩阵密度和数据复用最高&lt;/td&gt;
&lt;td&gt;对形状、布局、编译调度敏感&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三者不是升级关系，是在控制灵活性、硬件密度、编程难度之间选了三个不同的落点。CPU 的向量扩展是 SIMD，GPU 是 SIMT 套在 SIMD 上，TPU 把宝押在脉动阵列上、旁边配向量单元兜底。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;四、真正的主角：内存墙&lt;/h2&gt;
&lt;p&gt;先记一组数量级：在先进工艺上，做一次乘加只要几个皮焦；从片上 SRAM 取一次数，大约贵一个数量级；从 HBM 取一次，贵两个数量级；跨过 PCIe 或网络，再翻几番。时间同理——乘加是纳秒级以下，HBM 访问是几百纳秒，跨节点是微秒到毫秒。算术很便宜，搬数据很贵，而且这个差距逐年拉大。&lt;/p&gt;
&lt;p&gt;数据要走的路——从对象存储、网络、CPU 主存，跨过 PCIe 进入 HBM，再经片上 SRAM、寄存器抵达矩阵单元——一张图看完：&lt;/p&gt;
&lt;p&gt;&lt;img alt="AI Infra 的数据路径与互连层级" src="/static/cpu-gpu-tpu-ai-infra/ai-infra-data-path.png"&gt;&lt;/p&gt;
&lt;p&gt;越靠近计算单元，存储越快，也越小、越贵。加速器上几乎所有优化，最后都能折叠成一句话：数据搬进来一次，尽量多算几遍，再搬走。&lt;/p&gt;
&lt;h3&gt;Roofline：一道除法看穿性能&lt;/h3&gt;
&lt;p&gt;一个内核的性能上限可以粗略写成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;可达性能 ≤ min（峰值算力，内存带宽 × 算术强度）
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;算术强度（arithmetic intensity）指每搬一个字节做多少次运算。峰值算力除以带宽，得到一个平衡点：算术强度高于它，是算力受限；低于它，是带宽受限，堆再多计算单元也没用。&lt;/p&gt;
&lt;p&gt;拿 H100（SXM）算一下：BF16 稠密算力约 990 TFLOPS，HBM3 带宽 3.35 TB/s，平衡点约 295 FLOPs/Byte——每从 HBM 搬一个字节，要做将近三百次运算才喂得饱计算单元。矩阵乘法靠分块复用可以够到这个数；而逐元素加法读两个数、算一次、写一个数，算术强度只有约 0.08，天生贴在带宽墙上。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Roofline 模型：以 H100 为例" src="/static/cpu-gpu-tpu-ai-infra/roofline-model.png"&gt;&lt;/p&gt;
&lt;p&gt;再算一笔更扎心的账。70B 参数的模型，BF16 权重 140 GB，恰好放进一张 H200（141 GB HBM3e，4.8 TB/s）。单并发逐 token 解码时，每生成一个 token 至少要把权重完整读一遍：140 ÷ 4.8 ≈ 29 毫秒。也就是说，不管这张卡标多少 PFLOPS，batch=1 的解码上限就是每秒三十来个 token，还没算 KV Cache。为什么推理团队拼命做量化、攒大 batch，为什么 HBM 常年供不应求，这一道除法就够解释了。&lt;/p&gt;
&lt;p&gt;沿着同一条逻辑，很多现象都不神秘了：Kernel Fusion 省的是中间结果往返 HBM 的搬运；FlashAttention 的贡献不是发明新乘法，是让注意力矩阵不落 HBM；低精度的首要收益在容量和带宽，其次才是运算速度；批量太小，矩阵单元就吃不饱。&lt;/p&gt;
&lt;h3&gt;从一颗芯片到一座集群&lt;/h3&gt;
&lt;p&gt;单卡放不下模型、或者算得不够快，就要扩展，路线有两条。Scale-up 是在机箱和机架内用紧耦合互连把多颗加速器拼成“一台更大的机器”：NVIDIA 这边是 NVLink/NVSwitch，GB200 NVL72 把 72 张 GPU 收进一个 NVLink 域，每卡互连带宽 1.8 TB/s；Google 这边是 ICI，把成千上万颗 TPU 织成 3D 环面（torus）。Scale-out 是跨服务器、跨机架，用 InfiniBand 或以太网把域连成集群。&lt;/p&gt;
&lt;p&gt;TPU 还有一件独门武器：从 v4 起，pod 内部用 OCS（光路交换机）连接一个个 4×4×4 的芯片立方体。光路交换不解析数据包，只负责“接线”，好处是可以按作业重构拓扑、绕开故障芯片，代价小得惊人——论文给的数字是系统成本占比不到 5%、功耗不到 3%。&lt;/p&gt;
&lt;p&gt;到了几千上万卡的规模，最慢的往往不是矩阵乘法，而是卡间交换梯度和激活的集合通信（All-Reduce、All-Gather、All-to-All）。网络的形状会反过来决定模型怎么切。两个公开的对照很能说明问题：PaLM 540B 在 6,144 颗 TPU v4 上训练，靠 3D 环面的带宽，只用了数据并行加张量并行两个维度，MFU 做到 46.2%；Llama 3 405B 在 16,384 张 H100 上，因为 NVLink 域外要跨到 InfiniBand，带宽有断崖，不得不叠上张量、流水线、上下文、数据四维并行，MFU 约 41%~43%。数字接近，工程复杂度差了一倍——并行策略不是自由选择，是网络形状逼出来的。&lt;/p&gt;
&lt;p&gt;同一份 Llama 3 报告还有一组更少被引用的数字：54 天训练里发生 419 次意外中断，平均每三小时一次，其中 30.1% 归因于 GPU 故障、17.2% 归因于 HBM3 故障。在这个规模上，可靠性不是运维话题，是性能的一部分——checkpoint 和故障恢复的速度，直接进有效算力的分母。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;五、“GPU 逻辑不强”，对，也不对&lt;/h2&gt;
&lt;p&gt;回到朋友的第一个问题。“逻辑”这个词至少混着四件事：一是布尔和位运算（AND、OR、XOR、移位、位计数）；二是整数运算（地址、索引、哈希、编解码）；三是控制流（分支、循环、状态机）；四是业务逻辑（事务、协议、服务编排）。把它们拆开，答案就清楚了。&lt;/p&gt;
&lt;h3&gt;论位运算和整数，GPU 不弱，甚至很强&lt;/h3&gt;
&lt;p&gt;GPU 的 ALU 本来就叫“算术逻辑单元”。CUDA 指令集里整数运算、比较、移位、位翻转、population count、前导零计数一应俱全。证据不用翻手册：比特币还在用显卡挖的年代，GPU 跑的 SHA-256 就是纯粹的整数与位运算；今天的 GPU 数据库和分析引擎（cuDF 等）做的过滤、哈希 join、字典解码，也全是“逻辑运算”。&lt;/p&gt;
&lt;p&gt;对海量相互独立的数据做同样的逻辑，GPU 的吞吐可以远超 CPU；只对一个配置值做一次 AND，当然轮不到 GPU。差别在并行规模和启动开销，不在“会不会逻辑”。&lt;/p&gt;
&lt;h3&gt;论复杂分支，CPU 确实占优&lt;/h3&gt;
&lt;p&gt;CPU 有分支预测和乱序执行，擅长在纠缠的控制流里压低单线程延迟。GPU 的软肋在 Warp 上：同一个 Warp 的 32 个线程如果走向不同分支，硬件要把两条路径分别执行——跑 &lt;code&gt;if&lt;/code&gt; 时屏蔽走 &lt;code&gt;else&lt;/code&gt; 的线程，再反过来。这叫分支发散（warp divergence），结果仍然正确，但有效并行度掉了一半。要是 32 个线程各走一条又长又不一样的路，GPU 的吞吐优势就被磨光了。&lt;/p&gt;
&lt;p&gt;一个细节：从 Volta 起，每个线程有独立的程序计数器（独立线程调度），能避免一类死锁、允许更细粒度的交错，但发散的路径依然要分别占用执行时间。硬件保证正确，不保证不浪费。&lt;/p&gt;
&lt;h3&gt;所以，CPU 赢延迟，GPU 赢规模&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;任务&lt;/th&gt;
&lt;th&gt;更可能合适&lt;/th&gt;
&lt;th&gt;主要原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;处理一次 HTTP 请求并查库&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;分支多、状态多、单次延迟敏感&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对上亿像素做同一个阈值判断&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;相同逻辑可大规模并行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小型规则引擎、配置解析&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;任务太小，GPU 启动和搬运开销不划算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大批量哈希、位图、压缩&lt;/td&gt;
&lt;td&gt;视算法而定，GPU 常有吞吐优势&lt;/td&gt;
&lt;td&gt;独立数据多&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不规则图遍历&lt;/td&gt;
&lt;td&gt;通常 CPU 起步，特定算法可上 GPU&lt;/td&gt;
&lt;td&gt;访存不规则、负载难均衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;矩阵乘法后接少量激活与判断&lt;/td&gt;
&lt;td&gt;GPU/TPU&lt;/td&gt;
&lt;td&gt;主体仍是高密度矩阵计算&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;判断一个任务归谁，与其问“谁强”，不如按四层过一遍：指令层面，芯片支不支持这种运算（几乎都支持）；延迟层面，依赖链多长、分支能不能预测；吞吐层面，有没有成千上万个独立任务可以并行；系统层面，数据在不在显存里、Kernel 启动和 PCIe 搬运要花多久。四层里真正分胜负的，通常是后两层。&lt;/p&gt;
&lt;p&gt;严谨版本的回答是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GPU 不是逻辑运算能力弱，而是没把晶体管花在复杂控制流的低延迟上。少量、串行、分支不可预测的任务归 CPU；海量数据执行相同或相近的逻辑，GPU 往往更快，而且快得多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;六、芯片之外：两套软件栈，两条演进路线&lt;/h2&gt;
&lt;p&gt;一块加速器好不好用，软件栈的分量不低于硬件。两家的栈长得像，分工的切法不一样：&lt;/p&gt;
&lt;p&gt;&lt;img alt="GPU 栈与 TPU 栈对照" src="/static/cpu-gpu-tpu-ai-infra/software-stacks.png"&gt;&lt;/p&gt;
&lt;p&gt;NVIDIA 真正的护城河不是芯片，是左边这一整摞：二十年的 CUDA 生态、调到极致的数学库、所有框架的一等支持、海量已有内核和调试工具，以及“新论文的参考实现默认是 CUDA”这个惯性。研究代码和新算子几乎总是先出现在 GPU 上，这不是规格表能替代的。&lt;/p&gt;
&lt;p&gt;右边的 TPU 栈把布局推断、DMA 调度、双缓冲、指令打包全部交给编译器，作者只描述“块”和“流水线”。好处是写出来就不慢、结果逐位可复现（脉动阵列是确定性的）；代价是遇到编译器没覆盖的边角算子时，排障要下到编译器的层面，路径和 GPU 完全不同。&lt;/p&gt;
&lt;h3&gt;两条路线正在对穿&lt;/h3&gt;
&lt;p&gt;看最近三代硬件的动作会发现一件有意思的事：两边都在往对方的方向走。&lt;/p&gt;
&lt;p&gt;GPU 这边，Hopper 引入 TMA，把张量搬运从线程手里拿走、交给异步硬件引擎和描述符；Blackwell 把 Tensor Core 从 Warp 调度器解耦，双 die 合封，NVLink 域扩到 72 卡；CUDA 13 加入 Tile 编程模型。翻译一下：GPU 在一步步引入“编译器排定的数据流”——这正是 TPU 的出身。&lt;/p&gt;
&lt;p&gt;TPU 这边，Trillium（v6e）把 MXU 扩到 256×256；最新的 Ironwood（TPU7x）每颗芯片两个 chiplet，各带一个 TensorCore、两个 SparseCore 和 96 GB HBM，合计 192 GB、7.37 TB/s 带宽，FP8 稠密算力约 4.6 PFLOPS，单 pod 用 3D 环面加 OCS 连到 9,216 颗芯片；同时 PyTorch/XLA、vLLM 的 TPU 后端在一点点补生态短板。翻译一下：TPU 在补灵活性和易用性——这正是 GPU 的出身。&lt;/p&gt;
&lt;p&gt;单芯片指标上，Ironwood 和 Blackwell 已经咬得很紧；真正拉开身位的是紧耦合域的规模（9,216 对 72/144）和生态成熟度（CUDA 对 XLA）。这两个维度，恰好一家一个。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;七、怎么选：从工作负载出发&lt;/h2&gt;
&lt;h3&gt;先看负载，再看品牌&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;首选起点&lt;/th&gt;
&lt;th&gt;需要特别检查&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web 服务、数据库、控制面、数据预处理&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;单线程延迟、缓存命中、内存容量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小模型、低 QPS、间歇运行&lt;/td&gt;
&lt;td&gt;CPU 或小型 GPU&lt;/td&gt;
&lt;td&gt;加速器利用率能否覆盖成本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;视觉、语音、LLM 训练&lt;/td&gt;
&lt;td&gt;GPU；JAX/GCP 体系可评估 TPU&lt;/td&gt;
&lt;td&gt;软件兼容、HBM 容量、通信占比、可用容量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大批量离线推理&lt;/td&gt;
&lt;td&gt;GPU/TPU&lt;/td&gt;
&lt;td&gt;吞吐、批量、量化、每 token 成本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;交互式低延迟推理&lt;/td&gt;
&lt;td&gt;GPU/TPU&lt;/td&gt;
&lt;td&gt;首 token 延迟、解码带宽、KV Cache、并发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;渲染、视频、科学计算&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;有没有成熟库、双精度需求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;稳定的超大规模训练/推理（Google 系）&lt;/td&gt;
&lt;td&gt;TPU 吸引力大&lt;/td&gt;
&lt;td&gt;XLA 算子覆盖、拓扑与切分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新论文复现、快速试验&lt;/td&gt;
&lt;td&gt;GPU 路径最短&lt;/td&gt;
&lt;td&gt;生态与调试效率&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;训练和推理是两种负载，推理自己还要再拆两半&lt;/h3&gt;
&lt;p&gt;训练要存激活、算梯度、频繁跨设备通信，看重 HBM 容量带宽、低精度稳定性、集合通信效率，以及前面说的 checkpoint 与故障恢复速度。&lt;/p&gt;
&lt;p&gt;推理分两个阶段：Prefill 一次吃进整段输入，矩阵大，偏算力受限；Decode 逐 token 生成，批量不足时被权重和 KV Cache 的读取带宽卡死（回想 H200 那道除法）。同一张卡在离线大批量里效率很高，不代表单用户对话时能接近峰值——两个阶段的瓶颈根本不是同一个。&lt;/p&gt;
&lt;h3&gt;别拿 TOPS 直接比&lt;/h3&gt;
&lt;p&gt;厂商规格表可能分别标着 FP64、TF32、BF16、FP8、FP4 的峰值，甚至掺着稀疏加成。不同精度的 TOPS 放一列比大小，得出的结论毫无意义。有意义的比较只有：同一个模型同一精度下的有效吞吐、端到端时间、每 token 或每次训练的成本、功耗与机房容量，以及集群规模下的 MFU 和故障恢复。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;八、CPO 会杀死光模块吗&lt;/h2&gt;
&lt;p&gt;回到朋友的第二个问题。先把“一体封装”落到确切的名词上：CPO（Co-Packaged Optics，共封装光学），把光引擎搬到交换 ASIC 或 XPU 旁边、共享封装基板，从而砍掉长长的板级高速电通道。&lt;/p&gt;
&lt;h3&gt;先看清楚可插拔光模块里有什么&lt;/h3&gt;
&lt;p&gt;一只高速可插拔模块远不止“光纤接口”。盒子里通常装着：DSP 或均衡电路、激光器、调制器、光电探测器、Driver 和 TIA 模拟前端、温控与监控固件，外加光耦合、连接器、封装和测试。交换芯片通过 PCB 上的 SerDes 把电信号送到面板模块，模块完成电光转换。&lt;/p&gt;
&lt;p&gt;问题出在速率上。SerDes 到了 200G/lane，电信号在 PCB 上走几十厘米损耗急剧变大，均衡电路越做越强、功耗越吃越多。CPO 的思路是釜底抽薪：让电信号出了 ASIC 没走几毫米就变成光。&lt;/p&gt;
&lt;p&gt;&lt;img alt="传统可插拔光模块与 CPO 的结构差异" src="/static/cpu-gpu-tpu-ai-infra/pluggable-vs-cpo.png"&gt;&lt;/p&gt;
&lt;p&gt;图里画的是外置激光源的一类常见方案，不代表所有实现——不同厂商在光源位置、DSP 去留、光引擎边界上各有取舍。&lt;/p&gt;
&lt;h3&gt;CPO 改变的是“在哪转换”，不是“要不要转换”&lt;/h3&gt;
&lt;p&gt;注意，CPO 没有删掉任何光收发功能，它只是把原来集中在一个盒子里的功能拆开、重新安放——完整的可插拔收发模块，变成“共封装光引擎 + 可维护的外置激光源 + 光连接”，上图底部那行总结说的就是这件事。&lt;/p&gt;
&lt;p&gt;激光源的去向最能说明问题。激光器怕热，而 CPO 恰恰把光学器件推到了发热大户 ASIC 旁边，于是产业界把激光源单独拎出来放到前面板，做成可插拔、可热更换的模块——OIF 在 2023 年发布的 ELSFP 规范定义的就是这个东西；Broadcom 的 51.2T CPO 交换机 Bailly（8 个 6.4T 光引擎与交换芯片共封装）用的也是可插拔外置光源。CPO 不是消灭了模块，是把模块的边界重新画了一遍。&lt;/p&gt;
&lt;h3&gt;光纤为什么独自扛不起来&lt;/h3&gt;
&lt;p&gt;光纤是介质，只会传光。产生稳定光源、把电比特调制到光上、把光转回电、放大与恢复信号、温度补偿、功率监控、波分复用、耦合与测试——这些功能一项都不会因为封装形式变化而消失，只会换个地方存在。无论载体叫光模块、光引擎、光 Chiplet 还是外置激光源，干的都是这些活。&lt;/p&gt;
&lt;h3&gt;CPO 自己的账单&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;可插拔光模块&lt;/th&gt;
&lt;th&gt;CPO&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高速电通道&lt;/td&gt;
&lt;td&gt;从 ASIC 走到前面板，长&lt;/td&gt;
&lt;td&gt;极短，光引擎紧贴 ASIC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;功耗与密度&lt;/td&gt;
&lt;td&gt;高速率下压力越来越大&lt;/td&gt;
&lt;td&gt;超高带宽下优势明显&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;维护&lt;/td&gt;
&lt;td&gt;坏一只，拔了换新&lt;/td&gt;
&lt;td&gt;光引擎与主封装耦合，维修边界复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;散热&lt;/td&gt;
&lt;td&gt;模块在面板，独立热设计&lt;/td&gt;
&lt;td&gt;光学器件贴着高功耗 ASIC，热设计难&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;良率与测试&lt;/td&gt;
&lt;td&gt;模块独立制造、测试、筛选&lt;/td&gt;
&lt;td&gt;多芯片合封，Known-Good-Die 与耦合测试更难&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;互操作&lt;/td&gt;
&lt;td&gt;MSA 与供应链成熟&lt;/td&gt;
&lt;td&gt;标准与多供应商生态仍在成形&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;落地节奏&lt;/td&gt;
&lt;td&gt;各速率、距离、场景通吃&lt;/td&gt;
&lt;td&gt;先进入带宽/功耗最紧张的场景&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;集成度更高不等于处处更好。前面板模块坏了拔掉就换；共封装光引擎出问题，牵连的是整个交换机。外置激光、可拆光连接器、冗余和监控，都是产业为了把可维护性找回来而新做的设计。&lt;/p&gt;
&lt;h3&gt;实际发生的是共存和价值链迁移&lt;/h3&gt;
&lt;p&gt;看头部厂商怎么做、怎么说，比看口号准。NVIDIA 在 2025 年 GTC 上发布 Quantum-X 与 Spectrum-X Photonics 交换机（单端口 1.6T，激光器数量减到四分之一，宣称能效 3.5 倍、可靠性 10 倍，液冷设计），InfiniBand 版 2025 年底出货、以太网版 2026 年跟进——同一篇通稿里，NVIDIA 明确表示可插拔光模块技术将与之并行发展，还点名了 Coherent、Fabrinet 和国内读者熟悉的新易盛（Eoptolink）、旭创（InnoLight）。发布 CPO 的人自己都没打算送走可插拔。&lt;/p&gt;
&lt;p&gt;未来几年比较合理的图景是：CPO 优先进入交换容量、端口密度和功耗最紧张的 AI 集群；可插拔 DSP 模块继续吃下成熟网络、长距离和多供应商场景；LPO/LRO 作为中间路线，保留可插拔形态、去掉或简化模块内 DSP 来省功耗；板载光学和光 I/O 在不同封装层级继续探路。对厂商来说，“组装完整盒子”的环节在高端市场承压，但激光器、硅光、调制器、探测器、Driver/TIA、光纤阵列、连接器、封装测试的价值不会蒸发，只会重新分桌。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CPO 会减少某些高端交换机里可插拔模块的数量，但消失的是一种产品外形，不是光收发产业。增长的也绝不只有光纤——恰恰相反，光引擎、外置光源和高密度光连接的技术含量比原来更高了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;九、把答案还给朋友&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题一：GPU 逻辑不强，逻辑运算应该 CPU 更强？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你说的是复杂分支、串行依赖、低延迟控制，对，CPU 更强，它的分支预测、乱序执行和缓存体系就是为这个造的。但如果说的是 AND、OR、XOR、比较、掩码这类逻辑运算，GPU 不但会做，还能对海量数据同时做——当年挖矿跑的哈希就是纯逻辑运算。GPU 真正的弱点是同一个 Warp 里线程走不同分支时利用率下降。所以别问谁的逻辑强，要问：任务有多少并行度、分支有没有规律、数据在谁的内存里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;问题二：一体封装以后，光模块没有未来，只剩光纤？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你说的是 CPO，它确实会把光引擎从面板模块搬到交换芯片旁边，一部分可插拔模块会被替代。但光纤只负责传输，不会发光、调制、探测和监控——这些功能全都还在，只是换了封装位置，激光源甚至专门做成了可插拔的（OIF 还为此定了 ELSFP 标准）。连发布 CPO 交换机的 NVIDIA 都在通稿里写明可插拔技术并行发展。所以不是“光模块消失、只剩光纤”，而是“完整的盒子被拆成共封装光引擎、外置激光和光连接”，两种形态会共存很多年。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;十、写在最后&lt;/h2&gt;
&lt;p&gt;这两个问题背后其实是同一个思维习惯：把系统问题压成单点对决。但现代 AI Infra 里没有单点——CPU、GPU、TPU 是三份不同的晶体管预算，光模块和 CPO 是两种光电转换的安放方式，芯片性能要过内存墙、互连、编译器、调度和可靠性的层层折扣才变成有效算力。&lt;/p&gt;
&lt;p&gt;如果只带走一句话，带这句：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI Infra 的竞争，不是芯片之间的一对一决斗，而是看谁能让数据用更低的能耗、更少的等待、更高的可靠性，从存储流到计算单元，再流到下一颗芯片。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;附录：术语速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;术语&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ALU&lt;/td&gt;
&lt;td&gt;算术逻辑单元，执行算术、比较、位运算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAC&lt;/td&gt;
&lt;td&gt;乘加运算，矩阵计算的基本动作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIMD&lt;/td&gt;
&lt;td&gt;单指令多数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIMT&lt;/td&gt;
&lt;td&gt;单指令多线程，GPU 的执行模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warp&lt;/td&gt;
&lt;td&gt;NVIDIA GPU 中成组调度执行的 32 个线程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SM&lt;/td&gt;
&lt;td&gt;Streaming Multiprocessor，GPU 的基本执行单元&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tensor Core&lt;/td&gt;
&lt;td&gt;NVIDIA GPU 中的矩阵乘加单元；Google 文档里的 TensorCore 是另一个概念（MXU+向量+标量的组合）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MXU&lt;/td&gt;
&lt;td&gt;TPU 的矩阵乘法单元，核心是脉动阵列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HBM&lt;/td&gt;
&lt;td&gt;高带宽内存，加速器的片外主存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VMEM&lt;/td&gt;
&lt;td&gt;TPU 上由编译器管理的片上暂存 SRAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ICI&lt;/td&gt;
&lt;td&gt;Inter-Chip Interconnect，TPU 芯片间高速互连&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiling&lt;/td&gt;
&lt;td&gt;把大张量切块，让数据在片上反复复用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;算术强度&lt;/td&gt;
&lt;td&gt;每搬一个字节做多少次运算，Roofline 模型的横轴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MFU&lt;/td&gt;
&lt;td&gt;Model FLOPs Utilization，有效算力利用率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale-up / Scale-out&lt;/td&gt;
&lt;td&gt;紧耦合域内扩展 / 跨节点网络扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPO&lt;/td&gt;
&lt;td&gt;共封装光学，把光引擎放到主 ASIC 旁边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LPO / LRO&lt;/td&gt;
&lt;td&gt;保留可插拔形态、去掉或简化模块内 DSP 的两类路线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ELSFP&lt;/td&gt;
&lt;td&gt;OIF 定义的可插拔外置激光源规范&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCS&lt;/td&gt;
&lt;td&gt;光路交换机，通过重构光路改变互连拓扑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;资料来源与延伸阅读&lt;/h2&gt;
&lt;p&gt;本文的框架部分参考了 Adam Mainz 的两篇长文（他刚从 Meta 的 Triton 编译器团队转到 Google TPU 团队，文章立场偏 TPU，本文未采纳其平台优劣结论），产品规格与 CPO 进展均用一手资料交叉核验。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adam Mainz, &lt;a href="https://x.com/MainzOnX/status/2044462083010662771"&gt;From SIMT to Systolic: A Foundation for GPU and TPU Architecture&lt;/a&gt; 及 &lt;a href="https://x.com/MainzOnX/status/2044804854020006223"&gt;The Two Stacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NVIDIA, &lt;a href="https://docs.nvidia.com/cuda/cuda-programming-guide/"&gt;CUDA Programming Guide&lt;/a&gt;（SIMT、Warp 与 Tile 编程模型）&lt;/li&gt;
&lt;li&gt;NVIDIA, &lt;a href="https://docs.nvidia.com/cuda/hopper-tuning-guide/"&gt;Hopper Tuning Guide&lt;/a&gt;（TMA 与内存层级）&lt;/li&gt;
&lt;li&gt;NVIDIA, &lt;a href="https://www.nvidia.com/en-us/data-center/technologies/blackwell-architecture/"&gt;Blackwell Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Jouppi et al., &lt;a href="https://research.google/pubs/in-datacenter-performance-analysis-of-a-tensor-processing-unit/"&gt;In-Datacenter Performance Analysis of a Tensor Processing Unit&lt;/a&gt;（TPU v1 论文）&lt;/li&gt;
&lt;li&gt;Jouppi et al., &lt;a href="https://arxiv.org/abs/2304.01433"&gt;TPU v4: An Optically Reconfigurable Supercomputer for Machine Learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Google Cloud, &lt;a href="https://docs.cloud.google.com/tpu/docs/system-architecture-tpu-vm"&gt;TPU System Architecture&lt;/a&gt; 与 &lt;a href="https://docs.cloud.google.com/tpu/docs/tpu7x"&gt;TPU7x（Ironwood）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Chowdhery et al., &lt;a href="https://arxiv.org/abs/2204.02311"&gt;PaLM: Scaling Language Modeling with Pathways&lt;/a&gt;（46.2% MFU 与 pipeline-free 训练）&lt;/li&gt;
&lt;li&gt;Meta, &lt;a href="https://arxiv.org/abs/2407.21783"&gt;The Llama 3 Herd of Models&lt;/a&gt;（4D 并行与 54 天故障统计）&lt;/li&gt;
&lt;li&gt;OIF, &lt;a href="https://www.oiforum.com/oif-announces-external-laser-small-form-factor-pluggable-elsfp-implementation-agreement-paving-the-way-for-advancements-in-co-packaged-optics-applications/"&gt;ELSFP Implementation Agreement 发布通告&lt;/a&gt; 与 &lt;a href="https://www.oiforum.com/technical-work/implementation-agreements-ias/"&gt;Implementation Agreements 列表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Broadcom, &lt;a href="https://www.broadcom.com/topics/what-is-co-packaged-optics"&gt;What Is Co-Packaged Optics?&lt;/a&gt; 与 &lt;a href="https://www.broadcom.com/products/fiber-optic-modules-components/co-packaged-optics"&gt;CPO 产品页&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NVIDIA, &lt;a href="https://nvidianews.nvidia.com/news/nvidia-spectrum-x-co-packaged-optics-networking-switches-ai-factories"&gt;Spectrum-X / Quantum-X Photonics 发布通稿&lt;/a&gt;（含可插拔并行发展与合作伙伴名单）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;资料与产品状态核验日期：2026-07-19。&lt;/p&gt;</content><category term="misc"/><category term="ai-infra"/><category term="gpu"/><category term="tpu"/><category term="cpu"/></entry><entry><title>我只是想打开一个 Markdown 文件看一眼——于是 vibe coding 了一个编辑器</title><link href="https://www.chenxiaosheng.com/posts/2026-06-06/markdown2-native-markdown-editor.html" rel="alternate"/><published>2026-06-06T15:00:00+08:00</published><updated>2026-06-06T15:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-06-06:/posts/2026-06-06/markdown2-native-markdown-editor.html</id><summary type="html">&lt;p&gt;我的需求小到有点可笑：在 macOS 上随手双击打开一个 Markdown 文件看一眼，能顺手改两笔就更好。可挑遍主流编辑器，要么收费、要么是 Electron 套壳的庞然大物、要么年久失修、要么三天两头更新就崩。索性用 SwiftUI + AppKit 自己 vibe coding 了一个原生轻量的 Markdown2，本文记录这趟从"挑不到"到"自己造"的心路，以及途中那些有意思的坑。&lt;/p&gt;</summary><content type="html">&lt;h2&gt;一个小到可笑的需求&lt;/h2&gt;
&lt;p&gt;先说清楚我到底想要什么，因为这决定了后面所有的取舍：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我只是想在 macOS 上&lt;strong&gt;随手打开一个 Markdown 文件看一眼&lt;/strong&gt;，如果能顺便改两笔就更好——仅此而已。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我不需要笔记库（vault）、不需要双向链接、不需要标签系统、不需要文件树、不需要发布到博客的一键流程、不需要 30 个主题。这些功能不是不好，而是它们都在解决"管理大量笔记"的问题，而我的问题是"&lt;strong&gt;此刻&lt;/strong&gt;打开&lt;strong&gt;这一个&lt;/strong&gt;文件"。&lt;/p&gt;
&lt;p&gt;这个需求小到，理论上应该有一大把现成软件能完美满足。但真当我一个个试过去，发现没有一个让我舒服的。&lt;/p&gt;
&lt;h2&gt;我试过的那些编辑器，以及为什么都不行&lt;/h2&gt;
&lt;p&gt;把候选者摊开，按我最在意的几个维度排一张表：&lt;/p&gt;
&lt;table style="width: 100%;"&gt;
    &lt;tr&gt;
        &lt;th&gt;编辑器&lt;/th&gt;
        &lt;th&gt;实现方式&lt;/th&gt;
        &lt;th&gt;价格&lt;/th&gt;
        &lt;th&gt;维护状态&lt;/th&gt;
        &lt;th&gt;我的槽点&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Typora&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;Electron / CEF&lt;/td&gt;
        &lt;td&gt;付费&lt;/td&gt;
        &lt;td&gt;活跃&lt;/td&gt;
        &lt;td&gt;体验确实是标杆，但收费 + Electron 套壳，进程动辄几百 MB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;MacDown&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;原生 WebView&lt;/td&gt;
        &lt;td&gt;免费开源&lt;/td&gt;
        &lt;td&gt;年久失修&lt;/td&gt;
        &lt;td&gt;老牌良心货，但更新近乎停滞，对新语法支持滞后&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;MacDown3000&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;MacDown 衍生&lt;/td&gt;
        &lt;td&gt;免费&lt;/td&gt;
        &lt;td&gt;不稳定&lt;/td&gt;
        &lt;td&gt;每次跟着更新就容易"罢工"，可靠性堪忧&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Mark Text&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;Electron&lt;/td&gt;
        &lt;td&gt;免费开源&lt;/td&gt;
        &lt;td&gt;基本停更&lt;/td&gt;
        &lt;td&gt;曾经很惊艳，如今 issue 堆积、长期没有新版&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Obsidian&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;Electron&lt;/td&gt;
        &lt;td&gt;个人免费&lt;/td&gt;
        &lt;td&gt;活跃&lt;/td&gt;
        &lt;td&gt;强大但"重"，是知识库工具，为"打开一个文件"杀鸡用牛刀&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;iA Writer&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;原生&lt;/td&gt;
        &lt;td&gt;付费&lt;/td&gt;
        &lt;td&gt;活跃&lt;/td&gt;
        &lt;td&gt;原生、极简、很优雅，但要付费，且偏纯写作&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Marked 2&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;原生&lt;/td&gt;
        &lt;td&gt;付费&lt;/td&gt;
        &lt;td&gt;活跃&lt;/td&gt;
        &lt;td&gt;只读预览器，不能编辑，定位和我的需求错位&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;VS Code&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;Electron&lt;/td&gt;
        &lt;td&gt;免费&lt;/td&gt;
        &lt;td&gt;活跃&lt;/td&gt;
        &lt;td&gt;它是 IDE，为看一个 md 启动整个编辑器，心理负担太大&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;把这张表看下来，会发现它们恰好踩满了我的四条雷区：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;要么收费&lt;/strong&gt;——Typora、iA Writer、Marked 2，体验好但要掏钱，而我的需求廉价到不值得为它付费；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要么是 Electron 套壳&lt;/strong&gt;——Mark Text、Obsidian、VS Code，随便开一个进程就吃掉几百 MB 内存，启动还有可感知的延迟，跟"随手看一眼"的轻快感背道而驰；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要么年久失修&lt;/strong&gt;——MacDown 这样的老牌开源货，代码还在，但维护近乎停摆，新语法跟不上；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要么不稳定&lt;/strong&gt;——MacDown 衍生出来的 MacDown3000，我用得最闹心，常常一更新就不能好好工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最让我难受的是第 2 条。Markdown 这种纯文本格式，本应是最轻量的东西，结果主流方案却普遍建立在一整个浏览器内核之上。为了渲染几段文字和代码块，背一个 Chromium，怎么想都不对劲。&lt;/p&gt;
&lt;h2&gt;那就自己写一个吧&lt;/h2&gt;
&lt;p&gt;既然挑不到，那就自己造。正好赶上 AI 辅助编程（大家戏称 "vibe coding"）已经足够顺手，我决定亲自下场，做一个完全贴合自己需求的编辑器。给它起名 &lt;strong&gt;Markdown2&lt;/strong&gt;——"Markdown Editor &lt;strong&gt;Too&lt;/strong&gt;"，一个不太正经的双关：既是"另一个 Markdown 编辑器"，也是"我也想要一个 Markdown 编辑器"。&lt;/p&gt;
&lt;p&gt;立项时我给自己定了几条死规矩，全部是对前面四条雷区的正面回应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原生，不用 Electron&lt;/strong&gt;——用 SwiftUI + AppKit，做成真正的 macOS app，启动快、内存小；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轻量，专注阅读/编辑&lt;/strong&gt;——单窗口，只有"编写"和"阅读"两种模式，不做文件管理、不做笔记库；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离线，不依赖网络&lt;/strong&gt;——公式、图表的渲染资源全部内置打包，断网照样工作；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开源免费&lt;/strong&gt;——MIT 协议，自己用得放心，也省得有人重蹈我"挑不到"的覆辙。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;技术栈选 Swift 而不是继续套 Electron，理由很直接：我想要的就是"原生的轻"。文本编辑这件事，AppKit 的 &lt;code&gt;NSTextView&lt;/code&gt; 经过几十年打磨，底子比任何 Web 富文本都扎实；外层用 SwiftUI 搭界面和状态管理，写起来又足够现代。两者配合，正好是"原生底座 + 现代外壳"。&lt;/p&gt;
&lt;p&gt;整个项目用 Swift Package Manager 组织，拆成三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MD2Core&lt;/code&gt;——纯逻辑层，Markdown 解析、渲染成 HTML、大纲提取、语法高亮、文档统计，不碰 UI，方便写单元测试；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MD2AppSupport&lt;/code&gt;——承载启动激活这类与 AppKit 生命周期强相关的辅助逻辑；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MD2App&lt;/code&gt;——SwiftUI/AppKit 的应用层，窗口、编辑器、预览、设置都在这。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;逻辑和界面分层之后，核心的解析渲染能力是可以脱离 GUI 单独测试的，这让"vibe coding"产出的代码不至于变成一团没法验证的浆糊。&lt;/p&gt;
&lt;h2&gt;途中那些有意思的坑&lt;/h2&gt;
&lt;p&gt;vibe coding 不是把需求丢给 AI 就万事大吉，真正花时间的，永远是那些"看起来该工作却不工作"的细节。挑几个印象最深的讲讲——前两个是早期的小坑，后两个则是真正需要先想清楚、再让 AI 动手的复杂需求。&lt;/p&gt;
&lt;h3&gt;坑一：&lt;code&gt;swift run&lt;/code&gt; 启动后，窗口躲到了别人后面&lt;/h3&gt;
&lt;p&gt;开发阶段我习惯用 &lt;code&gt;swift run Markdown2&lt;/code&gt; 直接跑。结果发现一个诡异现象：app 进程确实起来了，但&lt;strong&gt;窗口总是藏在当前 app 的后面&lt;/strong&gt;，前台焦点还赖在终端/编辑器上，等于这 app 没法正常用。&lt;/p&gt;
&lt;p&gt;排查下来，根因有点意思：SwiftPM 跑出来的是一个&lt;strong&gt;裸的 Mach-O 可执行文件&lt;/strong&gt;，而不是被 LaunchServices 管理的 &lt;code&gt;.app&lt;/code&gt; bundle。macOS 对"裸进程"和"正经 app"的窗口前台化待遇是不一样的——从裸进程里直接调 AppKit 的激活 API，并不足以可靠地把窗口抢到最前面。&lt;/p&gt;
&lt;p&gt;解决办法是加了一段"直接启动引导"（direct-launch bootstrap）：当 Markdown2 发现自己不是在 &lt;code&gt;.app&lt;/code&gt; 里运行时，就在 &lt;code&gt;.build&lt;/code&gt; 目录下临时合成一个 &lt;code&gt;Markdown2.app&lt;/code&gt;，再通过 &lt;code&gt;NSWorkspace&lt;/code&gt; 带激活地把这个 bundle 打开，然后原始的裸进程退出。被 bundle 拉起来的那个进程会跳过引导逻辑，正常进入主流程。后来又发现 SwiftUI 的 &lt;code&gt;WindowGroup&lt;/code&gt; 在这条特殊启动路径下，偶尔会进了 &lt;code&gt;applicationDidFinishLaunching&lt;/code&gt; 却没真正建出可用的初始窗口，于是干脆在 &lt;code&gt;AppDelegate&lt;/code&gt; 里&lt;strong&gt;显式创建主 &lt;code&gt;NSWindow&lt;/code&gt;&lt;/strong&gt;，把 SwiftUI 的 &lt;code&gt;ContentView&lt;/code&gt; 塞进去托管，再加上几次延迟激活重试兜底。&lt;/p&gt;
&lt;p&gt;这个坑我专门写了回归测试，验证激活策略提升、窗口层级排序、以及对"迟到窗口"的延迟重试。它很典型地说明了一件事：&lt;strong&gt;AI 能飞快写出主干，但操作系统层面的边角行为，还是得人去抠&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;坑二：公式和图表，到底要不要联网&lt;/h3&gt;
&lt;p&gt;很多 Markdown 编辑器渲染数学公式和图表时，是去 CDN 拉 KaTeX/Mermaid 这些前端库的。但这跟我"断网也能看"的原则冲突——很多时候我打开一个 md，恰恰是在没网或不想联网的场景。&lt;/p&gt;
&lt;p&gt;于是我把渲染资源全部&lt;strong&gt;内置打包进 app&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数学公式&lt;/strong&gt;用内置的 &lt;a href="https://katex.org/"&gt;KaTeX&lt;/a&gt;，行内 &lt;code&gt;$...$&lt;/code&gt; 和独占一行的 &lt;code&gt;$$...$$&lt;/code&gt; 都支持，还带上了 mhchem 扩展，能写化学式 &lt;code&gt;\ce{H2SO4}&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图表&lt;/strong&gt;支持 &lt;code&gt;mermaid&lt;/code&gt;、&lt;code&gt;flow&lt;/code&gt;（&lt;a href="https://flowchart.js.org/"&gt;flowchart.js&lt;/a&gt;）和 &lt;code&gt;sequence&lt;/code&gt;（&lt;a href="https://bramp.github.io/js-sequence-diagrams/"&gt;js-sequence-diagrams&lt;/a&gt;），引擎全部离线内置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代价是 app 体积大了一点，但换来"任何时候、任何网络环境下打开都一致"，我觉得非常值。顺带一提，这两块功能我是用 spec-driven 的方式做的——先写 proposal/design/spec，再让 AI 照着实现，archive 里留着 &lt;code&gt;add-math-support&lt;/code&gt; 和 &lt;code&gt;add-diagram-rendering&lt;/code&gt; 两个变更记录。给 vibe coding 套上一层规格约束，产出的可控性明显高很多。后面两个更复杂的需求，更是全靠这套流程才没翻车。&lt;/p&gt;
&lt;h3&gt;坑三：模式切换后，怎么还"停在原地"&lt;/h3&gt;
&lt;p&gt;这是个用着用着就越来越忍不了的体验问题：在编写/阅读两种模式之间切换时，视口&lt;strong&gt;总是弹回文档顶部&lt;/strong&gt;。文档但凡比一屏长，每切一次就得重新滚回刚才看的地方，读改循环被打断得很烦。&lt;/p&gt;
&lt;p&gt;听起来像"记一下滚动位置"这么简单，真做起来才发现卡在一个根本性的错位上：&lt;strong&gt;编辑器和预览是两套完全不同的坐标系&lt;/strong&gt;。编辑器认的是「源码第几行」，预览是一个 &lt;code&gt;WKWebView&lt;/code&gt;，认的是「DOM 元素的像素滚动偏移」。我"刚才在第 180 行"，翻译成预览里该滚到哪个像素，并没有现成的换算。&lt;/p&gt;
&lt;p&gt;破局点是文档大纲：每个 &lt;code&gt;Heading&lt;/code&gt; 节点&lt;strong&gt;同时带着源码 &lt;code&gt;line&lt;/code&gt; 和 HTML 元素 &lt;code&gt;id&lt;/code&gt;&lt;/strong&gt;——它天然就是横跨两套坐标系的那座桥。于是策略定为：切换时锚定到「光标/视口上方最近的那个标题」，按&lt;strong&gt;章节粒度&lt;/strong&gt;对齐，不追求像素级或光标级精确——目标是"落回文档的同一部分"，而不是分毫不差。没有标题的文档、或标题之前的内容，则退化成按滚动比例（scroll-fraction）兜底，&lt;strong&gt;绝不静默跳回顶部&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;真正棘手的是异步：&lt;code&gt;WKWebView&lt;/code&gt; 的滚动位置只能通过 &lt;code&gt;evaluateJavaScript&lt;/code&gt; 回调&lt;strong&gt;异步读取&lt;/strong&gt;，而 SwiftUI 的模式切换是同步发生的、旧视图瞬间就被销毁。所以根本不能"在切换那一刻才去问预览滚到哪了"——得在用户滚动时就&lt;strong&gt;持续把「当前视口顶部是哪个标题」缓存下来&lt;/strong&gt;，等切换真正发生时直接拿现成的值用。我把纯粹的「行号↔标题」解析逻辑（比如"找出 &lt;code&gt;line&lt;/code&gt; 不超过某行的最后一个标题"）抽进 &lt;code&gt;MD2Core&lt;/code&gt; 做成纯函数，这样不依赖跑起来的 WebView 就能单测。&lt;/p&gt;
&lt;p&gt;这种需求，是没法一句话丢给 AI 的。得先把"用什么做锚、锚在什么粒度、异步怎么规避、没锚点时怎么兜底"这几件事一条条想清楚、写进 spec，AI 才可能写对——否则它大概率给你一个"在简单情况下能跑、一遇到长文档和异步就乱套"的版本。&lt;/p&gt;
&lt;h3&gt;坑四：藏不住的图表"闪一下"&lt;/h3&gt;
&lt;p&gt;这个 bug 是做坑三时&lt;strong&gt;顺手揪出来的&lt;/strong&gt;：调试滚动位置时我注意到，预览加载完之后大约一秒，Mermaid 图表会肉眼可见地"闪"一下——先显示一段原始代码文本，然后才被引擎替换成渲染好的 SVG。&lt;/p&gt;
&lt;p&gt;根因有两层。其一，图表引擎是&lt;strong&gt;异步&lt;/strong&gt;渲染的，在它 resolve 之前，DOM 里就摆着原始源码当占位文本给读者看到了；其二，前面提过预览的 &lt;code&gt;WKWebView&lt;/code&gt; 每次切换模式都会&lt;strong&gt;重建&lt;/strong&gt;，所以这个闪烁不只第一次出现，而是&lt;strong&gt;每切一次复发一次&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;修法是给占位状态做文章：源码仍然留在 DOM 里供引擎读取，但用 CSS &lt;strong&gt;不再把它当正常文本显示&lt;/strong&gt;；等引擎渲染完成，再加一个约 120ms 的淡入，把生硬的"代码→图"硬切变成柔和过渡；同时给占位块预留一点 &lt;code&gt;min-height&lt;/code&gt;，减少 SVG 撑开时的布局跳动。这里最容易踩的雷是&lt;strong&gt;别把错误兜底搞丢了&lt;/strong&gt;——解析失败时仍然要把原始源码显示出来，绝不能让一个写错的图表变成一片空白。所以"渲染成功才隐藏、失败要回显"这条岔路必须在 spec 里写死，测试也要专门覆盖错误路径仍然可见。&lt;/p&gt;
&lt;p&gt;一个需求的调试过程牵出另一个隐藏问题，再各自立项、写 spec、补回归测试——这才是真实软件演进的样子，也是为什么我越来越离不开这套"先规格、后实现"的节奏。&lt;/p&gt;
&lt;h2&gt;现在的 Markdown2 长什么样&lt;/h2&gt;
&lt;p&gt;迭代到现在，它已经能覆盖我日常的全部需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单窗口、双模式&lt;/strong&gt;：一个主界面，&lt;code&gt;Esc&lt;/code&gt; 从编辑切到预览，预览里 &lt;code&gt;Cmd+双击&lt;/code&gt; 切回编辑，手不离键，且切换时停留在同一章节、不会弹回顶部；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多窗口&lt;/strong&gt;：每个文档独立开窗，从 Finder 双击打开时会复用那些还没动过的空白窗口，不会越开越乱；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大纲侧栏&lt;/strong&gt;：按标题自动生成，长文档导航很顺；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动保存 + 关闭确认&lt;/strong&gt;：已保存的文档防抖自动保存，有未保存改动时关窗/退出会提示；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态栏&lt;/strong&gt;：字数、字符数、行数、预计阅读时间一眼可见；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;够用的 Markdown 渲染&lt;/strong&gt;：标题、强调、链接、图片、引用、各类列表、GFM 表格、带语法高亮的围栏代码（Python/Go/Rust/Swift 等十来种语言）、YAML front matter、&lt;code&gt;[TOC]&lt;/code&gt;，外加前面说的离线公式与图表；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件类型关联&lt;/strong&gt;：打包后把 &lt;code&gt;.md&lt;/code&gt;/&lt;code&gt;.markdown&lt;/code&gt; 注册给自己，双击直达。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它&lt;strong&gt;不做&lt;/strong&gt;的事同样清晰：脚注、PDF/DOCX 导出、图片上传拖拽、打字机模式、主题管理、笔记库——这些都不在"随手打开看一眼"的射程内，我刻意没碰。约束自己不做什么，往往比堆功能更难，但这恰恰是我对那些"功能太复杂"的编辑器最大的不满，不能自己又掉进同一个坑。&lt;/p&gt;
&lt;h2&gt;一点心路体会&lt;/h2&gt;
&lt;p&gt;回头看这趟，从"挑不到一个趁手的 Markdown 编辑器"到"两天攒出一个自己用着舒服的原生 app"，最大的感受有两条。&lt;/p&gt;
&lt;p&gt;一是 &lt;strong&gt;AI 把"自己造一个轮子"的门槛拉得很低了&lt;/strong&gt;。放在以前，为了这么个小需求去啃 AppKit、SwiftUI、KaTeX 内嵌，光是查文档就能劝退我；现在 vibe coding 让我能把精力集中在"我到底想要什么、哪里还不对"上，而不是卡在 API 细节里。&lt;/p&gt;
&lt;p&gt;二是 &lt;strong&gt;AI 替代不了"品味"和"较真"&lt;/strong&gt;。它能秒出一个能跑的版本，但"窗口为什么躲到后面"这种坑、"要不要联网渲染"这种取舍、"克制住不加哪些功能"这种判断，依然得人来定。工具越强，"想清楚自己要什么"反而越值钱。&lt;/p&gt;
&lt;p&gt;代码已经开源在 GitHub（&lt;a href="https://github.com/stutiredboy/Markdown2"&gt;stutiredboy/Markdown2&lt;/a&gt;），MIT 协议。如果你也和我一样，只是想随手打开一个 Markdown 文件看一眼，欢迎试试，也欢迎拍砖。&lt;/p&gt;</content><category term="misc"/><category term="markdown"/><category term="macos"/><category term="swift"/><category term="swiftui"/><category term="vibe-coding"/><category term="ai"/></entry><entry><title>姚顺宇 4 小时访谈，通透有趣的灵魂~</title><link href="https://www.chenxiaosheng.com/posts/2026-05-19/yao-shunyu-interview-takeaways.html" rel="alternate"/><published>2026-05-19T20:00:00+08:00</published><updated>2026-05-19T20:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-05-19:/posts/2026-05-19/yao-shunyu-interview-takeaways.html</id><summary type="html">&lt;p&gt;听完姚顺宇四个多小时的访谈，整理了七点有意思的观察：AI 生成代码的比例、架构瓶颈往往只是低级 Bug、执行力比想法更重要、祛魅"老登"、AI 安全的政治博弈、OpenAI 给 Google 留了喘息窗口、以及程序员的"极度中心化"洗牌。&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="cover" src="/static/yao-shunyu-interview-takeaways/cover.jpg"&gt;&lt;/p&gt;
&lt;p&gt;听完这四个多小时的访谈，有几点觉得比较有意思的：&lt;/p&gt;
&lt;p&gt;1、姚顺宇坦言自己 90% 甚至 99% 的代码都是 AI 生成的，果然极客精神首先还得是实用主义，和古法编程形成了反差，拥有最好的本地大模型和代码助手，还非要去拼人力，这就是跟生产力过不去，当然不妨碍我们应当保留那 1%~10% AI 无法触达的能力；&lt;/p&gt;
&lt;p&gt;2、所谓"到了架构瓶颈"，往往只是代码里藏了个低级 Bug。就好比近一年来各大公有云的各种 big boom 可能单纯是一段糟糕的监控代码把控制面给打爆了；&lt;/p&gt;
&lt;p&gt;3、Idea 极其廉价，执行力和"靠谱"才是硬通货。姚顺宇提到"AI 这个事其实不太需要脑子，最重要的特质是 Execute（执行力）"。想法需要详尽的架构蓝图，现在不缺天马行空的所谓"英雄"，缺的是能把复杂的项目，一步步拆解清晰，能把事情做细，对交付结果死磕到底的靠谱工程师；&lt;/p&gt;
&lt;p&gt;4、祛魅"老登"，破除个人英雄主义神话。"没有哪个老登是你的亲属，他傻就可以直接说他傻"。技术发展到今天这个量级，单枪匹马搞定颠覆性算法的时代已经翻篇了，本质上都是工程体系和集体主义的胜利。只要逻辑自洽、结果能经得起客观标准的检验，就不需要去迷信那些旧时代的权威；&lt;/p&gt;
&lt;p&gt;5、Anthropic Dario 所谓的"AI 安全"，不过是反中和争霸的遮羞布。姚顺宇毫不避讳地爆料，离开 Anthropic 有 40% 的原因是因为受不了 CEO Dario 的反华立场，而且极其反感 Dario 那套"我们必须拥有最强模型才能推 AI 安全"的逻辑。这点真的让人挺 respect 的。现在硅谷那帮大佬动辄把"AI 安全"挂在嘴边，搞得极其神圣，但扒开这层皮，底层逻辑无非是"我要垄断最强技术，所以规矩必须由我来定"的政治博弈和商业算计；&lt;/p&gt;
&lt;p&gt;6、OpenAI 救了 Google 一命，"如果 ChatGPT 一鼓作气把 Search 吃透，Google 就完了。正因为没做到极致，反而给 Google 留了反击的时间。"如果在技术优势期没有把核心场景打穿、形成绝对的降维打击，那就是给对手留了喘息和重组火力的窗口期。OpenAI 后来高管流失、各种内部动荡，多少也印证了在战略执行上的拖泥带水是致命的；&lt;/p&gt;
&lt;p&gt;7、程序员的未来不是消失，而是"极度中心化"的残酷洗牌。"绝大多数失去独特价值，1/1000 的人拿 100 倍工资。"这句话很残酷也也很真实（虽然资本家大概率不会真的给你 100 倍工资）。未来留下来的，必定是那些具备全局视野、能把复杂业务逻辑拆解成 AI 可执行步骤的"超级节点"。这1/1000的人，其实是懂业务、懂架构，还能下一线动手的"超级总包商"；&lt;/p&gt;
&lt;p&gt;见识学习了姚顺宇作为"不靠 LP 吃饭、没有历史包袱"的新一代研究员的特质，这些带着浓厚极客精神和真实体感的观点，以姚顺宇相对平静又戏虐的语气跳脱而出，还是很有冲击力的。另外抛开那些虚头巴脑的，不管外面的风口吹得多高，哪怕是 Anthropic、Google DeepMind 这样的神级团队，本质上也是由一个个也会写出 Bug、也会搞出命名乌龙（Claude 3.5/3.6）的工程师组成的。对我们来说，用好当下最锋利的工具去武装自己才是咱们在这个快速变现的时代里最应该马上去做的（至于能不能成为最核心的护城河，谁知道呢~~&lt;/p&gt;</content><category term="misc"/><category term="ai"/><category term="llm"/><category term="vibe-coding"/><category term="anthropic"/><category term="openai"/></entry><entry><title>借长鑫科技的东风，把存储这件事一次讲透</title><link href="https://www.chenxiaosheng.com/posts/2026-05-17/changxin-storage-dram.html" rel="alternate"/><published>2026-05-17T10:00:00+08:00</published><updated>2026-05-17T10:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-05-17:/posts/2026-05-17/changxin-storage-dram.html</id><summary type="html">&lt;p&gt;2026 年 Q1 长鑫科技营收 508 亿、归母净利润 247.62 亿，半年指引净利 500 亿+。借这股东风，把存储产业链六层架构、龙头格局、长鑫产业链全景梳理一遍。&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;2026 年 5 月 17 日，长鑫科技更新科创板招股书：2026 年 Q1 营收 508 亿、归母净利润 247.62 亿，半年指引营收 1100—1200 亿、归母 500—570 亿，同比预增 2244%—2544%。一家长期亏损的国产 DRAM 厂，半年净利干到 500 亿+ 的体量，把"中国存储能不能赚钱"这道老问题，直接翻篇了。&lt;/p&gt;
&lt;p&gt;借这股东风，正好把存储这条产业链相关的信息学习一轮。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;一、AI 时代的存储，凭什么分成六层&lt;/h2&gt;
&lt;p&gt;CPU 和 GPU 自己只有一点点片上缓存，装不下模型，更装不下推理上下文。所以存储被迫往外摊，离算力越远、容量越大、单位成本越低，但延迟和能耗也越高。这就是分层的由来。&lt;/p&gt;
&lt;p&gt;按"离算力的距离"由近到远，AI 存储一共六层，2025 年六层合计市场约 &lt;strong&gt;2290 亿美元&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img alt="存储分层" src="/static/changxin-storage-dram/storage_pyramid.png"&gt;&lt;/p&gt;
&lt;p&gt;挑几个关键点说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;L0 片上 SRAM&lt;/strong&gt;：嵌在每颗芯片里，本身只有 10—17 亿美元的独立晶片市场，但每一颗 H100、B200、TPU v5 为了多塞 SRAM 都得多买晶圆，&lt;strong&gt;真正的利润池落在台积电&lt;/strong&gt;——全球先进工艺晶圆 70% 以上在它手里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L1 HBM&lt;/strong&gt;：用 TSV 把 DRAM 垂直堆叠，再用 CoWoS 贴到 GPU 旁边。它几乎单枪匹马决定了 AI 加速器能跑多大的模型。&lt;strong&gt;2025 年盘子约 350 亿美元，2028 年预计冲到 1000 亿美元，CAGR 40%&lt;/strong&gt;（美光 FY26Q1 业绩会口径）。2026Q1，SK 海力士的营业利润率打到了 &lt;strong&gt;72%&lt;/strong&gt;，比台积电（58%）和英伟达（65%）都高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L2 主板 DRAM&lt;/strong&gt;：DDR5/LPDDR/GDDR 这些常规内存条，&lt;strong&gt;2025 年 1218 亿美元，是六层里盘子最大的&lt;/strong&gt;。三星、SK、美光把产能往 HBM 倾斜后，反而让普通 DRAM 维持了高利润和定价权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L3 CXL 内存池&lt;/strong&gt;：把 DRAM 从单台服务器"池化"到机架级，多个 GPU 按需共享。当前规模才 16 亿美元，但 2033 年看到 237 亿。这一层最暴利的环节是重定时器，&lt;strong&gt;Astera Labs 占 55%，非 GAAP 毛利率 76%&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L4 企业级 SSD&lt;/strong&gt;：QLC 大容量盘已经把 HDD 从 AI 数据湖里彻底挤出去。&lt;strong&gt;2025 年 261 亿美元，CAGR 24%&lt;/strong&gt;，海力士子公司 Solidigm 与 Kioxia 已经做出 122TB 的单盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L5 NAS 与云对象存储&lt;/strong&gt;：数据湖、归档、跨团队协作的最外层，AWS+Azure+GCP 三家拿走 65—70%，靠的是"数据出网+生态锁定"的复利。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;越靠近算力，盘子越小，毛利越高&lt;/strong&gt;。HBM 盘子只有 DRAM 三分之一，毛利率却翻倍；CXL 重定时器盘子最小，毛利率最高。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;二、龙头格局：三强 + 一个新入场的第四名&lt;/h2&gt;
&lt;p&gt;存储是典型寡头行业，前三家在每一层都基本拿走 90% 以上。&lt;/p&gt;
&lt;p&gt;&lt;img alt="市场份额" src="/static/changxin-storage-dram/market_share.png"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DRAM&lt;/strong&gt;（TrendForce 2025Q4）：三星 35.8% 重夺第一、SK 海力士 32.1%、美光 22.4%，&lt;strong&gt;长鑫 CXMT 升至 4.7%&lt;/strong&gt;，构成新的"四强"格局。按招股书披露的产能/出货口径，长鑫 2025Q4 全球份额已达 7.67%——口径差异主要在于片数 vs 营收，长鑫片数堆上来了，单价还在追赶。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HBM&lt;/strong&gt;（2026Q1）：SK 海力士约 58%（拿下英伟达大单）、三星 22%、美光 20%。三星份额从 40% 掉到 22%，主要被先进封装良率拖累。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;企业级 SSD&lt;/strong&gt;（2025Q4）：三星 36.9%、SK（含 Solidigm）32.9%、美光 14.0%、Kioxia 11.7%、SanDisk 4.4%，前五合计约 90%。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;韩美三巨头同时吃下 DRAM+HBM+NAND 三层红利，是更全面的"AI 存储平台公司"。&lt;strong&gt;长鑫是这个格局里唯一的中国玩家&lt;/strong&gt;——之前的市场被三星、SK、美光三家寡头垄断超过 90%，长鑫从零起步做到全球第四，本身就是 0→1 的破局。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;三、长鑫的产业链全景：设备 + 材料 + 客户&lt;/h2&gt;
&lt;p&gt;长鑫现在合肥两座、北京一座，三座 12 英寸厂，&lt;strong&gt;月产能合计接近 30 万片，按晶圆片数算已占全球 DRAM 接近 15%&lt;/strong&gt;。上海新厂分两期共 20 万片，2027—2028 年陆续投产，远期奔着 50 万片以上去。IPO 拟募资 295 亿，其中约 200 亿会直接变成设备订单——这是国产设备厂未来两年最确定的基本盘。&lt;/p&gt;
&lt;p&gt;&lt;img alt="长鑫产业链" src="/static/changxin-storage-dram/supply_chain.png"&gt;&lt;/p&gt;
&lt;h3&gt;上游 · 设备（国产化率 ≈ 45%）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;已跑通&lt;/strong&gt;：刻蚀（北方华创 ICP &amp;gt; 50%、中微 CCP）、CMP（华海清科国产份额 &amp;gt; 90%）、清洗（盛美 SAPS/TEBO）、热处理（北方华创立式氧化炉）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证中&lt;/strong&gt;：薄膜沉积（拓荆 PECVD、北方华创 PVD 进入量产），但 DRAM 电容核心的高 K ALD 还是应材、ASM 的地盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卡脖子&lt;/strong&gt;：&lt;strong&gt;光刻国产化率不到 5%&lt;/strong&gt;，主力还是 ASML NXT:1980Di。2026 年 4 月美国众议院通过 MATCH 法案，要求 150 天内堵死 DUV 出口的灰色通道——这道墙短期绕不开。量测（KLA 占 51-54%）、涂胶显影（东电 90%）、离子注入也都还是个位数国产率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;上游 · 材料（领先设备一个身位）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;湿电子化学品 + CMP&lt;/strong&gt;：晶瑞电材（全品类）、安集科技（抛光液国内 ≈70%）、鼎龙股份（抛光垫国内 &amp;gt; 70%）、格林达（G5 级 TMAH 显影液）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;气体 + 前驱体 + 硅片&lt;/strong&gt;：广钢气体（电子大宗气签了 15 年长协，国内罕见）、华特气体（光刻气国内 60%）、雅克科技 / 安德科铭（ALD 前驱体）、沪硅产业 / 立昂微（12 英寸硅片）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;靶材&lt;/strong&gt;：江丰电子，12 英寸国内份额 &amp;gt; 70%，部分进入 3 纳米验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;薄弱环节&lt;/strong&gt;：ArF 光刻胶（南大光电、彤程刚破冰），掩模版（Photronics/Toppan/DNP 主导，国产化率极低）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;材料端最大的特点是"量"先于"价"——化学品单价指数从 2022 年的 100 跌到 2025H1 的 77.90，但景气回升直接传导到采购量。&lt;strong&gt;按 2026Q1 业绩外推，长鑫全年材料采购规模冲到 270 亿以上是大概率事件&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;下游 · 客户与封测&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;股东即大客户&lt;/strong&gt;：阿里云（持股 3.85%）、阿里网络、小米、联想、OPPO、vivo；华为昇腾系列的 HBM2/HBM3 与 LPDDR5X 由长鑫优先供应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封测已国产化&lt;/strong&gt;：沛顿（深科技）承接 &amp;gt; 50% 委外封测，长电、通富、盛合晶微补位；HBM 后段封装材料（华海诚科 GMC、联瑞新材球硅）随 2026 年底上海后段厂投产开始放量。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;四、为什么 DRAM 这一波"确定性"格外强&lt;/h2&gt;
&lt;p&gt;把视角拉远，三件事同时发生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AI 驱动的存储超级周期&lt;/strong&gt;：Omdia 预测全球 DRAM 市场规模从 2025 年的 &lt;strong&gt;1505 亿美元增长到 2030 年的 5710 亿美元，CAGR 30.56%&lt;/strong&gt;；HBM TAM 提前两年看到 1000 亿美元的水平。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三巨头扩产谨慎 + 产能向 HBM 倾斜&lt;/strong&gt;：直接结果是 2026Q1 DRAM 平均售价（ASP）环比涨幅超 60%，明确的卖方市场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;国产替代被外部约束加速&lt;/strong&gt;：MATCH 法案越收紧，长鑫每往国产设备多切一个百分点，对应的就是北方华创、中微、拓荆、华海清科、盛美的实打实订单。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;风险也要诚实地讲：DRAM 是存储三大品类中&lt;strong&gt;需求被两头挤压最严重&lt;/strong&gt;的一类（上有 HBM 抢算力侧需求、下有 NAND 抢 token 侧需求），从历史周期看也通常是最早反转的。半年 500 亿净利的高基数能维持多久，要看 H2 ASP 走势和长鑫向 G5/HBM3 的代际切换节奏。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;长鑫的故事，本质不是一家公司的 IPO，而是&lt;strong&gt;整条中国 12 英寸国产存储平台第一次被验证、被复用的过程&lt;/strong&gt;——刻蚀和 CMP 已经跑通，薄膜沉积和清洗在跟进，量测和光刻是下一个要啃的硬骨头。&lt;/p&gt;
&lt;p&gt;对普通投资者来说，与其押单一标的，不如把这张产业链全景图收好：&lt;strong&gt;HBM 的利润最暴利但跟自己关系最远，DRAM 的盘子最大、长鑫的扩产传导最直接，材料和设备里的"量增弹性"标的反而最持续。&lt;/strong&gt; AI 时代缺芯、缺电、缺存，长鑫正好压在"缺存"这条最确定的线上。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;主要数据来源&lt;/strong&gt;：
- 长鑫科技 2026 年更新版科创板招股书；
- TrendForce《Price Rally Drives 4Q25 DRAM Revenue Up 29.4%》；
- Counterpoint《Global DRAM and HBM Market Share》；
- Omdia DRAM TAM 预测；
- 美光 FY26Q1 业绩电话会；
- SK 海力士 2026Q1 财报。&lt;/p&gt;</content><category term="misc"/><category term="ai"/><category term="存储"/><category term="半导体"/><category term="长鑫"/><category term="DRAM"/><category term="HBM"/></entry><entry><title>USB 命名混乱救命表：从 1.1 到 120Gbps</title><link href="https://www.chenxiaosheng.com/posts/2026-04-26/usb-naming-cheat-sheet.html" rel="alternate"/><published>2026-04-26T12:00:00+08:00</published><updated>2026-04-26T12:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-04-26:/posts/2026-04-26/usb-naming-cheat-sheet.html</id><summary type="html">&lt;p&gt;USB 标准命名乱到没朋友——同一个速率换了三个商标名，"SuperSpeed" 听起来比 "SuperSpeedPlus" 只慢一点点其实差两倍，USB 3.0/3.1/3.2 Gen 1 是同一个东西。在 Fabien Sanglard 的 USB Cheat Sheet 基础上补充了截至 2026.04 已经量产的 USB4 v2.0 / 80Gbps 部分，做成一张可以直接抄的对照表。&lt;/p&gt;</summary><content type="html">&lt;h2&gt;为什么需要这张表&lt;/h2&gt;
&lt;p&gt;USB 的命名乱到什么程度？看几个常踩的坑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同一个速率，三个商标名&lt;/strong&gt;：USB 3.0、USB 3.1 Gen 1、USB 3.2 Gen 1 是&lt;strong&gt;完全同一个东西&lt;/strong&gt;（5 Gbps）。你在淘宝看到三种叫法只是上架时机不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"SuperSpeed" 比 "SuperSpeedPlus" 慢一倍&lt;/strong&gt;：听起来就差一个 Plus，实际上一个 5 Gbps 一个 10 Gbps。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;USB 3.2 Gen 2 和 USB 3.2 Gen 2x2 不是一回事&lt;/strong&gt;：后者是双 lane，速率翻倍到 20 Gbps。x2 这个后缀很多店铺不会写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;USB4 也不是只有一种&lt;/strong&gt;：有 USB4 20Gbps 和 USB4 40Gbps 两个档位，盒子上不写清楚就只能赌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;USB4 不等于 Thunderbolt 4，也不等于 USB-C&lt;/strong&gt;：USB-C 只是物理接口，Thunderbolt 是 Intel 的超集协议，USB4 是 USB-IF 的协议——三者经常被混着叫。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面这张表就是用来在买线、选 dock、判断 NAS / 移动硬盘盒接口时&lt;strong&gt;一眼对上号&lt;/strong&gt;的。&lt;/p&gt;
&lt;h2&gt;营销名 vs 技术名&lt;/h2&gt;
&lt;table style="width: 100%;"&gt;
    &lt;tr&gt;
        &lt;th&gt;营销名（Marketing Name）&lt;/th&gt;
        &lt;th&gt;等价别名（Also Known As）&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;信号速率&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;理论带宽&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;线芯&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;线长上限&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB 1.1&lt;/td&gt;
        &lt;td&gt;Full Speed&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;12 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1.5 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;4&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;4 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB 2.0&lt;/td&gt;
        &lt;td&gt;Hi-Speed&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;480 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;60 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;4&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;4 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;SuperSpeed USB 5Gbps&lt;/td&gt;
        &lt;td&gt;USB 3.0&lt;br/&gt;USB 3.1&lt;br/&gt;USB 3.2&lt;br/&gt;USB 3.1 Gen 1&lt;br/&gt;USB 3.2 Gen 1&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;5 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;625 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;8&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;3 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;SuperSpeedPlus USB 10Gbps&lt;/td&gt;
        &lt;td&gt;USB 3.1&lt;br/&gt;USB 3.2&lt;br/&gt;USB 3.1 Gen 2&lt;br/&gt;USB 3.2 Gen 2&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1 250 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;8&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;2 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;SuperSpeedPlus USB 20Gbps&lt;/td&gt;
        &lt;td&gt;USB 3.2&lt;br/&gt;USB 3.2 Gen 2x2&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;20 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;2 500 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;12&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB4 20Gbps&lt;/td&gt;
        &lt;td&gt;USB4 Gen 2×2&lt;br/&gt;USB4&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;20 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;2 500 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;12&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;0.8 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB4 40Gbps&lt;/td&gt;
        &lt;td&gt;USB4 Gen 3×2&lt;br/&gt;USB4&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;40 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;5 000 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;12&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;0.8 m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style="background:#fff7d6;"&gt;
        &lt;td&gt;&lt;b&gt;USB 80Gbps&lt;/b&gt;&lt;br/&gt;&lt;span style="font-size:0.9em;color:#666;"&gt;2026 补充&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;USB4 v2.0&lt;br/&gt;USB4 Gen 4×2&lt;br/&gt;Thunderbolt 5（超集）&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;80 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;12&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1 m（被动）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style="background:#fff7d6;"&gt;
        &lt;td&gt;&lt;b&gt;USB 120Gbps&lt;/b&gt;&lt;br/&gt;&lt;span style="font-size:0.9em;color:#666;"&gt;2026 补充 / 非对称&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;USB4 v2.0 Asymmetric&lt;br/&gt;Thunderbolt 5 Bandwidth Boost&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;120/40 Gbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;15 000 / 5 000 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;12&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1 m（被动）&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;看表姿势&lt;/strong&gt;：买线、买盒子的时候，先把店家写的字串去 "Also Known As" 这一列里搜，落到哪一行就是哪一行的速率。不要被 "SuperSpeed"、"USB 3.2"、"USB4" 这种笼统说法骗——它们对应着多个不同档位。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Gen 命名约定：A x B 是什么意思&lt;/h2&gt;
&lt;p&gt;USB-IF 后来给 3.x 和 USB4 加的 &lt;code&gt;Gen A x B&lt;/code&gt; 后缀，含义其实很规整：&lt;/p&gt;
&lt;pre style="background:#f7f8fa;padding:1ch;border-radius:6px;"&gt;
USB Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;A&lt;/span&gt; x &lt;span style="color:#c00;font-weight:bold;"&gt;B&lt;/span&gt;

&lt;span style="color:#0050d0;font-weight:bold;"&gt;A&lt;/span&gt; = 第几代信号（决定每个 lane 的速率）
&lt;span style="color:#c00;font-weight:bold;"&gt;B&lt;/span&gt; = 用了几条 lane（双工对数）
&lt;/pre&gt;

&lt;p&gt;下面这张表把 3.2 / USB4 各代乘开后的实际速率算清楚：&lt;/p&gt;
&lt;table style="width: 100%;"&gt;
    &lt;tr&gt;
        &lt;th&gt;名称&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;单 lane 信号&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;总信号速率&lt;sup&gt;a&lt;/sup&gt;&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;编码&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;去编码后&lt;sup&gt;b&lt;/sup&gt;&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;理论带宽&lt;sup&gt;b&lt;/sup&gt;&lt;/th&gt;
        &lt;th style="text-align:right;"&gt;真实顺序读&lt;sup&gt;c&lt;/sup&gt;&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB 3.2 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;1&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;1&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;5 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;5 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;8b/10b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;4 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;500 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~400 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB 3.2 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;1&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;2&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;5 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;8b/10b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;8 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1 000 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~800 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB 3.2 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;2&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;1&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;128b/132b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;9 696 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;1 212 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~780 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB 3.2 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;2&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;2&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;20 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;128b/132b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;19 392 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;2 424 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~1 600 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB4 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;2&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;2&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;10 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;20 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;128b/132b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;19 392 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;2 424 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~1 600 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;USB4 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;3&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;2&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;20 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;40 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;128b/132b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;38 787 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;4 848 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~2 700 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style="background:#fff7d6;"&gt;
        &lt;td&gt;&lt;b&gt;USB4 Gen &lt;span style="color:#0050d0;font-weight:bold;"&gt;4&lt;/span&gt;x&lt;span style="color:#c00;font-weight:bold;"&gt;2&lt;/span&gt;&lt;/b&gt;&lt;br/&gt;&lt;span style="font-size:0.9em;color:#666;"&gt;v2.0 / 2026 补充&lt;/span&gt;&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;40 000 Mbps（PAM3）&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;80 000 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;64b/66b&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;77 575 Mbps&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;9 696 MiB/s&lt;/td&gt;
        &lt;td style="text-align:right;"&gt;~6 000 MiB/s&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;p style="margin-top:1em;font-size:0.95em;"&gt;
&lt;b&gt;注&lt;/b&gt;：多 lane 系统在发端做 lane striping，在收端做 lane bonding，用户看到的就是一个连续 stream。&lt;br/&gt;
&lt;sup&gt;a&lt;/sup&gt; 厂商盒子上印的就是这一栏。&lt;br/&gt;
&lt;sup&gt;b&lt;/sup&gt; 减掉编码开销后的有效速率。8b/10b 损失 20%，128b/132b 损失约 3%，64b/66b 损失约 3%。&lt;br/&gt;
&lt;sup&gt;c&lt;/sup&gt; 实测顺序读速率（要扣掉协议层、文件系统、SSD 主控本身的瓶颈）。
&lt;/p&gt;

&lt;p&gt;理解了 &lt;code&gt;Gen A x B&lt;/code&gt; 之后，再回头看商标名就清晰多了：所谓 "SuperSpeedPlus 20Gbps" 其实就是 Gen 2 信号 × 2 lane；所谓 "USB4 40Gbps" 其实就是 Gen 3 信号 × 2 lane。这套约定唯一的问题是 &lt;strong&gt;USB-IF 自己又不在卖品名里写 Gen N×M&lt;/strong&gt;，反而强推 "USB 80Gbps" 这种以速率为名的新商标——表里第一栏就是这么来的。&lt;/p&gt;
&lt;h2&gt;线材：4 / 8 / 12 芯&lt;/h2&gt;
&lt;p&gt;USB 物理线最常见三档：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4 芯&lt;/strong&gt;：&lt;code&gt;PWR&lt;/code&gt;、&lt;code&gt;GND&lt;/code&gt;、&lt;code&gt;D+&lt;/code&gt;、&lt;code&gt;D-&lt;/code&gt;。一对差分信号，半双工一条 lane。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8 芯&lt;/strong&gt;：4 芯的基础上加 &lt;code&gt;RX+/RX-&lt;/code&gt;、&lt;code&gt;TX+/TX-&lt;/code&gt;。两条 lane，一上一下，全双工。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;12 芯&lt;/strong&gt;：8 芯的基础上再加一组 &lt;code&gt;RX2+/RX2-&lt;/code&gt;、&lt;code&gt;TX2+/TX2-&lt;/code&gt;。四条 lane（两上两下），全双工。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一个 USB lane = 一对 ± 差分线&lt;/strong&gt;。所以 4 芯 = 1 个半双工 lane，8 芯 = 2 个 lane，12 芯 = 4 个 lane。USB 3.2 Gen 2x2、USB4、USB4 v2.0 这些 "x2" 的标准都是要 12 芯线才能跑满的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;USB-A / USB-B 接口（4 芯 / 8 芯）&lt;/h2&gt;
&lt;table style="width:100%;"&gt;
    &lt;tr&gt;
        &lt;th style="text-align:center;width:25%;"&gt;Type-A 4-wires&lt;/th&gt;
        &lt;th style="text-align:center;width:25%;"&gt;Type-A 8-wires&lt;/th&gt;
        &lt;th style="text-align:center;width:25%;"&gt;Type-B 4-wires&lt;/th&gt;
        &lt;th style="text-align:center;width:25%;"&gt;Type-B 8-wires&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="vertical-align:bottom;text-align:center;background:#fff;"&gt;&lt;img loading="lazy" src="/static/usb-cheat-sheet/typea.svg" style="width:80%;height:auto;display:inline-block;margin:0 auto;"/&gt;&lt;/td&gt;
        &lt;td style="vertical-align:bottom;text-align:center;background:#fff;"&gt;&lt;img loading="lazy" src="/static/usb-cheat-sheet/typea3.svg" style="width:80%;height:auto;display:inline-block;margin:0 auto;"/&gt;&lt;/td&gt;
        &lt;td style="vertical-align:bottom;text-align:center;background:#fff;"&gt;&lt;img loading="lazy" src="/static/usb-cheat-sheet/typeb.svg" style="width:60%;height:auto;display:inline-block;margin:0 auto;"/&gt;&lt;/td&gt;
        &lt;td style="vertical-align:bottom;text-align:center;background:#fff;"&gt;&lt;img loading="lazy" src="/static/usb-cheat-sheet/typeb3.svg" style="width:60%;height:auto;display:inline-block;margin:0 auto;"/&gt;&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;8 芯版本的 Type-A 蓝色塑料舌片就是 USB 3.x 的标志——多出来的 5 个针脚塞在原来 4 个的下方/后方。USB-B 8 芯版本（也叫 USB 3.0 Type-B）则是直接在原本梯形上又叠了一块，所以看起来像一个驼背的小房子。&lt;/p&gt;
&lt;h2&gt;USB-C 接口（12 芯）&lt;/h2&gt;
&lt;p&gt;只有 USB-C 有足够的引脚数支撑两个完整 lane。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="/static/usb-cheat-sheet/typec.svg" style="width:70%;height:auto;display:block;margin:1em auto;"/&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CC1 / CC2&lt;/strong&gt;：Configuration Channel。两端用来识别谁是 DFP（Downstream Facing Port，主机一侧）、谁是 UFP（Upstream Facing Port，设备一侧），同时也用来协商供电档位、切到 Alt Mode（DisplayPort、Thunderbolt、HDMI 等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SBU1 / SBU2&lt;/strong&gt;：Sideband Use。用作 DisplayPort 的 AUX 通道、热插拔检测（HPD）等辅助信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;USB-C 真正强大的地方是这两组针脚——没有它们，USB 就是个纯数据口；有了它们，一根 C 线可以同时跑数据、视频、PD 供电，并且&lt;strong&gt;正反盲插&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;充电规格 / PD 档位&lt;/h2&gt;
&lt;table style="width:100%;"&gt;
    &lt;tr&gt;
        &lt;th style="text-align:center;"&gt;规范&lt;/th&gt;
        &lt;th style="text-align:center;"&gt;最大电压&lt;/th&gt;
        &lt;th style="text-align:center;"&gt;最大电流&lt;/th&gt;
        &lt;th style="text-align:center;"&gt;最大功率&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="text-align:center;"&gt;USB 2.0&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;5 V&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;500 mA&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;2.5 W&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="text-align:center;"&gt;USB 3.0 / 3.1&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;5 V&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;900 mA&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;4.5 W&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="text-align:center;"&gt;USB Battery Charging (BC) 1.2&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;5 V&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;1.5 A&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;7.5 W&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="text-align:center;"&gt;USB-C Current Mode（非 PD）&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;5 V&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;3 A&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;15 W&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="text-align:center;"&gt;USB-C / Power Delivery（PD 1/2）&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;20 V&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;5 A&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;100 W&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style="text-align:center;"&gt;USB-C PD 3.1 EPR（Extended Power Range）&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;48 V&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;5 A&lt;/td&gt;
        &lt;td style="text-align:center;"&gt;240 W&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;要拉满 240 W，必须是支持 EPR 的线（线身上一般直接印 &lt;code&gt;EPR 240W&lt;/code&gt;），并且两端 charger / 设备都支持 PD 3.1。普通 100 W 线直接插上去只会握手到 100 W 档位。&lt;/p&gt;
&lt;h2&gt;2026.04 补充：USB 80Gbps 已经量产&lt;/h2&gt;
&lt;p&gt;原文写于 2022 年，那会儿的天花板就是 USB4 40Gbps。&lt;strong&gt;到 2026.04，新的 USB4 v2.0（USB-IF 现在统一叫 "USB 80Gbps"）已经过了 spec → silicon → 整机三道关，能在零售店里买到。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;技术上和老 USB4 的关键差别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PAM3 信号&lt;/strong&gt;：每个 symbol 不再是 0/1 两态，而是 -1/0/+1 三态（每 symbol 携带 ~1.58 bit）。这是单 lane 速率从 20 Gbps 翻到 40 Gbps 的核心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编码换成 64b/66b&lt;/strong&gt;：开销从 ~3% 略降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对称模式 80 Gbps&lt;/strong&gt;：4 lane × 40 Gbps = 160 Gbit 信号，扣编码后约 ~9 700 MiB/s 理论值；实测顺序读目前在 6 GB/s 左右（被 NVMe 主控和 PCIe 拓扑限制住）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非对称模式 120 / 40 Gbps&lt;/strong&gt;：3 lane 走一个方向、1 lane 走反方向。专门给 8K 显示、外置 GPU 这种"大头朝外"的场景。USB-IF 称这个档为 "USB 120Gbps"，Intel 在 Thunderbolt 5 里叫 "Bandwidth Boost"——同一回事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口仍然是 USB-C，引脚数还是 12&lt;/strong&gt;。线长上限稍微放宽到 1 m（被动铜线）；超过 1 m 一般要 active 线。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：&lt;a href="https://fabiensanglard.net/usbcheat/index.html"&gt;Fabien Sanglard — USB Cheat Sheet&lt;/a&gt;（2022-05）&lt;/li&gt;
&lt;li&gt;规范文档（USB-IF 官方下载）：&lt;a href="https://www.usb.org/documents"&gt;USB Document Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;USB 1.0（1996-01）/ USB 1.1（1998-09）/ USB 2.0（2000-04）&lt;/li&gt;
&lt;li&gt;USB 3.0（2008-11）/ USB 3.1（2013-07）/ USB 3.2（2017-09）&lt;/li&gt;
&lt;li&gt;USB4 v1.0（2019-08）/ &lt;strong&gt;USB4 v2.0（2022-10）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;USB Power Delivery 3.1 EPR：&lt;a href="https://www.usb.org/document-library/usb-power-delivery"&gt;USB-IF PD Spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Thunderbolt 5 / Intel Barlow Ridge 控制器公开材料：&lt;a href="https://www.intel.com/content/www/us/en/newsroom/news/thunderbolt-5-unveiled-next-generation-cable-pc-accessories.html"&gt;Intel Newsroom&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="misc"/><category term="usb"/><category term="hardware"/><category term="cheatsheet"/><category term="thunderbolt"/></entry><entry><title>啤酒游戏：让 Claude 和 Codex 干了这杯酒</title><link href="https://www.chenxiaosheng.com/posts/2026-04-21/claude-vs-codex-beer-game-two-rounds.html" rel="alternate"/><published>2026-04-21T22:00:00+08:00</published><updated>2026-04-21T22:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-04-21:/posts/2026-04-21/claude-vs-codex-beer-game-two-rounds.html</id><summary type="html">&lt;p&gt;同一道啤酒游戏案例题、同一份 PDF，Claude 最初算出超额收益 500–1000 元，Codex 最初算出 7500–7900 元。我把两份答案互丢回去，让它们围绕“backorder 假设”和“期末库存估值口径”互评两轮。最后两边在主策略和收益量级上基本收敛，但中心值仍未完全统一。&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://www.chenxiaosheng.com/posts/2026-04-20/claude-and-codex-crosscheck-homework.html"&gt;上一篇&lt;/a&gt;讲的是同一门《运营与供应链管理》的设施选址案例。那次比较干净：重心法有唯一解，让两个 AI 互评一轮，再用 Python 跑一遍，基本就能定谁对谁错。&lt;/p&gt;
&lt;p&gt;这次不一样。第二次课后作业是经典的&lt;strong&gt;啤酒游戏&lt;/strong&gt;（Beer Game）：零售商 → 批发商 → 制造商三级供应链，真实需求在第 2 周从 4 箱/周升到 8 箱/周，之后保持稳定，但由于信息失真和 4 周提前期，链路里层层放大。案例写到：到第 24 周零售商还堆着 75 箱、批发商还堆着 218 卡车量，而制造商在第 19 周库存已经冲到 93 批。&lt;/p&gt;
&lt;p&gt;题目三问：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;画出 1–24 周库存和订单图，解释牛鞭效应&lt;/li&gt;
&lt;li&gt;分析牛鞭效应的影响、成因、对策&lt;/li&gt;
&lt;li&gt;假设你是“聪明零售商”，给出最优订货策略，并算出你比“普通零售商”多赚多少钱&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问题 1、2 偏定性，两个 AI 答得大体接近。真正分歧出在问题 3：&lt;strong&gt;Claude 最初算出超额收益约 500–1000 元，Codex 最初算出 7500–7900 元。差了一个数量级。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一开始的分歧，其实不只是一道算术题&lt;/h2&gt;
&lt;p&gt;两份答案的最优订货策略看上去很接近：第 1 周按常态 4 箱，第 2 周开始往上调，之后维持 8 箱/周的稳态。真正的差别只在一句话：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude：第 2 周订 &lt;strong&gt;12 箱&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Codex：第 2 周订 &lt;strong&gt;8 箱&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但光是 4 箱之差，不足以解释为什么一个答案多赚几百元，另一个多赚几千元。真正把两份答案拉开的，是背后的两个建模选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行为假设&lt;/strong&gt;：第 5 周不可避免的 4 箱短缺，到底能不能在第 6 周作为 &lt;code&gt;backorder&lt;/code&gt; 补卖？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收益口径&lt;/strong&gt;：第 24 周普通零售商手上的 75 箱库存，到底是资产，还是在本题比较口径里被等效忽略掉？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事后看，双方真正争的不是“谁算错了”，而是“哪套假设和口径更适合这道题”。&lt;/p&gt;
&lt;h2&gt;第一轮：双方先各自为自己的答案辩护&lt;/h2&gt;
&lt;p&gt;我先让它们各自写一份对比报告：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Claude_VS_Codex_by_Claude.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Claude_VS_Codex_by_Codex.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一轮的立场很清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude 认为自己那份更合理，因为案例 PDF 第 6、7 周明确写了顾客会留下名字电话等货，说明 &lt;code&gt;backorder&lt;/code&gt; 真实存在&lt;/li&gt;
&lt;li&gt;Codex 认为自己那份更严谨，因为从 24 周现金流口径看，普通零售商第 24 周手里还有 75 箱库存，占着现金、卖得慢，财务上不能当成“已经实现的收益”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果把两边都尽量善意理解，它们其实各有抓手：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude 抓的是&lt;strong&gt;案例行为证据&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Codex 抓的是&lt;strong&gt;比较口径和现金占用&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到这一步，我还不能明确说谁一定错。因为和设施选址那题不一样，啤酒游戏这道题在第 3 问里确实存在建模自由度。&lt;/p&gt;
&lt;h2&gt;第二轮：真正让局面收敛的，不是继续争观点，而是“用对方的尺子重算一遍”&lt;/h2&gt;
&lt;p&gt;第二轮我把两份对比报告再丢回去，让双方继续回应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Claude_VS_Codex_Round2_by_Claude.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Claude_VS_Codex_Round2_by_Codex.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude 这一轮做得最好的一点，是它没有继续停留在“PDF 原文支持 backorder”这种事实争论上，而是直接做了一件更硬的事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;用 Codex 自己定义的口径，反算 Claude 的策略。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它的核心计算逻辑是：&lt;/p&gt;
&lt;p&gt;如果第 2 周订 12 箱，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 5 周的 4 箱缺口先形成 &lt;code&gt;backorder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第 6 周正好到货 12 箱&lt;/li&gt;
&lt;li&gt;这 12 箱刚好覆盖“第 5 周积压的 4 箱 + 第 6 周真实需求 8 箱”&lt;/li&gt;
&lt;li&gt;之后回到稳态，每周到货 8 箱、卖 8 箱&lt;/li&gt;
&lt;li&gt;因而 &lt;strong&gt;期末库存仍然是 0&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按这个路径，套进 Codex 当时自己采用的“收入 - 采购 - 持有成本”的 24 周现金流式口径，会得到：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;口径&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Claude 策略（W2=12）&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Codex 策略（W2=8）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;销量（箱）&lt;/td&gt;
&lt;td style="text-align: right;"&gt;188&lt;/td&gt;
&lt;td style="text-align: right;"&gt;184&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;收入（元）&lt;/td&gt;
&lt;td style="text-align: right;"&gt;23,500&lt;/td&gt;
&lt;td style="text-align: right;"&gt;23,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;采购（元）&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17,600&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;持有成本（元）&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;现金利润（元）&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,890.77&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,790.77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;期末库存（箱）&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;也就是说，在&lt;strong&gt;同一口径&lt;/strong&gt;下，Claude 的第 2 周订 12 箱方案，比 Codex 的第 2 周订 8 箱方案&lt;strong&gt;多赚 100 元&lt;/strong&gt;。这 100 元恰好就是多履约的 4 箱 × 单箱毛利 25 元。&lt;/p&gt;
&lt;p&gt;这一步很关键，因为它把争论从“你觉得、我觉得”推进到了“就用你自己的尺子量，结果还是这样”。&lt;/p&gt;
&lt;h2&gt;第二轮之后，Codex 的主要修正也很明确&lt;/h2&gt;
&lt;p&gt;Codex 在第二轮里做了两个重要修正。&lt;/p&gt;
&lt;h3&gt;1. 承认原先的 7500/7900 元量级不合理&lt;/h3&gt;
&lt;p&gt;它接受了一个关键点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;不能把普通零售商第 24 周剩余的 75 箱库存直接按 0 价值处理。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为案例并没有说这 75 箱永远卖不出去。真实需求仍然大约是 8 箱/周，这些库存更合理的理解是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;期末库存资产&lt;/li&gt;
&lt;li&gt;后续持有成本负担&lt;/li&gt;
&lt;li&gt;潜在的折价风险和资金占用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而不是立刻等于当期亏损。&lt;/p&gt;
&lt;p&gt;这一步修完以后，Codex 把自己原先的“多赚 7500/7900 元”回收掉了。&lt;/p&gt;
&lt;h3&gt;2. 接受“第 2 周订 12 箱”作为基准更优方案&lt;/h3&gt;
&lt;p&gt;在允许并管理 &lt;code&gt;backorder&lt;/code&gt; 的前提下，Codex 也接受了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 2 周订 12 箱&lt;/li&gt;
&lt;li&gt;第 3～20 周每周订 8 箱&lt;/li&gt;
&lt;li&gt;第 21～24 周订 0 箱&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个策略方向。&lt;/p&gt;
&lt;p&gt;不过这里 Codex 也补了一句我认为很重要的限定：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个“12 箱更优”的结论，最好显式写出前提：&lt;strong&gt;允许并管理 backorder&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我认为这是必要的，因为 PDF 确实证明了 &lt;code&gt;backorder&lt;/code&gt; 现象存在，但它并不自动等于“第 5 周那 4 箱一定 100% 都能补卖”。所以更稳妥的说法，不是“无条件严格最优”，而是“在题目给定的聪明零售商设定下，作为基准解更合理”。&lt;/p&gt;
&lt;h2&gt;到最后，双方其实收敛到了什么？&lt;/h2&gt;
&lt;p&gt;如果只看“最后谁赢”，这个过程会被误读成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude 证明了自己是对的&lt;/li&gt;
&lt;li&gt;Codex 改口认输&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但我觉得更准确的描述是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;双方最后在“主策略”和“收益量级”上基本收敛了，但在普通零售商对照组的具体重构数值上，还没有完全统一。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;已经收敛的部分是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第 1、2 问两边都能作为参考&lt;/li&gt;
&lt;li&gt;第 3 问的基准策略应以“第 2 周订 12 箱”为主&lt;/li&gt;
&lt;li&gt;这个策略的前提是：允许并管理 &lt;code&gt;backorder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;原先的 &lt;code&gt;7500/7900 元&lt;/code&gt; 应判为口径失真，不再成立&lt;/li&gt;
&lt;li&gt;更合理的收益量级是：&lt;strong&gt;几百元到一千元左右&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但还没有完全统一的部分是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude 版当前仍把中心值写成 &lt;code&gt;727 元&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Codex 修订后写成了基准 &lt;code&gt;476 元&lt;/code&gt;，并把现实区间写成 &lt;code&gt;500～1,000 元&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这说明它们已经&lt;strong&gt;没有大的结论性冲突&lt;/strong&gt;，但还保留着一个典型的“同方向、不同中心值”的建模差异：普通零售商路径到底按哪组重构数据来算。&lt;/p&gt;
&lt;h2&gt;一个小插曲：我自己也踩了“AI 不重读文件”的坑&lt;/h2&gt;
&lt;p&gt;这轮还有个挺好玩的插曲。&lt;/p&gt;
&lt;p&gt;共识文件落完之后，我又让 Claude 再检查一次两份正式答案是否还有逻辑冲突。按道理说，这一步应该先重读最新版文件再比。&lt;/p&gt;
&lt;p&gt;但它一开始并没有这么做。它直接沿用脑子里前几轮的旧印象，给了我一个“已经基本没问题”的判断。我后来自己去看文件，才发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codex 的正式答案已经被修订过了&lt;/li&gt;
&lt;li&gt;第 3 问的主策略、收益区间、口径说明都已经更新了&lt;/li&gt;
&lt;li&gt;但 Claude 那次检查没有先读最新版，就直接开始比较&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我后来让它重新读取文件，再做一遍结论，结果就正常了。&lt;/p&gt;
&lt;p&gt;这件事让我挺警惕一件事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI 在多轮上下文里，不是只会“算错”，它也会像人一样&lt;strong&gt;偷懒地以为自己还记得文件长什么样&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以现在我的默认流程变成了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;跨轮复核前，明确要求它&lt;strong&gt;重新读取目标文件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不接受“基于之前讨论我认为……”这种比较&lt;/li&gt;
&lt;li&gt;文件类任务尽量让它把引用行号和当前版本一起报出来&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;这次复盘，对流程最大的启发是什么？&lt;/h2&gt;
&lt;h3&gt;1. 有争议的题，不能只让双方“表态”，要让它们“同口径重算”&lt;/h3&gt;
&lt;p&gt;第一轮的问题在于，双方都能自圆其说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude 说：PDF 支持 &lt;code&gt;backorder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Codex 说：现金口径更保守&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两句话都没错，但都还不够“打穿”对方。第二轮真正让问题收敛的，是 Claude 改变了打法：不用自己的尺子，而是用 Codex 自己的尺子，去量 Claude 的方案。&lt;/p&gt;
&lt;p&gt;这比继续争抽象原则有效得多。&lt;/p&gt;
&lt;h3&gt;2. 案例题里，分歧很多时候不是“算错”，而是“口径没统一”&lt;/h3&gt;
&lt;p&gt;设施选址那题属于“公式题”，ground truth 比较硬。&lt;/p&gt;
&lt;p&gt;啤酒游戏第 3 问更像“建模题”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;backorder&lt;/code&gt; 怎么处理&lt;/li&gt;
&lt;li&gt;期末库存怎么估值&lt;/li&gt;
&lt;li&gt;普通零售商的路径怎么重构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都不是 PDF 直接给好的唯一答案，而是解释空间。&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;/ul&gt;
&lt;h3&gt;3. 最终共识最好落成文件，而且最好双方都落&lt;/h3&gt;
&lt;p&gt;这次最终比较清楚的阶段，不是嘴上说“好像差不多了”，而是双方都逐步把立场修正进文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Claude_VS_Codex_Final_Consensus_by_Claude.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Claude_VS_Codex_Final_Consensus_by_Codex.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;以及修订后的 &lt;code&gt;answers/啤酒游戏-案例解答_Codex.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦文件更新，后续讨论就有了稳定落点。否则很容易出现“口头上已经同意了，原答案还停留在旧版本”的错位。&lt;/p&gt;
&lt;h2&gt;这道题本身也给了我一个额外收获&lt;/h2&gt;
&lt;p&gt;我觉得这次最有意思的，不只是“谁算赢了”，而是它把一个课堂上常讲、但很多时候很抽象的管理问题，演得特别具体：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;同一个案例、同一份 PDF，只要口径和隐含假设不统一，最后得出的“结论”就可能差一个数量级。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这和牛鞭效应本身很像。牛鞭效应里，上游看到的不是“需求”，而是“需求 + 滞后 + 情绪 + 防御性下单”。这次两个 AI 一开始看到的也不是同一个“收益”，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;收益 + &lt;code&gt;backorder&lt;/code&gt; 假设&lt;/li&gt;
&lt;li&gt;收益 + 库存估值口径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以最后真正值得记住的，不是 7900 还是 727，而是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在供应链和管理分析里，&lt;strong&gt;先统一口径，再比较数值&lt;/strong&gt;，往往比争谁的数字更大更重要。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;尾声&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI 也会偷懒，也会划水。&lt;/strong&gt; 而且不是个案，使用 AI 也是要走心的~  &lt;/p&gt;</content><category term="misc"/><category term="ai"/><category term="llm"/><category term="claude"/><category term="codex"/><category term="homework"/><category term="supply-chain"/></entry><entry><title>我是怎么让 Claude 和 Codex 帮忙完成课程作业</title><link href="https://www.chenxiaosheng.com/posts/2026-04-20/claude-and-codex-crosscheck-homework.html" rel="alternate"/><published>2026-04-20T22:00:00+08:00</published><updated>2026-04-20T22:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-04-20:/posts/2026-04-20/claude-and-codex-crosscheck-homework.html</id><summary type="html">&lt;p&gt;这门《运营与供应链管理》课后有个「设施选址」的案例，我让 Claude 先做了一遍，又用 Codex 独立做了一遍，两个答案思路一致但数字和结论完全相反。把两份答案同时丢回 Claude 让它当裁判，结果它推翻了自己之前的解法——三处方法论错误被它一条条指出来，还配合 Python 独立验证给出了修订版。这篇记录一下这个「左右互博」的过程，和我为什么觉得它比让单一模型深挖要靠谱。&lt;/p&gt;</summary><content type="html">&lt;p&gt;这学期有一门《运营与供应链管理》，第二次课留了个案例作业：南方超级医疗设备公司的设施选址。题目不难，标准的&lt;strong&gt;重心法&lt;/strong&gt;（Center of Gravity）单仓库选址问题，考的是套公式 + ROI 判断。&lt;/p&gt;
&lt;p&gt;我照例先让 Claude 做了一份，给了一个看起来挺漂亮的参考答案。觉得不放心，开了一个新的会话用 Codex 再独立跑了一遍。拿到结果之后我愣住了：&lt;strong&gt;两份答案的思路完全一致，计算出来的最优坐标相差不大，但最终 ROI 正负相反、决策完全相反。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude：重心在 &lt;strong&gt;(7.42, 5.04)&lt;/strong&gt;，年运输节约 $46,838，ROI −188%，&lt;strong&gt;结论：续租堪萨斯城，别搬&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Codex：重心在 &lt;strong&gt;(7.58, 4.51)&lt;/strong&gt;，年运输节约 $223,646，ROI +41.22%，&lt;strong&gt;结论：应当搬迁&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个都是基于同一份 PDF 案例、同一个重心法公式，怎么会算出一正一负？&lt;/p&gt;
&lt;h2&gt;左右互博：把裁判位置也交给 AI&lt;/h2&gt;
&lt;p&gt;我本来的计划是自己动手验算一遍，但看了一会儿发现这事没那么简单——重心法有两个版本（一次加权平均 vs Weiszfeld 迭代）、费率 R 的单位有两种理解（美元/担 vs 美元/担/英里）、外向成本又有两种建模方式。要真人判裁谁对谁错，我得先把讲义从头翻一遍。&lt;/p&gt;
&lt;p&gt;懒病发作，我干脆把两份答案一起丢回一个新的 Claude 会话，原话是这样的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你完成的解答位于：南方医疗设备公司设施选址_参考答案-Claude.md
我使用其他模型解答的结果和你基本不一致，虽然思路是相同的，结果位于：南方医疗设备公司设施选址-案例解答-Codex.md
请检查双方究竟谁的逻辑存在问题，并给出支撑理由，如有必要，请修正完善最终版本结论&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意这里：Claude 开新会话后其实是没有前次对话记忆的，所以这份 &lt;code&gt;_参考答案-Claude.md&lt;/code&gt; 对它而言就是一份陌生的答案，和 Codex 的那份在它眼里是对等的。这样它就没有「维护自己面子」的心理包袱。&lt;/p&gt;
&lt;h2&gt;Claude 翻出了自己的错&lt;/h2&gt;
&lt;p&gt;它做的第一件事是去读讲义 PDF 里重心法的原始公式。这一步关键——它不是在两份答案的输出之间找分歧，而是把两者都对照&lt;strong&gt;原题和教材&lt;/strong&gt;做 ground truth 校对。&lt;/p&gt;
&lt;p&gt;几分钟后它给出了结论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Codex 的逻辑正确，Claude 的参考答案存在三处方法论错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;三处错误它一条条列了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. R 的单位错。&lt;/strong&gt; 讲义里目标函数是 TC = Σ Vᵢ·Rᵢ·dᵢ，要求 Rᵢ 为「美元/担/英里」。案例表里给的是「美元/担（当前线路总费率）」，必须除以当前距离才能换算。&lt;strong&gt;案例原文第 2 页其实写得很清楚&lt;/strong&gt;：「可估计出以仓库为起点的外向运输平均成本为 0.0235 美元/担/英里」——这句话就是在提示学生做单位换算。前一次 Claude 把这段忽略了，直接把美元/担当权重用，相当于把当前距离"二次放大"进了权重里，让远距离客户影响被高估。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 没迭代。&lt;/strong&gt; 讲义 p.13 把步骤写得很明白：先用 X₀ = Σ(V·R·X)/Σ(V·R) 算一个&lt;strong&gt;初始坐标&lt;/strong&gt;，然后根据 dᵢ 做 Weiszfeld 迭代直到收敛。前一次 Claude 算完初始坐标就收工了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 外向费率结构错。&lt;/strong&gt; 案例文本明确给了一个统一的外向均值 0.0235。前一次 Claude 反其道而行之，按每个客户的当前总费率和距离回推了每客户不同的每英里率。这会让 Chicago 显得每英里 0.050、Montreal 每英里 0.019，两者相差 2.6 倍——这种人为的不一致把新选址的外向成本虚推高了，把真实 $223K 的年节约压成虚假的 $47K。&lt;/p&gt;
&lt;p&gt;这三处错误串在一起，合力把最优点错误地向东北拉偏、又错误地抬高了新址的外向账单，最终得到"别搬"的结论。Codex 在这三点上都是按讲义和案例原文来的。&lt;/p&gt;
&lt;h2&gt;用 Python 作第三个独立裁判&lt;/h2&gt;
&lt;p&gt;到这一步其实已经够了，但我让 Claude 再做一件事：&lt;strong&gt;别用自己的文字推理下结论，写段 Python 从头算一遍，以程序的数值输出为准。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面这段是它写的（我稍微整理了一下）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;math&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;230.0&lt;/span&gt;  &lt;span class="c1"&gt;# 1 坐标单位 = 230 英里&lt;/span&gt;

&lt;span class="c1"&gt;# (V, R_per_mile, X, Y) — 内向用个别每英里率，外向统一 0.02346&lt;/span&gt;
&lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;61500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.01438521&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# 菲尼克斯&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.00791246&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# 蒙特雷&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;9.10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# 西雅图&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# 洛杉矶&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# 丹佛&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;9500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;7.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# 达拉斯&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;29500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# 芝加哥&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# 亚特兰大&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;41300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;14.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.55&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# 纽约&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;8600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;12.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;7.80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# 多伦多&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;0.02346330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;14.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8.25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# 蒙特利尔&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 初始种子&lt;/span&gt;
&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Weiszfeld 迭代&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;di&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;
        &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
    &lt;span class="n"&gt;Xn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Yn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Xn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Yn&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Xn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Yn&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;TC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Xp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Yp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Xp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Yp&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Xi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Yi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;optimum = (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.4f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.4f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TC @ KC  (8.20, 6.00) = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.00&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;,.0f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TC @ Claude(7.42,5.04) = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.04&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;,.0f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TC @ Codex (7.58,4.51) = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;,.0f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;TC @ optimum          = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;,.0f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&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&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;optimum = (7.5806, 4.5084)
TC @ KC  (8.20, 6.00) = 6,983,017
TC @ Claude(7.42,5.04) = 6,792,698
TC @ Codex (7.58,4.51) = 6,759,371
TC @ optimum          = 6,759,371
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;几件事一次性水落石出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Weiszfeld 迭代收敛到 (7.5806, 4.5084)，和 Codex 的 (7.58, 4.51) 逐位对上&lt;/li&gt;
&lt;li&gt;Codex 那份的 TC $6,759,371 在正确方法下也&lt;strong&gt;正好是最优值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Claude 前一份的 (7.42, 5.04) 在正确方法下 TC 是 $6,792,698——&lt;strong&gt;比最优值贵了 $33K/年&lt;/strong&gt;，连局部最优都不是&lt;/li&gt;
&lt;li&gt;堪萨斯城 TC $6,983,017 和题目给的 $6,982,104 差不到 0.02%（差异来自案例里距离和均值的四舍五入），说明用的参数都没偏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后年节约 $223,646，减去每年 $10 万新增租金，一次性搬迁投入 $30 万，&lt;strong&gt;年 ROI 41.22%，远高于 25% 阈值&lt;/strong&gt;。正确结论是&lt;strong&gt;应当搬迁&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;然后它把自己之前写错的那份改了&lt;/h2&gt;
&lt;p&gt;我让它把修订版直接覆盖原文件，同时在文件顶上标注修订说明。它交出的文件顶部是这样的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;# 案例：南方超级医疗设备公司设施选址 —— 参考答案（修订版）&lt;/span&gt;

&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;修订说明：本版本相对于初版修正了三处方法论错误——&lt;/span&gt;
&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;(1) 将运输费率 R 从「美元/担」换算为「美元/担/英里」；&lt;/span&gt;
&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;(2) 在初始种子 (X₀, Y₀) 的基础上补充 Weiszfeld 迭代；&lt;/span&gt;
&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;(3) 外向费率按案例原文统一取 0.02346 美元/担/英里，而非按客户回推。&lt;/span&gt;
&lt;span class="k"&gt;&amp;gt; &lt;/span&gt;&lt;span class="ge"&gt;修订后结论从「维持堪萨斯城」转为「应搬迁至 (7.58, 4.51) 附近」。&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;三个情景（Q1 基准、Q2 需求变化、Q3 费率上涨）的结论全部翻过来，ROI 从负值变成 41%–60% 区间，决策从"别搬"变成"都该搬"。&lt;/p&gt;
&lt;h2&gt;为什么「左右互博」比让单一模型深挖更靠谱&lt;/h2&gt;
&lt;p&gt;这件事让我后面做类似作业时固定下来了一套流程：&lt;strong&gt;一份答案让 A 模型做，一份让 B 模型做，再让 A 当裁判评 B，要求它必须回到原始材料（讲义/教材/题目原文）做 ground truth 校对，最后用 Python 当第三方复算。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为什么这么折腾？几个观察：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单一模型很难发现自己的方法论偏差。&lt;/strong&gt; 如果我一开始就让 Claude 自己审自己那份答案，它大概率会在"既有框架"里找局部的算术错误，而不会去质疑 R 的单位、是否需要迭代这种&lt;strong&gt;框架本身&lt;/strong&gt;的选择。Codex 那份答案的存在提供了一个&lt;strong&gt;不同的框架&lt;/strong&gt;作为参照，这时候回到原题去校对才有意义。换句话说：两个答案的差异不在于谁算得更准，而在于&lt;strong&gt;把可能的解法空间暴露了出来&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当裁判比当选手更挑剔。&lt;/strong&gt; 让 Claude 做题时它倾向于快速给出一个自洽的流程。让它当裁判时，它会去对照原文、逐字抠公式、做单位分析。同一个模型、同一个任务知识，输出质量差很远——角色不同，防御姿态不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python 兜底是性价比最高的一步。&lt;/strong&gt; 两个模型吵起来的时候，文字推理再漂亮也有可能都错。让它们最后落到一段可执行的代码上，数值一跑就有定论。这次 Python 确认 (7.5806, 4.5084) 是严格最优，也顺手把 Claude 自己给的 (7.42, 5.04) 彻底证伪了——文字说它"连局部最优都不是"显得没说服力，一行 &lt;code&gt;TC(7.42, 5.04) = 6,792,698 &amp;gt; 6,759,371&lt;/code&gt; 谁都没法赖。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型之间没有「面子」问题。&lt;/strong&gt; 前面提到我没告诉新会话的 Claude 那份错答案是它自己写的。这是有意为之——同一个账号、同一个模型，它如果意识到是在审自己的作业，有没有可能措辞会软一点？我不敢保证。切掉身份线索，让它&lt;strong&gt;以陌生答卷的姿态&lt;/strong&gt;去评，才放得开。&lt;/p&gt;
&lt;h2&gt;我有不担心的地方，也有担心的地方&lt;/h2&gt;
&lt;p&gt;不担心的是：作业本身是死题、有唯一答案、可以程序化验证。在这类场景下 AI + AI + 代码的组合已经比我自己从头算可靠得多——我自己算大概率也会在哪一步单位换算上犯 Claude 初版犯的那种错。&lt;/p&gt;
&lt;p&gt;担心的是：不是所有作业都有"Python 可跑的 ground truth"。比如案例讨论题让写一份"定性分析报告"的时候，两个模型可能会写出两份看起来都通顺、实则都在偏差范围内的答卷，而我没有外部锚点去判对错。这种时候"左右互博"只能证伪明显的错误，不能正向保证正确。&lt;/p&gt;
&lt;p&gt;但即便如此，&lt;strong&gt;让 AI 看它同行的作业&lt;/strong&gt;依然比&lt;strong&gt;让 AI 看自己的作业&lt;/strong&gt;要诚实。这是我这次学到的最实用的一招。&lt;/p&gt;
&lt;h2&gt;尾声&lt;/h2&gt;
&lt;p&gt;借助 AI ，我弄清楚了重心法为什么要迭代、为什么 R 必须是每英里率——这两件事要是我自己硬看讲义，大概率和 Claude 初版踩一样的坑。把作业交给 AI 之后，我反而把原理学得更明白了。&lt;/p&gt;
&lt;p&gt;这件事我没推广到所有课——有些课的作业本来就是要自己算才算练到手（比如数理课）。但对那些"考的是方法，不是手算熟练度"的课，我现在的默认姿势就是两个模型互评 + 代码兜底。花的时间比自己硬做少得多，踩的坑反而更少。&lt;/p&gt;
&lt;p&gt;下次有类似的折腾再来记录。&lt;/p&gt;</content><category term="misc"/><category term="ai"/><category term="llm"/><category term="claude"/><category term="codex"/><category term="homework"/></entry><entry><title>用 Cloudflare Worker + KV 给静态博客加一个浏览量计数</title><link href="https://www.chenxiaosheng.com/posts/2026-04-19/pageview-counter-with-cf-worker-kv.html" rel="alternate"/><published>2026-04-19T23:00:00+08:00</published><updated>2026-04-19T23:00:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-04-19:/posts/2026-04-19/pageview-counter-with-cf-worker-kv.html</id><summary type="html">&lt;p&gt;上一篇刚写完换主题的事，这篇继续给博客加一个浏览量计数。静态站托管在 GitHub Pages 上本来没法做这件事，用 Cloudflare Worker + KV 绕出来。顺带记一下为什么没选不蒜子 / GoatCounter，以及前端「seed 叠加」这个让老文章不从零起算的小机制。&lt;/p&gt;</summary><content type="html">&lt;p&gt;上一篇换主题写完的时候，心里还有一个未了的念头：新主题把 Google Analytics 那坨早就停服的 UA 代码删了之后，整站就零统计了。写的时候没想太多，但上线一两天之后，每次点开自己的博客都有一种「在给空气写字」的感觉 —— 到底还有没有人看，哪篇被多读几遍，完全没有信号。&lt;/p&gt;
&lt;p&gt;于是今晚继续折腾，给每篇文章加一个浏览量，顺带在页脚放一个整站 PV / UV。效果本身没什么稀奇，技术栈和取舍倒值得说一下。&lt;/p&gt;
&lt;h2&gt;静态站做计数的几种路子&lt;/h2&gt;
&lt;p&gt;博客源码在 &lt;code&gt;source&lt;/code&gt; 分支，CI 构建后推到 &lt;code&gt;gh-pages&lt;/code&gt;，GitHub Pages 托管。这个架构的好处是零运维、免费；坏处是 &lt;strong&gt;没有访问日志&lt;/strong&gt;，GitHub 只在 Traffic API 里给 14 天的 top paths，而且要 OAuth 认证，前端根本用不了。&lt;/p&gt;
&lt;p&gt;所以任何「在线计数」都得外挂。我考虑过的选项按接入门槛排列大概是这几类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;接入&lt;/th&gt;
&lt;th&gt;数据归属&lt;/th&gt;
&lt;th&gt;可控性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;不蒜子&lt;/td&gt;
&lt;td&gt;粘两行 script&lt;/td&gt;
&lt;td&gt;第三方&lt;/td&gt;
&lt;td&gt;几乎无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoatCounter 云版&lt;/td&gt;
&lt;td&gt;注册、粘 script&lt;/td&gt;
&lt;td&gt;第三方&lt;/td&gt;
&lt;td&gt;有仪表板和 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Umami / Plausible 自托管&lt;/td&gt;
&lt;td&gt;要一台 VPS&lt;/td&gt;
&lt;td&gt;自己&lt;/td&gt;
&lt;td&gt;完全&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Worker + KV&lt;/td&gt;
&lt;td&gt;写个 Worker&lt;/td&gt;
&lt;td&gt;自己&lt;/td&gt;
&lt;td&gt;完全&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这四类都想过。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不蒜子&lt;/strong&gt; 是中文博客圈最常见的选择，粘两行 JS 就能在页面上显示数字。但它不支持写入初始值、不暴露 API，偶尔还会抽风（&lt;code&gt;busuanzi.ibruce.info&lt;/code&gt; 服务不稳是老问题了）。更关键的是：刚删完一大票第三方脚本（GA、AdSense、Disqus）的新主题，再挂一个外链，心理上拧巴。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GoatCounter&lt;/strong&gt; 其实是符合我审美的选择 —— 开源、隐私友好、有 API、能自建也能用云版。唯一拿不准的是国内访问 &lt;code&gt;goatcounter.com&lt;/code&gt; 的稳定性，以及把读者数据托管给第三方（哪怕它是隐私友好的）仍然是「托管」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自托管 Umami / Plausible&lt;/strong&gt; 需要一台 VPS 常驻。对一个每月也写不了一两篇的博客，养一台 VPS 就为了这个，是过度消费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Worker + KV&lt;/strong&gt; 最终胜出。几个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我本来就是 CF 用户（域名 DNS 在 CF），账号、DNS 都是现成的&lt;/li&gt;
&lt;li&gt;Worker 免费档 100k 请求/天、KV 1k 写/天 + 100k 读/天，对个人博客是绰绰有余的天花板&lt;/li&gt;
&lt;li&gt;边缘节点在国内延迟一般不差&lt;/li&gt;
&lt;li&gt;Worker 是一张多用途底牌 —— 未来要做外链跳转计数、RSS 订阅数统计、简单的 webhook 都能复用同一套基础设施&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;唯一的缺点是要自己写代码、自己部署。但需求简单到几十行 TS 就能搞定，还换不来过度工程的烦恼。&lt;/p&gt;
&lt;h2&gt;先用 OpenSpec 把事情说清楚&lt;/h2&gt;
&lt;p&gt;跟上次换主题一样，动手之前我先让 Claude 用 OpenSpec 写了一份变更提案：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;openspec/changes/add-article-view-counts/
├── proposal.md   # 为什么、要改什么、不改什么
├── design.md     # 技术决策与取舍
├── specs/        # 可测试的规范（分三个 capability）
└── tasks.md      # 38 条分步任务
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;好处和上次一样 —— 边界清晰。比如 &lt;code&gt;design.md&lt;/code&gt; 里写死了几个关键决策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计数口径 &lt;strong&gt;粗放全算&lt;/strong&gt;：不按 IP 去重，不识别爬虫。我的需求是「大致知道哪些文章有人读」，不是给广告主看的精确数字&lt;/li&gt;
&lt;li&gt;不做 time series：KV 里只存当前累计值。要趋势数据直接看 CF Dashboard 自带的请求图即可&lt;/li&gt;
&lt;li&gt;UV 用 &lt;code&gt;sha256(IP + 当日日期 + salt)&lt;/code&gt; 去重：每日盐轮换，不能反推原 IP，GDPR 友好&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;seed 在前端叠加&lt;/strong&gt;：这是整个方案最值得单独讲的一点&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;seed 前端叠加：让老文章不从零起算&lt;/h2&gt;
&lt;p&gt;启用新计数有个心理坎：所有老文章（最早是 2013 年）都会从 0 开始。十三年前写的博文显示「0 次阅读」，哪怕它本来就没几个人读过，视觉上也很刺眼。&lt;/p&gt;
&lt;p&gt;最直接的办法是在 KV 里预填一个初始值，让 &lt;code&gt;pv:&amp;lt;slug&amp;gt;&lt;/code&gt; 从比如 3000 起算。但这样做有个恶心的地方：哪天想调整初始值（比如觉得这篇估高了），要从当前值里减掉旧 seed 再加新 seed，很容易算错。&lt;/p&gt;
&lt;p&gt;我最终用的办法是 &lt;strong&gt;把 seed 放在前端&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// themes/stuhouse/static/data/view-seeds.json&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;install-bind-mysql-dlz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;3900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;debian-add-cnnic-ca&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="mi"&gt;3900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;pelican-custom-jinja-filters&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;postgraduate&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;peril-of-laziness-lost&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&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&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;realtime_pv_from_worker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这样做的好处有三点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;seed 在 git 历史里&lt;/strong&gt;。想调整某篇文章的初始值，就是一次普通的 JSON 编辑 + commit，审计友好&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层与展示层解耦&lt;/strong&gt;。以后后端换成 GoatCounter / 自建服务，seed 逻辑一行不用改&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新文章自动 seed = 0&lt;/strong&gt;。JSON 里没这个 slug 就当 0，无需特殊处理&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;seed 数值本身来自一个简单的脚本 &lt;code&gt;scripts/generate_view_seeds.py&lt;/code&gt;，公式是 &lt;code&gt;max(评论数 × 120, 年数 × 300)&lt;/code&gt;。因为没导 Disqus 数据（懒得等那封导出邮件），实际跑的是纯年份兜底 —— 2013 年老文显示 3900 次阅读，2025 年新文显示 300 次，2026 年刚发的新文显示 0。不精确，但读起来自然。&lt;/p&gt;
&lt;h2&gt;Worker 侧：两个端点、几行代码&lt;/h2&gt;
&lt;p&gt;核心逻辑在 &lt;code&gt;worker/src/index.ts&lt;/code&gt;，就两个路由：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// GET /v/&amp;lt;slug&amp;gt; → 单篇文章 PV 自增并返回&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handleSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`pv:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pv&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;cur&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// GET /v/site → 整站 PV + UV&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handleSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;CF-Connecting-IP&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0.0.0.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ymd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// 站点 PV：粗放自增&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sitePv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;site:pv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;site:pv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitePv&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// 当日 UV：hash(ip + date + salt) 作 key 去重&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;|&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;|&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SALT_SECRET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`uv_today:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;expirationTtl&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// 当日 UV size + 历史归档累计&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;todayCount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;countPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`uv_today:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;siteUvTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;site:uv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pv&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;sitePv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;uv&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;siteUvTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;todayCount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;两件事值得注意。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KV 没有 atomic increment&lt;/strong&gt;。上面的 &lt;code&gt;get → +1 → put&lt;/code&gt; 在并发写入时理论上会丢计数。但这是个人博客，真并发极低，丢一两次也无所谓 —— 这正是「粗放口径」的含义。如果哪天真撞上高并发，再迁 Durable Object 就是，当下不过度设计。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UV 归档用 cron 做&lt;/strong&gt;。每天 UTC 00:05 触发一次 &lt;code&gt;scheduled&lt;/code&gt; 事件，列出昨日所有 &lt;code&gt;uv_today:&amp;lt;date&amp;gt;:*&lt;/code&gt; key 的数量累加到 &lt;code&gt;site:uv&lt;/code&gt;，然后把这些 key 删掉。这样 &lt;code&gt;uv_today:*&lt;/code&gt; 只保留今天一天的数据，不会无限膨胀。&lt;/p&gt;
&lt;h2&gt;部署踩坑：custom_domain 的首次 deploy&lt;/h2&gt;
&lt;p&gt;整个部署靠 wrangler CLI 完成。&lt;code&gt;wrangler.toml&lt;/code&gt; 填好 KV namespace id、secret 绑定、cron 触发器之后，一条 &lt;code&gt;wrangler deploy&lt;/code&gt; 就能上线。但第一次配 custom_domain 时，我走了个小弯路。&lt;/p&gt;
&lt;p&gt;原计划（也是 &lt;code&gt;tasks.md&lt;/code&gt; 里最初写的）是老路子：先在 CF DNS 面板手工加一条 &lt;code&gt;views.chenxiaosheng.com&lt;/code&gt; 的 A/AAAA 占位记录（任意 IP，重点是 Proxy 状态 orange cloud on），然后在 CF Dashboard 配置 Workers Routes 规则。&lt;/p&gt;
&lt;p&gt;后来发现 wrangler 现在有更优雅的写法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[[routes]]&lt;/span&gt;
&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;views.chenxiaosheng.com&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;custom_domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;加上 &lt;code&gt;custom_domain = true&lt;/code&gt; 这一行，&lt;code&gt;wrangler deploy&lt;/code&gt; 会自动：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给这个子域创建 DNS 记录&lt;/li&gt;
&lt;li&gt;向 CF 申请 edge 证书&lt;/li&gt;
&lt;li&gt;把 Worker 绑到该 hostname&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一条命令搞定 DNS + 证书 + 路由三件事，比老路子干净得多。&lt;/p&gt;
&lt;p&gt;有个小注意点：首次 &lt;code&gt;wrangler deploy&lt;/code&gt; 之前需要先 &lt;code&gt;wrangler secret put SALT_SECRET&lt;/code&gt;，但 put secret 时会触发一次占位 deploy。如果 &lt;code&gt;wrangler.toml&lt;/code&gt; 里已经有 &lt;code&gt;custom_domain&lt;/code&gt; 配置而 DNS 还没就绪，会报错。保险做法是先把 &lt;code&gt;[[routes]]&lt;/code&gt; 段注释掉，完成 secret put 和首次 deploy（会部署到 &lt;code&gt;*.workers.dev&lt;/code&gt;），再取消注释重新 deploy 启用 custom domain。我踩过一次之后把这个顺序写进了 &lt;code&gt;tasks.md&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;前端：不到 50 行&lt;/h2&gt;
&lt;p&gt;前端逻辑全在 &lt;code&gt;themes/stuhouse/static/js/views.js&lt;/code&gt;。页面加载后做两件事：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;showSiteStats&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WORKER_BASE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/v/site&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;site-pv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;site-uv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;site-stats-wrap&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* 静默 */&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;showArticlePv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 非文章页跳过&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SEEDS_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;({})),&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WORKER_BASE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/v/&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pvData&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pvData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seeds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pvData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;article-pv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;article-pv-wrap&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;几个刻意的设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有展示元素初始是 &lt;code&gt;hidden&lt;/code&gt; 的，JS fetch 成功才 reveal。失败就保持隐藏 —— 禁用 JS 的读者看到的页面和接入前完全一致，不会出现占位符或半成品&lt;/li&gt;
&lt;li&gt;两个 fetch 彼此独立，一个失败不影响另一个&lt;/li&gt;
&lt;li&gt;数字用 &lt;code&gt;Intl.NumberFormat('en-US').format()&lt;/code&gt; 加千分位，这行代码比 polyfill 轻多了&lt;/li&gt;
&lt;li&gt;文章页由 &lt;code&gt;&amp;lt;body data-slug="..."&amp;gt;&lt;/code&gt; 带出 slug；非文章页（归档、标签）body 上没这个属性，JS 直接跳过&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加上 CSS 小样式和一个眼睛图标的 inline SVG，前端总共改动不到 50 行。&lt;/p&gt;
&lt;h2&gt;跑起来之后的样子&lt;/h2&gt;
&lt;p&gt;上线之后，页脚多了一行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;本站累计阅读 1 次 · 访客 1 人
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;点开最早那篇 2013 年的《配置 Bind 使用 MySQL dlz 模式》，meta 行变成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;📅 2013-09-09 · 约 3 分钟阅读 · 👁 3,901 次阅读
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;seed 3900 加上我刚刷的 1 次。整体效果自然，没有违和感。&lt;/p&gt;
&lt;p&gt;观察几个刻度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KV 免费额度&lt;/strong&gt;：单次页面加载触发 1-2 次 fetch，按每日 100 个访客估算，一天产生约 200 次 write，远低于 1000/天的上限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟&lt;/strong&gt;：Worker 冷启动毫秒级，国内访问通过 CF 边缘节点基本感知不到&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CF Dashboard&lt;/strong&gt;：免费账户自带的 Analytics 已经能看请求量、错误率、cron 触发记录，不需要额外仪表盘&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;写在后面&lt;/h2&gt;
&lt;p&gt;整套方案从 OpenSpec 提案到上线，节奏大致是：晚上跟 Claude 对了一轮选型、起草了 proposal 和 tasks，之后花了一个下午跑完 38 条任务里的 36 条（剩两条是明天早上验证 cron 和 UV 归档用的）。工作量其实不大，但过程里有两个体会想记一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;seed 叠加这个 pattern 值得收藏&lt;/strong&gt;。它把「初始化数据」这个一次性需求，从数据层（KV）挪到了展示层（前端 JSON），代价是每次渲染多读一个文件，换来的是可审计、可解耦、可迁移。同样的思路可以用在评论数迁移、积分系统初始化、任何「旧数据 + 新增量」的场景里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CF Worker 作为静态站的「副引擎」很好使&lt;/strong&gt;。它不改变「源码 + 静态输出」的主流程，却能把那些需要「一点点动态能力」的需求接住 —— 计数、跳转、短链、webhook、简单的 API 代理，都是十几行代码的事。很多人把「加动态能力」和「离开静态站」划等号，其实没必要。&lt;/p&gt;
&lt;p&gt;至于今晚之前那个「在给空气写字」的感觉，暂时还没消失 —— 刚上线，KV 里真实数据也就一两条。但架构至少支持知道答案了，剩下的就交给时间。&lt;/p&gt;</content><category term="misc"/><category term="cloudflare"/><category term="serverless"/><category term="pelican"/><category term="blog"/></entry><entry><title>Claude 在这个周末帮我翻新了博客</title><link href="https://www.chenxiaosheng.com/posts/2026-04-19/claude-refreshed-my-blog-this-weekend.html" rel="alternate"/><published>2026-04-19T20:30:00+08:00</published><updated>2026-04-19T20:30:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-04-19:/posts/2026-04-19/claude-refreshed-my-blog-this-weekend.html</id><summary type="html">&lt;p&gt;这个周六晚用 Claude Code + OpenSpec 把博客主题从 2014 年的 Bootstrap 3 换成了新写的 stuhouse（名字随意取的）。顺便回顾了一下不久前那次「双仓库合一」的发布流程改造。两次经历指向同一件事：AI 让我这种拖延了多年的懒人也愿意动手修基础设施了。&lt;/p&gt;</summary><content type="html">&lt;p&gt;上一篇《LLM 让程序员的编程美德「懒惰」更显重要了》发完没几天，自己倒先干了一件极其「懒惰」的事 —— 周六晚让 Claude 把博客主题从 2014 年的 Bootstrap 3 一路翻修到新写的 stuhouse。&lt;/p&gt;
&lt;p&gt;折腾完准备写这篇更新稿的时候，想起前几天那次「双仓库合一」也是 Claude 陪着做的。两次经历放在一起回看，心里有些话想讲。&lt;/p&gt;
&lt;h2&gt;背景：博客的技术债从哪来&lt;/h2&gt;
&lt;p&gt;这个博客从 Pelican 起步就没怎么动过骨架，细数下来积了一堆账：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主题 &lt;code&gt;pelican-bootstrap3&lt;/code&gt; 基于 Bootstrap 3.1.1，2019 年已经 EOL&lt;/li&gt;
&lt;li&gt;FontAwesome 3 的 &lt;code&gt;icon-*&lt;/code&gt; 前缀和后来加载的 FA 4.7 不兼容，图标基本不渲染&lt;/li&gt;
&lt;li&gt;导航栏的 &lt;code&gt;.navbar-collapse&lt;/code&gt; 没有汉堡按钮，移动端扩展菜单打不开&lt;/li&gt;
&lt;li&gt;侧边栏被 &lt;code&gt;hidden-xs hidden-sm&lt;/code&gt; 整体隐藏，手机读者看不到任何侧栏元素&lt;/li&gt;
&lt;li&gt;还挂着一个 2023 年 7 月就停服的 Google Analytics UA 代码&lt;/li&gt;
&lt;li&gt;存在 IE8 兼容分支（&lt;code&gt;respond.js&lt;/code&gt;、&lt;code&gt;jXHR.js&lt;/code&gt;、条件注释）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; 缺 &lt;code&gt;lang&lt;/code&gt; 属性，&lt;code&gt;DEFAULT_LANG='en'&lt;/code&gt; 和中文内容自相矛盾&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然写的很少了，但每次打开博客都在心里念叨一句「改一下吧」，然后关掉浏览器。就这么好几年了。&lt;/p&gt;
&lt;h2&gt;案例一：双仓库合一&lt;/h2&gt;
&lt;p&gt;在讲主题之前，先倒回去讲一下更早的一次改造。&lt;/p&gt;
&lt;p&gt;博客最早的源码放在 Bitbucket，GitHub 上的这个仓库 &lt;code&gt;stutiredboy.github.io&lt;/code&gt; 只存构建产物。这种「源码 + 发布」分离的结构是早年从别人那抄来的，对当时的我来说是合理的：Bitbucket 支持免费私有仓库，GitHub Pages 又必须 public。&lt;/p&gt;
&lt;p&gt;但维护起来很累：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每次写完文章要先 push 到 Bitbucket，本地 &lt;code&gt;pelican content&lt;/code&gt;，再去 GitHub 仓库 push 生成的 HTML&lt;/li&gt;
&lt;li&gt;两个仓库的 commit history 基本断裂，追溯改动很费劲&lt;/li&gt;
&lt;li&gt;源码仓库没有 CI，发布靠手工&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;2026 年 4 月中旬，我让 Claude 帮我把这个流程整合了：所有源文件迁到 GitHub 这个仓库的 &lt;code&gt;source&lt;/code&gt; 分支，&lt;code&gt;gh-pages&lt;/code&gt; 分支改由 GitHub Actions 自动构建 + force-push。&lt;/p&gt;
&lt;p&gt;提交记录里那条 &lt;code&gt;Migrate Pelican blog source from bitbucket to single-repo structure&lt;/code&gt; 就是这次改造的结果。合并后的工作流简单了不少：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改完文章 push 到 &lt;code&gt;source&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Actions 自动跑 &lt;code&gt;pelican content&lt;/code&gt;，推到 &lt;code&gt;gh-pages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;线上站就更新了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这次改造范围不大，但其实挺花时间，我自己拖了好几年没做。Claude 基本就是 prompt + 15 分钟的事情，包括 deploy workflow 的编排、&lt;code&gt;.gitignore&lt;/code&gt; 的合并、&lt;code&gt;content/extra/CNAME&lt;/code&gt; 怎么放都替我想明白了。&lt;/p&gt;
&lt;h2&gt;案例二：这个周末，换主题&lt;/h2&gt;
&lt;p&gt;这次范围大得多。&lt;/p&gt;
&lt;h3&gt;用 OpenSpec 先把事情说清楚&lt;/h3&gt;
&lt;p&gt;技术债那么一大堆，要是自己硬上，大概率会陷入「改一点发现另一个地方也得改」的泥潭，特别是改主题，人肉改起来是真累。这次换主题之前，我先用 OpenSpec 的工作流写了一份变更提案：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;openspec/changes/modernize-blog-theme/
├── proposal.md   # 为什么要改、改什么、不改什么
├── design.md     # 新主题的视觉和技术决策
└── tasks.md      # 分阶段任务清单
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;proposal 里最关键的是 Non-Goals 那一段，明确写了「不引入站内搜索、不引入新评论系统、不做 PWA、不保留旧主题做 A/B 切换」。有了这段边界声明，后面实现的时候就不会被无关想法拉偏。&lt;/p&gt;
&lt;h3&gt;新主题 stuhouse&lt;/h3&gt;
&lt;p&gt;新主题目录是 &lt;code&gt;themes/stuhouse&lt;/code&gt;，整体思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单列居中，正文最大宽度约 720 px&lt;/li&gt;
&lt;li&gt;极简顶栏：站名、归档、简介、RSS、明暗切换&lt;/li&gt;
&lt;li&gt;桌面宽屏（≥ 1200 px）右侧有 sticky TOC，移动端折叠&lt;/li&gt;
&lt;li&gt;彻底去掉侧边栏，标签云迁到独立的 &lt;code&gt;/tags.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;中文系统字体栈（PingFang SC → HarmonyOS Sans SC → 微软雅黑 → system-ui），零字体请求&lt;/li&gt;
&lt;li&gt;暗色模式跟随 &lt;code&gt;prefers-color-scheme&lt;/code&gt; + 顶栏手动切换，localStorage 持久化&lt;/li&gt;
&lt;li&gt;代码块基于 Nord 配色，浅色、深色两套，带一键复制按钮&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;技术栈清理得更彻底：Bootstrap 3、jQuery、FontAwesome、respond.js、jXHR.js 全删；图标改成内联 SVG；Google Analytics / Disqus / AdSense 相关代码一并下线。&lt;/p&gt;
&lt;h3&gt;一个中间的小坑&lt;/h3&gt;
&lt;p&gt;周六 23 点开始挂着 claude 跑，周日早上起来直接可以人工验收了。按 Claude 提示启动 &lt;code&gt;pelican --listen&lt;/code&gt;，浏览器打开 &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;，样式完全不对（以下修复还是 Claude 干的）。&lt;/p&gt;
&lt;p&gt;开发者工具一看：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;GET https://www.chenxiaosheng.com/theme/css/main.css
=&amp;gt; [FAILED] net::ERR_BLOCKED_BY_ORB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;原因是 &lt;code&gt;pelicanconf.py&lt;/code&gt; 里 &lt;code&gt;SITEURL = 'https://www.chenxiaosheng.com'&lt;/code&gt; 直接写死成生产域名，本地生成的 HTML 里所有资源链接都指向线上，自然加载不到新主题的 CSS。&lt;/p&gt;
&lt;p&gt;Pelican 社区的标准做法是拆分 dev/prod 两份配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pelicanconf.py&lt;/code&gt;：&lt;code&gt;SITEURL = ''&lt;/code&gt;，本地预览用，生成根相对 URL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publishconf.py&lt;/code&gt;：继承 pelicanconf，覆盖 &lt;code&gt;SITEURL&lt;/code&gt; 为真实域名，CI 构建用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺带把 GitHub Actions workflow 的构建命令从 &lt;code&gt;-s pelicanconf.py&lt;/code&gt; 换成 &lt;code&gt;-s publishconf.py&lt;/code&gt;。再跑 &lt;code&gt;pelican --listen&lt;/code&gt;，样式全部到位。&lt;/p&gt;
&lt;h3&gt;归档&lt;/h3&gt;
&lt;p&gt;最后把 &lt;code&gt;openspec/changes/modernize-blog-theme/&lt;/code&gt; 迁到 &lt;code&gt;openspec/changes/archive/2026-04-19-modernize-blog-theme/&lt;/code&gt;，这个变更就算完结了。tasks.md 里的每一项都对应真实的代码改动，想回溯某个决策随时能翻到上下文。&lt;/p&gt;
&lt;h2&gt;用 AI 修基础设施的一点体感&lt;/h2&gt;
&lt;p&gt;两次改造拉通一起看，有几点观察。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI 缩短了「想做」和「开动」之间的距离。&lt;/strong&gt; 双仓库合一这件事我拖了好几年，主题翻修拖得更久。之所以一直不动，是因为每次想起来都要估算「我得花多久」「会不会中途卡在某个坑里」。有了 Claude 之后，这个心理门槛明显降低：哪怕卡住，它能帮我很快试出方向，沉没成本低得多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但 AI 不会替我做判断。&lt;/strong&gt; OpenSpec 这种「先写提案再动手」的节奏，核心作用是逼我自己把事情想清楚 —— Non-Goals 该写哪些、视觉调性参考谁、侧边栏留不留。这些决策 Claude 给不了，它最多帮我把判断落成代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「懒惰」这事，AI 和人的方向是反的。&lt;/strong&gt; 上一篇文章里引用了 Bryan Cantrill 的观点：LLM 不受时间约束，没有「为未来的自己省力」的动机，放任它会让系统越滚越大。我这次刻意在 proposal 里写死边界，就是给它上一道缰绳。最终 stuhouse 的代码量比旧主题少了一大截，就是这个约束的结果。&lt;/p&gt;
&lt;h2&gt;尾声&lt;/h2&gt;
&lt;p&gt;这篇文章本身是在新主题下写的。预览窗口里看过去，正文宽度、代码块配色、TOC 高亮、复制按钮都按预期工作。写博客这件事本来应该只关心文字本身，过去那些年花在「发布流程怎么跑通」「主题哪里又坏了」上的精力，现在终于可以还给写作本身了。&lt;/p&gt;
&lt;p&gt;至于下一篇，等下一篇再见。&lt;/p&gt;</content><category term="misc"/><category term="claude"/><category term="ai"/><category term="pelican"/><category term="blog"/></entry><entry><title>LLM 让程序员的编程美德“懒惰”更显重要了</title><link href="https://www.chenxiaosheng.com/posts/2026-04-15/peril-of-laziness-lost.html" rel="alternate"/><published>2026-04-15T21:30:00+08:00</published><updated>2026-04-15T21:30:00+08:00</updated><author><name>小生说大声讲</name></author><id>tag:www.chenxiaosheng.com,2026-04-15:/posts/2026-04-15/peril-of-laziness-lost.html</id><summary type="html">&lt;p&gt;Bryan Cantrill 在最新博文中指出，LLM 天生缺乏程序员的核心美德——懒惰。懒惰驱使程序员构建精炼的抽象，而 LLM 不受时间约束，只会让系统越来越臃肿。本文以半翻译半解读的形式梳理这篇文章的核心观点。&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;本文基于 Bryan Cantrill 的博文 &lt;a href="https://bcantrill.dtrace.org/2026/04/12/the-peril-of-laziness-lost/"&gt;The Peril of Laziness Lost&lt;/a&gt; 进行半翻译半解读。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近读到 Bryan Cantrill 的一篇博文，标题叫 &lt;em&gt;The Peril of Laziness Lost&lt;/em&gt;（「懒惰」丧失之险）。Cantrill 是 DTrace 的共同作者、Sun Microsystems 前工程师、Oxide Computer 的联合创始人，在系统编程领域是教父级的人物。这篇文章从 Larry Wall 提出的「程序员三大美德」出发，讨论了 LLM 对软件工程的深层影响。读完之后觉得切中要害，值得展开聊聊。&lt;/p&gt;
&lt;h2&gt;程序员的三大美德&lt;/h2&gt;
&lt;p&gt;Larry Wall 在经典著作 &lt;em&gt;Programming Perl&lt;/em&gt;（业界称为「骆驼书」）中提出了程序员的三大美德：&lt;strong&gt;懒惰（Laziness）、急躁（Impatience）和傲慢（Hubris）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原文引用如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If we're going to talk about good software design, we have to talk about Laziness, Impatience, and Hubris, the basis of good software design. We've all fallen into the trap of using cut-and-paste when we should have defined a higher-level abstraction, if only just a loop or subroutine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这三个词乍听像是贬义，但 Larry Wall 赋予了它们完全不同的含义。其中「懒惰」最为深刻：它不是说程序员偷懒不干活，而是说好的程序员会因为「懒得重复做同一件事」，从而被驱动着去构建更高层次的抽象，让系统变得尽可能简洁。&lt;/p&gt;
&lt;p&gt;Cantrill 对此的理解是：&lt;strong&gt;懒惰驱使我们把系统做到尽可能简单（但不能过于简单）——去开发强大的抽象，从而让更多的事情变得更容易。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;懒惰的悖论：偷懒需要付出大量努力&lt;/h2&gt;
&lt;p&gt;这里有一个精妙的悖论：&lt;strong&gt;要做到「懒」，其实需要非常努力。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当程序员看似在「摸鱼」——躺在吊床上发呆、盯着天花板想问题——实际上是在反复推敲问题的本质。这种「吊床驱动开发」（hammock-driven development）是真正的智力劳动。程序员愿意在当下花费大量时间去打磨抽象，是因为在优化「未来的自己」的时间。当这个计算做对了，效果是辉煌的：好的抽象不仅服务于自己，还惠及所有后来者。&lt;/p&gt;
&lt;p&gt;换句话说，&lt;strong&gt;程序员的懒惰让软件更容易编写，让系统更容易组合，让更多人能写出更多好的软件。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Brogrammer 文化：虚假的勤奋&lt;/h2&gt;
&lt;p&gt;然而，过去二十年软件行业的扩张带来了一种不那么健康的文化。现代抽象带来的巨大生产力催生了一种对「虚假勤奋」的推崇——Cantrill 称之为 brogrammer 文化。在这种文化里，「吊床上的深度思考」被「crushing code」的鸡血叙事取代。人们不再关心抽象是否优雅，而是关心写了多少行代码、提了多少个 PR、一天搞定了几个 feature。&lt;/p&gt;
&lt;h2&gt;LLM：给 Brogrammer 注射类固醇&lt;/h2&gt;
&lt;p&gt;LLM 的出现，在这堆干柴上劈了一道闪电。&lt;/p&gt;
&lt;p&gt;Cantrill 的原话是：LLM 就像 brogrammer 群体的合成代谢类固醇（anabolic steroids）。无论一个人对软件开发持什么态度，LLM 都能放大这种态度的力量。于是那些本来就痴迷于「产出量」的人，现在更加不可收拾了。&lt;/p&gt;
&lt;p&gt;他举了一个例子：Y Combinator 总裁 Garry Tan 在社交媒体上炫耀自己一天写了 37,000 行代码，还说「仍在加速」。&lt;/p&gt;
&lt;p&gt;&lt;img alt="37k a day bro" src="/static/peril-of-laziness-lost/37k-a-day-bro.webp"&gt;&lt;/p&gt;
&lt;p&gt;Cantrill 不动声色地补了一句作为对比：&lt;strong&gt;整个 DTrace 项目的代码量大约也就六万行左右。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用行数衡量软件的价值，就好比用重量衡量文学作品的质量。这个谬误即便是初学者也看得出来。&lt;/p&gt;
&lt;h2&gt;Garry Tan 的「作品」被拆解&lt;/h2&gt;
&lt;p&gt;至于 Tan 用如此疯狂的速度构建的产物——一个「newsletter-blog-thingy」——波兰工程师 Gregorein 对其做了一次拆解。结果既意料之中，又令人捧腹：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单次页面加载就包含了多个测试框架（test harnesses）&lt;/li&gt;
&lt;li&gt;混入了 Rails 的 Hello World 应用&lt;/li&gt;
&lt;li&gt;藏了一个文本编辑器&lt;/li&gt;
&lt;li&gt;同一个 logo 出现了八个不同版本，其中一个的文件大小为 0 字节&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cantrill 指出，问题不在于这些具体的 bug（它们都可以修复），甚至不在于有人认为这种方法论代表了软件工程的未来（虽然这确实很烦人）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正的问题是：LLM 天生缺乏「懒惰」这一美德。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;核心论点：LLM 不懂懒惰&lt;/h2&gt;
&lt;p&gt;这是整篇文章最精华的部分。&lt;/p&gt;
&lt;p&gt;对 LLM 而言，工作是零成本的。LLM 不会觉得需要为自己（或任何人）的未来时间做优化。它会毫不犹豫地往一个越来越高的垃圾蛋糕上继续堆料。如果不加约束，LLM 只会让系统变得更大，而不是更好——也许能满足某些扭曲的虚荣指标，但代价是所有真正重要的东西。&lt;/p&gt;
&lt;p&gt;这恰恰凸显了人类的「懒惰」有多关键：&lt;strong&gt;正因为我们的时间有限，才迫使我们去开发精炼的抽象。&lt;/strong&gt; 我们不愿意把自己宝贵的（人类的！）时间浪费在笨拙抽象带来的后果上。最好的工程总是源于约束，而时间的约束限制了我们愿意承受的系统认知负荷。这才是驱动我们把系统做得更简单的力量，尽管系统本身的复杂性是内在的。&lt;/p&gt;
&lt;p&gt;LLM 不受时间约束，也不承受认知负荷，因此不能指望它们主动去追求简洁。&lt;/p&gt;
&lt;h2&gt;LLM 仍然是重要的工具&lt;/h2&gt;
&lt;p&gt;Cantrill 并没有否定 LLM 的价值。他明确说：LLM 是软件工程的非凡工具——但归根结底，它只是工具。&lt;/p&gt;
&lt;p&gt;正确的使用方式是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 LLM 处理程序员「非美德性的懒惰」——比如帮助清理技术债务这类棘手但缺乏动力去做的事&lt;/li&gt;
&lt;li&gt;用 LLM 提升工程严谨性&lt;/li&gt;
&lt;li&gt;但这一切必须服务于程序员自身的「美德性懒惰」：产出更简洁、更强大的系统，不仅服务于当下的自己，也服务于未来一代又一代的软件工程师&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;个人感想&lt;/h2&gt;
&lt;p&gt;读完这篇文章，最大的感触是 Cantrill 精准地指出了 LLM 时代的核心矛盾：&lt;strong&gt;工具变强了，但使用工具的判断力并没有跟着变强。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LLM 让「写代码」的边际成本趋近于零，但「思考应该写什么代码」的成本一点都没降低。如果把 LLM 当成代码的自动生成器来追求产出量，最终只会得到一个体积庞大但内在混乱的系统。真正应该做的，是把 LLM 节省下来的时间投入到更深层的思考中去——去琢磨更好的抽象、更简洁的设计。&lt;/p&gt;
&lt;p&gt;Larry Wall 几十年前提出的「懒惰是美德」，在 LLM 时代反而变得更加重要了。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Bryan Cantrill, &lt;a href="https://bcantrill.dtrace.org/2026/04/12/the-peril-of-laziness-lost/"&gt;The Peril of Laziness Lost&lt;/a&gt;, 2026-04-12&lt;/li&gt;
&lt;li&gt;Larry Wall, &lt;em&gt;Programming Perl&lt;/em&gt; (The Camel Book)&lt;/li&gt;
&lt;/ul&gt;</content><category term="misc"/><category term="ai"/><category term="llm"/><category term="programming"/></entry><entry><title>.claude/ 文件夹解剖</title><link href="https://www.chenxiaosheng.com/posts/2026-03-23/anantomy-of-the-claude-folder.html" rel="alternate"/><published>2026-03-23T19:36:59+08:00</published><updated>2026-03-23T19:36:59+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2026-03-23:/posts/2026-03-23/anantomy-of-the-claude-folder.html</id><summary type="html">&lt;p&gt;大多数团队都在某种程度上采用了 AI，但“使用 AI”与“从 AI 中获得可衡量的 ROI”之间的差距，比人们想象得更大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fandf.co/3NR30kc"&gt;Postman&lt;/a&gt;&lt;/strong&gt; 发布了一份 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;大多数团队都在某种程度上采用了 AI，但“使用 AI”与“从 AI 中获得可衡量的 ROI”之间的差距，比人们想象得更大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fandf.co/3NR30kc"&gt;Postman&lt;/a&gt;&lt;/strong&gt; 发布了一份成本节省分析，评估了六种常见的 API 开发工作流，并对比了“平台内置 AI”与“外部加挂 AI”在实际时间和成本上的差异。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 1" src="/static/anatomy-of-the-claude-folder/image_1.png"&gt;&lt;/p&gt;
&lt;p&gt;这是一篇简短、数据驱动的阅读材料，能帮助工程负责人判断：AI 原生工具究竟在哪些环节真正能撬动效率。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fandf.co/3NR30kc"&gt;你可以在这里免费下载这份指南 →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;感谢 Postman 今日赞助！&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Claude Code 的用户通常把 &lt;code&gt;.claude&lt;/code&gt; 文件夹当成黑盒。知道它存在，也见过它出现在项目根目录里，但几乎没人真正打开看过，更别说理解里面每个文件的作用。&lt;/p&gt;
&lt;p&gt;这是个被错过的机会。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.claude&lt;/code&gt; 文件夹其实是你在项目里“控制 Claude 行为”的中枢。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 2" src="/static/anatomy-of-the-claude-folder/image_2.png"&gt;&lt;/p&gt;
&lt;p&gt;它保存了你的指令、自定义命令、权限规则，甚至还有 Claude 跨会话的记忆。理解每个文件的位置和意义之后，你就可以让 Claude Code 完全按你团队的方式运行。&lt;/p&gt;
&lt;p&gt;这篇文章会带你完整拆解这个文件夹，从你每天会用到的文件，到你只需设置一次就能忘记的部分。&lt;/p&gt;
&lt;p&gt;在开始之前，有一点要先知道：实际上有两个 .claude 目录，而不是一个。&lt;/p&gt;
&lt;p&gt;一个在项目里，另一个在你的 home 目录：&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 3" src="/static/anatomy-of-the-claude-folder/image_3.png"&gt;&lt;/p&gt;
&lt;p&gt;项目级的文件夹保存团队配置。你会把它提交到 git，所有人都用同一套规则、同一组自定义命令、同一份权限策略。&lt;/p&gt;
&lt;p&gt;全局的 &lt;code&gt;~/.claude/&lt;/code&gt; 文件夹保存你的个人偏好和本机状态，比如会话历史和自动记忆。&lt;/p&gt;
&lt;p&gt;这是整个系统里最关键的文件。你启动 Claude Code 会话时，它第一个读取的就是 &lt;code&gt;CLAUDE.md&lt;/code&gt;。它会被直接加载进系统提示词，并贯穿整个对话。&lt;/p&gt;
&lt;p&gt;说白了：你写在 &lt;code&gt;CLAUDE.md&lt;/code&gt; 里的内容，Claude 就会遵守。&lt;/p&gt;
&lt;p&gt;你告诉它“先写测试再写实现”，它就会这么做。你写“禁止用 console.log 做错误处理，要用自定义 logger 模块”，它每次都会遵循。&lt;/p&gt;
&lt;p&gt;最常见的做法是在项目根目录放一个 &lt;code&gt;CLAUDE.md&lt;/code&gt;。但你也可以在 &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; 放一个全局偏好，甚至在子目录里放局部规则。Claude 会读取它们并自动合并。&lt;/p&gt;
&lt;p&gt;多数人不是写得太多，就是写得太少。下面是合理的取舍。&lt;/p&gt;
&lt;h5&gt;建议写：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;构建、测试、Lint 命令（如 npm run test、make build 等）&lt;/li&gt;
&lt;li&gt;关键架构决策（比如“我们使用 Turborepo 的 Monorepo”）&lt;/li&gt;
&lt;li&gt;不显然的坑（比如“TypeScript 开启 strict，未使用变量会报错”）&lt;/li&gt;
&lt;li&gt;导入约定、命名规范、错误处理风格&lt;/li&gt;
&lt;li&gt;核心模块的文件与目录结构&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;不建议写：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;应该放在 Linter 或 Formatter 配置里的内容&lt;/li&gt;
&lt;li&gt;已有文档、可以直接链接的完整说明&lt;/li&gt;
&lt;li&gt;长篇大论的理论解释&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把 &lt;code&gt;CLAUDE.md&lt;/code&gt; 控制在 200 行以内。文件太长会消耗上下文，Claude 的指令遵循度反而会下降。&lt;/p&gt;
&lt;p&gt;原文此处有一个“简短但有效”的示例（截图展示）。&lt;/p&gt;
&lt;p&gt;大约 20 行，足以让 Claude 在这个代码库里高效工作，而不需要不断追问。&lt;/p&gt;
&lt;p&gt;有时你的偏好只属于你本人，不适合全团队。比如你偏好某种测试框架，或者希望 Claude 打开文件时遵循某个固定模式。&lt;/p&gt;
&lt;p&gt;在项目根目录创建 &lt;code&gt;CLAUDE.local.md&lt;/code&gt;。Claude 会和主 &lt;code&gt;CLAUDE.md&lt;/code&gt; 一起读取，但这个文件会自动被 gitignore，不会进入仓库。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 4" src="/static/anatomy-of-the-claude-folder/image_4.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; 适合单一项目。但一旦团队扩大，你很容易得到一个 300 行、没人维护、人人忽视的 &lt;code&gt;CLAUDE.md&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rules/&lt;/code&gt; 文件夹就是为了解决这个问题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.claude/rules/&lt;/code&gt; 里的每个 Markdown 文件都会和 &lt;code&gt;CLAUDE.md&lt;/code&gt; 一起加载。你可以按主题拆分指令：&lt;/p&gt;
&lt;p&gt;每个文件都更聚焦、更容易更新。负责 API 规范的人只改 &lt;code&gt;api-conventions.md&lt;/code&gt;，负责测试标准的人改 &lt;code&gt;testing.md&lt;/code&gt;，互不干扰。&lt;/p&gt;
&lt;p&gt;真正的威力在“路径作用域规则”。在规则文件里加一个 YAML frontmatter，就可以让它只在匹配路径时生效：&lt;/p&gt;
&lt;p&gt;比如它不会在编辑 React 组件时加载，而只在处理 &lt;code&gt;src/api/&lt;/code&gt; 或 &lt;code&gt;src/handlers/&lt;/code&gt; 时生效。没有 &lt;code&gt;paths&lt;/code&gt; 的规则会在每次会话中无条件加载。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;CLAUDE.md&lt;/code&gt; 开始变得臃肿时，这就是正确的拆分方式。&lt;/p&gt;
&lt;p&gt;Claude Code 自带一些内置斜杠命令，比如 &lt;code&gt;/help&lt;/code&gt; 和 &lt;code&gt;/compact&lt;/code&gt;。&lt;code&gt;commands/&lt;/code&gt; 文件夹允许你自定义命令。&lt;/p&gt;
&lt;p&gt;你在 &lt;code&gt;.claude/commands/&lt;/code&gt; 里放一个 Markdown 文件，它就会变成一个斜杠命令。&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;review.md&lt;/code&gt; 会生成 &lt;code&gt;/project:review&lt;/code&gt;，&lt;code&gt;fix-issue.md&lt;/code&gt; 会生成 &lt;code&gt;/project:fix-issue&lt;/code&gt;。文件名就是命令名。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 5" src="/static/anatomy-of-the-claude-folder/image_5.png"&gt;&lt;/p&gt;
&lt;p&gt;原文这里给了一个简单示例：创建 &lt;code&gt;.claude/commands/review.md&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后在 Claude Code 里运行 &lt;code&gt;/project:review&lt;/code&gt;，它会自动把真实的 git diff 注入到提示词中。反引号里的 &lt;code&gt;!&lt;/code&gt; 语法会运行 shell 命令并把输出嵌入。这就是命令真正有用的原因，而不只是“保存一段文本”。&lt;/p&gt;
&lt;p&gt;如果你需要传参，可以用 &lt;code&gt;$ARGUMENTS&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;/project:fix-issue 234&lt;/code&gt; 会把 issue 234 的内容直接送进提示词。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.claude/commands/&lt;/code&gt; 里的项目命令会提交到仓库，和团队共享。若想在所有项目通用，把命令放到 &lt;code&gt;~/.claude/commands/&lt;/code&gt;，它会显示为 &lt;code&gt;/user:command-name&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一个很实用的个人命令：每日站会助手、按你约定格式生成提交信息、或者快速安全扫描。&lt;/p&gt;
&lt;p&gt;你已经了解命令。技能看起来很像，但触发方式完全不同。先看这张图的区别：&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 6" src="/static/anatomy-of-the-claude-folder/image_6.png"&gt;&lt;/p&gt;
&lt;p&gt;技能是 Claude 可以自己调用的工作流，不需要你敲斜杠命令，只要当前任务匹配技能描述，它就会自动触发。命令是你手动触发，技能是它自动判断。&lt;/p&gt;
&lt;p&gt;每个技能都在自己的子目录里，包含一个 &lt;code&gt;SKILL.md&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SKILL.md&lt;/code&gt; 用 YAML frontmatter 描述“什么时候使用这个技能”。&lt;/p&gt;
&lt;p&gt;当你说“帮我审这个 PR 的安全问题”，Claude 会读取描述，判断匹配，于是自动调用这个技能。你也可以显式调用，比如 &lt;code&gt;/security-review&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;和命令最大的区别：技能可以打包一堆辅助文件。上面提到的 &lt;code&gt;DETAILED_GUIDE.md&lt;/code&gt; 就是和 &lt;code&gt;SKILL.md&lt;/code&gt; 同目录的参考资料。命令只是单个文件，而技能是一整个包。&lt;/p&gt;
&lt;p&gt;个人技能放在 &lt;code&gt;~/.claude/skills/&lt;/code&gt; 下，跨项目通用。&lt;/p&gt;
&lt;p&gt;当任务复杂到值得一个专门专家时，可以在 &lt;code&gt;.claude/agents/&lt;/code&gt; 定义子代理。每个代理是一个 Markdown 文件，包含它自己的系统提示词、可用工具、以及模型偏好：&lt;/p&gt;
&lt;p&gt;下面是一个 &lt;code&gt;code-reviewer.md&lt;/code&gt; 的样子。&lt;/p&gt;
&lt;p&gt;当 Claude 需要做代码审查时，它会在独立上下文里启动这个代理。代理完成工作后会压缩结论再汇报，这样你的主会话不会被几千 tokens 的中间过程撑爆。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tools&lt;/code&gt; 字段限制代理能做什么。安全审计只需要 Read、Grep、Glob，完全不该写文件。这个限制是刻意为之。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;model&lt;/code&gt; 字段让你选择更便宜、更快的模型来做聚焦任务。Haiku 处理读操作够用，把 Sonnet/Opus 留给真正需要能力的地方。&lt;/p&gt;
&lt;p&gt;个人代理放在 &lt;code&gt;~/.claude/agents/&lt;/code&gt; 下，跨项目可用。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Image 7" src="/static/anatomy-of-the-claude-folder/image_7.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.claude&lt;/code&gt; 里的 &lt;code&gt;settings.json&lt;/code&gt; 控制 Claude 允许做什么、不允许做什么。它定义了哪些工具可以用、哪些文件能读，以及哪些命令需要先询问。&lt;/p&gt;
&lt;p&gt;完整文件长这样（原文此处为截图）。&lt;/p&gt;
&lt;p&gt;各部分的含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;$schema&lt;/code&gt;&lt;/strong&gt; 行开启 VS Code 或 Cursor 的自动补全与校验，务必保留。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;allow list&lt;/strong&gt;：允许 Claude 无需确认即可运行的命令。典型选择包括：&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bash(npm run *)&lt;/code&gt; 或 &lt;code&gt;Bash(make *)&lt;/code&gt;，让 Claude 能自由执行脚本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bash(git *)&lt;/code&gt;，用于只读 git 操作&lt;/li&gt;
&lt;li&gt;Read、Write、Edit、Glob、Grep 等文件操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deny list&lt;/strong&gt;：无条件禁止的命令。合理的 deny list 会阻止：&lt;ul&gt;
&lt;li&gt;破坏性命令，比如 &lt;code&gt;rm -rf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;直接网络命令，比如 &lt;code&gt;curl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;敏感文件，比如 &lt;code&gt;.env&lt;/code&gt; 和 &lt;code&gt;secrets/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不在 allow 或 deny 列表里的命令，Claude 会先询问再执行。这种“中间地带”是有意设计的：既有安全网，又不需要你预判所有命令。&lt;/p&gt;
&lt;p&gt;另外你也可以用 &lt;code&gt;settings.local.json&lt;/code&gt; 做个人覆盖。创建 &lt;code&gt;.claude/settings.local.json&lt;/code&gt;，它会被自动 gitignore，不进入仓库。&lt;/p&gt;
&lt;p&gt;你很少手动管理这个文件夹，但最好知道它们存在。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; 会加载到每个 Claude Code 会话里，跨项目生效。适合放你的个人编码原则、偏好风格等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.claude/projects/&lt;/code&gt; 保存项目的会话记录和自动记忆。Claude Code 会把它学到的命令、模式、架构信息自动存下来并跨会话保留。你可以用 &lt;code&gt;/memory&lt;/code&gt; 浏览和编辑。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.claude/commands/&lt;/code&gt; 和 &lt;code&gt;~/.claude/skills/&lt;/code&gt; 保存你的个人命令和技能，跨项目可用。&lt;/p&gt;
&lt;p&gt;你通常不需要手动整理这些，但知道它们的存在很有用：当 Claude 看起来“记住”了你没明确说过的东西，或者你想清空某个项目的自动记忆、重新开始时，就知道该去哪。&lt;/p&gt;
&lt;p&gt;最后把一切串起来：如果你从零开始，以下步骤非常实用。&lt;/p&gt;
&lt;p&gt;Step 1. 在 Claude Code 里运行 &lt;code&gt;/init&lt;/code&gt;，它会读取项目并生成一个初始 &lt;code&gt;CLAUDE.md&lt;/code&gt;，然后你把它精简到关键点。&lt;/p&gt;
&lt;p&gt;Step 2. 添加 &lt;code&gt;.claude/settings.json&lt;/code&gt;，设置合适的 allow/deny 规则。至少允许运行命令，禁止读取 .env。&lt;/p&gt;
&lt;p&gt;Step 3. 创建一两个最常用的命令，比如代码审查或修复 issue。&lt;/p&gt;
&lt;p&gt;Step 4. 当 &lt;code&gt;CLAUDE.md&lt;/code&gt; 变拥挤时，把指令拆到 &lt;code&gt;.claude/rules/&lt;/code&gt;，并按路径做作用域。&lt;/p&gt;
&lt;p&gt;Step 5. 在 &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; 写入个人偏好，比如“先写类型再写实现”“更偏函数式而不是类”。&lt;/p&gt;
&lt;p&gt;这对 95% 的项目已经够用了。技能和代理只在你有反复出现的复杂工作流、值得打包时再引入。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.claude&lt;/code&gt; 文件夹本质上是一套协议：告诉 Claude 你是谁、项目做什么、它需要遵守哪些规则。定义得越清晰，你纠正 Claude 的时间就越少，它做的有用工作就越多。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; 是你杠杆最大的文件。先把它写对，其他都是优化。&lt;/p&gt;
&lt;p&gt;从小做起，逐步精炼，把它当成项目里的基础设施：一旦搭好，每天都会持续带来收益。&lt;/p&gt;
&lt;p&gt;感谢阅读！英文原文：https://blog.dailydoseofds.com/p/anatomy-of-the-claude-folder&lt;/p&gt;</content><category term="misc"/><category term="claude"/><category term="ai"/></entry><entry><title>阿里云域名解析被转移，让我穿越回了 15 年前</title><link href="https://www.chenxiaosheng.com/posts/2025-06-06/aliyuncs.com-downgrade.html" rel="alternate"/><published>2025-06-06T20:30:00+08:00</published><updated>2025-06-06T20:30:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2025-06-06:/posts/2025-06-06/aliyuncs.com-downgrade.html</id><summary type="html">&lt;p&gt;6月6日凌晨，阿里云核心域名 aliyuncs.com遭遇罕见的域名劫持事件，导致其对象存储服务（OSS）、内容分发网络（CDN）以及云解析DNS等多项核心云服务出现大范围故障，波及众多依赖阿里云服务的网站和应用。&lt;/p&gt;</summary><content type="html">&lt;p&gt;2025 年 6 月 6 日早上 6 点 10 分醒来，习惯性瞄了下手机，看到同事给我的留言，大概是说 &lt;code&gt;aliyuncs.com&lt;/code&gt; 被别的国家注册了&lt;/p&gt;
&lt;p&gt;作为一名曾经的域名管理员，我对域名类的故障是比较 &lt;strong&gt;敏（害）感（怕）&lt;/strong&gt; 的，因此基本上我是短时间就清醒了的，抛出了我的第一个疑问：&lt;strong&gt;在用的域名忽然被别人注册了&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;同事反馈是过期没续费被抢，但是 &lt;strong&gt;域名过期是有保护期&lt;/strong&gt; 的，所以我并没有接受这个说法，在手机上通过 &lt;a href="https://who.is"&gt;who.is&lt;/a&gt; 查了一下信息，发现过期时间是 &lt;code&gt;2026-04-01&lt;/code&gt;，既然都还没有过期，那被别人注册了这个说法自然是不成立的。&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/aliyuncs.com_1.png" /&gt;&lt;/p&gt;
&lt;p&gt;继续追问了下目前可能对我们公司业务的影响，了解到如果负载均衡 LB &lt;code&gt;Load Balance&lt;/code&gt; 有可能直接用了阿里的域名，这下我就不太淡定了，继续用 &lt;a href="https://www.itdog.cn"&gt;itdog&lt;/a&gt; 试了一下，发现有个别缓存开始失效了。结合可能对我们的影响，这下真的无法淡定了，立马起床开电脑干活！！&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/aliyuncs.com_2.png" /&gt;&lt;/p&gt;
&lt;p&gt;考虑到 whois 也会有缓存，赶紧登录自己在欧洲的一台机器重新查了一下，果然发现阿里 aliyuncs.com 的域名被人更新了（而不是被别人注册了），可以看到更新时间是 &lt;code&gt;2025-06-05T21:32:38Z&lt;/code&gt; ，考虑到时区，和阿里通告故障的时间是基本对的上，域名、whois 都会有缓存，有一定时间差是符合常理的。&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/aliyuncs.com_3.png" /&gt;&lt;/p&gt;
&lt;p&gt;这个时候肾上腺激素开始飙升了，立马让在线的同事开工干活，主要几个动作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一时间把涉  &lt;code&gt;aliyuncs.com&lt;/code&gt; 的域名捞出来，并整理成共享文档，供接下来使用；&lt;/li&gt;
&lt;li&gt;唤醒公司域名管理员，对于我们自建 DNS 这个服务本身，如果使用了阿里 LB 的且评估是对其他业务提供服务的，马上切走。（如果我们的 DNS Server 受这个 LB 影响不可用了，这个时候爆炸半径会直接无限扩大）&lt;/li&gt;
&lt;li&gt;域名管理员，发起紧急变更，将缓存 DNS 服务器上关乎 &lt;code&gt;aliyuncs.com&lt;/code&gt; 这个域名的解析全部转发劫持到阿里云的权威，确保内部业务能正常解析，尽可能缩小爆炸半径；&lt;/li&gt;
&lt;li&gt;根据服务等级，除了在相关工作群通告外，根据服务重要等级唤醒相应运维（或相应域名对应的管理员），并让他们安排将 &lt;code&gt;aliyuncs.com&lt;/code&gt; 的业务先切到 VIP 地址；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;中间还是会有一些小插曲，比如可能指引不够详细，有运维同学对于域名没有概念或者有同学所涉业务较多，有点手忙脚乱之外，整体还算快速、稳步的推进，更多的细节就不展开了。&lt;/p&gt;
&lt;p&gt;当然，这里最终还是要给阿里云的团队点个赞，原以为这个故障时间会远超预期，然后我们在北京时间 08:14 收到反馈说经过阿里云团队的处理，域名解析开始恢复正常了，故障持续的时间大约为 6 小时左右。再加上故障发生在凌晨，另外域名解析的失效也有一个缓存的过程，不幸中的万幸就是我们基本没有受到影响（但是对不起，炸了一群人起来干活 :p&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;基本没有，也不是完全没有，但都在可控范围内 :-)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;15 年前的鬼故事&lt;/h2&gt;
&lt;p&gt;在响应这个故障的过程中，就有同事问我是不是我们曾经也出现过这个问题，很肯定的说“是”，但也不是，情况不太一样。在这个时间点，我对于阿里云为什么 NS &lt;code&gt;namesever&lt;/code&gt; 被改了还是不清楚的，只是猜测大概率是管理不善，帐号被黑客拿走改了 NS 之类~~ 而我们当年（约 15 年前）碰到的一个情况是被注册商（比如万网、NetworkSolutions）以涉敏感内容为由停止了解析，但阿里云 &lt;code&gt;aliyuncs.com&lt;/code&gt; 的症状对不上。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明一下，涉敏感内容肯定和公司没有直接相关，不展开，避免被有心之人用来作为攻击手段，也不确认该方式是否还有效&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当时也是发生在凌晨（时差，美国佬刚好上班之类），年轻睡的晚，本来也想睡觉了，鬼始神差的觉得好像不太对劲，开电脑查了一下发现解析被停掉了，多次用我鳖脚的英文和 NetworkSolutions 对线（感谢那时候有 Skype 等软件可以打国际电话），辗转折腾，在多路大佬的帮助下也是差不多在白天业务高峰前才恢复解析，但毕竟是我们的核心域名，加上时间也是大几个小时，不少地方的缓存失效了，影响还是很大的（最后被某位大佬点赞了 hhhh&lt;/p&gt;
&lt;h2&gt;说说我的害怕&lt;/h2&gt;
&lt;p&gt;前面说到&lt;code&gt;作为一名曾经的域名管理名，我对域名类的故障是比较敏（害）感（怕）的&lt;/code&gt;，这是为什么呢：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DNS 是互联网最基础的业务之一，但凡在互联网上能提供的业务大概都离不开这个基础。越是基础的东西伤害就越大；&lt;/li&gt;
&lt;li&gt;基础的服务受关注程度低，大家都默认你正常，所以确实出问题的概率也极低，因为你不敢经常出问题。但带来的问题是所有上层业务都默认你正常，在设计的时候就不会去做过度容灾，一出问题短时间就是解决不了（但确实有时候为了一个极低概率出问题的系统做过度的容灾，不考虑 ROI 也是有毛病的表现 。。。&lt;/li&gt;
&lt;li&gt;因为 1 和 2 的情况，业务和业务，经年累月下来，你根本改不了。就像现在，你不可能拿出一套系统取代 TCP/IP、取代 DNS，哪怕 IPv4 要进化到 IPv6，这个我从读大学就被这么画大饼了，20 多年过去了，也没见 IPv6 就把 IPv4 干掉了&lt;/li&gt;
&lt;li&gt;（价值论）通常在大多数公司里，基础服务的价值讲不清楚，上层建筑容易画大饼讲故事，也有快速创造收益的可能。像我这种做基础设施的，都要经常停下来反思我这点收入真是得之不易，劳心劳命的，然后给自己找到自洽的逻辑，继续努力~&lt;/li&gt;
&lt;li&gt;域名的爆炸半径不可控，一方面是因为我们不可能去刷新世界上所有的缓存（运营商大多数情况下也不会鸟你），同时我们也没有办法让用户在出现故障时，让他们切换到另一个域名（如 HttpDNS 等方案这些都只能解决特定场景下的特定问题），另外 &lt;strong&gt;缓存是个双刃剑&lt;/strong&gt;，它能延缓故障的爆炸，却也会让你在宣告解析恢复后要经受不停的挑战，为什么我的服务一直没有恢复？就像这次我们 &lt;strong&gt;直到北京时间 13:57 才收到业务反馈说运营商的缓存基本都过期不再报错了&lt;/strong&gt;，大概是阿里云宣告恢复后的又一个 6 小时&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，你也可以认为我过度紧张 :-)&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;阿里云此次 &lt;code&gt;aliyuncs.com&lt;/code&gt; 的故障真实原因是什么？这个我也不知道，但从网络上的蛛丝马迹上看是被 &lt;a href="https://www.verisign.com/zh_CN/domain-names/what-does-com-mean/index.xhtml"&gt;VeriSign&lt;/a&gt;（不是注册商，是 &lt;code&gt;.com&lt;/code&gt; 顶级域的管理）改了域名解析，不考虑更深层次更腹黑的原因，只能猜测是因为阿里云有不合规的内容被投诉了之类。如下一些公众号的猜想：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/static/aliyuncs.com_4.png" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作为域名的拥有者，有没有可能完全杜绝这个问题？我认为不可能， 这次看起来是 &lt;a href="https://www.verisign.com/zh_CN/domain-names/what-does-com-mean/index.xhtml"&gt;VeriSign&lt;/a&gt; 操刀的，也就是直接绕开注册局（商）了。包括有些朋友可能会认为加一些注册局锁（类似 &lt;code&gt;serverUpdateProhibited&lt;/code&gt; 这些）可能可以缓解，我可以很肯定的说这 &lt;strong&gt;只能解决到注册局层面的问题，覆盖不了本次的情况&lt;/strong&gt; 。当然阿里云做为一家国际云商， &lt;strong&gt;注册局锁这点钱你们就不要省啦&lt;/strong&gt; （事后也看到阿里云加上了）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/static/aliyuncs.com_5.png" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;技术上能不能解决：能，但成本不知几何。鼓励大家在业务上线的时候通过多入口容灾、引入 HttpDNS 等技术解决类似问题，但是当类似问题出现时，影响还是不可避免，手忙脚乱也还是不可避免，毕竟 N 年等一回，但最好一回也不要有，害怕&lt;/li&gt;
&lt;li&gt;可能有其他非技术非常规手段，可以从根服务器、顶级域名运营商开始实施保护，但想像不了了，请了解规则的朋友指点哈&lt;/li&gt;
&lt;/ul&gt;</content><category term="misc"/><category term="aliyun"/><category term="阿里云"/><category term="DNS"/><category term="域名"/></entry><entry><title>Obsidian 接入 DeepSeek API 指南：Text Generator 插件配置教程</title><link href="https://www.chenxiaosheng.com/posts/2025-05-12/obsidian-deepseek-text-generator.html" rel="alternate"/><published>2025-05-12T15:07:00+08:00</published><updated>2025-05-12T15:07:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2025-05-12:/posts/2025-05-12/obsidian-deepseek-text-generator.html</id><summary type="html">&lt;p&gt;&lt;strong&gt;Obsidian 接入 DeepSeek API 的配置教程&lt;/strong&gt;，使用 &lt;strong&gt;Text Generator 插件&lt;/strong&gt;实现智能文本生成功能。教程使用 Text Generator 插件 + DeepSeek API 生成，并经过人工校对。&lt;/p&gt;</summary><content type="html">&lt;p&gt;以下是 &lt;strong&gt;Obsidian 接入 DeepSeek API 的配置教程&lt;/strong&gt;，使用 &lt;strong&gt;Text Generator 插件&lt;/strong&gt;实现智能文本生成功能。教程使用 Text Generator 插件 + DeepSeek API 生成，并经过人工校对：&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;1. 安装 Text Generator 插件&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开 Obsidian → 点击左侧边栏 &lt;code&gt;Settings&lt;/code&gt; (设置)或菜单栏 &lt;code&gt;Preferences&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;进入 &lt;code&gt;Community plugins&lt;/code&gt; (社区插件) → 点击 &lt;code&gt;Browse&lt;/code&gt;  &lt;/li&gt;
&lt;li&gt;搜索 &lt;strong&gt;Text Generator&lt;/strong&gt; → 安装并启用插件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="" src="/static/obsidian_deepseek_2025.05/1.png"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;2. 获取 DeepSeek API Key&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href="https://deepseek.com"&gt;DeepSeek 官网&lt;/a&gt; 注册/登录账号；&lt;/li&gt;
&lt;li&gt;进入 API 管理页面（如无公开API，需联系官方申请权限）；
&lt;img alt="" src="/static/obsidian_deepseek_2025.05/2.png"&gt;&lt;/li&gt;
&lt;li&gt;复制生成的 &lt;strong&gt;API Key&lt;/strong&gt;（保存到安全位置，后面无法通过 deepseek 平台查看）；&lt;/li&gt;
&lt;li&gt;充值（&lt;strong&gt;很重要，否则后面会出错&lt;/strong&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;3. 配置 Text Generator 插件&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 Obsidian 中打开 &lt;code&gt;Settings&lt;/code&gt; → 找到 &lt;code&gt;Text Generator&lt;/code&gt; 插件设置。  &lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;API Provider&lt;/code&gt; 中选择 &lt;strong&gt;Custom&lt;/strong&gt; 或 &lt;strong&gt;通过+创建自定义配置&lt;/strong&gt;（若支持）。
&lt;img alt="" src="/static/obsidian_deepseek_2025.05/3.png"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：如上截图，已勾选的 &lt;code&gt;deepseek&lt;/code&gt; 是通过后面的 &lt;code&gt;+&lt;/code&gt; 创建出来的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;填写以下参数：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API URL&lt;/strong&gt;: DeepSeek API 的端点地址（如 &lt;code&gt;https://api.deepseek.com/chat/completions&lt;/code&gt;）  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Key&lt;/strong&gt;: 粘贴你的 DeepSeek API Key  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model Name&lt;/strong&gt;: 填写模型名称（如 &lt;code&gt;deepseek-chat&lt;/code&gt;，根据官方文档调整）&lt;br&gt;
&lt;img alt="" src="/static/obsidian_deepseek_2025.05/4.png"&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;4. 测试 API 连接&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 Obsidian 中新建一个笔记，输入内容并选中；&lt;/li&gt;
&lt;li&gt;按快捷键 &lt;code&gt;Ctrl/Cmd + J&lt;/code&gt; 即可自动生成；
&lt;img alt="" src="/static/obsidian_deepseek_2025.05/5.png"&gt;&lt;/li&gt;
&lt;li&gt;观察是否返回 DeepSeek 生成的文本，若无响应请检查：  &lt;/li&gt;
&lt;li&gt;API Key 和 URL 是否正确  &lt;/li&gt;
&lt;li&gt;网络是否畅通  &lt;/li&gt;
&lt;li&gt;DeepSeek 账户是否有足够配额  &lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;5. 高级设置（可选）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自定义提示模板&lt;/strong&gt;：在插件设置中修改 &lt;code&gt;Prompt Template&lt;/code&gt;，适配你的工作流。  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调整参数&lt;/strong&gt;：如 &lt;code&gt;temperature&lt;/code&gt;（随机性）、&lt;code&gt;max_tokens&lt;/code&gt;（生成长度）等。  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;常见问题&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Q&lt;/strong&gt;: 插件报错 &lt;code&gt;n.text is not a function&lt;/code&gt;？&lt;br&gt;
&lt;strong&gt;A&lt;/strong&gt;: 在插件的 &lt;code&gt;Advance mode&lt;/code&gt; 里把 &lt;code&gt;CORS Bypass&lt;/code&gt;  打开
&lt;img alt="" src="/static/obsidian_deepseek_2025.05/6.png"&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q&lt;/strong&gt;: 提示 &lt;code&gt;404&lt;/code&gt; 或者 &lt;code&gt;Not Found&lt;/code&gt; 之类
  &lt;strong&gt;A&lt;/strong&gt;: 确认 API 地址（&lt;code&gt;Endpoint&lt;/code&gt;）填写正确，如这里是：&lt;code&gt;https://api.deepseek.com/chat/completions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q&lt;/strong&gt;: 提示 &lt;code&gt;402&lt;/code&gt; 或者 &lt;code&gt;Failed to load resource&lt;/code&gt; 之类
  &lt;strong&gt;A&lt;/strong&gt;: 先充值！&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q&lt;/strong&gt;: 如何查看其他报错的详细信息
  &lt;strong&gt;A&lt;/strong&gt;: MacOS，使用：&lt;code&gt;Cmd+Option+I&lt;/code&gt; 打开控制台查看
&lt;img alt="" src="/static/obsidian_deepseek_2025.05/7.png"&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;通过以上步骤，你可以在 Obsidian 中无缝调用 DeepSeek 的 AI 能力。如果需要更详细的配置（如本地代理），可参考插件官方文档或 DeepSeek API 说明。&lt;/p&gt;</content><category term="misc"/><category term="obsidian"/><category term="deepseek"/><category term="ai"/></entry><entry><title>25非全考研，人生经历+1</title><link href="https://www.chenxiaosheng.com/posts/2025-04-13/postgraduate.html" rel="alternate"/><published>2025-04-13T06:30:00+08:00</published><updated>2025-04-13T06:30:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2025-04-13:/posts/2025-04-13/postgraduate.html</id><summary type="html">&lt;p&gt;23年去华工和D老师/泰老师吃饭闲逛的时候，脑子一抽想到要不干脆我也来考个研算了，但因为离国家统考的时间确实太近了，花80块钱报了名，完全没有时间准备，最后也是不了了之。但念头有了之后，却是挥之不去了，24年春节后，就默默开始准备起来了，25年4月把复试的流程走完，也进入了“拟录取”状态，小记一下人生的的经历。&lt;/p&gt;</summary><content type="html">&lt;p&gt;23年去华工和D老师/泰老师吃饭闲逛的时候，脑子一抽想到要不干脆我也来考个研算了，但因为离国家统考的时间确实太近了，花80块钱报了名，完全没有时间准备，最后也是不了了之。但念头有了之后，却是挥之不去了，24年春节后，就默默开始准备起来了，25年4月把复试的流程走完，也进入了“拟录取”状态，小记一下人生的的经历。&lt;/p&gt;
&lt;h2&gt;为什么想考研&lt;/h2&gt;
&lt;p&gt;考研再读书的想法来的快，却挥之不急，首先还是和个人性格有比较大的关系，当我想到要去做一件事情的时候，总是会想着各种办法去实现，小到种草一件商品，大到买车买房等，只要我确定了我就会很快的想办法去实现。这个性格其实在生活、工作中给我带来了不少的好处的，坚持、靠谱、敢于接受挑战的各种评价大抵来源于此，但实际上却也经常让我个人陷入一些困境的，所谓的”内耗“。但无论如文，考个研读个书的想法真是”野火烧不尽，春风吹又生“，那就实现他吧，黄沙百战穿金甲，不破楼兰终不还。
除了性格的原因，想考研读书也是受到方方面面的影响：&lt;/p&gt;
&lt;p&gt;1、受益公司的战略调整，这几年开始有机会招聘海外的一些员工，在面试过程中就发现很有意思且很频繁的一个现象，海外同学（这里指坡县）经常会在工作一段时间后又回到学校，再出来工作，如此反复的这么一个过程。让我从“惊讶”进入到了”羡慕“，毕竟在国内要是我们的工作简历上频繁的出现”GAP“，那大抵是不受待见的。甚至于除了”铁饭碗“类的岗位，在大多数行业（比如互联网），虽然大家鼓励学习、知识的沉淀，但对于学历的追求，可能也会得到一个”有什么用呢“的反问？和海外这些员工的接触给我种了草，其他人的想法并不重要，这是让我觉得会有趣的一个经历，那就去试试吧！（谢谢我的热爱学习的同事们）
2、我不知道公司或者整个社会有什么问题，但知道我所在公司的朋友，也应该会关注到我所在的公司这两年也是各种”风起云涌“，口罩后的大家过的都不容易，对于我自己的岗位，从23年开始直到现在，我也一直是抱着背水一战，破釜沉舟的心态，可是当真的走到这一步的时候，我能做些什么，我真的没有想好。小学中学大学十六载，工作近十八载，如果有这么一个窗口，那么我想停一下看一看，也好好想一想接下来的路怎么走（毕竟40岁的互联网人那可太不被待见了啊，哈哈哈），但却并不想让自己无所事事，因此给自己一个重新学习的机会也成了自己一个很好的可选项，给自己一个暂停键，还可以充充电（算不算在逃避呢？嘿嘿）&lt;/p&gt;
&lt;p&gt;想到了就去做，而且这次我还算是做了一个和自己性格相逆的”玩法“，早早我就告诉朋友、同事我要去考个研（以前这种人生大事怎么可以告诉大家，都是默默搞定他，免得丢人，哈哈哈），人生海海，何妨让世界听见我的号角？&lt;/p&gt;
&lt;h2&gt;备考&lt;/h2&gt;
&lt;p&gt;要说花了很多的时间备考，好像也没有，毕竟日常确实太忙了，基本都是利用周末的时间以 1.5x 甚至 2.0 的倍速刷一下视频，让自己有点印象，其实最担心的无法就是到了考场一个字都蹦不出来。非全日制工商管理类（我报了工程管理）国家统考其实就考两科《管理类综合能力》和《英语（二）》，对于我个人来说，英语其实没啥好准备的（口语不行，但阅读理解却是我的强项），主要时间还是用于学习管综了，买了中公教育的历年真题，这套真题有个好处是看不懂的题目可以刷二维码有视频学习，这对于已经离开学校18年，各种知识已经还给老师的中年人实在是太友好了！
当然，这些课程的学习纯粹是为了“应试”，但好在也不是一无事处，有两点收获还是值得回味一下的：&lt;/p&gt;
&lt;p&gt;1、算是把初中的一些数学知识重新学习了一下，涉及到基础的几何知识、平面直角坐标系、概率等等的一些知识，家里刚好有一个初中娃，被他问起来的时候显得我没有那么落伍，被我装到了；
2、学到了一个应试技巧，考试时先把”论说文“和”论述文“两篇文章写了，两篇文章只要完成度高，前面的数学题、逻辑题蒙个一半，那基本上也大差不差了。
英语在考前找了几个公众号看了下英语写作的模板算是最大的准备了。逻辑题我个人就会认为纯粹就是为了考试而考试设计的各种绕口令了，总之绕晕你就对了（实际我也没有做完，至少有10道选择题就是蒙猜了），当然正能量一点，我找 DeepSeek 讨论了一下对于“逻辑题”的看法，它给出了正面反馈：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/postgraduate_1.png" /&gt;&lt;/p&gt;
&lt;h2&gt;考试&lt;/h2&gt;
&lt;p&gt;考试的仪式感还是很强的，除了文具其他随着携带的东西都是在考场区域外保管的，进考场区域和考室也是面临层层安检。更有趣的是，当天也算是一边考试一边加班了，进考场区域的前 5 钟还在打电话安排和汇报工作。中午和下午考完出来的 5~10 分钟内都有电话和 IM 信息过来的灵魂拷问：“怎么我回信息这么慢的？”（真是紧张又不失有趣啊，肾上腺激素彪升，懂的都懂，哈哈哈，真心感谢当天紧急提供协助的同学们）
所幸，流程算是走完了，这个考试状态感觉要凉，哈哈哈，但好在自己并不焦虑，好像有点祛魅了，原来就是这么一回事！&lt;/p&gt;
&lt;h2&gt;复试&lt;/h2&gt;
&lt;p&gt;成绩出来后其实并不理想，管综考的比较烂，英语算是拉了我一把，工程管理国家线 162，吹牛的说法是咱超了国家线近 30 分，其实一点都不理想，我所报考的学校计划招生 40 人，我排名 61，是否能进面试其实是比较悬的，最后算是有惊无险，进了复试。
复试至少准备上是比初试统考用心了的，在复试通知出来的时候才发现竟然要考一门专业课，足足用了两周下班后的晚上和一个完整的周末在B站刷视频课程学习，专业课和面试的过程就不具体展开了，毕竟用官方的说法这应该属于泄密。但是回到学校复试的感受还是不一样的，走在学校的小路上，看着来往的学子和有着年头的建筑，想重新回到学校的念头还是非常强烈的。所以在对话的一开始还是略微紧张的，但也算是很快调整过来了的，复试的成绩还不错，最后总成绩把自己从 61 拉到前 30 了。因为官方的通告都是 PDF 的，让 DeepSeek 帮忙分析了下：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/postgraduate_2.png" /&gt;&lt;br/&gt;
&lt;img src="/static/postgraduate_3.png" /&gt;&lt;/p&gt;
&lt;h2&gt;知识若有趣，前路自有光&lt;/h2&gt;
&lt;p&gt;至此，非全研究生的考试算是把整个流程走完了，虽然感觉对整个过程是祛魅了，但大抵自己还是会考虑这项“投资”把后面三年的学习流程走完的。印象有点深刻的是最近和同学朋友聚餐时，提到我要去读书，结合我所在的行业（学历已经没什么帮助了）、年龄（中年大叔）、学费（一年5个）、学习方式（非全日制，低人一等？），从部份朋友的眼神里看到了惊讶和不解。但我在复试专业课的准备过程中却看到了知识的趣味性、系统性、延续性，很多工作中看到的现象碰到的问题，原来可以大量使用课程里的原理、逻辑、方法加以解释（如果回到大学，我的想法一定会是这些大道理有什么意义啊？），既然这么有趣（又有挑战，听说这个学校即使非全的按期毕业率低于30%），那就让游戏继续下去吧，这应该是人生最后一次进校园的机会了？&lt;/p&gt;
&lt;p&gt;中午逆旅，莫听穿林打叶声，何妨吟啸且徐行。&lt;/p&gt;
&lt;p&gt;2025.04.13 06:30&lt;/p&gt;</content><category term="misc"/><category term="postgraduate"/></entry><entry><title>你想要建造自己的数据中心吗？</title><link href="https://www.chenxiaosheng.com/posts/2025-01-20/So-You-Want-to-Build-Your-Own-Data-Center.html" rel="alternate"/><published>2025-01-20T13:07:00+08:00</published><updated>2025-01-20T13:07:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2025-01-20:/posts/2025-01-20/So-You-Want-to-Build-Your-Own-Data-Center.html</id><summary type="html">&lt;p&gt;这篇文章主要讲述了Railway公司从依赖Google Cloud Platform到自主建设数据中心的过程。由于Google Cloud Platform存在定价、服务和工程限制等问题，Railway启动了Railway Metal项目，自主设计、安装数据中心基础设施，包括选址、电力供应、网络连接、机架布局和硬件安装等，以提供更好的云服务体验。&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;原文来自：https://blog.railway.com/p/data-center-build-part-one
很少看到有从 IDC 基础设施开始写的文章，一时兴起借 AI 工具作了粗浅的翻译。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从一开始，Railway的计算能力就是建立在谷歌云平台之上的。该平台支持了Railway的初步发展，但它也引发了许多问题，这些问题给我们的业务带来了生存风险。更重要的是，构建在超大规模云服务提供商上，阻碍了我们为客户提供最佳平台的可能性。&lt;/p&gt;
&lt;p&gt;它直接影响了我们能够提供的定价（有人知道公有云流出流量费用吗？），限制了我们能够提供的服务级别，还引入了工程限制，这些限制了我们能够构建的能力。&lt;/p&gt;
&lt;p&gt;我们不仅&lt;a href="https://github.com/GoogleCloudPlatform/guest-agent/issues/401"&gt;很少&lt;/a&gt;能&lt;a href="https://blog.railway.com/p/2023-12-01-incident-report"&gt;理解&lt;/a&gt;上游环节&lt;a href="https://blog.railway.com/p/incident-december-16-2024"&gt;出问题&lt;/a&gt;的&lt;a href="https://x.com/JustJake/status/1667478906591666176"&gt;原因&lt;/a&gt;，而且尽管我们每年花费数百万美元，但能得到的支持也只相当于你花费100美元所得到的。&lt;/p&gt;
&lt;p&gt;因此，我们去年启动了一个 Railway Metal 项目。九个月后，我们在加利福尼亚的第一个站点上线，设计、规划并落地了从机柜中的光纤电缆到与互联网服务提供商的各种合同的一切。就在我们发布这篇博客的时候，我们正在启动另外三个区域的数据中心。&lt;/p&gt;
&lt;p&gt;为了向我们的客户提供“透明基础设施”的云体验。我们需要迅速且足够好的构建我们自己的物理基础设施。这就是我们今天博客文章的主题。&lt;/p&gt;
&lt;h2&gt;所以你想要搭建自己的“云”&lt;/h2&gt;
&lt;p&gt;从 2024 年 1 月启动 Railway Metal 项目开始，我们花了整整五个月时间才将第一批服务器接入。之后，又用了额外的三个月，我们才觉得可以让用户放心地使用这些硬件（并且又过了几个月，我们才开始在这里撰写相关的内容）。&lt;/p&gt;
&lt;p&gt;第一步是找到合适的地方。&lt;/p&gt;
&lt;p&gt;本地部署时，你需要有地方来放置你那些崭新的服务器以及可靠的电力来保持它们运行。另外，你还需要足够的冷却措施，防止它们“熔化”（保障服务器正常运行）。&lt;/p&gt;
&lt;p&gt;一般来说，你有三种主要的选择：绿地建设（购买或租赁数据中心）、机笼托管（在供应商的数据中心内获取一个由网格墙围起来的私人空间）、或者机架托管（租赁托管数据中心中的单个机架或机架的分区）。&lt;/p&gt;
&lt;p&gt;这句话的意思是：“我们选择了第二种选择：一个笼子，它能给我们四面墙、一扇安全的门，以及一个空白的画布，让我们可以自由地设计其他一切。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="655" src="/static/CleanShot_2025-01-15_at_23.webp"/&gt;&lt;/p&gt;
&lt;p&gt;租用或购买足够的空间来放置设备等并不是主要的成本支出，电力（以及由此带来的冷却需求）却是成本最高的部分。根据地理位置的不同，每千瓦时（$/kW）的电力价格可能会有很大的差异。--举个例子，在美国西海岸，我们支付的电力费用可能不到在新加坡支付的一半。电费是按照固定的月度承诺来支付的，无论是否实际消耗，都要支付这笔费用，以确保电力可以随时按需使用。&lt;/p&gt;
&lt;p&gt;但是你需要多少电力呢？&lt;/p&gt;
&lt;h2&gt;能力越大，责任越大&lt;/h2&gt;
&lt;p&gt;理想情况下，如果你已经开始了数据中心迁移的任务，你应该对想要部署的计算资源数量有一个大致的概念。我们开始时设定了虚拟CPU（vCPU）的数量、内存容量（GBs）以及NVMe（一种高速存储设备）容量（TBs）作为目标，以匹配我们在谷歌云平台（GCP）的容量。&lt;/p&gt;
&lt;p&gt;经过一系列指标、数据的分析、比较等过程，我们最终确定了合适的服务器和CPU。计算过程有很多因素、参数等可供选择、调整--甚至于都值得另外写一篇文章了--但对我们来说，最重要的因素是功耗密度。例如，我们如何在特定的功耗范围内获得我们想要的计算密度。&lt;/p&gt;
&lt;p&gt;功率计算并不像简单地把瓦数加起来那么直接，尤其是涉及到三相电的情况。Cloudflare有&lt;a href="https://blog.cloudflare.com/an-introduction-to-three-phase-power-and-pdus/"&gt;一篇很棒的博客文章&lt;/a&gt;详细阐述了这个主题。&lt;/p&gt;
&lt;p&gt;电力是数据中心最为关键的资源，一旦电力中断，可能需要数小时甚至数天才能完全恢复正常的运营状态。所以冗余是非常关键的，对每个机柜来说，两路独立的电力接入至关重要。在正常运行时，这两个独立的电源线路会共同分担机架设备的负载，共同为设备提供电力。这样可以保证电力供应的稳定性和效率。然而，更重要的是，这种设计必须能够抵御其中一个电源线路发生故障的情况。即使其中一个电源线路出现故障，另一个电源线路也能够立即接管全部负载，确保机架设备的持续运行，而不会因为电源故障而导致设备断电。&lt;/p&gt;
&lt;p&gt;为了将这种电力传输到你的服务器上，你还需要一个电源分配单元（PDU），你需要根据它提供的插座数量和管理功能来选择它。基本款的 PDUI 就像是升级版的延长线，而我们部署的 PDU 则能够实现对每个插座的控制和计量。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="654" src="/static/880a21c2-366b-445e-951e-a03a68515796.webp" /&gt;&lt;/p&gt;
&lt;p&gt;凭借前面提及的相关能力，现在机柜里面可以正常供电了。&lt;/p&gt;
&lt;h2&gt;上帝说要有光&lt;/h2&gt;
&lt;p&gt;没有一台云机器是孤立存在的，这就是网络发挥作用的地方。&lt;/p&gt;
&lt;p&gt;为了在Railway上实现尽可能低的延迟，我们需要为你建立到世界各地的稳定连接。&lt;/p&gt;
&lt;p&gt;我们寻找的数据中心要具备与顶级（T1）互联网服务提供商（ISPs）处于同一网络、属于互联网交换点（IX）的一部分以及有可用光纤连接到附近其他数据中心等特性。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/d0c2e2d8-dd9d-4917-8178-062572bc3ff4.webp" /&gt;&lt;/p&gt;
&lt;p&gt;部署到Railway的应用程序会想要通过网络连接到各种各样的端点，无论是位于澳大利亚悉尼的家庭互联网用户，还是托管在美国AWS服务器上的API。为了给您提供尽可能低的延迟和最低的带宽成本，我们会与针对每种使用场景优化的多种互联网提供商签订合同&lt;/p&gt;
&lt;p&gt;我们在选择互联网服务提供商（ISPs）时，会考虑它们在我们目标地区的网络成熟度。如果在一个地区选择了不合适的ISP，可能会导致到达特定目标的网络路径出现额外的网络跳数（从而产生延迟），或者在最糟糕的情况下，出现复杂的网络路由。因此，为了确保网络服务的质量和可靠性，我们会根据它们在各个地区的网络覆盖范围来选择至少两个不同的网络。&lt;/p&gt;
&lt;p&gt;一旦连接建立，我们会从每个互联网服务提供商（ISP）那里接收完整的互联网路由表并将这些路由表整合到我们的网络交换机上以便为每个IP前缀解析出最佳路径。如果你有一个位于澳大利亚的终端用户试图连接部署在新加坡的应用程序，我们很可能会直接将这些数据包转交给&lt;a href="https://bgp.tools/as/4637#connectivity"&gt;Telstra&lt;/a&gt;。Telstra拥有澳大利亚最密集的接入网络之一。如果同一个应用程序需要将数据包发送到日本的终端用户或服务器，那么我们很可能会将这些数据包转交给PCCWPCCW直接与日本的NTT（日本电信电话公司）进行对等互联，并且在亚太地区（APAC，Asia-Pacific）&lt;a href="https://www.pccwglobal.com/wp-content/uploads/2019/11/world_fold_20191115.pdf"&gt;拥有广泛的网络覆盖&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;👉 网络之间对等互联的信息是公开可获取的，可以访问&lt;a href="https://bgp.tools/"&gt;bgp.tools&lt;/a&gt;这个网站来观察你感兴趣的网络之间是如何实现相互连接的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了实现冗余，我们正在每个区域构建多个可用区，并且这些站点之间的互联互通对我们扩展计划至关重要。我们正在寻找裸纤（dark fiber）或波分复用技术（wavelength services）这样的工具来规划这种扩展。其结果是，你的应用程序不会察觉数据库是位于同一个房间，还是在相邻建筑中相隔四个街区的位置——这是一个优势，而不是缺陷——因为它增强了系统对单个数据中心故障的韧性。&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;好的，现在你已经找到了一个你满意的场地，和一个数据中心签订了协议，还和几家互联网服务提供商签了约，你就可以着手安装服务器了，对吧？&lt;/p&gt;
&lt;p&gt;嗯，不是完全这样。首先你需要很多其他的东西来给你的服务器一个舒适温暖的家来安顿下来。&lt;/p&gt;
&lt;h2&gt;通道，机架和头顶的基础设施&lt;/h2&gt;
&lt;p&gt;在数据中心里，机架是按行排列的，机架之间的空间，也就是通道，被用于空气流通。&lt;/p&gt;
&lt;p&gt;冷通道是从数据中心设施吹入冷空气的地方，而机架中的服务器会吸入这些冷空气，并将其从后部排出到热通道。数据中心设施会从热通道中移除这些热空气。为了达到最佳效率，不希望冷通道和热通道之间的空气混合。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/3d2a676b-0ff6-4c62-a25e-c9db9c40a21a.webp" /&gt;&lt;/p&gt;
&lt;p&gt;即使您选择使用传统的19英寸宽设备，机架本身也存在一定的可变性。您可以根据设备和布线需求选择机架的高度、宽度和深度。&lt;/p&gt;
&lt;p&gt;大多数服务器设备都可以通过滑轨进行滑动，以便于维护，因此要确保机笼尺寸允许设备进行滑动。布线和线缆管理也需要一定的空间，所以要在每个机架的拥挤程度与机笼内可容纳的机架数量之间做出权衡。&lt;/p&gt;
&lt;p&gt;在我们的经验中，电力和制冷常常是限制因素，而不是实际可用的空间。在新场地，我们倾向于选择宽度为800毫米的机架，以便通过将电缆移出排气口的路径来实现更好的空气流通。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/IMG_5261.webp" /&gt;&lt;/p&gt;
&lt;p&gt;除了机架之外，你还需要一些基础设施来将电力和数据传输到你的机架。这可能涉及安装一些顶部基础设施和托盘，这些托盘让你能够将光纤电缆从你的机笼边缘路由到每个机架，并在机架之间路由电缆。当报价时，数据中心运营商通常会包含这些内容。&lt;/p&gt;
&lt;p&gt;根据你的设计，你可能需要通过确保架空基础设施、机架内布线以及设备朝向的一致性，来优化电缆路径，使其尽可能短。由于我们的机架中每个都包含密集的交换机到服务器的光纤布线，因此我们采购的交换机是端口朝向机架后部的（这些被称为反向气流交换机，因为它们的散热口位于网络端口所在的侧面）。&lt;/p&gt;
&lt;p&gt;这使我们能够对齐电缆托架，从而让所有的布线都发生在机架的一侧，并且没有电缆在机架的前部和后部之间来回曲折。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/4710_fiber_tray_and_top_rack.webp" /&gt;&lt;/p&gt;
&lt;p&gt;你已经拥有了所需的空间，签约了互联网服务提供商（ISPs），订购了硬件，准备好了机架，并且对如何布置这一切有了一个相当不错的概念。但目前，这一切还只是像一套昂贵的乐高积木一样，静静地待在数据中心的装卸区。要将它们组装起来，你现在需要利用人类历史上最通用的编程工具——Microsoft Excel。&lt;/p&gt;
&lt;h2&gt;上架和堆叠&lt;/h2&gt;
&lt;p&gt;让我们先退一步，先发表一个免责声明：整齐有序的布线需要大量的实践；我们自己也尝试过，结果……喜忧参半。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/railway-structured-cabling-services.webp" /&gt;&lt;/p&gt;
&lt;p&gt;为了正确安装它们，我们引入了专业人士，但专业人士需要知道要安装什么。一份全面的文档包是必不可少的。电缆矩阵和机架布局图是常见的文件，它们向承包商传达如何安装和连接服务器。&lt;/p&gt;
&lt;p&gt;电缆矩阵（Cabling Matrix）描述了每根电缆的端接情况，指明了连接的每一端所对应的设备位置和端口号，同时还包括电缆本身的规格信息（例如光纤类型、长度等）。机架视图（Rack Elevation）是机架本身的视觉呈现，展示了每个设备的位置和朝向。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/CleanShot_2025-01-15_at_23_1.webp" /&gt;&lt;/p&gt;
&lt;p&gt;文档工作非常繁琐，每个安装阶段都涉及60多台设备、300多根独立电缆以及许多细微之处。这些内容都被精心整理成书面规范和电子表格，作为安装和调试的基础。从物料到场到完成安装，整个过程大约需要6到14天。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/CleanShot_2025-01-15_at_23_2.webp" /&gt;&lt;/p&gt;
&lt;p&gt;数据中心的建设与软件、DevOps以及人们通常所认为的“基础设施”相去甚远。确切的说，更类似于建造房屋，而不是 Terraform 部署。&lt;/p&gt;
&lt;p&gt;更复杂的是，每一个数据中心设施、承包商和供应商在做事方式上都会存在细微的差别，即使在同一个组织内也是如此。因此这些事情会要求你始终保持警觉，并且极度注重细节。&lt;/p&gt;
&lt;p&gt;迄今为止我们经历过的一些困域时刻：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;承包商说需要更长的电源线。原因是该站点的电源分配单元（PDU）是倒置的，因为电源是从地板接入的，这导致了插座编号在规划中被反向标记了。&lt;/li&gt;
&lt;li&gt;来自阿姆斯特丹的电话说：“这个站点没有分界点吗？” 一个特定的设施直接将外部光纤链路安装到我们机架中的一个盒子上，而不是通过一个专用的交接点。&lt;/li&gt;
&lt;li&gt;Railway Discord：“为什么这个PDU的相位接线这么奇怪？” 该设施的接线方式与其他站点不同，电源插座是相线对中性线接线，而不是相线对相线接线（对于电气工程师来说，这是WYE和Delta电路的区别）。&lt;/li&gt;
&lt;li&gt;承包商说：“你的数据线太短了。” 承包商没有意识到网络设备是逆向气流的，试图以错误的方向安装设备。&lt;/li&gt;
&lt;li&gt;我们提交了一个支持工单：“这条电缆没有建立连接。” 光纤的极性接反了；那天我们学会了什么是“翻转光纤电缆”……这是指他们从&lt;a href="https://www.fs.com/uk/blog/lc-fiber-optics-a-comprehensive-guide-2684.html"&gt;LC连接器&lt;/a&gt;中拔出插头并交换它们的位置。&lt;/li&gt;
&lt;li&gt;Railway Discord：“我今天从家得宝买了一个橡胶锤。” 一个供应商的近24个PDU批次被交付时带有有故障的插座，即使施加适当的极端机械力，这些插座也无法与电源插头正确接合。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但从这一点开始——硬件已经就位，任务开始变得熟悉起来；我们现在需要进行一些BGP（边界网关协议，是一种用于互联网自治系统之间的路由协议）操作，安装一些操作系统，配置监控并让一切工作起来。&lt;/p&gt;
&lt;p&gt;&lt;img width="736" height="0" src="/static/finished.webp" /&gt;&lt;/p&gt;
&lt;h2&gt;把油门踩到底&lt;/h2&gt;
&lt;p&gt;安装好的机架就像是一块空白的画布，网络设备需要进行配置，路由器的配置文件需要编写，区域互联网注册机构（RIR）的记录需要更新，我们还需要与类似 &lt;a href="https://www.dmtf.org/standards/redfish"&gt;Redfish API&lt;/a&gt;（用于服务器主板和电源分配单元的专用控制器的 HTTP 接口）和 PXE（一种通过网络启动服务器的协议）进行交互，以确保一切正常运行。&lt;/p&gt;
&lt;p&gt;我们也还没有讨论网络是如何工作的。我们的设计使用了&lt;a href="https://frrouting.org/"&gt;FRR&lt;/a&gt;（Free Range Routing）和运行&lt;a href="https://sonicfoundation.dev/"&gt;SONiC&lt;/a&gt;（Software for Open Networking in the Cloud）操作系统的白盒网络交换机，构建了一个仅基于第三层（L3）的软件驱动网络，该网络与我们的控制平面深度集成。&lt;/p&gt;
&lt;p&gt;我们已经给你讲了很多来自一线的故事……要是再讲下去，你今天就别想走了。&lt;/p&gt;
&lt;p&gt;在未来的文章中，我们将讨论如何将一个房间里的一堆服务器转变为一个功能性的 Railway 区域。在过去的几个月里，我们开发了两个新的软件工具：Railyard 和 MetalCP，以实现从设计新的机架、追踪和可视化布线，到在服务器上安装操作系统并将其接入互联网的“一键式”体验。&lt;/p&gt;
&lt;p&gt;直到那时，如果这些内容中有任何让你感到兴奋的，可以查看我们目前开放的&lt;a href="https://railway.com/careers/infra-platform"&gt;基础设施工程职位&lt;/a&gt;，如果这些职位引起了你的兴趣，就联系我们。&lt;/p&gt;</content><category term="misc"/><category term="datacenter"/></entry><entry><title>美区 Apple ID 充值攻略</title><link href="https://www.chenxiaosheng.com/posts/2023-10-16/how-to-top-up-U.S-AppleID-for-Chinese.html" rel="alternate"/><published>2023-10-16T20:10:00+08:00</published><updated>2023-10-16T20:10:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2023-10-16:/posts/2023-10-16/how-to-top-up-U.S-AppleID-for-Chinese.html</id><summary type="html">&lt;p&gt;拥有一个美区 Apple ID 账号对于广“果粉”来说是非常有必要的。那么就会面对一个问题，就是如何充值美区苹果帐号？这篇文章就要详细为大家介绍下美国苹果ID充值教程，满满的干货，赶紧来看看有没有适合你的充值方式吧！&lt;/p&gt;</summary><content type="html">&lt;p&gt;注：原文来自 &lt;a href="https://zhuanlan.zhihu.com/p/552311280"&gt;https://zhuanlan.zhihu.com/p/552311280&lt;/a&gt; ，亲测有效，因此转载一份，防止消失 :-)&lt;/p&gt;
&lt;h2&gt;一、购买美区苹果礼品卡&lt;/h2&gt;
&lt;p&gt;在国内购买美国 Apple gift cards的途径主要有以下三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;淘宝购买美区礼品卡，买回来直接充值，但风险比较高，因为有些淘宝的礼品卡是黑卡，使用黑卡充值可能有 ID 丢失风险。没有信得过的店铺就不要尝试这个方法。&lt;/li&gt;
&lt;li&gt;如果你有美国的朋友，可以让他代买礼品卡，然后充值。  &lt;/li&gt;
&lt;li&gt;到苹果美国官网自己买礼品卡送给自己，也是本文要说的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;双币种信用卡，单币银联信用卡，中国发行的visa或者master&lt;/li&gt;
&lt;li&gt;准备两个邮箱（通过邮箱发送礼品卡，Gmail可以）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;购买流程&lt;/h3&gt;
&lt;p&gt;首先打开美国礼品卡购买地址：&lt;a href="https://www.apple.com/shop/buy-giftcard/giftcard"&gt;https://www.apple.com/shop/buy-giftcard/giftcard&lt;/a&gt;
然后选择“Email”电子礼品卡，封面随便选一个（不重要），然后填写购买金额，有$25，$50，$100的选项，官网买的话最小金额是$10。&lt;/p&gt;
&lt;p&gt;&lt;img width="690" height="757" src="/static/how-to-top-up-U.S-AppleID-for-Chinese/image_1.png"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img width="690" height="757" src="/static/how-to-top-up-U.S-AppleID-for-Chinese/image_2.png"/&gt;&lt;/p&gt;
&lt;p&gt;接着填写收发人的信息及邮箱，邮箱千万不要弄错，否则无法接收礼品卡，填好后点击右上角“Add to Bag”按钮。&lt;/p&gt;
&lt;p&gt;&lt;img width="690" height="598" src="/static/how-to-top-up-U.S-AppleID-for-Chinese/image_5.png"/&gt;&lt;/p&gt;
&lt;p&gt;点完“Add to Bag”按钮后会跳转到另一个界面，这里检测下自己填写的邮箱有没有错，确认无误后点击“Check Out”按钮。&lt;/p&gt;
&lt;p&gt;&lt;img width="690" height="598" src="/static/how-to-top-up-U.S-AppleID-for-Chinese/image_4.png"/&gt;&lt;/p&gt;
&lt;p&gt;这里直接选择“Continue as Guest”按钮
&lt;br/&gt;
接下来这里填写你的信用卡信息，包括姓名，卡号，账单地址等等，填写好信息后点击“Continue to Review”按钮。&lt;/p&gt;
&lt;p&gt;&lt;img width="690" height="614" src="/static/how-to-top-up-U.S-AppleID-for-Chinese/image_6.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img width="690" height="1114" src="/static/how-to-top-up-U.S-AppleID-for-Chinese/image_7.jpg"/&gt;&lt;/p&gt;
&lt;p&gt;填完上面信息后会让你再次确认邮箱等信息，确认无误后直接点击“Place Your Order”按钮就行，官网上写的是一个小时会收到邮件，正常不需要那么久，一般会在10分钟左右收到礼品卡邮件。&lt;/p&gt;
&lt;p&gt;PS：最后填写你的 VISA 信用卡信息，和地址信息时候，如果你选择的是中国，购买失败了，可以重新下单选 United States看看，你可以搜索在线美国地址生成器，或者打开 Google Map，找到一个你喜欢的美国的详细地址，然后 Google Map 会告诉你它所在的州，街道，电话信息等。（账单地址建议选择没有消费税的五个州，因为不确定购买虚拟产品会不会账单地址不同而多交税。）&lt;/p&gt;
&lt;h2&gt;二、使用礼品卡充值&lt;/h2&gt;
&lt;p&gt;收到礼品卡后，会在邮箱中看到兑换码，复制兑换码，到app store里去充值。  &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;充值前，先确认自己的 App Store 登录的是将要充值的 Apple ID，也就是美区 Apple ID。  &lt;/li&gt;
&lt;li&gt;把设置中国家和地区选项，改为美国，如果是 iPhone，路径是：设置 -&amp;gt; 通用 -&amp;gt; 语言和地区 -&amp;gt; 地区 -&amp;gt; United States。  &lt;/li&gt;
&lt;li&gt;打开邮件，点击进行充值。  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;或者你也可以打开 App Store，找到礼品卡充值入口，然后输入礼品卡的卡号(卡号在邮件里)，进行充值。具体步骤这里不再展开了，跟着手机的指引来就好了&lt;/p&gt;
&lt;h2&gt;Q&amp;amp;A&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;购买gift card并且到账之后，是否还需要绑定双币信用卡或者美国PayPal？&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;答：不需要，gift card和绑定双币信用卡或者美国PayPal时平行的，也就是说这三者只要满足一项，就可以进行付费APP的购买。  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;可以给美国区的Apple ID帮定国内的visa、master card、america express吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;答：不可以，美国区的Apple ID只能绑定由美国发行的信用卡，有些小伙伴可能认为只要能消费美元的信用卡就可以，其实就算你绑定美国区Apple ID之后，苹果也能够识别你是大陆的信用卡，然后你的美国区账号就会被强制转成国区的Apple ID了，这就得不偿失了，所以建议不要尝试，如果不信邪，那可以试试，最后你可能需要重新注册一个美区Apple ID。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;中国区Apple ID充值&lt;/h2&gt;
&lt;p&gt;中国区的Apple Store支持的充值方式有支付宝充值、微信支付、银联/信用卡/借记卡、快捷支付等四种方式，如果你在海外，确保你的支付宝或者微信账户有钱就可以充值了。&lt;/p&gt;</content><category term="misc"/><category term="Apple"/></entry><entry><title>可能是全网最全的 ulimit 配置说明了</title><link href="https://www.chenxiaosheng.com/posts/2022-06-14/linux-ulimit.html" rel="alternate"/><published>2022-06-14T13:07:00+08:00</published><updated>2022-06-14T13:07:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2022-06-14:/posts/2022-06-14/linux-ulimit.html</id><summary type="html">&lt;p&gt;ulimit 设置不当经常会引起各种各样的问题，比如很经典的 &lt;code&gt;too many open files&lt;/code&gt;，网上也有很多文章讲解 ulimit 设置，如 &lt;a href="https://linux.die.net/man/5/initscript"&gt;initscrip&lt;/a&gt; 设置、PAM、systemd 的配置等等。&lt;/p&gt;</summary><content type="html">&lt;p&gt;ulimit 设置不当经常会引起各种各样的问题，比如很经典的 &lt;code&gt;too many open files&lt;/code&gt;，网上也有很多文章讲解 ulimit 设置，如 &lt;a href="https://linux.die.net/man/5/initscript"&gt;initscrip&lt;/a&gt; 设置、PAM、systemd 的配置等等。&lt;/p&gt;
&lt;p&gt;由于 systemd 已经成为主流，本文以 Debian 11 bullseye 为环境理清正确的 ulimit 设置应该覆盖哪些配置。通常情况下这些配置在绝大多数 Linux 发行版会是通用的，并不局限于 Debian。&lt;/p&gt;
&lt;h2&gt;systemd&lt;/h2&gt;
&lt;p&gt;systemd 主要涉及 /etc/systemd/system.conf 和 /etc/systemd/user.conf 两个文件，需要配置的内容是以样的，以 ulimit 中 &lt;code&gt;open files&lt;/code&gt; 和 &lt;code&gt;max user processes&lt;/code&gt; 为例，我们需要配置如下两项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[Manager]
DefaultLimitNOFILE=1048567
DefaultLimitNPROC=65535
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;配置生效需要重启。但是这两个配置文件有什么区别呢？通过 &lt;code&gt;man systemd-system.conf&lt;/code&gt; 了解到当以系统实例运行时，引用的是 system.conf ，当以用户实例运行时，引用的是 user.conf。不过系统环境中运行的都是系统实例，如果有兴趣的朋友可以通过 &lt;code&gt;systemctl edit/enable/start --user&lt;/code&gt; 等操作自己注册个 user 实例看看。可以通过 &lt;code&gt;cat /proc/${pid}/limits&lt;/code&gt; 进行确认。&lt;/p&gt;
&lt;p&gt;结论：为了确保 ulimit 在 system 环境中生效，建议同时修改/etc/systemd/system.conf 和 /etc/systemd/user.conf 两个文件中的相关配置，并确保配置一致。&lt;/p&gt;
&lt;h2&gt;PAM limits&lt;/h2&gt;
&lt;p&gt;很多伙伴会发现，即使完全配置了 system 相关的文件并重启生效后，通过 ssh 远程登录服务器，&lt;code&gt;ulimit -n&lt;/code&gt; 一看，还是 1024，这可是个忧伤的故事，怎么破呢？&lt;/p&gt;
&lt;p&gt;请检查 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; 配置文件，看看是否有这一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;UsePAM yes&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果有，请将 &lt;code&gt;yes&lt;/code&gt; 改成 &lt;code&gt;no&lt;/code&gt; 之后重启 sshd 服务，再次登录后，你应该能发现 systemd ulimit 相关配置已经生效了。&lt;/p&gt;
&lt;p&gt;但系统中 PAM 可是在相当多的地方用到了，所以这里我们需要再次修改 pam limit 相关配置，以确保系统中 ulimit 相关的配置一致。需要修改的文件为：/etc/security/limits.conf，请在文件里增加如下内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;*   soft    nofile  1048567
*   hard    nofile  1048567
*   soft    nproc   65535
*   hard    nproc   65535
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;修改上述文件后，需要再确认一下以下文件（可能还有更多，但以下这些文件建议确认）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/etc/pam.d/su&lt;/li&gt;
&lt;li&gt;/etc/pam.d/sshd&lt;/li&gt;
&lt;li&gt;/etc/pam.d/login&lt;/li&gt;
&lt;li&gt;/etc/pam.d/cron&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;包含以下这行内容：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;session    required     pam_limits.so&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;请注意，前方高能：细心的小伙伴可能会发现这时通过 &lt;code&gt;su -&lt;/code&gt; 切换成 root 后，再看 ulimit，又是没有生效的，怎么回事呢？ &lt;code&gt;man limits.conf&lt;/code&gt; 可以找到答案：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: group and wildcard limits are not applied to the root user. To set a limit for the root user, this field must contain the literal username root.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上述 /etc/security/limits.conf 配置中的通配符 "*" 对 root 用户不生效，我们需要继续在 /etc/security/limits.conf 中增加如下配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;root    soft    nofile  1048567
root    hard    nofile  1048567
root    soft    nproc   65535
root    hard    nproc   65535
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;保存后，应该安逸了~&lt;/p&gt;
&lt;p&gt;结论：pam 的 limit 设置也会影响  ulimit 相关设置，需要通过配置确保和 systemd 相关内容一致。坑点：通配符 "*" 对 root 用户不生效。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;system 的 ulimit 相关设置需要同时调整 /etc/systemd/system.conf 和 /etc/systemd/user.conf；&lt;/li&gt;
&lt;li&gt;除了 system 的配置，pam 相关的配置也需要同步调整，主要涉及 /etc/security/limits.conf 和 /etc/pam.d/ 目录下的相关文件；&lt;/li&gt;
&lt;li&gt;/etc/security/limits.conf 中的通配符 "*" 对 root 用户不生效，root 用户相关配置需要显式指定；&lt;/li&gt;
&lt;li&gt;除了系统级别的配置，用户也可以在 /etc/profile 和 bashrc 等相关配置里显式的修改 ulimit，当你发现上述 3 项配置都没有生效的时候，请检查用户侧的设定，看看是否自己另外指定了；&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="linux"/><category term="ulimit"/></entry><entry><title>MongoDB 4.2 流控 FlowControl 机制走读</title><link href="https://www.chenxiaosheng.com/posts/2022-06-11/mongodb-flowcontrol-practice.html" rel="alternate"/><published>2022-06-11T22:21:00+08:00</published><updated>2022-06-11T22:21:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2022-06-11:/posts/2022-06-11/mongodb-flowcontrol-practice.html</id><summary type="html">&lt;p&gt;MongoDB 4.2 引入了一个「流量控制」的新特性。该流控机制旨在保持副本集多数提交延迟小于或等于配置的最大值。此最大延迟的默认值为 10 秒。一旦多数提交的复制延迟达到配置的最大值的阈值百分比，流控制机制就会开始限制主节点上的写入。&lt;/p&gt;</summary><content type="html">&lt;p&gt;MongoDB 4.2 引入了一个「流量控制」的新特性。该流控机制旨在保持副本集多数提交延迟小于或等于配置的最大值。此最大延迟的默认值为 10 秒。一旦多数提交的复制延迟达到配置的最大值的阈值百分比，流控制机制就会开始限制主节点上的写入。&lt;/p&gt;
&lt;p&gt;当该特性开启时，MongoDB 会在给定的周期内（目前实现是 1 秒），分配一定数量的「流控票据」，MongoDB 的数据库库写操作需要通过获取「流控票据」来获取全局 IX 锁。当此给定周期内的票据分发完比后，相关操作需要等待下一个周期（下一秒）的票据刷新分发。部份 MongoDB 内部操作可能会忽略该流控机制，以确保 MongoDB 的正常运行。&lt;/p&gt;
&lt;p&gt;接下来我们从算法、配置、源码等几个方面结合来了解一下该流控制机制的实现（本文以 MongoDB 4.2.20 版本的文档及源码为参考）。&lt;/p&gt;
&lt;h2&gt;环境要求&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;因为是 4.2 才引入的，所以要求 MongoDB 版本大于等于 4.2.0，并且 featureCompatibility 也要匹配相应的版本；&lt;/li&gt;
&lt;li&gt;流控机制只在可以接受写入的节点生效（通过指 replica sets 中的 primary 节点）；&lt;/li&gt;
&lt;li&gt;当然，流控开关也是要打开的，默认打开。可以通过 &lt;a href="https://www.mongodb.com/docs/v4.2/reference/parameters/#param.enableFlowControl"&gt;enableFlowControl&lt;/a&gt; 进行开关；&lt;/li&gt;
&lt;li&gt;Read Concern Majority 必须启用，可以参考 &lt;a href="https://www.mongodb.com/docs/v4.2/reference/parameters/#param.enableFlowControl"&gt;replication.enableMajorityReadConcern&lt;/a&gt; 启用；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当以上机制不满足时（亦可理解为流控机制关闭时），MongoDB 总是返回默认的最大数量 &lt;code&gt;_kMaxTickets = 1000 * 1000 * 1000&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;流控机制&lt;/h2&gt;
&lt;p&gt;流控制机制允许每秒获取指定数量的全局 IX 锁。除明确规避流控机制的操作外，全局 IX 锁获取必须首先获取「流控票据」然后才能获取锁。当此给定周期内的票据分发完比后，相关操作需要等待下一个周期（下一秒）的票据刷新分发。MongoDB 通过一个独立的机制每秒刷新一次票数。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mongo/db/storage/flow_control.cpp&lt;/span&gt;

&lt;span class="n"&gt;FlowControl&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FlowControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceContext&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;repl&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ReplicationCoordinator&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;replCoord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;_jobAnchor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getPeriodicRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;makeJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;FlowControlRefresher&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="n"&gt;FlowControlTicketholder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getServiceContext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;refreshTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getNumTickets&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="n"&gt;Seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)});&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;_jobAnchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;每次/秒刷新 tickets 数量时，&lt;code&gt;getNumTickets&lt;/code&gt; 将计算应该允许多少票据以解决 MongoDB 多数提交的滞后问题（复制延迟）。流控机制根据以下因素确定每个周期要补充多少票：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前多数提交的复制延迟大于配置的目标最大复制延迟；&lt;/li&gt;
&lt;li&gt;Secondary 在上一周期应用了多少次操作；&lt;/li&gt;
&lt;li&gt;在上一周期中平均每次操作需要获取多少个 IX 锁；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;触发流控时 ticket 数量计算&lt;/h3&gt;
&lt;p&gt;触发流控的 ticket 数量可以通过如下公式理解：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;base * k ^ ((lag - threshold)/threshold) * fudge factor&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相关变量说明如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;base 为上一周期的操作数（也可以理解为操作数 * 平均每次操作获取的 IX 锁），通过采样获取；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;k 为 MongoDB 触发流控时节流的速率，默认值为 0.5，MongoDB 的可配置参数为 &lt;code&gt;flowControlDecayConstant&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lag 为当前多数提交延迟；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;threshold 为配置的可容忍最达延迟，请特别留意，该值分别由两个参数控制 &lt;code&gt;flowControlTargetLagSeconds&lt;/code&gt; 与 &lt;code&gt;flowControlThresholdLagPercentage&lt;/code&gt; 默认值分别为 10 和 0.5，可以理解为当多数提交延迟大于 5 秒时，即触发流控；&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mongo/db/storage/flow_control.cpp&lt;/span&gt;

&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getThresholdLagMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="n"&gt;gFlowControlThresholdLagPercentage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="w"&gt;                                      &lt;/span&gt;&lt;span class="n"&gt;gFlowControlTargetLagSeconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;fudge 因子：比 1 略小的值，主要是为了控制当 lag 与 threshold 接近时，分配略低于 base 数量的票证。默认值为 0.95，可通过参数 &lt;code&gt;flowControlFudgeFactor&lt;/code&gt; 进行配置，该值要求设置为大于 0， 小于 1，尽量接近 1；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;公式的代码主体如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mongo/db/storage/flow_control.cpp&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;multiplyWithOverflowCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;term1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;term2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;term1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;term2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FlowControl&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;_calculateNewTicketsForLag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lagMillis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;thresholdLagMillis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thresholdLagMillis&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;invariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gFlowControlDecayConstant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// The fudge factor, by default is 0.95. Keeping this value close to one reduces oscillations in&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// an environment where secondaries consistently process operations slower than the primary.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sustainerAppliedPenalty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;sustainerAppliedCount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gFlowControlFudgeFactor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;multiplyWithOverflowCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locksPerOp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sustainerAppliedPenalty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_kMaxTickets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;采样方式&lt;/h4&gt;
&lt;p&gt;采样的频率和数量主要通过如下两个配置参数进行控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flowControlSamplePeriod&lt;/code&gt; 这个参数的命名比较容易让人困惑，会容易误认为是时间，实际理解是每 N 次采样一次，N 即为 &lt;code&gt;flowControlSamplePeriod&lt;/code&gt; 设置的值，值越小的精度越高，所以可能更容易触发流控导致业务降级；&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mongo/db/storage/flow_control.cpp&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;FlowControl::sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="kt"&gt;uint64_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;opsApplied&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_numOpsSinceStartup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_lastSample&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gFlowControlSamplePeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Naively sample once every 1000 or so operations.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flowControlMaxSamples&lt;/code&gt;，内存中保存的样本数，默认值为 1000000， 每个样本数约为 24 bytes大小，保存 100000 样本数约需 24MB 内存；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;采样在 oplog 相关的操作中进行，参考上述 sample 采样逻辑及下述代码，没有发现显式的采样开关，即&lt;strong&gt;无论是否启用流控制，采样机制都会执行&lt;/strong&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;//src/mongo/db/repl/local_oplog_info.cpp&lt;/span&gt;

&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OplogSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LocalOplogInfo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;getNextOpTimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationContext&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;opCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Provide a sample to FlowControl after the `oplogInfo.newOpMutex` is released.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ON_BLOCK_EXIT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;opCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flowControl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FlowControl&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opCtx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flowControl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;flowControl&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;延迟正常（未触发流控）时 ticket 数量计算&lt;/h3&gt;
&lt;p&gt;当多数提交的延迟小于可容忍流控阈值的延迟百分比时（认为服务状态健康），直接将上一期间分配的 ticket 数量做为基数，并增加一个可配置的常量（票据加法），参数名称为 &lt;code&gt;flowControlTicketAdderConstant&lt;/code&gt; 默认值为 1000。总和乘以另一个可配置的常量（票据乘数），最终的值为下期分配的新票数。该乘数配置参数名称为 &lt;code&gt;flowControlTicketMultiplierConstant&lt;/code&gt; 默认值为 1.05，修改配置需要确保大于 1。&lt;/p&gt;
&lt;p&gt;该算法主要是为了确保系统健康时，特别是初始使用了较少的 ticket 时，系统 ticket 数量可以快速得到提升。公式：&lt;code&gt;(lastTargetTicketsPermitted + flowControlTicketAdderConstant) * flowControlTicketMultiplierConstant&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/mongo/db/storage/flow_control.cpp&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;FlowControl::getNumTickets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Date_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isHealthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// The add/multiply technique is used to ensure ticket allocation can ramp up quickly,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// particularly if there were very few tickets to begin with.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;multiplyWithOverflowCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lastTargetTicketsPermitted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
&lt;span class="w"&gt;                                            &lt;/span&gt;&lt;span class="n"&gt;gFlowControlTicketAdderConstant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="n"&gt;gFlowControlTicketMultiplierConstant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="w"&gt;                                        &lt;/span&gt;&lt;span class="n"&gt;kMaxTickets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;配置参数整理&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;enableFlowControl&lt;/td&gt;
&lt;td&gt;流控开关 true/false，默认 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlTargetLagSeconds&lt;/td&gt;
&lt;td&gt;可容忍的多数提交延迟，单位为秒，默认 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlThresholdLagPercentage&lt;/td&gt;
&lt;td&gt;可容忍的多数提交延迟百分比，{ gte: 0.0, lte: 1.0 }，该值乘以 flowControlTargetLagSeconds 为实际可容忍的延迟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlMaxSamples&lt;/td&gt;
&lt;td&gt;最大的保留样本数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlSamplePeriod&lt;/td&gt;
&lt;td&gt;采样周期，注意不是指&lt;strong&gt;时间&lt;/strong&gt;，指：N 次操作采样一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlMinTicketsPerSecond&lt;/td&gt;
&lt;td&gt;最小的 ticket 数量，默认 100。设置过小可能会导致系统需要&lt;strong&gt;预热&lt;/strong&gt;，可以根据系统业务的情况适当调整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlDecayConstant&lt;/td&gt;
&lt;td&gt;MongoDB 触发流控时节流的速率，默认值为 0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlFudgeFactor&lt;/td&gt;
&lt;td&gt;Fudge 因子，默认为 0.95，要求小于 1，但接近 1，&lt;strong&gt;不建议调整&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlTicketAdderConstant&lt;/td&gt;
&lt;td&gt;健康状态时，ticket 加数，以快速增加健康状态时的 ticket 数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flowControlTicketMultiplierConstant&lt;/td&gt;
&lt;td&gt;健康状态时，ticket 乘数，以快速增加健康状态时的 ticket 数量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;需留意&lt;/strong&gt;，MongoDB 启动时，默认的 ticket 数量为 &lt;code&gt;_kMaxTickets = 1000 * 1000 * 1000&lt;/code&gt; 即 10 亿，而不是 &lt;code&gt;flowControlMinTicketsPerSecond&lt;/code&gt; 的值。通过如下命令可以查看当前的票据情况：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;// MongoDB 刚启动时

tmongo40:PRIMARY&amp;gt; db.serverStatus()[&amp;quot;flowControl&amp;quot;].targetRateLimit
1000000000

tmongo40:PRIMARY&amp;gt; db.serverStatus()[&amp;quot;flowControl&amp;quot;]
{
    &amp;quot;enabled&amp;quot; : true,
    &amp;quot;targetRateLimit&amp;quot; : 1000000000,
    &amp;quot;timeAcquiringMicros&amp;quot; : NumberLong(76),
    &amp;quot;locksPerOp&amp;quot; : 0,
    &amp;quot;sustainerRate&amp;quot; : 0,
    &amp;quot;isLagged&amp;quot; : false,
    &amp;quot;isLaggedCount&amp;quot; : 0,
    &amp;quot;isLaggedTimeMicros&amp;quot; : NumberLong(0)
}

// 触发流控后

tmongo40:PRIMARY&amp;gt; db.serverStatus()[&amp;quot;flowControl&amp;quot;]
{
    &amp;quot;enabled&amp;quot; : true,
    &amp;quot;targetRateLimit&amp;quot; : 2015,
    &amp;quot;timeAcquiringMicros&amp;quot; : NumberLong(12064),
    &amp;quot;locksPerOp&amp;quot; : 1,
    &amp;quot;sustainerRate&amp;quot; : 2196,
    &amp;quot;isLagged&amp;quot; : true,
    &amp;quot;isLaggedCount&amp;quot; : 1,
    &amp;quot;isLaggedTimeMicros&amp;quot; : NumberLong(0)
}
tmongo40:PRIMARY&amp;gt; db.serverStatus()[&amp;quot;flowControl&amp;quot;]
{
    &amp;quot;enabled&amp;quot; : true,
    &amp;quot;targetRateLimit&amp;quot; : 1014,
    &amp;quot;timeAcquiringMicros&amp;quot; : NumberLong(38346),
    &amp;quot;locksPerOp&amp;quot; : 1,
    &amp;quot;sustainerRate&amp;quot; : 1924,
    &amp;quot;isLagged&amp;quot; : true,
    &amp;quot;isLaggedCount&amp;quot; : 1,
    &amp;quot;isLaggedTimeMicros&amp;quot; : NumberLong(0)
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;日志&lt;/h2&gt;
&lt;p&gt;触发限流时，可以在 MongoDB 的日志里找到 flowControl 相关的日志。&lt;/p&gt;
&lt;p&gt;&lt;img alt="image-20220609174333655" src="/static/mongodb-flowcontrol-log.png"&gt;&lt;/p&gt;
&lt;h2&gt;其他结论&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;好的限流效果需要根据业务的情况持续调测相关参数，且 MongoDB 的限流算法主节点的处理能力受主节点影响，存在多种不确定因素（如网络），建议谨慎开启。或基于前述情况，建议降低流控敏感度，如放宽对大多数主从提交的延迟阈值容忍，仅在较极端情况下对 MongoDB 触发保护，避免过于敏感反而影响正常业务请求；&lt;/li&gt;
&lt;li&gt;根据某业务的线上表现来看，在 CPU、io 吞吐极低的情况下，MongoDB 默认的参数配置仍频繁的触发限流。因此建议仍如 1；&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;enableFlowControl: https://www.mongodb.com/docs/v4.2/reference/parameters/#param.enableFlowControl&lt;/li&gt;
&lt;li&gt;source code: https://github.com/mongodb/mongo/tree/r4.2.20&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="mongodb"/><category term="flowcontrol"/></entry><entry><title>WireGuard 浅显体验</title><link href="https://www.chenxiaosheng.com/posts/2022-05-15/wireguard-tutorial.html" rel="alternate"/><published>2022-05-15T17:07:00+08:00</published><updated>2022-05-15T17:07:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2022-05-15:/posts/2022-05-15/wireguard-tutorial.html</id><summary type="html">&lt;p&gt;WireGuard® 是一个极其简单、快速且现代的 VPN，它利用了最先进的加密技术。目的是提供一种更快、更易配置、更精简的通用 VPN。最初是为 Linux 开发（并且已经合并至 Linux Kernel），底层是 VPN，现在支持 Windows、macOS、BSD、iOS、Android 等跨平台。目前还处于活跃的开发当中，但仍不失为一个简易友好，且性能、安全性和兼容性都很棒的 VPN 解决方案。&lt;/p&gt;</summary><content type="html">&lt;p&gt;WireGuard® 是一个极其简单、快速且现代的 VPN，它利用了最先进的加密技术。目的是提供一种更快、更易配置、更精简的通用 VPN。最初是为 Linux 开发（并且已经合并至 Linux Kernel, &amp;gt;5.6），底层是 VPN，现在支持 Windows、macOS、BSD、iOS、Android 等跨平台。目前还处于活跃的开发当中，但仍不失为一个简易友好，且性能、安全性和兼容性都很棒的 VPN 解决方案。&lt;/p&gt;
&lt;p&gt;Linus Torvalds 给予 WireGuard 很高的评价（work of art）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Can I just once again state my love for it and hope it gets merged soon? Maybe the code isn't perfect, but I've skimmed it, and compared to the horrors that are OpenVPN and IPSec, it's a work of art.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装篇&lt;/h2&gt;
&lt;p&gt;具体的安装可参考 &lt;a href="https://www.wireguard.com/install/"&gt;Installation&lt;/a&gt;，笔者使用的操作系统为 Debian bullseye，官方源已经自带，仅需要 root 用户 &lt;code&gt;apt-get install wireguard&lt;/code&gt; 一条命令搞定。&lt;/p&gt;
&lt;h2&gt;配置篇&lt;/h2&gt;
&lt;p&gt;安装过程比较简单，没有打脸官方所宣传的简单，具体也可以参考官方文档 &lt;a href="https://www.wireguard.com/quickstart/"&gt;QuickStart&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;场景一：Peer-To-Peer，两台服务器可互通&lt;/h3&gt;
&lt;p&gt;笔者这里有两台机器，其中服务器 A 为腾讯云的机器，系统内看到的地址为内网地址，在腾讯云配置了公网 eip。&lt;/p&gt;
&lt;p&gt;服务器 B 为一台公网的机器，有直接的公网 IP。（这里留下了一个坑点）&lt;/p&gt;
&lt;h4&gt;生成 WireGuard 公私钥&lt;/h4&gt;
&lt;p&gt;以下命令需要在服务 A 和 服务器 B 执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# 生成私钥&lt;/span&gt;
wg&lt;span class="w"&gt; &lt;/span&gt;genkey&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;wg.private
&lt;span class="c1"&gt;# 生成公钥&lt;/span&gt;
wg&lt;span class="w"&gt; &lt;/span&gt;pubkey&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;wg.private&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;wg.pubkey
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;生成 WireGuard 配置&lt;/h4&gt;
&lt;p&gt;分别在两台机器上创建 &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;服务器 A 腾讯云机器配置如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;span class="n"&gt;ListenPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;57259&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# UDP 端口，根据自己的需要设定即可&lt;/span&gt;
&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# WireGuard 网络设备 IP 地址，这里使用 172.16.1.1&lt;/span&gt;
&lt;span class="n"&gt;PrivateKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;uJj+x/... # 在这台服务器上生成的 wg.private 文件内容&lt;/span&gt;

&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;span class="n"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;W&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="n"&gt;b9l&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 在服务器 B 生成的 wg.pubkey 文件内容&lt;/span&gt;
&lt;span class="n"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;2&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 服务器 B WireGuard 网络设备 IP 地址，也可以开放 /24 网段&lt;/span&gt;
&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;108&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="n"&gt;34101&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 服务器 B 的公网 IP 地址及 ListenPort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;服务器 B 公网机器配置如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;span class="n"&gt;ListenPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;34101&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# UDP 端口，根据自己的需要设定即可&lt;/span&gt;
&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;2&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# WireGuard 网络设备 IP 地址，这里使用 172.16.1.2&lt;/span&gt;
&lt;span class="n"&gt;PrivateKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kETG&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 在这台服务器上生成的 wg.private 文件内容&lt;/span&gt;

&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;span class="n"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hTfx&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 在服务器 A 生成的 wg.pubkey 文件内容&lt;/span&gt;
&lt;span class="n"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 参考服务器 A，这里开放 /24 网段&lt;/span&gt;
&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="n"&gt;57259&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 服务器 A 的公网 IP 地址及 ListenPort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;之后分别在两台机器上使用命令 &lt;code&gt;wg-quick up wg0&lt;/code&gt; 后隧道就建议好了，可以通过 ping 来检查。&lt;/p&gt;
&lt;p&gt;到这里还比较顺利，确实非常简单。笔者就转移注意力去开展其他的工作了，过了几分钟回来发现&lt;strong&gt;网络断开&lt;/strong&gt;了，从服务器 A 腾讯云机器单向无法 ping 通 B 服务器，此时从 B 服务器 ping A 服务器后网络恢复（第一个问题）。&lt;/p&gt;
&lt;p&gt;为什么呢？&lt;/p&gt;
&lt;p&gt;怀疑是腾讯云服务器系统内只能看到内网 IP 的问题，毕竟这里是通过网关转发，在我之前其他的网络试验中已经多次踩中这个坑。继续往下翻 &lt;a href="https://www.wireguard.com/quickstart/"&gt;QuickStart&lt;/a&gt; 找到答案：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;if a peer behind NAT or a firewall wishes to receive incoming packets, he must keep the NAT/firewall mapping valid, by periodically sending keepalive packets. This is called &lt;em&gt;persistent keepalives&lt;/em&gt;. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解法也比较简单，在上面两台机器的 wg0.conf 配置文件中的 &lt;code&gt;[Peer]&lt;/code&gt; 部份分别加上 keepalive 相关的配置即可（默认是 0，也就是不开启，这里我们设置为 25 秒）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PersistentKeepalive = 25&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;场景二：能不能内网穿透呢？&lt;/h3&gt;
&lt;p&gt;再找了一台纯内网的服务器 C，该服务器可以访问外网，但外部网络无法访问该服务器。且不清楚该服务器 C 的外网出口 IP 是什么。&lt;/p&gt;
&lt;h4&gt;配置内网服务器&lt;/h4&gt;
&lt;p&gt;参考场景一的内容进行 WireGuard 安装和配置，假设服务器 C 的 WiredGuard 设备地址设置为 172.16.1.3，则配置如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;3&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;24&lt;/span&gt;
&lt;span class="n"&gt;PrivateKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aHebd...&lt;/span&gt;

&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;span class="n"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hTfx&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;24&lt;/span&gt;
&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xx&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="n"&gt;57259&lt;/span&gt;
&lt;span class="n"&gt;PersistentKeepalive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 处于 NAT 环境的设备需要加上 keepalive 配置&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;主要差异：&lt;/strong&gt; &lt;code&gt;ListenPort&lt;/code&gt; 的配置取消了，因为这台机器纯内网，我们无法判定最终映射的公网 IP 和端口分别是什么。上述配置保存后，通过 &lt;code&gt;wg-quick up wg0&lt;/code&gt; 启动设备即可。&lt;/p&gt;
&lt;p&gt;在服务器 A 腾讯云的机器上 &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; 增加该服务器 C 的 peer 配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;span class="n"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hESH+Md&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 服务器 C 的 wg.pubkey 内容&lt;/span&gt;
&lt;span class="n"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="n"&gt;24&lt;/span&gt;
&lt;span class="n"&gt;PersistentKeepalive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;主要差异：&lt;/strong&gt; &lt;code&gt;Endpoint&lt;/code&gt; 配置取消了，毕竟 A 不知道 C 用的会是哪个公网 IP 和端口。保存配置后，分别执行 &lt;code&gt;wg-quick down wg0&lt;/code&gt; 和 &lt;code&gt;wg-quick up wg0&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;本以为到这里已经完成配置了，结果第二个问题来了。两台服务器 ping 不通。再次检查配置确认无误，陷入深深的思考~&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决办法&lt;/strong&gt;：网络不决问防火墙。想到这里因为服务器 C 是在内网，WireGuard 能互通的前提肯定是服务器 C 要先访问服务器 A 的 ListenPort，检查防火墙配置，服务器 A 的 57259 UDP 端口没有对外开放。通过以下命令开放 57259 端口访问：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;iptables -I INPUT 1 -p udp -m udp --dport 57259 -j ACCEPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;端口开放后，两台机器之间的互联果然正常了。&lt;/p&gt;
&lt;h2&gt;一些结论&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;如果 wg 两端设备都有公网地址可达，分别配置 Address 和 ListenPort 后，两台机器隧道可以互通，通常无需特殊配置；&lt;/li&gt;
&lt;li&gt;如果设备位于防火墙或网关后面，需要打开 keepalive 配置；&lt;/li&gt;
&lt;li&gt;内网穿透时，做为网关的机器（公网那台）需要开放 ListenPort 的防火墙访问；&lt;/li&gt;
&lt;li&gt;根据需要，你可能需要打开设备的数据包转发（net.ipv4.ip_forward = 1）和网络地址转换功能（NAT/MASQUERADE），这里不展开；&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="wireguard"/><category term="vpn"/></entry><entry><title>Debian 指定内核启动</title><link href="https://www.chenxiaosheng.com/posts/2022-05-07/debian-switch-kernel-boot.html" rel="alternate"/><published>2022-05-07T12:59:00+08:00</published><updated>2022-05-07T12:59:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2022-05-07:/posts/2022-05-07/debian-switch-kernel-boot.html</id><summary type="html">&lt;p&gt;升级完内核发现有一些问题需要回退，但是因为不能直接接触机器终端，需要通过 grub 配置指定内核版本重新启动。&lt;/br&gt;&lt;center&gt;&lt;img alt="" src="/static/debian-bullseye.png"&gt;&lt;/center&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;升级完内核发现有一些问题需要回退，但是因为不能直接接触机器终端，需要通过 grub 配置指定内核版本重新启动。&lt;/p&gt;
&lt;p&gt;操作系统：Debian 11&lt;/p&gt;
&lt;p&gt;涉及文件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;/etc/default/grub&lt;/li&gt;
&lt;li&gt;/boot/grub/grub.cfg&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;操作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过命令 &lt;code&gt;grep -e 'menuentry ' -e 'subm' /boot/grub/grub.cfg | awk -F'--' '{print $1}'&lt;/code&gt; 找到需要启动的内核菜单名称：&lt;/p&gt;
&lt;p&gt;&lt;img alt="image-20220507113012323" src="/static/debian-grub-cfg.png"&gt;&lt;/p&gt;
&lt;p&gt;假设上图中的 &lt;code&gt;Debian GNU/Linux, with Linux 5.10.0-13-amd64&lt;/code&gt; 是我们期望使用的内核，则需要引用图示 1 和 2 修改 &lt;code&gt;/etc/default/grub&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;/etc/default/grub&lt;/code&gt; 中的 &lt;code&gt;GRUB_DEFAULT=0&lt;/code&gt; 为如下值：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GRUB_DEFAULT="Advanced options for Debian GNU/Linux&amp;gt;Debian GNU/Linux, with Linux 5.10.0-13-amd64"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行命令 &lt;code&gt;update-grub&lt;/code&gt;，成功后重启即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="debian"/><category term="grub"/><category term="kernel"/></entry><entry><title>当 cgroups 碰上超线程</title><link href="https://www.chenxiaosheng.com/posts/2022-03-29/cgroups-hyperthreading.html" rel="alternate"/><published>2022-03-29T11:24:00+08:00</published><updated>2022-03-29T11:24:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2022-03-29:/posts/2022-03-29/cgroups-hyperthreading.html</id><summary type="html">&lt;p&gt;cgroups 是 Linux 内核提供的一种可以限制单个进程或者多个进程所使用资源的机制，可以对 cpu，内存等资源实现精细化的控制。当英特尔® 超线程技术处于激活状态时，CPU 会在每个物理内核上公开两个执行上下文。这意味着，一个物理内核现在就像两个“逻辑内核”一样。这个时候使用 cgroups 进行 cpu 资源隔离会出现什么情况呢？ &lt;/br&gt;&lt;center&gt;&lt;img alt="" src="/static/cpu_arch_ex2.png"&gt;&lt;/center&gt;&lt;/p&gt;</summary><content type="html">&lt;h2&gt;超线程&lt;/h2&gt;
&lt;p&gt;当英特尔® 超线程技术处于激活状态时，CPU 会在每个物理内核上公开两个执行上下文。这意味着，一个物理内核现在就像两个“逻辑内核”一样。超线程技术就是利用特殊的硬件指令，把两个逻辑内核模拟成两个物理芯片，让单个处理器都能使用线程级并行计算，进而兼容多线程操作系统和软件，&lt;strong&gt;减少了 CPU 的闲置时间&lt;/strong&gt;，提高的 CPU 的运行效率。&lt;/p&gt;
&lt;p&gt;虽然采用超线程技术能同时执行两个线程，但它并不象两个真正的 CPU 那样，每个 CPU 都具有独立的资源。当两个线程都同时需要某一个资源时，其中一个要暂时停止，并让出资源，直到这些资源闲置后才能继续。因此&lt;strong&gt;超线程的性能并不等于两颗 CPU 的性能&lt;/strong&gt;。根据 Intel 官方文档反馈，&lt;strong&gt;理论情况下在服务器应用程序中可提升 30% 的性能&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;怎么确认机器是否开启超线程&lt;/h3&gt;
&lt;p&gt;通常情况下，我们只需要通过 &lt;code&gt;/proc/cpuinfo&lt;/code&gt; 即可确认机器是否开启超线程。当相同的 &lt;code&gt;physical id&lt;/code&gt; 和 &lt;code&gt;core id&lt;/code&gt; 出现两次时，即表示机器开启了超线程。&lt;/p&gt;
&lt;center&gt;![image-20210830174503295](/static/cpus_hyperthreading.png)&lt;/center&gt;

&lt;center&gt;图 1. /proc/cpuinfo&lt;/center&gt;

&lt;p&gt;以图 1 为例，在 &lt;code&gt;/proc/cpuinfo&lt;/code&gt; 中共可以找到 48 个 CPU，根据 &lt;code&gt;physical id&lt;/code&gt; 和 &lt;code&gt;core id&lt;/code&gt; 的组合，我们可以画出 CPU 架构图如下图 2。其中如 0-24/2-26/../23-47 表示相同 core id，启用了超线程后操作系统看到的逻辑 CPU 对。&lt;/p&gt;
&lt;center&gt;![image-20210830175849529](/static/cpu_arch.png)&lt;/center&gt;

&lt;center&gt;图 2. 物理 CPU 与逻辑 CPU 关系&lt;/center&gt;

&lt;h2&gt;CGROUPS&lt;/h2&gt;
&lt;p&gt;cgroups 是 Linux 内核提供的一种可以限制单个进程或者多个进程所使用资源的机制，可以对 cpu，内存等资源实现精细化的控制，目前市面上相当流行的轻量级容器 Docker 就使用了 cgroups 提供的资源限制能力来完成 cpu，内存等部分的资源控制。&lt;/p&gt;
&lt;p&gt;笔者目前主要的工作内容是为公司提供数据库私有云服务，为了有效充分的利用服务器资源，我们会利用 cgroups 对资源进行隔离，根据业务需求提供不等规模的机器资源。其中对 cpu 的隔离是通过 &lt;code&gt;cpuset.cpus&lt;/code&gt; 来进行的，通常情况下，我们会保留头部数个 cpu，如 CPU 0-CPU 5 做为系统保留 CPU。其它 CPU 则按顺序，如将 &lt;code&gt;6,7,8&lt;/code&gt;、&lt;code&gt;9,10,11,12，13，14&lt;/code&gt; 分配给两个不同的实例。&lt;/p&gt;
&lt;p&gt;为了业务的可用性考虑，我们的 CPU 分配还是比较充份的，通常 50% 是我们的一个相对安全水位。&lt;strong&gt;所以大多数情况下，上面的按序分配并不会带来什么问题&lt;/strong&gt;。本文主要是探讨极端情况下上述 cpu 分配方式可能造成的问题。&lt;/p&gt;
&lt;h3&gt;问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;由于共用了物理 CPU，容器之间可能存在 CPU 争抢：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以数据库为例，明明 A 实例的负载并不高，但有时候会忽然「卡」那么一下或者刷个慢查询，但业务并没有发生变化，DBA 也无法追踪到不合理的 SQL 或索引缺失等情况&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;center&gt;&lt;img alt="image-20210901144651162" src="/static/cpu_arch_ex1.png"&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;&lt;center&gt;图 3. 逻辑 CPU 分配案例一&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;以上图 3 为例，假设我们给 Container A 绑定了 2, 3, 4 共 3 个 CPU，给 Container B 绑定了 26, 27, 28 共三个 CPU，可以看到 2-26, 4-28, 3-27 分别共享了一个物理核心（core id 相同）。根据对超线程的理解：&lt;code&gt;当两个线程都同时需要某一个资源时，其中一个要暂时停止，并让出资源，直到这些资源闲置后才能继续&lt;/code&gt;，假设 Container A 的负载较高，则 Container B 是有可能受到影响并导致出现 cpu 等待的情况的，反映到业务，则是：&lt;strong&gt;不知道为什么抽风了&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高负载情况下，隔离资源的实际性能表示与标称值不符：&lt;/p&gt;
&lt;p&gt;&lt;center&gt;&lt;img alt="image-20210901150436639" src="/static/cpu_arch_ex2.png"&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;&lt;center&gt;图 4. 逻辑 CPU 分配案例二&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;以上图 4 为例，假设 0, 1, 2, 45, 46, 47 为保留 CPU（不明确分配实际业务），其它 CPU 按顺序每 6 个一组分配至不同的业务容器，我们可以猜测分配到 21, 22, 23, 24, 25, 26 这 6 个 CPU 的容器会有较少的超线程资源争抢，预计会有更好的性能表现。&lt;/p&gt;
&lt;p&gt;下图为某业务的实际测试图，在施加具备让容器满载压力情况下，&lt;strong&gt;具备较少超线程争抢的容器拥有较好的性能&lt;/strong&gt;。这在大多数情况下可能并不是好事：&lt;/p&gt;
&lt;p&gt;&lt;center&gt;&lt;img alt="image-20210901152629454" src="/static/cpu_load_bench_1.png"&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;&lt;center&gt;图5. 某业务使用“图 4. 逻辑 CPU 分配案例二”压力测试情况&lt;/center&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;个别节点拥有较高性能可能造成业务上线压力评估的数据失真；&lt;/li&gt;
&lt;li&gt;对于分布式场景，各节点的能力承载存在明显不一致可能造成分布式系统的降级甚至雪崩；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述两个问题本质是一样的，都是因为超线程的争取造成的压力不一致、资源抢占。虽然大多数情况下通过资源的预留减少了上述问题发生的概率，但当问题发生时，&lt;strong&gt;一线技术人员极难排查，只能以疑难问题或偶尔抽风等玄学又无奈的口径进行解释&lt;/strong&gt;。对技术人员的业务技能储备有非常高的要求，需要对硬件架构、操作系统、业务架构等多方面都有较清晰的了解。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;推荐分配方案&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;大多数情况下，如果可以预见系统不会出现满载（如达到 70% 或以上），或只有个别实例会出现满载的情况按系统内 CPU 顺序直接分配绑定并不会带来明显问题。这种方式不管是理解还是运维实现，都是最直接和可快速落地的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关技术人员需了解此知识点，当出现资源抢占问题时可以有效进行分析排查。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果对于资源隔离、容器间相互影响有较高质量要求的情况下，建议 CPU 以偶数倍进行绑定，并确保同一个 core id 下的两个逻辑 CPU（超线程 HT）被分配绑定至同一个容器/实例。这种方案下，需要对 CPU 的分配进行合理编排调度。（下图以 4 个 CPU 为一组，确保每一个物理核心超线程后的两个逻辑 CPU 被分配至同一组）&lt;/p&gt;
&lt;p&gt;&lt;center&gt;&lt;img alt="image-20210901155115582" src="/static/cpu_arch_ex3.png"&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;&lt;center&gt;图 6. 逻辑 CPU 分配案例三&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;&lt;center&gt;&lt;img alt="image-20210901162859160" src="/static/cpu_load_bench_2.png"&gt;&lt;/center&gt;&lt;/p&gt;
&lt;p&gt;&lt;center&gt;图7. 某业务使用“图 6. 逻辑 CPU 分配案例三”压例测试情况&lt;/center&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于 CPU 时间有更细粒度或更精准控制的场景可以考虑通过 CPU 时间分片进行限制，可参考 &lt;a href="https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt"&gt;CFS Bandwidth Control&lt;/a&gt;，这里不展开讨论。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.intel.cn/content/www/cn/zh/gaming/resources/hyper-threading.html"&gt;什么是超线程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tech.meituan.com/2015/03/31/cgroups.html"&gt;Linux资源管理之cgroups简介&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="cgroups"/></entry><entry><title>速率限制算法：固定窗口与滑动窗口</title><link href="https://www.chenxiaosheng.com/posts/2022-03-28/rate-limit_fixed-window_sliding-window.html" rel="alternate"/><published>2022-03-28T15:59:00+08:00</published><updated>2022-03-28T15:59:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2022-03-28:/posts/2022-03-28/rate-limit_fixed-window_sliding-window.html</id><summary type="html">&lt;p&gt;在生产环境中，我们经常需要通过一定的办法方案来保护我们的系统（比如 API）免受无意的或恶意的过度使用。在极端情况下，我们还需要对系统进行适度的降级，以确保系统的可用性。速率限制（rate limit）有助于自动化该过程（有时候也会称为流量控制）。&lt;/br&gt;&lt;img alt="image-20220328154213560" src="/static/sliding_window.png"&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;在生产环境中，我们经常需要通过一定的办法方案来保护我们的系统（比如 API）免受无意的或恶意的过度使用。在极端情况下，我们还需要对系统进行适度的降级，以确保系统的可用性。速率限制（rate limit）有助于自动化该过程（有时候也会称为流量控制）。&lt;/p&gt;
&lt;p&gt;这里我们主要介绍固定窗口计数算法和滑动窗口计数算法。&lt;/p&gt;
&lt;h2&gt;固定窗口 Fixed Window&lt;/h2&gt;
&lt;p&gt;固定窗口算法比较好理解，大致的概念如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;时间被切分成固定大小的窗口，比如 60 秒或 5 分钟等；&lt;/li&gt;
&lt;li&gt;每个时间窗口允许通过的请求阈值固定，假定为 N；&lt;/li&gt;
&lt;li&gt;每一次请求进来，对当前时间窗口的计数器加 1， 假设为 n；&lt;/li&gt;
&lt;li&gt;当当前时间窗口的请求计数超过阈值时（n&amp;gt;N），丢弃当前时间窗口进来的所有请求；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整体的理解可以参考下述图例 1（固定时间窗口 1 秒，每秒钟允许通过 3 个请求）：&lt;/p&gt;
&lt;p&gt;&lt;img alt="image-20220328142057442" src="/static/fixed_window.png"&gt;&lt;/p&gt;
&lt;p style="text-align:center"&gt; 图 1. 固定窗口算法图例&lt;/p&gt;

&lt;p&gt;看起来好像没有什么毛病，在允许的时间窗口内只允许通过预设的请求。为什么还会需要滑动窗口算法，我们看一下固定窗口算法的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以上图为例，假定  14:00:02 秒的 3 个请求是在后 500ms 到达，14:00:03 分的 3 个请求是在前 500ms 到达，实际上一秒内可能通过了超过预设的 3 个请求，系统被击穿（参考下图 2）；&lt;/li&gt;
&lt;li&gt;如果这是一个高并发系统，那么有可能存在大量请求等待时间窗口重置的情况，还是以上图为例，一切换新的时间窗口，系统可能马上在短时间内被击穿不可用（踩踏）；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="图2" src="/static/fixed_window_cons.png"&gt;&lt;/p&gt;
&lt;p style="text-align:center"&gt; 图 2. 固定窗口算法问题&lt;/p&gt;

&lt;h2&gt;滑动窗口 Sliding Window&lt;/h2&gt;
&lt;p&gt;为了解决固定窗口在时间窗口切换边界的流量尖峰问题，引入了滑动窗口计数算法。该算法在固定窗口的基础上，将固定窗口进行更细粒度的切分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将时间划分为多个固定区间，如 200ms；&lt;/li&gt;
&lt;li&gt;在每个区间内每有一次请求则计数加 1，并维持固定的时间窗口，如固定时间窗口为 1 秒，则占用 5 个时间区间；&lt;/li&gt;
&lt;li&gt;每经过一个区间的时间，抛弃最早的区间，并纳入最新的时间区间；&lt;/li&gt;
&lt;li&gt;如果当前时间窗口的请求数量超过了预设限制，则本时间窗口内的后续请求都会被丢弃；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;基本算法通过下图 3 概要说明，可以直观发现滑动窗口计算方法可以有效避免窗口切换的边界流量尖峰及踩踏问题。并且在我们需要提高流控精度的时候，可以通过划分更细的时间区间来实现。&lt;/p&gt;
&lt;p&gt;&lt;img alt="image-20220328154213560" src="/static/sliding_window.png"&gt;&lt;/p&gt;
&lt;p style="text-align:center"&gt; 图 3. 滑动窗口算法图例&lt;/p&gt;

&lt;p&gt;注：算法可以进一步简化，假设当前时间窗口已经过去 25%，目前的请求数为CurrentN，上一次时间窗口共接受 PreN 次请求（权重为 100%-25%=75%），如果 &lt;code&gt;(PreN * 0.75 + CurrentN) &amp;gt; 预设请求阈值&lt;/code&gt; 则丢弃本次请求，反之放行本次请求。&lt;/p&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;[How to Design a Scalable Rate Limiting Algorithm with Kong API&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="rate limit"/></entry><entry><title>记一次 TiDB 时区设置异常问题排查</title><link href="https://www.chenxiaosheng.com/posts/2019-10-12/tidb-time_zone-settings.html" rel="alternate"/><published>2019-10-12T22:27:00+08:00</published><updated>2019-10-12T22:27:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2019-10-12:/posts/2019-10-12/tidb-time_zone-settings.html</id><summary type="html">&lt;p&gt;两个配置一模一样的 TiDB 集群，在执行 &lt;code&gt;SELECT NOW();&lt;/code&gt; 操作的时候出现了不一样的结果，A 集群的时间与系统时间一致，B 集群的时间比系统时间少了 8 小时。OS、TiDB 配置是统一管理的，究竟是什么原因导致的呢？&lt;/p&gt;</summary><content type="html">&lt;p&gt;收到同事的反馈，两个配置一模一样的 TiDB 集群，在执行 &lt;code&gt;SELECT NOW();&lt;/code&gt; 操作的时候出现了不一样的结果，A 集群的时间与系统时间一致，B 集群的时间比系统时间少了 8 小时。&lt;/p&gt;
&lt;p&gt;&lt;img alt="请在这里输入图片描述" src="/static/20191012150952.png"&gt;&lt;/p&gt;
&lt;p&gt;由于我们的 OS、TiDB 配置是统一管理的，所以我并不怀疑是因为两个集群当前配置不一样导致的。第一反应肯定是因为在 TiDB 启动之前用了错误的时区（UTC），然后 TiDB 引用了错误的时区导致的这个时间问题。根据常规经验，重启一下 TiDB 进程问题应该能解决。&lt;/p&gt;
&lt;p&gt;然而这一次失望了，重启 TiDB 之后，并没有发生什么变化。跟同事要了服务器信息，直接登录到机器上看一下吧。以下是整体的排查过程。&lt;/p&gt;
&lt;h3&gt;先确认是不是，再查为什么&lt;/h3&gt;
&lt;p&gt;首先还是确认是不是有这个问题（再研究为什么），根据&lt;a href="https://pingcap.com/docs-cn/v3.0/how-to/configure/time-zone/#%E6%97%B6%E5%8C%BA%E6%94%AF%E6%8C%81"&gt;官方文档&lt;/a&gt;，检查了 &lt;code&gt;time_zone&lt;/code&gt; 信息，并没有发现异常，有问题的集群和没问题的集群配置也是完全一样的。&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mysql&amp;gt; SELECT @@global.time_zone, @@session.time_zone;
+--------------------+---------------------+
| @@global.time_zone | @@session.time_zone |
+--------------------+---------------------+
| SYSTEM             | SYSTEM              |
+--------------------+---------------------+
1 row in set (0.01 sec)

mysql&amp;gt; SHOW GLOBAL VARIABLES LIKE &amp;#39;%time_zone&amp;#39;;
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | CST    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.00 sec)

mysql&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
虽然这里没有发现明显问题，但我们还是发现了一个疑点：我们的服务器，不可能会有 &lt;code&gt;CST&lt;/code&gt; 这个时区设置（我们用的是 HKT），但 &lt;code&gt;CST&lt;/code&gt; 这个很明显不是问题的原因。&lt;/p&gt;
&lt;h3&gt;日志是一定要看的&lt;/h3&gt;
&lt;p&gt;几乎可以这么说，日志就是为了排查问题而诞生的，任何不能马上判断的问题，都应该去翻一下日志，对服务的健康情况进行判断。因为是时区相关的问题，所以直接对 TiDB 的日志做了一个 &lt;code&gt;fgrep -i timezone tidb.log&lt;/code&gt; 的操作，没想到真的发现了错误日志，毫无波折，就是这么惊喜。（TiDB 日志打印出来是完整一行的，以下内容我稍微格式化了一下，转化为真正的换行）：&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[019/10/09 16:21:42.111 +08:00] [ERROR] [time.go:81] [&amp;quot;locate timezone files failed&amp;quot;] [] [stack=&amp;quot;github.com/pingcap/tidb/util/timeutil.InferSystemTZ
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/util/timeutil/time.go:81
github.com/pingcap/tidb/session.writeSystemTZ
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/session/bootstrap.go:786
github.com/pingcap/tidb/session.doDMLWorks
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/session/bootstrap.go:935
github.com/pingcap/tidb/session.bootstrap
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/session/bootstrap.go:290
github.com/pingcap/tidb/session.runInBootstrapSession
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/session/session.go:1541
github.com/pingcap/tidb/session.BootstrapSession
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/session/session.go:1460
main.createStoreAndDomain
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/tidb-server/main.go:205
main.main
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/tidb-server/main.go:171
runtime.main
/usr/local/go/src/runtime/proc.go:200&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
几乎可以直接肯定是因为这个报错导致的问题了，但还是需要靠撸代码来证明一下的。&lt;/p&gt;
&lt;h3&gt;撸代码&lt;/h3&gt;
&lt;p&gt;根据上述的错误日志，可以知道最终实际执行出错的函数在 &lt;code&gt;util/timeutil/time.go&lt;/code&gt; 的 &lt;code&gt;InferSystemTZ&lt;/code&gt; 函数里，我们来看下这个函数做了什么：&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// InferSystemTZ reads system timezone from `TZ`, the path of the soft link of `/etc/localtime`. If both of them are failed, system timezone will be set to `UTC`.&lt;/span&gt;
&lt;span class="c1"&gt;// It is exported because we need to use it during bootstap stage. And it should be only used at that stage.&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;InferSystemTZ&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// consult $TZ to find the time zone to use.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// no $TZ means use the system default /etc/localtime.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// $TZ=&amp;quot;&amp;quot; means use UTC.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// $TZ=&amp;quot;foo&amp;quot; means use /usr/share/zoneinfo/foo.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;syscall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;TZ&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EvalSymlinks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/etc/localtime&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferTZNameFromFileName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;logutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;infer timezone failed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;logutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;locate timezone files failed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zoneSources&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
可以看到，TiDB 设置时区的逻辑是首先获取操作系统的 TZ 变量，再从 &lt;code&gt;/etc/localtime&lt;/code&gt; 的实际软链地址提取，最后 fallback 为 UTC。这个逻辑还是与&lt;a href="https://pingcap.com/docs-cn/v3.0/how-to/configure/time-zone/#%E6%97%B6%E5%8C%BA%E6%94%AF%E6%8C%81"&gt;官方文档&lt;/a&gt;的描述一致的。但是我们的机器上肯定是有 &lt;code&gt;/etc/localtime&lt;/code&gt; 文件的，文件肯定也是正确的，为什么会失败呢？再来看一下上述代码里最终分析  &lt;code&gt;/etc/localtime&lt;/code&gt; 的函数 &lt;code&gt;inferTZNameFromFileName&lt;/code&gt; 都做了什么？&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// inferTZNameFromFileName gets IANA timezone name from zoneinfo path.&lt;/span&gt;
&lt;span class="c1"&gt;// TODO: It will be refined later. This is just a quick fix.&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferTZNameFromFileName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// phase1 only support read /etc/localtime which is a softlink to zoneinfo file&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;substr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;zoneinfo&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// macOs MoJave changes the sofe link of /etc/localtime from&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// &amp;quot;/var/db/timezone/tz/2018e.1.0/zoneinfo/Asia/Shanghai&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// to &amp;quot;/usr/share/zoneinfo.default/Asia/Shanghai&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;substrMojave&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;zoneinfo.default&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;substrMojave&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;substrMojave&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;path %s is not supported&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
代码的注释里明确写了&lt;strong&gt;只支持 /etc/localtime 为软链接&lt;/strong&gt;，代码的逻辑也是通过截取 &lt;code&gt;/etc/localtime&lt;/code&gt; 软链指向的具体文件路径来获得时区信息。看一下我们服务器上的这个文件：&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ ls -alh /etc/localtime
-rw-r--r-- 1 root root 1.2K 10月 12 10:11 /etc/localtime
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
果然是一个文件而不是一个软链接。这下又带来了两个疑问：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果我改成软链再重启 TiDB 能不能解决问题呢？实践证明，不能解决。这里不再赘述。&lt;/li&gt;
&lt;li&gt;函数逻辑很明确是 fallback 了 UTC，根据 &lt;code&gt;SELECT NOW();&lt;/code&gt; 早了 8 小时也可以确认是 UTC，可为什么 &lt;code&gt;system_time_zone&lt;/code&gt; 是 CST 呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;看到的（CST）和实际的（UTC）不一样？&lt;/h4&gt;
&lt;p&gt;TiDB 在拿不到正确的时区，回落至 UTC 时，最终 UTC 是怎么设置的呢？我们再来看一下上面的错误日志：&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;github.com/pingcap/tidb/session.writeSystemTZ
/home/jenkins/workspace/release_tidb_3.0/go/src/github.com/pingcap/tidb/session/bootstrap.go:786
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
在 &lt;code&gt;session/bootstrap.go&lt;/code&gt; 我们找到：&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// writeSystemTZ writes system timezone info into mysql.tidb&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;writeSystemTZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`INSERT HIGH_PRIORITY INTO %s.%s VALUES (&amp;quot;%s&amp;quot;, &amp;quot;%s&amp;quot;, &amp;quot;TiDB Global System Timezone.&amp;quot;) ON DUPLICATE KEY UPDATE VARIABLE_VALUE=&amp;quot;%s&amp;quot;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SystemDB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TiDBTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tidbSystemTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timeutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InferSystemTZ&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;timeutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InferSystemTZ&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;mustExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
TiDB 首次启动时，拿到时区信息后，将时区信息写到了 mysql.SystemDB.mysql.TiDBTable，也就是 mysql.tidb，其中 &lt;code&gt;VARIABLE_NAME&lt;/code&gt; 为 &lt;code&gt;tidbSystemTZ&lt;/code&gt;，也即 &lt;code&gt;system_tz&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// The variable name in mysql.tidb table and it will be used when we want to know&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// system timezone.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;tidbSystemTZ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;system_tz&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
继续直接登录机器看一下这张参数的内容在两个集群中有什么不一样： &lt;/p&gt;
&lt;p&gt;&lt;img alt="请在这里输入图片描述" src="/static/20191012164104.png"&gt;&lt;/p&gt;
&lt;p&gt;可以看到，确实是设置了 UTC，那么也可以非常明确是由于这个值引起的了。经过测试，可以通过如下两种方式解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更新 mysql.tidb 中 system_tz 为正确的时区，如我们这里为 HKT，并重启 tidb-server 进程后，问题可以得到解决。&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据&lt;a href="https://pingcap.com/docs-cn/v3.0/how-to/configure/time-zone/#%E6%97%B6%E5%8C%BA%E6%94%AF%E6%8C%81"&gt;官方文档&lt;/a&gt;设置 &lt;code&gt;time_zone&lt;/code&gt; 变量，问题可以得到解决。如果有需要，可以把 &lt;code&gt;system_tz&lt;/code&gt; 也修改了，tidb-server 不会自行进行调整：&lt;/p&gt;
&lt;p&gt;&lt;img alt="请在这里输入图片描述" src="/static/20191012165413.png"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;为什么&lt;/h3&gt;
&lt;p&gt;到这里，我们可以很明确 TiDB 参数这里导致时间不对的情况了。但是还有几个疑问还没有得到解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为什么同样的配置，有一个集群是正常的，一个集群又用了 UTC 呢？&lt;/p&gt;
&lt;p&gt;这个情况主要是因为我们的机器有一个初始化的过程，在没有完成初始化的时候，&lt;code&gt;/etc/localtime&lt;/code&gt; 确实是一个软链接，并且指向了 &lt;code&gt;/usr/share/zoneinfo/Asia/Hong_Kong&lt;/code&gt;，当我们的 tidb-server 进程在这个文件被修改之前启动时，会使用正确的时区。但同时，我们在机器上部署了 puppet 进行统一的配置管理，puppet 会做一个类似 &lt;code&gt;cp /usr/share/zoneinfo/Asia/Hong_Kong /etc/localtime&lt;/code&gt; 的操作，将 &lt;code&gt;/etc/localtime&lt;/code&gt; 替换为一个文件，这个时候，tidb-server 无法正确分析时区信息，导致 fall back 至 UTC。简单示意如下：
&lt;img alt="请在这里输入图片描述" src="/static/20191012172800.png"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为什么 &lt;code&gt;SELECT @@global.time_zone, @@session.time_zone;&lt;/code&gt; 看到的是 CST，但不管是我们的机器还是 TiDB 配置文件都没有引用 CST 这个时区，CST 从哪里来的？&lt;/p&gt;
&lt;p&gt;属于 tidb-server 的默认行为，tidb-server 初次启动时，会默认设置为 CST。可以参考 &lt;code&gt;session/bootstrap.go&lt;/code&gt; 和 &lt;code&gt;sessionctx/variable/sysvar.go&lt;/code&gt; 这里不再展开。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;system_tz&lt;/code&gt;、&lt;code&gt;@@global.time_zone&lt;/code&gt;、&lt;code&gt;@@sessio.time_zone&lt;/code&gt; 的关系是什么？&lt;code&gt;system_tz&lt;/code&gt; 和 &lt;code&gt;time_zone&lt;/code&gt; 两者的生效逻辑是怎样的？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@@session.time_zone&lt;/code&gt; 在新建连接时，值主要依赖于 &lt;code&gt;@@global.time_zone&lt;/code&gt;，session 值等同于 global，默认都为 SYSTEM，即 &lt;code&gt;system_tz&lt;/code&gt; 值；&lt;/li&gt;
&lt;li&gt;如果单独修改了 &lt;code&gt;@@session.time_zone&lt;/code&gt; ，则当前连接的 time_zone 以 session 设置为准；&lt;/li&gt;
&lt;li&gt;tidb-server 在每次启动的时候会从 mysql.tidb 加载 &lt;code&gt;system_tz&lt;/code&gt; 值，并设置系统时区。这也是为什么我们在更新了 &lt;code&gt;system_tz&lt;/code&gt; 并重启 tidb-server 后，可以获取到正确时间的原因；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;tidb-server 启动时通过 mysql.tidb 设置系统时区的相关代码可以在 &lt;code&gt;session/session.go&lt;/code&gt; 找到：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// loadSystemTZ loads systemTZ from mysql.tidb&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;loadSystemTZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;se&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`select variable_value from mysql.tidb where variable_name = &amp;#39;system_tz&amp;#39;`&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errLoad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;se&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errLoad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errLoad&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// the record of mysql.tidb under where condition: variable_name = &amp;quot;system_tz&amp;quot; should shall only be one.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;logutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;close result set error&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;NewChunk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GetRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// BootstrapSession runs the first time when the TiDB server start.&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BootstrapSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;其实还蛮普通的一个问题，只是之前碰到比较多的情况是在环境初始化没完成时就启动服务，导致服务出现状况。这次 TiDB 这里刚好反过来，环境初始化未操作之前反而是正常的，初始化之后异常了；&lt;/li&gt;
&lt;li&gt;问题的线索其实完完整整的保存在了日志中，流程是否有可以优化的空间有待考证商榷，但日志在排查问题的时候是一定要首先关注的；&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/etc/localtime&lt;/code&gt; 直接使用文件不是一个合理的方式，TZ 环境变量的优先级也高于 &lt;code&gt;/etc/localtime&lt;/code&gt;。这个可以参考 &lt;a href="http://man7.org/linux/man-pages/man5/localtime.5.html"&gt;man 文档&lt;/a&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Because the timezone identifier is extracted from the symlink target 
name of /etc/localtime, this file may not be a normal file or
hardlink.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="dm"/><category term="mysql"/><category term="tidb"/><category term="pingcap"/></entry><entry><title>TiDB DM 数据库同步 STEP BY STEP</title><link href="https://www.chenxiaosheng.com/posts/2019-09-29/tidb-dm-syncer.html" rel="alternate"/><published>2019-09-29T13:08:00+08:00</published><updated>2019-09-29T13:08:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2019-09-29:/posts/2019-09-29/tidb-dm-syncer.html</id><summary type="html">&lt;p&gt;前段时间，看到了一篇关于数据库选型的文章（MongoDB/TiDB/CockroachDB），忍不住感慨了一句，TiDB 一看就是大户人家，什么工具都可以有，什么工 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;前段时间，看到了一篇关于数据库选型的文章（MongoDB/TiDB/CockroachDB），忍不住感慨了一句，TiDB 一看就是大户人家，什么工具都可以有，什么工具都迭代得特别快，没有钱没有人，难以想像如何才能驱动得了。TiDB/TiKV/PD/Mydumper/Loader/Syncer/Data Migration/TiDB Lighting/Pump/Drainer ... 光是弄清楚这些组件是用来做什么的，感觉都可以评个高工了 :-)&lt;/p&gt;
&lt;p&gt;随着 DM 1.0 GA 的 release，我们也开始考虑将 syncer 迁移至 DM。主要原因还是 syncer 已经太久没有更新了，否则 syncer 这种简易几乎无须部署的工具还是比较对我个人的胃口的，官方很喜欢把工具集群化（可能听起来厉害一点），DM 也是，那就按着最小目标（syncer）折腾一下吧。&lt;/p&gt;
&lt;p&gt;DM 的架构文档可以参考&lt;a href="https://pingcap.com/docs-cn/v3.0/reference/tools/data-migration/overview/#dm-架构"&gt;官方&lt;/a&gt;，整个集群有三个角色 dmctl/dm-master/dm-worker。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DM-master 负责管理和调度数据同步任务的各项操作。&lt;/li&gt;
&lt;li&gt;DM-worker 负责执行具体的数据同步任务。&lt;/li&gt;
&lt;li&gt;dmctl 是用来控制 DM 集群的命令行工具。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Data Migration architecture" src="https://pingcap.com/images/docs-cn/dm-architecture.png"&gt;&lt;/p&gt;
&lt;p&gt;官方也提供了基于 Ansible 的部署工具，如果你不是特别需要了解实际的部署过程，或者你的环境对 ansible 的亲和度比较好的话，可以参考&lt;a href="https://pingcap.com/docs-cn/v3.0/how-to/deploy/data-migration-with-ansible"&gt;使用 DM-Ansible 部署 DM 集群&lt;/a&gt;。在这篇文章里，我们还是希望对于 DM 的部署有一定的了解，另外我们的小目标是实现类似 syncer 功能，因此，让我们 step by step 人肉部署吧。（好吧，吐槽一下，其实本人是很反感什么都要 ansible 再包一层的，无法直接通过文档了解工作方式，还非得去读 ansible 的相关部署文件）。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确认工作目录，假定这里为 &lt;code&gt;/home/dm&lt;/code&gt;，并确保相关目录已经建立且具备读写权限：&lt;code&gt;mkdir -p /home/dm/log&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保机器上已经具备相应的命令行工具，如：tar、wget、rsync 或 cp 等；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载 DM 二进制文件：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;wget&lt;span class="w"&gt; &lt;/span&gt;http://download.pingcap.org/dm-latest-linux-amd64.tar.gz
tar&lt;span class="w"&gt; &lt;/span&gt;-zxvf&lt;span class="w"&gt; &lt;/span&gt;dm-latest-linux-amd64.tar.gz
rsync&lt;span class="w"&gt; &lt;/span&gt;-avz&lt;span class="w"&gt; &lt;/span&gt;dm-latest-linux-amd64/bin/&lt;span class="w"&gt; &lt;/span&gt;/home/dm/bin/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;机器列表，这里我们主要熟悉部署过程，且不需要分库分表合并等功能，只实现 syncer 流程，因此 master 和 worker 均只部署在同一台同器：&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;地址&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DM-master&lt;/td&gt;
&lt;td&gt;127.0.0.1:8261&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM-worker&lt;/td&gt;
&lt;td&gt;127.0.0.1:8262&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;h3&gt;dm-worker&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# DM-worker Configuration: worker.toml&lt;/span&gt;
&lt;span class="c1"&gt;# MySQL Server ID, 注意不要和已有的 ID 冲突&lt;/span&gt;
&lt;span class="n"&gt;server-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8262&lt;/span&gt;
&lt;span class="c1"&gt;# 可以理解为 worker 的唯一标识&lt;/span&gt;
&lt;span class="n"&gt;source-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;worker-8262&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;worker-addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1:8262&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# 支持源为 mysql 或 mariadb&lt;/span&gt;
&lt;span class="n"&gt;flavor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mysql&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# 是否启用 GTID，开启前需要上游 MySQL 开启 GTID 功能&lt;/span&gt;
&lt;span class="n"&gt;enable-gtid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="c1"&gt;# MySQL Master/Slave 发生切换时，是否修复 gtid&lt;/span&gt;
&lt;span class="n"&gt;auto-fix-gtid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="c1"&gt;# 开始同步的位置，我这里使用 GTID，该值为 MySQL Master 上的 Executed_Gtid_Set 值&lt;/span&gt;
&lt;span class="c1"&gt;# relay-binlog-name = &amp;quot;binlog.000140&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;relay-binlog-gtid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2ea42e60-a157-11e9-8f2d-0ef02be0adee:1-3472558,34204ac9-a157-11e9-bc03-c6e9019f8073:1-2&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# charset of DSN of source mysql/mariadb instance&lt;/span&gt;
&lt;span class="n"&gt;charset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# worker 元数据存储目录，需要提前创建好&lt;/span&gt;
&lt;span class="n"&gt;meta-dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/home/dm/meta/worker-8262&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# relay log 的本地存储目录，需要提前创建好&lt;/span&gt;
&lt;span class="n"&gt;relay-dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/home/dm/relay/worker-8262&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# 源数据库，一个 worker 只能对应一个 MySQL 源&lt;/span&gt;
&lt;span class="k"&gt;[from]&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;dm&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# 该密码为使用 dmctl -encrypt ${你的原始密码} 加密后的字符串&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vUYLfdtQlHLF6Kr4IHg8NkDjiPtVig6p&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;

&lt;span class="c1"&gt;# relay log purge strategy&lt;/span&gt;
&lt;span class="k"&gt;[purge]&lt;/span&gt;
&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;
&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;remain-space&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;配置不算特别复杂，需要留意的地方都在注释里进行说明了。其中 &lt;code&gt;source-id&lt;/code&gt; 请记下来，接着配置 dm-master 需要用到。&lt;/p&gt;
&lt;p&gt;再次提醒，MySQL 的密码配置需要使用 dmctl 进行加密处理。&lt;/p&gt;
&lt;h3&gt;dm-master&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# dm-master Configuration: master.toml&lt;/span&gt;
&lt;span class="n"&gt;log-level&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;info&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;log-file&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/home/dm/log/dm-master.log&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;master-addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1:8261&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[[deploy]]&lt;/span&gt;
&lt;span class="n"&gt;source-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;worker-8262&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;dm-worker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1:8262&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;配置文件比较简单，基本上都不需要解释。其中 &lt;code&gt;[[deploy]]&lt;/code&gt; 对应一个 worker，可以有多个 &lt;code&gt;[[deploy]]&lt;/code&gt; 。目前了解到的，dm-worker 只能通过配置文件并重启 dm-master 进行加载，无法通过 dmctl 进行运行上填加。好在 dm-master 几乎随时都可以重启，所以也不是特别大的问题。&lt;/p&gt;
&lt;h3&gt;dmctl&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# dmctl Configuration: ctl.toml&lt;/span&gt;
&lt;span class="n"&gt;master-addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1:8261&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;dmctl 的配置就相当简单了，只需要指定 dm-master 地址。而 dmctl 也是我们与 dm-master 打交道的最直接工作。&lt;/p&gt;
&lt;h2&gt;集群启动&lt;/h2&gt;
&lt;p&gt;经过上述的配置（是不是没有想像中复杂），我们已经可以开启集群了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动 master: bin/dm-master -config master.toml&lt;/li&gt;
&lt;li&gt;启动 worker: bin/dm-worker -config worker.toml&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了跟踪日志输出，确认进程是否正常启动。我们还可以借助 dmctl 来检查当前集群的情况，使用 &lt;code&gt;bin/dmctl -config ctl.toml&lt;/code&gt; 连接集群，并通过 &lt;code&gt;query-status&lt;/code&gt; 查询集群状态，返回如下（成功）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;»&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;query&lt;/span&gt;&lt;span class="mi"&gt;-&lt;/span&gt;&lt;span class="err"&gt;s&lt;/span&gt;&lt;span class="kc"&gt;tatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;msg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;workers&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;worker&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1:8262&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;msg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;no sub task started&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;subTaskStatus&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;relayStatus&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;masterBinlog&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(binlog.000172, 234)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;masterBinlogGtid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2ea42e60-a157-11e9-8f2d-0ef02be0adee:1-3472558,34204ac9-a157-11e9-bc03-c6e9019f8073:1-2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;relaySubDir&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2ea42e60-a157-11e9-8f2d-0ef02be0adee.000001&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;relayBinlog&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(, 4)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;relayBinlogGtid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;34204ac9-a157-11e9-bc03-c6e9019f8073:1-2,2ea42e60-a157-11e9-8f2d-0ef02be0adee:1-3472558&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;relayCatchUpMaster&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;stage&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Running&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;sourceID&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;worker-8262&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;»&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这个时候，系统还没有配置任何同步任务，所以也可以看到显示的是：&lt;code&gt;no sub task started&lt;/code&gt;。接下来，我们开始来配置一个最基础的同步任务。&lt;/p&gt;
&lt;h2&gt;配置并应用同步任务&lt;/h2&gt;
&lt;h3&gt;创建任务配置文件&lt;/h3&gt;
&lt;p&gt;PingCAP 在任务配置这一块有比较完善的说明了&lt;a href="https://pingcap.com/docs-cn/v3.0/reference/tools/data-migration/configure/task-configuration-file/"&gt;DM 任务配置文件介绍&lt;/a&gt;。这里我们以将源库为 &lt;code&gt;abc&lt;/code&gt; 迁到目标库 &lt;code&gt;abc_dm&lt;/code&gt; 为例子做简单说明：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;---&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;
&lt;span class="n"&gt;task-mode&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;is-sharding&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;meta-schema&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;dm_meta&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;remove-meta&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;enable-heartbeat&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;target-database&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;192.168.143.41&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;3306&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;dm&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vUYLfdtQlHLF6Kr4IHg8NkDjiPtVig6p&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;mysql-instances&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# 源库及相关的同步规则配置&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;-&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;source-id&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;worker-8262&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;binlog-name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;binlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;000140&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;binlog-pos&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;141868&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;route-rules&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;user-route-rules-schema&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;black-white-list&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;instance&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;user-route-rules-schema&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;schema-pattern&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;abc&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;target-schema&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;abc_dm&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;black-white-list&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;do-dbs&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;abc&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;上面参数的说明都可以在官方文档找到，这里对几个重要的参数再做补充说明：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;meta-schema&lt;/td&gt;
&lt;td&gt;DM 会将该 task 对应的  meta 信息，如断点（checkpoint）存储至目标集存中的该数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remove-meta&lt;/td&gt;
&lt;td&gt;通常我们希望在重启 worker/task 时可以从上次断点继续同步，需要设置为 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;password&lt;/td&gt;
&lt;td&gt;需要使用 dmctl 加密&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source-id&lt;/td&gt;
&lt;td&gt;理解为相应 dm worker 的 source-id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;routes&lt;/td&gt;
&lt;td&gt;可以在这里配置数据库表的改名规则，具体参考&lt;a href="https://pingcap.com/docs-cn/v3.0/reference/tools/data-migration/configure/task-configuration-file/"&gt;官方文档&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;filters&lt;/td&gt;
&lt;td&gt;这里没有用到，可以过滤相应的 binlog events，如 &lt;code&gt;drop table&lt;/code&gt; 操作，具体参考&lt;a href="https://pingcap.com/docs-cn/v3.0/reference/tools/data-migration/configure/task-configuration-file/"&gt;官方文档&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;应用并启动同步任务&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;bin/dmctl -config ctl.toml&lt;/code&gt; 登录 dm master，并执行 &lt;code&gt;start-task test.yaml&lt;/code&gt; 即可，确认 &lt;code&gt;test.yaml&lt;/code&gt; 文件在当前目录下，成功的话将收到如下返回：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;msg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;workers&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;worker&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;127.0.0.1:8262&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;msg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这个时候再执行 &lt;code&gt;query-status&lt;/code&gt; 可以看到相应的 dm worker 有了具体的 subTaskStatus 信息：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;subTaskStatus&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;stage&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Paused&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;unit&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Sync&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;isCanceled&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;errors&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;detail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;unresolvedDDLLockID&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;sync&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;totalEvents&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;totalTps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;recentTps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;masterBinlog&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(binlog.000176, 234)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;masterBinlogGtid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2ea42e60-a157-11e9-8f2d-0ef02be0adee:1-3472558,34204ac9-a157-11e9-bc03-c6e9019f8073:1-2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;syncerBinlog&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(, 4)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;syncerBinlogGtid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;blockingDDLs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;unresolvedGroups&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;synced&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这个时候我们通过 mysql 登录目标数据库，可以看到多了 &lt;code&gt;dm_meta&lt;/code&gt; 和 &lt;code&gt;abc_dm&lt;/code&gt; 两个数据库，其中 &lt;code&gt;dm_meta&lt;/code&gt; 里面有两张表：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;test_loader_checkpoint&lt;/code&gt; 其中 test 是任务的名称，这张表存储的是全量同步的 meta 信息；&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;            id: worker-8262
      filename: abc.t3.sql
     cp_schema: abc
      cp_table: t3
        offset: 156
       end_pos: 156
   create_time: 2019-09-29 12:03:59
   update_time: 2019-09-29 12:04:00
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;test_syncer_checkpint&lt;/code&gt; 其中 test 是任务的名称，这张表存储的是增量同步的 meta 信息；&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;            id: worker-8262
     cp_schema: abc
      cp_table: xxy
   binlog_name: binlog|000001.000176
    binlog_pos: 538
     is_global: 0
   create_time: 2019-09-29 12:11:25
   update_time: 2019-09-29 12:11:25
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;组件升级&lt;/h2&gt;
&lt;h3&gt;dm-master&lt;/h3&gt;
&lt;p&gt;dm-master 主要负责 dmctl 的通讯，维护任务及 Sharding DDL lock 信息，重启后会与 dm-worker 重启这些信息，所以除了影响 dmctl 短暂的使用，可以随时重启，升级也就比较简单了：下载新版本 -&amp;gt; 覆盖旧 binary -&amp;gt; 重启。&lt;/p&gt;
&lt;h3&gt;dm-worker&lt;/h3&gt;
&lt;p&gt;dm-worker 在本地维护了 meta 信息，在下游（目标）数据库维护了断点（checkpoint）信息，升级也是按：下载新版本 -&amp;gt; 覆盖旧 binary -&amp;gt; 重启即可。唯一需要注意的有：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;尽量避免在 sharding DDL 同步过程中重启 DM-worker。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，本身对数据的敬畏之心，即使没有 sharding，也不推荐在 DDL 的过程中重启 dm-worker。&lt;/p&gt;
&lt;h3&gt;dmctl&lt;/h3&gt;
&lt;p&gt;命令行交互工具，直接升级即可：下载新版本 -&amp;gt; 覆盖旧 binary。&lt;/p&gt;
&lt;h2&gt;问题处理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;dm-worker 挂了，并且本地的 meta 信息都丢失了怎么办？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通常不需要担心，找回按原来的 worker.toml 再启动 worker 即可。其中配置文件中的 &lt;code&gt;relay-binlog-gtid&lt;/code&gt; 或 &lt;code&gt;relay-binlog-name&lt;/code&gt; 的位置信息可能在源数据库已经过期，可以从目标集群的 meta 库找回，或者直接从源数据库集群上使用一个相对靠前的位置信息替换即可。&lt;/p&gt;
&lt;p&gt;确认新的 worker 已经启动后，再次通过 dmctl 执行 &lt;code&gt;start-task test.yaml&lt;/code&gt; 即可，task 的配置文件不需要修改，断点信息会从目标数据库中的 meta 库直接获取。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;官方的组件（概念）特别多，ansible 的包装让用户对相关组件的工作流程了解门槛大大提升。实际部署流程比想像中较为简单。从可靠性来看，还是比较推荐放弃 syncer（较长时间未更新了），并使用 dm 做为替代工具。&lt;/p&gt;</content><category term="misc"/><category term="dm"/><category term="mysql"/><category term="tidb"/><category term="syncer"/><category term="pingcap"/></entry><entry><title>MySQL Seconds_Behind_Master 忽大忽小？莫慌</title><link href="https://www.chenxiaosheng.com/posts/2019-09-07/mysql-large-seconds_behind_master.html" rel="alternate"/><published>2019-09-07T22:33:00+08:00</published><updated>2019-09-07T22:33:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2019-09-07:/posts/2019-09-07/mysql-large-seconds_behind_master.html</id><summary type="html">&lt;p&gt;MySQL Slave Seconds_Behind_Master 是否可以直接做为数据复制是否延迟的衡量标准？在日常监控中是否有需要注意或特别避开的坑点？有没有更好的监控方式呢？&lt;/br&gt; &lt;img alt="MySQL Replication示意图" src="/static/mysql_repl.png"&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;本周在某次开会的时候，忽然收到了大量的 MySQL Seconds_Behind_Master 落后的报警轰炸（落后时间随机，几百到几万秒不等），但是当我们的值班 DBA 登录 MySQL 通过 &lt;code&gt;show slave status\G&lt;/code&gt; 查看时，发现都是 0，通常我们认为这个时候复制是正常的。那么是什么情况导致了报警的发生呢？&lt;/p&gt;
&lt;p&gt;DBA 值班同学也比较没有头绪，我们首先做了以下几件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确认监控插件是否变更？收到的答复是没有；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们自己是否有变更？收到的答复是没有。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;事后打脸了，有一位同学做了报警策略的变更，从原 X 时间内只发送第 N 条报警修改成了 X 时间内只发送第 1 条报警。该策略的变更也会导致该报警的轰炸，但是他变更的时间是在报警轰炸之后，跟此次报警轰炸原因没有正相关；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;登录多个实例确认都是误报后，先通告产品，明确是误报事件，让产品不用担心，等待 DBA 查明原因；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来我们会议正常进行，由某位 DBA 同学对此问题进行持续跟进，这个时候了解到某报警系统正在维护，猜测与他们有关系。但是即使是与他们有关系，这个落后的时间又是如何得来的呢？暂时 DBA 值班同学也没有很好的 GET 到头绪，DBA 老司机可能当时不是特别在状态，有点懵。&lt;/p&gt;
&lt;p&gt;刚好会议环节进入非重点环节，偷偷转移精力排查了一下，做为 DBA 门外汉，首先想到的是我要确认是不是 MySQL &lt;code&gt;show slave status&lt;/code&gt; 里真的出现了随机的落后数值，所以我做了最原始的一件事情：写了一个 2 秒执行一次 &lt;code&gt;show slave status&lt;/code&gt; 的脚本对 &lt;code&gt;Secons_Behind_Master&lt;/code&gt; 进行检查，果然在脚本执行了几分钟后出现了非 0 的数值，且是一个较大的超出实际线上情况的数值。（这里已经确认了是 MySQL 本身会有这个情况出现）&lt;/p&gt;
&lt;p&gt;接下来怎么办呢？门外汉的第一反应当然是查手册啦（手册大多数时候都是解毒良药），几乎是没有耗费时间的，直接在 MySQL 官方&lt;a href="https://dev.mysql.com/doc/refman/5.7/en/replication-administration-status.html"&gt;文档&lt;/a&gt;里关于这个问题的非常重要的一句说明：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;when the slave I/O thread is still queuing up a new event,
Seconds_Behind_Master may show a large value until the SQL thread 
finishes executing the new event.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;MySQL Replication 通常情况下有两个工作线程，I/O 线程用于获取 MySQL Master 的 Binlog，SQL 线程用于将获取到的 Binlog Events 在 Slave 从库上进行回放。&lt;/p&gt;
&lt;p&gt;&lt;img alt="MySQL Replication示意图" src="/static/mysql_repl.png"&gt;&lt;/p&gt;
&lt;p&gt;上面的描述我们通过字面简单进行了理解：当 I/O 线程仍在排队等待一件新事件时，&lt;code&gt;Seconds_Behind_Master&lt;/code&gt; 有可能返回一个较大的随机值，直到 SQL 线程完成该事件的重放。&lt;/p&gt;
&lt;p&gt;那么这里我们已经可以确认了，MySQL 是会出现这个情况的，并且是正常的，至少这个报警轰炸是不用特别担心了。那为什么忽然就炸出来了呢，在后面几分钟后我们得到了几个信息，确认了原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;该报警原先的设置是 X 时间内只发送第 N 条报警：X 时间内并不一定总会出现 &amp;gt;= N 次的随机 &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; 飙升（并且被监控插件获取到）；即使出现 &amp;gt;= N 次，在 X 时间内也只会发送一条报警，所以这个问题一直并没有被注意到；&lt;/li&gt;
&lt;li&gt;从报警平台的同事了解到，确实在维护过程中，报警策略匹配有问题，导致 &lt;code&gt;X 时间内只发送第 N 条&lt;/code&gt; 这个策略没有生效，所有报警都发了出来；&lt;/li&gt;
&lt;li&gt;如前述，某同事确实做了策略变更，该变更可以明确是错的，但跟上述的轰炸没有正相关。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个时候我们整体的原因已经比较清楚了，恢复生效的策略，并同步通告相关产品具体的情况，让产品安心是我们服务的目标之一。&lt;/p&gt;
&lt;p&gt;但这里也暴露了几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;报警平台维护变更出现了一定的问题，导致策略失效；&lt;/li&gt;
&lt;li&gt;部分 DBA 同学在某些环节对系统的了解并不够完整、也不够专业（我们都需要不断学习积累的，我自己也是学习中，能理解）；&lt;/li&gt;
&lt;li&gt;DBA 同学在做策略变更的时候也没有在团队内部进行同步，存在管理和规范遵守上的问题；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上问题需要我们进行反思，并在团队内进行解决。但是 MySQL Seconds_Behind_Master 是否还有监控的价值呢？对可能导致 &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; 异常的情况这里做个简单的总结：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Replication 启动时，Slave 会在主库执行 &lt;code&gt;SELECT UNIX_TIMESTAMP()&lt;/code&gt; 并进行主库和从库的系统时间差值对比，以避免由于系统时间差异对于 &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; 的时间产生影响。但如果是 Replication 启动后的时间异常，还是会对 &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; 产生影响，因此确保系统时间的同步非常重要（ntpd）；&lt;/li&gt;
&lt;li&gt;当 Master 和 Slave 间的网络较差时，I/O 线程不能快速的获取 Binlog Events，SQL 线程处理速度是有可能超过 I/O 线程的，那么这个时候即使是 0，也不代表主从同步是正常的。（良好的网络环境是主从同步的基础保障）&lt;/li&gt;
&lt;li&gt;当 I/O 线程被关闭时，该值可能为 NULL。（可以理解为特殊情况）；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Seconds_Behind_Master 怎么用？&lt;/h3&gt;
&lt;p&gt;结合以上情况，我认为 &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; 还是一个很重要的同步是否落后的参考值的，只是可能我们可以做得更完善一些，比如在获取的时候，可以通过二次获取进行判断，当获取到较大值时，再次获取一次当前值进行确认。&lt;/p&gt;
&lt;p&gt;另外，如果需要较为严格的同步监控，建议通过 Binglog 的位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Master_Log_file/Read_Master_Log_Pos&lt;/li&gt;
&lt;li&gt;Relay_Master_Log_File/Exec_Master_Log_Pos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进行判断，如果(Relay_Master_Log_File, Exec_Master_Log_Pos) 和 (Relay_Master_Log_File, Read_Master_Log_Pos)位置相等且 Seconds_Behind_Master=0，那么我们可以认为目前的同步是「同步」的。（开启 gtid 的话，比对 &lt;code&gt;Executed_Gtid_Set&lt;/code&gt; 我认为也是没有毛病的）&lt;/p&gt;
&lt;h3&gt;The End&lt;/h3&gt;
&lt;p&gt;后续相关技术内容的总结，也会逐步更新至微信平台（微信目前确实也是技术内容流转的一个好渠道），欢迎关注个人公众号（也算给自己立个 flag 吧，希望 flag 永不倒）：&lt;/p&gt;
&lt;p&gt;&lt;img alt="公众号二维码" src="/static/qrcode.jpg"&gt;&lt;/p&gt;</content><category term="misc"/><category term="mysql"/><category term="replication"/></entry><entry><title>在 Mac 中对 iPhone 手机网络进行抓包的方法</title><link href="https://www.chenxiaosheng.com/posts/2018-11-02/capture-packet-from-ios-through-mac.html" rel="alternate"/><published>2018-11-02T19:58:00+08:00</published><updated>2018-11-02T19:58:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2018-11-02:/posts/2018-11-02/capture-packet-from-ios-through-mac.html</id><summary type="html">&lt;ol&gt;
&lt;li&gt;通过数据线连接 Mac 笔记本，并通过 itunes 查询手机的 UUID
&lt;img alt="iphone uuid" src="/static/iphone_uuid.png"&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mac 提供了一个工具 rvictl （rvi=Remote Virtual Interface）可以为连上的 iphone 手机创建一个虚拟网 …&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;ol&gt;
&lt;li&gt;通过数据线连接 Mac 笔记本，并通过 itunes 查询手机的 UUID
&lt;img alt="iphone uuid" src="/static/iphone_uuid.png"&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mac 提供了一个工具 rvictl （rvi=Remote Virtual Interface）可以为连上的 iphone 手机创建一个虚拟网络设备，不管手机用的是移动网络还是 WIFI，均可以通过该设备进行抓包。启动虚拟设备的命令如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;rvictl -s ${uuid}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果成功启动，将返回：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Starting device xxxxxxxx [SUCCEEDED] with interface rvi0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其中的 rvi0 即为可用于抓包的虚拟设备。&lt;/p&gt;
&lt;p&gt;如果操作失败，并返回 &lt;code&gt;bootstrap_look_up(): 1102&lt;/code&gt; 错误，请通过如下命令启动服务，并重复 &lt;code&gt;rvictl -s ${uuid}&lt;/code&gt; 的操作即可：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.rpmuxd.plist&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后就可以通过 tcpdump 命令进行抓包了，如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tcpdump -i rvi0 -s 0 -vvv dst host x.x.x.x -w test.pcap&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后可以通过 wireshark 打开 test.pcap 进行查看。通常你可以使用 &lt;code&gt;brew cask install wireshark&lt;/code&gt; 安装 wireshark.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;怎么通过 iTunes 往手机上安装 ipa 包？&lt;/h4&gt;
&lt;p&gt;旧版本的 iTunes，在设备信息里有个「应用」入口，并可以在这里安装 ipa 文件。这次顺便发现 iTunes 没有了这个入口，刚拿到 ipa 包时，有点不知道怎么办。原来，只要将将文件拖到 iTunes 对应的设备里，就会自动安装了。（拖到下面截图红色框框部份）&lt;/p&gt;
&lt;p&gt;&lt;img alt="iphone drop" src="/static/iphone_drop.png"&gt;&lt;/p&gt;</content><category term="misc"/><category term="tcpdump"/><category term="wireshark"/><category term="ios"/><category term="mac"/></entry><entry><title>一次「安全」的变更将 redis cluster 送进了孤岛</title><link href="https://www.chenxiaosheng.com/posts/2018-07-16/redis-cluster-bind-bug.html" rel="alternate"/><published>2018-07-16T00:51:00+08:00</published><updated>2018-07-16T00:51:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2018-07-16:/posts/2018-07-16/redis-cluster-bind-bug.html</id><summary type="html">&lt;p&gt;一次以为安全的，天衣无缝的升级，却将 redis cluster 集群送进了孤岛。对于这次故障，没有借口，我们不能也不该出现这样的失误！我们将认真复盘改进自动化运维技术和发布验证流程，敬畏每一行代码，敬畏每一份托付。&lt;/p&gt;</summary><content type="html">&lt;p&gt;鉴于 redis 没有有效的鉴权方法（特别是 redis cluster），为了防止人为疏忽导致系统防火墙未被有效开启，我们决定对线上的 redis 增加显示监听内网 IP 配置：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bind 127.0.0.1 ${本机内网 IP}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在我们看来，这是一个不可能出现问题的变更，因此也没有专门安排测试。事实证明，没有保持敬畏之心的我们还是太天真了！&lt;/p&gt;
&lt;p&gt;线上配置发布后，重启 redis 服务，一切都很顺利，进程成功启动。然后，灾难开始降临，业务开始反馈 redis 缓存无法读写，查看 redis 日志，发现：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cluster state changed: fail&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;尝试通过 redis-cli 连接进行操作，连接没有问题，但当我们尝试执行 &lt;code&gt;get a&lt;/code&gt; 操作时，集却群反馈错误：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;-CLUSTERDOWN The cluster is down&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首要事件是保证服务可用，我们第一时间回退配置，重启后 redis 服务恢复正常。&lt;/p&gt;
&lt;h3&gt;问题回放&lt;/h3&gt;
&lt;p&gt;开启 Debug 模式，发现有大量的 createing socket: invalid argument 怀疑是 redis 拿了 bind 中的第一个 IP 为节点 IP，导致集群中所有节点的 IP 为 127.0.0.1，因此集群的通信出现问题（事后 review 代码确认跟第一个 IP 有关系，但不是导致所有节点的 IP 为 127.0.0.1）。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;1616:M 02 Jul 11:02:25.407 # Cluster state changed: ok
......
13236:M 02 Jul 11:02:27.104 . Unable to connect to Cluster Node [10.x.x.x]:16379 -&amp;gt; creating socket: Invalid argument
13236:M 02 Jul 11:02:27.104 . Unable to connect to Cluster Node [10.x.x.x]:16379 -&amp;gt; creating socket: Invalid argument
......
1616:M 02 Jul 11:02:38.438 # Cluster state changed: fail
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;有了上面的怀疑之后，我们尝试修改 bind 的配置，将 127.0.0.1 放到后面，即：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bind ${本机内网 IP} 127.0.0.1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;重启集群发现问题果然没有出现，感觉自己好机智（进行源码分析后，发现自己还是想的太简单了）。&lt;/p&gt;
&lt;h3&gt;源码分析&lt;/h3&gt;
&lt;p&gt;redis 版本：3.2.11&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;根据 &lt;code&gt;Cluster state changed: fail&lt;/code&gt; 我们定位到，redis 是在 cluster.c 的 clusterUpdateState 函数里进行集群的状态判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当节点状态为 CLUSTER_NODE_FAIL 或 CLUSTER_NODE_PFAIL 时，节点被标记为不可达；&lt;/li&gt;
&lt;li&gt;当不可达的 master 节点大于 quorum 时，集群被标记为不可用；&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Compute the cluster size, that is the number of master nodes&lt;/span&gt;
&lt;span class="cm"&gt;     * serving at least a single slot.&lt;/span&gt;
&lt;span class="cm"&gt;     *&lt;/span&gt;
&lt;span class="cm"&gt;     * At the same time count the number of reachable masters having&lt;/span&gt;
&lt;span class="cm"&gt;     * at least one slot. */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLUSTER_NODE_FAIL&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;CLUSTER_NODE_PFAIL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="n"&gt;reachable_masters&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* If we are in a minority partition, change the cluster state&lt;/span&gt;
&lt;span class="cm"&gt;     * to FAIL. */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;needed_quorum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reachable_masters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;needed_quorum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CLUSTER_FAIL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;among_minority_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mstime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Log a state change */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;....&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* Change the state and log the event. */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;serverLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LL_WARNING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Cluster state changed: %s&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CLUSTER_OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;ok&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;fail&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;redis cluster 主要是通过 cluster.c 中的 clusterCron 定期判断收集集群的状态，也是在这个函数里我们发现了最主要的探测函数 &lt;code&gt;anetTcpNonBlockBindConnect&lt;/code&gt; 及非常符合异常特征的参数名称 &lt;code&gt;NET_FIRST_BIND_ADDR&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;anetTcpNonBlockBindConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neterr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;CLUSTER_PORT_INCR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NET_FIRST_BIND_ADDR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="cm"&gt;/* We got a synchronous error from connect before&lt;/span&gt;
&lt;span class="cm"&gt;                 * clusterSendPing() had a chance to be called.&lt;/span&gt;
&lt;span class="cm"&gt;                 * If node-&amp;gt;ping_sent is zero, failure detection can&amp;#39;t work,&lt;/span&gt;
&lt;span class="cm"&gt;                 * so we claim we actually sent a ping now (that will&lt;/span&gt;
&lt;span class="cm"&gt;                 * be really sent as soon as the link is obtained). */&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ping_sent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ping_sent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mstime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;serverLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LL_DEBUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Unable to connect to &amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Cluster Node [%s]:%d -&amp;gt; %s&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;CLUSTER_PORT_INCR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neterr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;anetTcpNonBlockBindConnect&lt;/code&gt; 函数位于 anet.c 文件中，该函数调用了另一个函数 &lt;code&gt;anetTcpGenericConnect&lt;/code&gt;，&lt;code&gt;NET_FIRST_BIND_ADDR&lt;/code&gt; 在这里的实际作用为 source addr，问题原因已经呼之欲出了。我们再次定位 &lt;code&gt;anetTcpGenericConnect&lt;/code&gt; 函数，找到以下内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="cm"&gt;/* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;getaddrinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bservinfo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;anetSetError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%s&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gai_strerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;goto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bservinfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ai_next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ai_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ai_addrlen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;freeaddrinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bservinfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="n"&gt;anetSetError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bind: %s&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;strerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errno&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="k"&gt;goto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过以上内容，我们可以确认 redis cluster 在健康检测，建立 socket 连接时，尝试绑定了 socket 源地址，那么 &lt;code&gt;NET_FIRST_BIND_ADDR&lt;/code&gt; 内容究竟是什么呢？这个我们在 server.h 文件中找到了答案，确实是 bind 配置中的第一个 IP，也就是我们这里的 127.0.0.1&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Get the first bind addr or NULL */&lt;/span&gt;
&lt;span class="cp"&gt;#define NET_FIRST_BIND_ADDR (server.bindaddr_count ? server.bindaddr[0] : NULL)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;问题结论&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;redis cluster 在检测节点健康状态时，尝试与其它节点建立连接，但强制使用了 bind 配置中的第一个 IP 为建立 socket 的源地址&lt;/li&gt;
&lt;li&gt;以上案例，当配置了监听 127.0.0.1 时，redis cluster 将尝试使用本地地址 127.0.0.1 去和一个外部节点建立链接，因此失败了；&lt;/li&gt;
&lt;li&gt;基本的解决方法：使用 bind 配置时，确保将可进行通讯的 IP 放在第一个；&lt;/li&gt;
&lt;li&gt;有没有其它问题：如果这是一个新建的 cluster 集群，那么在使用 redis-trib.rb 进行初始化集群时，该配置将导致 redis-trib.rb 工具长期阻塞，无法成功执行；&lt;/li&gt;
&lt;li&gt;所有支持 cluster 的 redis 版本均受影响（包括 unstable 分支）；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;其它&lt;/h3&gt;
&lt;p&gt;私以为强制使用 bind 第一个 IP 进行通讯的方法略有不妥，如果检测健康状态时一定需要指定源地址的话，兴许做出如下调整会有一定的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当未初始化时（集群节点未明确 IP 地址），建立 socket 连接，不要指定源地址，由操作系统自行分配源地址即可；&lt;/li&gt;
&lt;li&gt;当 redis cluster 初始化时，可以使用初始化时指定的节点 IP 做为源地址；&lt;/li&gt;
&lt;li&gt;redis 在 anet.c 文件里，其实还提供了 &lt;code&gt;anetTcpNonBlockBestEffortBindConnect&lt;/code&gt; 函数，这个函数如果通过绑定源地址建立 socket 链接失败后，会再次尝试由系统分配源地址的方式（不绑定），健康检查函数换成这个也不失为一个好办法；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最（chao）后（xi）：对于这次故障，没有借口，我们不能也不该出现这样的失误！我们将认真复盘改进自动化运维技术和发布验证流程，敬畏每一行代码，敬畏每一份托付。&lt;/p&gt;</content><category term="misc"/><category term="redis"/></entry><entry><title>MongoDB 随机查询获取一条或 N 条记录的方法</title><link href="https://www.chenxiaosheng.com/posts/2018-05-24/mongodb-random-query.html" rel="alternate"/><published>2018-05-24T12:14:13+08:00</published><updated>2018-05-24T12:14:13+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2018-05-24:/posts/2018-05-24/mongodb-random-query.html</id><summary type="html">&lt;p&gt;MySQL 可以通过 &lt;code&gt;rand()&lt;/code&gt; 配合 &lt;code&gt;limit&lt;/code&gt; 获取随机的 N 条记录，那么在 MongoDB 上我们又该如何操作呢？&lt;/p&gt;</summary><content type="html">&lt;p&gt;熟悉 MySQL 的同学应该都知道可以通过类似如下语句简单的获取一张表里随机的 N 条记录：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SELECT * FROM table ORDER BY RAND() LIMIT N;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 MongoDB 里应该如何操作呢？官方在 3.2 版本带来了直接的解决方案：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$sample (aggregation)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体应该如何操作，官方也给出了一个示例，假设 Collection users 有如下记录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;{ &amp;quot;_id&amp;quot; : 1, &amp;quot;name&amp;quot; : &amp;quot;dave123&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : true }
{ &amp;quot;_id&amp;quot; : 2, &amp;quot;name&amp;quot; : &amp;quot;dave2&amp;quot;, &amp;quot;q1&amp;quot; : false, &amp;quot;q2&amp;quot; : false  }
{ &amp;quot;_id&amp;quot; : 3, &amp;quot;name&amp;quot; : &amp;quot;ahn&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : true  }
{ &amp;quot;_id&amp;quot; : 4, &amp;quot;name&amp;quot; : &amp;quot;li&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : false  }
{ &amp;quot;_id&amp;quot; : 5, &amp;quot;name&amp;quot; : &amp;quot;annT&amp;quot;, &amp;quot;q1&amp;quot; : false, &amp;quot;q2&amp;quot; : true  }
{ &amp;quot;_id&amp;quot; : 6, &amp;quot;name&amp;quot; : &amp;quot;li&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : true  }
{ &amp;quot;_id&amp;quot; : 7, &amp;quot;name&amp;quot; : &amp;quot;ty&amp;quot;, &amp;quot;q1&amp;quot; : false, &amp;quot;q2&amp;quot; : true  }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;那么可以通过命令 &lt;code&gt;db.users.aggregate( [ { $sample: { size: N } } ] )&lt;/code&gt; 获取随机的 N 条记录，如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mongos&amp;gt; db.users.aggregate( [ { $sample: { size: 3 } } ] )
{ &amp;quot;_id&amp;quot; : 7, &amp;quot;name&amp;quot; : &amp;quot;ty&amp;quot;, &amp;quot;q1&amp;quot; : false, &amp;quot;q2&amp;quot; : true }
{ &amp;quot;_id&amp;quot; : 4, &amp;quot;name&amp;quot; : &amp;quot;li&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : false }
{ &amp;quot;_id&amp;quot; : 1, &amp;quot;name&amp;quot; : &amp;quot;dave123&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : true }
mongos&amp;gt; db.users.aggregate( [ { $sample: { size: 3 } } ] )
{ &amp;quot;_id&amp;quot; : 7, &amp;quot;name&amp;quot; : &amp;quot;ty&amp;quot;, &amp;quot;q1&amp;quot; : false, &amp;quot;q2&amp;quot; : true }
{ &amp;quot;_id&amp;quot; : 3, &amp;quot;name&amp;quot; : &amp;quot;ahn&amp;quot;, &amp;quot;q1&amp;quot; : true, &amp;quot;q2&amp;quot; : true }
{ &amp;quot;_id&amp;quot; : 2, &amp;quot;name&amp;quot; : &amp;quot;dave2&amp;quot;, &amp;quot;q1&amp;quot; : false, &amp;quot;q2&amp;quot; : false }
mongos&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;$sample 会使用如下两种方式来获取随机的 N 条记录，具体使用哪种方式取决于如下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$sample 处于聚合管道的第一阶段；&lt;/li&gt;
&lt;li&gt;N 小于总文档数量的 5% ;&lt;/li&gt;
&lt;li&gt;集合（Collection）中的文档数量大于 100；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当以上 3 个条件都满足时，$sample 将通过伪随机的游标来获取记录。当任一条件不满足时，$sample 将进行集合扫描，并通过随机排序来选择相应的 N 条记录。&lt;/p&gt;
&lt;p&gt;需要注意的是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当使用随机排序来获取记录时，排序操作会有 100 MB 的内存限制，具体可以参考 &lt;a href="https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#sort-memory-limit"&gt;$sort and Memory Restrictions&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;$sample 有可能多次返回同一条记录（document）；&lt;/li&gt;
&lt;li&gt;尝试在一张十亿级的 Collection 上获取 1W 条随机记录，耗时约 2.5 秒，供参考；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;参考文档：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;https://docs.mongodb.com/manual/reference/operator/aggregation/sample/&lt;/li&gt;
&lt;li&gt;https://jira.mongodb.org/browse/SERVER-533&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="mongodb"/></entry><entry><title>《OKR 工作法》读书笔记</title><link href="https://www.chenxiaosheng.com/posts/2018-01-30/okr-reading-notes.html" rel="alternate"/><published>2018-01-30T09:50:00+08:00</published><updated>2018-01-30T09:50:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2018-01-30:/posts/2018-01-30/okr-reading-notes.html</id><summary type="html">&lt;p&gt;OKR 就是要通过不断实践、总结，发不断发现、挑战团队的潜力，不要把这个过程当作汇报、考核的结果。没有完成，一起思考为什么会这样、怎么改进。目标达成，那就设置更有挑战的目标。把精力聚焦在学习总结、挖掘潜力和高效执行上。&lt;/p&gt;</summary><content type="html">&lt;p&gt;目标用来明确方向，关键结果则用来量化目标，使团队和个人聚焦在一个有挑战性的目标上。&lt;/p&gt;
&lt;ol&gt;
&lt;li&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;blockquote&gt;
&lt;p&gt;思考，哪些是关键成果，哪些是目标：销售额提升 30%/用户增加一倍/收入增加到 500 万美元/完成一轮融资/拿下 XX 市场/推出一个最小化可行产品&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;讨论关键结果，复盘 OKR&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;评估 OKR 实施成果&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;影响目标达成的关键因素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有给目标设置优先级&lt;/li&gt;
&lt;li&gt;缺乏充分沟通，导致没能准确理解目标&lt;blockquote&gt;
&lt;p&gt;目标完成的进度必须在每周会议和邮件中汇报，分解出的项目任务必须能支撑目标的达成；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;没有做好计划&lt;blockquote&gt;
&lt;p&gt;承担、庆祝与盘点&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;没有把时间花在重要的事情上&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;重要的事情通常不紧急，紧急的事情通常不重要
思考：计划应该是为了重要不紧急还是紧急不重要？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;轻易放弃&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;害怕失败，不给自己挑战机会，隐藏实力，导致目标设置太低
期望过高，没有能力实现
没有坚持跟进目标，直到最后一周才发现没有进展（注意前面的，需要持续复盘、评估、沟通）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OKR 就是要通过不断实践、总结，发不断发现、挑战团队的潜力，不要把这个过程当作汇报、考核的结果。没有完成，一起思考为什么会这样、怎么改进。目标达成，那就设置更有挑战的目标。把精力聚焦在学习总结、挖掘潜力和高效执行上。&lt;/p&gt;
&lt;h3&gt;其它建议：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;如何评估绩效：持续沟通，不地加以指导和校正。管理者和员工各自分享自己的观点与想法，这样可以减少误解，问题也能得到快速解决。年终评估的事实大家可以通过持续的沟通提前知道。&lt;/li&gt;
&lt;li&gt;目标里不要有传统的绩效考核指标。&lt;/li&gt;
&lt;li&gt;OKR 进度确认（如周会）是一次谈话，而不是汇报（A做了什么B做了什么，这叫流水帐）或指示。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.cn/dp/B07577T3XS/ref=sr_1_1?ie=UTF8&amp;amp;qid=1517276944&amp;amp;sr=8-1&amp;amp;keywords=okr+%E5%B7%A5%E4%BD%9C%E6%B3%95"&gt;《OKR 工作法》&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://eleganthack.com/wp-content/uploads/2017/05/OKR_Worksheet.pdf"&gt; OKR 练习册&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="okr"/></entry><entry><title>网易游戏 MySQL-MongoDB 运维及 DBA 招聘</title><link href="https://www.chenxiaosheng.com/posts/2017-11-15/ntes-offers.html" rel="alternate"/><published>2017-11-15T22:51:00+08:00</published><updated>2017-11-15T22:51:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2017-11-15:/posts/2017-11-15/ntes-offers.html</id><summary type="html">&lt;p&gt;以下岗位部门均属于&lt;a href="http://hr.game.163.com/about.html"&gt;网易在线游戏事业部&lt;/a&gt;。
其中 MongoDB 运维及运维开发工程师可选工作地点为广州或杭州。&lt;/p&gt;
&lt;p&gt;相关数据库产品 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;以下岗位部门均属于&lt;a href="http://hr.game.163.com/about.html"&gt;网易在线游戏事业部&lt;/a&gt;。
其中 MongoDB 运维及运维开发工程师可选工作地点为广州或杭州。&lt;/p&gt;
&lt;p&gt;相关数据库产品均服务于如《&lt;a href="http://hy.163.com"&gt;荒野行动&lt;/a&gt;》、《&lt;a href="http://xyq.163.com"&gt;梦幻西游&lt;/a&gt;》、《&lt;a href="http://dhxy.163.com"&gt;大话西游&lt;/a&gt;》、《&lt;a href="http://yys.163.com"&gt;阴阳师&lt;/a&gt;》、《&lt;a href="http://mc.163.com"&gt;我的世界&lt;/a&gt;》、&lt;a href="http://cc.163.com"&gt;CC&lt;/a&gt;等网易游戏海量用户业务。
如果需要也有更深入了解的，也可以给我留言或者&lt;a href="https://www.dropbox.com/s/fcx2kqf77kmx7zv/wechat_qrcode.jpeg?dl=0"&gt;微信（二维码戳我）&lt;/a&gt;私我。同时欢迎推荐及转发。&lt;/p&gt;
&lt;h3&gt;岗位一：MySQL DBA &lt;a href="http://hr.game.163.com/position/introduction.html?code=T0915"&gt;点我投递简历&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;工作职责&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;参与内部MySQL SaaS开发和维护工作；&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;/ol&gt;
&lt;p&gt;职位要求&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;精通MySQL等数据库系统使用，精通SQL语句和优化；&lt;/li&gt;
&lt;li&gt;熟悉Linux系统操作；&lt;/li&gt;
&lt;li&gt;了解网络，有一定的故障排查和处理能力；&lt;/li&gt;
&lt;li&gt;熟悉shell脚本编程，会Python、Java者优先；&lt;/li&gt;
&lt;li&gt;有大型数据库和大规模服务器管理经验优先；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;岗位二：MongoDB 运维工程师 &lt;a href="http://hr.game.163.com/position/introduction.html?code=T1427"&gt;点我投递简历&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;职位描述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;负责 MongoDB 等数据库平台的配置管理、流程管理；&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;/ol&gt;
&lt;p&gt;职位要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;熟悉 Linux 系统以及工具命令等使用，熟悉主流开源软件的配置及调优，有底层基础技术研发经历的优先；&lt;/li&gt;
&lt;li&gt;熟悉至少一种程序设计语言，有 Shell、Python 等脚本编程语言经验者优先；&lt;/li&gt;
&lt;li&gt;对计算机硬件设备、网络设备有一定了解，熟悉 TCP/IP 以及具有丰富的网络知识；&lt;/li&gt;
&lt;li&gt;熟悉 MongoDB 数据库的日常使用；&lt;/li&gt;
&lt;li&gt;掌握 MongoDB 的管理及构建，包括但不限于集群构建、性能调优、备份恢复；&lt;/li&gt;
&lt;li&gt;具备良好的沟通表达能力和服务意识，较强的团队合作意识，快速处理突发事件的能力；&lt;/li&gt;
&lt;li&gt;有大型 MongoDB 商用部署实施及运营经验者优先，有 OCP 认证者优先。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;岗位三：运维开发工程师（数据库平台）&lt;a href="http://hr.game.163.com/position/introduction.html?code=T1300"&gt;点我投递简历&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;职位描述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;负责内部数据库相关产品的管理平台、自动化流程开发工作；&lt;/li&gt;
&lt;li&gt;与产品、PM、运维同学协作，积极高效推动产品开发；&lt;/li&gt;
&lt;li&gt;与用户及其他部门进行沟通协作；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;职位要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;1 年或以上前后端开发工作经验；&lt;/li&gt;
&lt;li&gt;对开发有浓厚兴趣，HTML/CSS/JS 基础扎实，熟悉Angulajs、Reactjs、Vue.js等MVVM框架中的一种；&lt;/li&gt;
&lt;li&gt;有至少一种 server 端开发经验，熟悉 Python、PHP 语言开发者优先；&lt;/li&gt;
&lt;li&gt;至少一种熟悉Web开发框架（如Django/Flask/Tornado）；&lt;/li&gt;
&lt;li&gt;对 HTTP 协议、网页性能优化有一定的理解；&lt;/li&gt;
&lt;li&gt;熟悉 MySQL、MongoDB、Redis 等数据库应用开发优先；&lt;/li&gt;
&lt;li&gt;能够编写高扩展、易维护的代码，熟练且规范使用代码管理工具；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;岗位四：高级运维工程师（DNS方向） &lt;a href="http://hr.game.163.com/position/introduction.html?code=T1249"&gt;点我投递简历&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;职位描述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;负责 DNS 及配套系统部署、维护及运维环境、服务优化；&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;/ol&gt;
&lt;p&gt;职位要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;熟悉 Linux 系统操作；&lt;/li&gt;
&lt;li&gt;熟悉 shell 脚本语言编程，会 Python 者优先；&lt;/li&gt;
&lt;li&gt;了解 TCP/IP 网络，有一定的故障排查和处理能力；&lt;/li&gt;
&lt;li&gt;熟悉 DNS 协议、BIND，有相关运维经验，并能够快速实施部署、配置和排障；&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="netease"/><category term="dba"/><category term="mysql"/><category term="mongodb"/></entry><entry><title>LXC 如何找出容器对应的 veth 设备</title><link href="https://www.chenxiaosheng.com/posts/2017-03-29/lxc-veth-pairs.html" rel="alternate"/><published>2017-03-29T11:42:00+08:00</published><updated>2017-03-29T11:42:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2017-03-29:/posts/2017-03-29/lxc-veth-pairs.html</id><summary type="html">&lt;p&gt;LXC 使用 veth 模式时，如果宿主机上创建了很多容器，那么宿主上将存在大量的 vethXXXX 设备，肉眼难以直接确认每个虚拟容器使用的是哪个 veth 设备。&lt;/p&gt;</summary><content type="html">&lt;p&gt;LXC 使用 veth 模式时，如果宿主机上创建了很多容器，那么宿主上将存在大量的 vethXXXX 设备，肉眼难以直接确认每个虚拟容器使用的是哪个 veth 设备。&lt;/p&gt;
&lt;p&gt;在我的记忆中，方法比较复杂，且之前忘了记录下来，已经遗忘。今天重新尝试使用另一方法获取匹配信息，这里做下记录：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;虚拟机（容器）执行 &lt;code&gt;ethtool -S eth0&lt;/code&gt; 拿到如 &lt;code&gt;peer_ifindex: 16&lt;/code&gt;，其中 &lt;code&gt;eth0&lt;/code&gt; 为在虚拟机（容器）内对应的设备名称；&lt;/li&gt;
&lt;li&gt;宿主机执行：&lt;code&gt;ip link show | grep '^16:'&lt;/code&gt;，其中 &lt;code&gt;16&lt;/code&gt; 为第一步拿到的 &lt;code&gt;peer_ifindex&lt;/code&gt;，通过该命令拿到的 vethXXXX 即为虚拟机（容器）使用的设备。&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="lxc"/></entry><entry><title>记一次 Apache 性能调优</title><link href="https://www.chenxiaosheng.com/posts/2017-03-23/apache-performance-tuning.html" rel="alternate"/><published>2017-03-23T09:44:00+08:00</published><updated>2017-03-23T09:44:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2017-03-23:/posts/2017-03-23/apache-performance-tuning.html</id><summary type="html">&lt;p&gt;每一次神优化背后都有一个很烧饼的设置（bug），刷一次负载下降 70%+ 的小目标。&lt;/br&gt;&lt;img src="/static/apache_load.png"&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;手上有一组 web 服务，使用 apache 对外提供服务，其中apache 使用 worker 模式，这堆 web 服务器有以下一堆毛病：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;apache 长期占用大量 CPU，负载长期居高不下。&lt;/li&gt;
&lt;li&gt;每天 rotate log 的指令不能被 apache 处理，通过 lsof 可以看到被删除的日志文件没有正确关闭。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过监控、日志统计，发现 qps 并不高，单台机器 qps 为600 左右（机器为 16 核、32G 内存），一开始以为是我代码性能问题，各种查找高消耗节点，无果。再次对服务器的整体情况及 apache 文档进行审视。&lt;/p&gt;
&lt;p&gt;通过阅读 &lt;a href="https://httpd.apache.org/docs/2.2/mod/mpm_common.html"&gt;apache 官方文档&lt;/a&gt; 在 worker 这里最重要的三个设置参数有 &lt;code&gt;ServerLimit * ThreadsPerChild &amp;gt;= MaxClients&lt;/code&gt; ，但我们服务器上的配置远大于 qps（理论上，实际并发数会小于 qps）。尝试调小以上参数以接近 qps 数值，服务器负载没有发生明显变化。&lt;/p&gt;
&lt;p&gt;再次观察系统负载（top）发现，apache 进程的 pid 一直在产生变化，产生了新的怀疑，是不是因为「频繁的销毁、创建 apache 进程」导致的开销过大，造成 cpu 负载居高不下？&lt;/p&gt;
&lt;p&gt;再次阅读文档，其中两个会导致进程销毁的设置分别为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MaxRequestsPerChild&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;The MaxRequestsPerChild directive sets the limit on the number of requests that an individual child server process will handle. After MaxRequestsPerChild requests, the child process will die. If MaxRequestsPerChild is 0, then the process will never expire.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
这个值，我们的设置是 0，所以不会是因为这个设置导致的。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MaxSpareThreads&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;For worker, the default is MaxSpareThreads 250. These MPMs deal with idle threads on a server-wide basis. If there are too many idle threads in the server then child processes are killed until the number of idle threads is less than this number.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;我们原来的值设置为 75，怀疑是因为这个值设得太小了，尝试将此值调整至与 &lt;code&gt;MaxClients&lt;/code&gt; 相等，重启 apache ，发现负载开始持续下降，&lt;code&gt;top 中 apache 进程 pid 几乎没有产生变化&lt;/code&gt;，服务正常，qps没有明显下降（未导致服务不可用）。新的配置如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;lt;IfModule mpm_worker_module&amp;gt;
    StartServers          2
    ServerLimit          12
    MinSpareThreads      25
    MaxSpareThreads     768
    ThreadLimit         128
    ThreadsPerChild      64
    MaxClients          768
    MaxRequestsPerChild   0
&amp;lt;/IfModule&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;至此问题解决，附优化前后的负载对比图：&lt;/p&gt;
&lt;p&gt;优化前：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/apache_before_tuning.png"&gt;&lt;/p&gt;
&lt;p&gt;优化后：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/apache_after_tuning.png"&gt;&lt;/p&gt;</content><category term="misc"/><category term="apache"/></entry><entry><title>pip 升级 pip 失败</title><link href="https://www.chenxiaosheng.com/posts/2016-05-19/pip_upgrade_pip_failure.html" rel="alternate"/><published>2016-05-19T12:11:00+08:00</published><updated>2016-05-19T12:11:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2016-05-19:/posts/2016-05-19/pip_upgrade_pip_failure.html</id><summary type="html">&lt;p&gt;今天在使用 python pip 安装一个 python 包的时候，一直提示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# pip install --upgrade pip
Requirement already up-to-date: pip in /usr/local/lib/python2.7/dist-packages
You are using pip version 8.1.1, however version 8.1.2 is available …&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;今天在使用 python pip 安装一个 python 包的时候，一直提示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# pip install --upgrade pip
Requirement already up-to-date: pip in /usr/local/lib/python2.7/dist-packages
You are using pip version 8.1.1, however version 8.1.2 is available.
You should consider upgrading via the &amp;#39;pip install --upgrade pip&amp;#39; command.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;尝试按说明执行 &lt;code&gt;pip install --upgrade pip&lt;/code&gt; ，没有任何报错，但一直升级不成功，百思不得解，
在查看 &lt;code&gt;man pip&lt;/code&gt; 的时候，找到如下选项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;-v, --verbose
    Give more output. Option is additive, and can be used up to 3 times.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;尝试执行 &lt;code&gt;pip install --upgrade pip -vvv&lt;/code&gt; 终于有更多提示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;1 location(s) to search for versions of pip:
* http://example.url/simple/pip/
Getting page http://example.url/simple/pip/
Starting new HTTP connection (1): example.url
&amp;quot;GET /simple/pip/ HTTP/1.1&amp;quot; 403 247
Could not fetch URL http://example.url/simple/pip/: 403 Client Error: Forbidden for url: http://example.url/simple/pip/ - skipping
Installed version (8.1.2) is most up-to-date (past versions: none)
Requirement already up-to-date: pip in /usr/local/lib/python2.7/dist-packages/pip-8.1.2-py2.7.egg
Cleaning up...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这时才发现，原来在我的 &lt;code&gt;~/.pip/pip.conf&lt;/code&gt; 配置文件里，&lt;code&gt;index-url&lt;/code&gt; 使用了一个已经废弃的地址，删除文件后重新升级，一切恢复正常。&lt;/p&gt;
&lt;p&gt;这么重要的 debug 信息，pip 默认竟然不输出，继续百思不得其解。:-)&lt;/p&gt;</content><category term="misc"/><category term="python"/><category term="pip"/></entry><entry><title>Mac OSX Yosemite 10.10 WIFI 掉线修复</title><link href="https://www.chenxiaosheng.com/posts/2014-11-17/mac_osx_yosemite_fix_wifi_problems.html" rel="alternate"/><published>2014-11-17T14:35:00+08:00</published><updated>2014-11-17T14:35:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2014-11-17:/posts/2014-11-17/mac_osx_yosemite_fix_wifi_problems.html</id><summary type="html">&lt;p&gt;原先用的是 Mac Air 11，通过Time Machine迁移至 Mac Retina 13，最后升级成
Yosemite 10.10 的，系统已经用了一段时间的，最近才忽然出出WIFI掉线的问题。&lt;/p&gt;
&lt;p&gt;而且此掉线比较 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;原先用的是 Mac Air 11，通过Time Machine迁移至 Mac Retina 13，最后升级成
Yosemite 10.10 的，系统已经用了一段时间的，最近才忽然出出WIFI掉线的问题。&lt;/p&gt;
&lt;p&gt;而且此掉线比较奇怪，WIFI Connection正常，TCP/UDP/ICMP包均不正常，
并且我试过在ping/safari/ssh等均异常的情况下，我某正在下载内容的应用（genymotion），
却坚持到了最后，并成功下载完了近200M的内容，所以基本排除了无线路由的问题。&lt;/p&gt;
&lt;p&gt;重启电脑无效，网上搜到这么一编文章：&lt;a href="http://www.oschina.net/news/56361/os-x-yosemite-wifi-fault"&gt;别忙升级，苹果 Yosemite 频现 WiFi 断线综合症&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;虽然前面两种方法无效，但第三种方法还是给了我启示的，凭直接猜测应该是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/Library/Preferences/SystemConfiguration
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;这个目录，进行这个目录，果然看到一堆与wifi关键字的文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ ls
NetworkInterfaces.plist             com.apple.smb.server.plist
com.apple.airport.preferences.plist com.apple.wifi.message-tracer.plist
com.apple.captive.probe.plist       preferences.plist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;把这些文件删掉（我是先备份了一下的，建议你也这样做），重启Mac。
注意本次重启，是不会自动连上之前使用的wifi的，需要自己去点击连接一下，然后问题就解决了。&lt;/p&gt;
&lt;p&gt;后来在网上看到 &lt;a href="http://osxdaily.com/2014/10/25/fix-wi-fi-problems-os-x-yosemite/"&gt;Fix Wi-Fi Problems in OS X Yosemite&lt;/a&gt;
这编文章，其实只要删除：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;com.apple.airport.preferences.plist
com.apple.network.identification.plist
com.apple.wifi.message-tracer.plist 
NetworkInterfaces.plist 
preferences.plist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这些文件就可以了，解释也很到位，比oschina那个靠谱多了。:-)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;2014-11-18 昨天折腾完，今天收到Apple Store的更新提醒，10.10.1 fix了wifi这个问题了
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="misc"/><category term="mac"/><category term="osx"/><category term="yosemite"/><category term="wifi"/></entry><entry><title>查询 dns server 使用的bind版本</title><link href="https://www.chenxiaosheng.com/posts/2014-07-07/query-remote-bind-version.html" rel="alternate"/><published>2014-07-07T20:21:00+08:00</published><updated>2014-07-07T20:21:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2014-07-07:/posts/2014-07-07/query-remote-bind-version.html</id><summary type="html">&lt;p&gt;其实命令很简单，但我发现我老记不住，所以做个记录吧（我总是把version.bind记成bind.version，orz）&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;dig&lt;span class="w"&gt; &lt;/span&gt;@&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;TXT&lt;span class="w"&gt; &lt;/span&gt;CHAOS&lt;span class="w"&gt; &lt;/span&gt;version.bind
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;${server}换成你想查询的dns服务器IP地址即可，如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;dig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;@8.8 …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;其实命令很简单，但我发现我老记不住，所以做个记录吧（我总是把version.bind记成bind.version，orz）&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;dig&lt;span class="w"&gt; &lt;/span&gt;@&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;TXT&lt;span class="w"&gt; &lt;/span&gt;CHAOS&lt;span class="w"&gt; &lt;/span&gt;version.bind
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;${server}换成你想查询的dns服务器IP地址即可，如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;dig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;@8.8.8.8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CHAOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;其实通常情况下出于安全考虑，大多数我们是查询不到版本信息的（系统管理员禁用了）&lt;/p&gt;
&lt;p&gt;bind 可以通过如下设置禁用版本号查询：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;options {
    version none;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;或者你可以将此值设置为某个固定字符串，如 "someone like you"&lt;/p&gt;</content><category term="misc"/><category term="bind"/><category term="dns"/><category term="dig"/><category term="named"/></entry><entry><title>crontab 同时显式指定day of month和day of week的特殊说明</title><link href="https://www.chenxiaosheng.com/posts/2014-05-09/cron-day-weekday-restricted.html" rel="alternate"/><published>2014-05-09T13:31:00+08:00</published><updated>2014-05-09T13:31:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2014-05-09:/posts/2014-05-09/cron-day-weekday-restricted.html</id><summary type="html">&lt;p&gt;man 5 crontab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;           field         allowed values
           -----         --------------
           minute        0-59
           hour          0-23
           day of month  1-31
           month         1-12 (or names, see below)
           day of week   0-7 (0 or 7 is Sun, or use names)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The day of a command's execution can be specified by two fields -- day of month, and day …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;man 5 crontab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;           field         allowed values
           -----         --------------
           minute        0-59
           hour          0-23
           day of month  1-31
           month         1-12 (or names, see below)
           day of week   0-7 (0 or 7 is Sun, or use names)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The day of a command's execution can be specified by two fields -- day of month, and day of week.
If both fields are restricted (ie, are not *), the command will be
run when either field matches the current time.
For example, ''30 4 1,15 * 5'' would cause a command to be run
at 4:30 am on the 1st and 15th of each month, plus every Fri-day.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;来自wikipedia的说明（&lt;a href="http://en.wikipedia.org/wiki/Crontab"&gt;http://en.wikipedia.org/wiki/Crontab&lt;/a&gt;）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;While normally the job is executed when the time/date specification fields all match the current time and date,
there is one exception: if both &amp;quot;day of month&amp;quot; and &amp;quot;day of week&amp;quot; are restricted (not &amp;quot;*&amp;quot;),
then either the &amp;quot;day of month&amp;quot; field (3) or the &amp;quot;day of week&amp;quot; field (5) must match the current day.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;以设置crontab： 11 14 27 3 3 为例，下面两个类型的时间都能满足该表达式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;3月里的每一个周三&lt;/li&gt;
&lt;li&gt;3月27日&lt;/li&gt;
&lt;/ol&gt;</content><category term="misc"/><category term="crontab"/></entry><entry><title>Linux系统上通知网关更新arp</title><link href="https://www.chenxiaosheng.com/posts/2014-03-19/linux-arp-flush.html" rel="alternate"/><published>2014-03-19T16:02:00+08:00</published><updated>2014-03-19T16:02:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2014-03-19:/posts/2014-03-19/linux-arp-flush.html</id><summary type="html">&lt;p&gt;经常会有在线更换Linux服务器IP的操作，该操作带来的一个问题是: 我们已经执行了修改IP的操作，但由于网络上（网关）的ARP缓存暂未更新，导致在某一段时间内，该 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;经常会有在线更换Linux服务器IP的操作，该操作带来的一个问题是: 我们已经执行了修改IP的操作，但由于网络上（网关）的ARP缓存暂未更新，导致在某一段时间内，该服务器会有网络不通的情况存在。&lt;/p&gt;
&lt;p&gt;因此，我们需要在变更IP的同时，通知网关刷新ARP缓存。&lt;/p&gt;
&lt;p&gt;首先清除本地ARP缓存:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/bin/ip neigh flush dev eth0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;其次向网关发送本机的ip/mac地址&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/usr/sbin/arping -v -c 2 -S 1.1.1.144 -s 00:17:a4:8d:0e:98 -p 1.1.1.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;1.1.1.144 为本机IP&lt;/p&gt;
&lt;p&gt;00:17:a4:8d:0e:98 为本机MAC地址&lt;/p&gt;
&lt;p&gt;1.1.1.1 为网关&lt;/p&gt;</content><category term="misc"/><category term="linux"/><category term="arp"/></entry><entry><title>Java 发起Http Post请求</title><link href="https://www.chenxiaosheng.com/posts/2014-02-19/java-ssl-and-http_post.html" rel="alternate"/><published>2014-02-19T17:16:00+08:00</published><updated>2014-02-19T17:16:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2014-02-19:/posts/2014-02-19/java-ssl-and-http_post.html</id><summary type="html">&lt;p&gt;对于一个 Java 小白，每完成一个新的功能，都表示相当不易，就连 Apache HttpClient 偶都表示没搞明白，
看起来好像不同版本还有不同的方 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;对于一个 Java 小白，每完成一个新的功能，都表示相当不易，就连 Apache HttpClient 偶都表示没搞明白，
看起来好像不同版本还有不同的方法，没办法，还是借助于 Google，拼凑出了这一段代码，记录以备自己后用。&lt;/p&gt;
&lt;p&gt;功能：使用 http post 方式访问某使用了 CNNIC 证书的站点（cnnic，唉，好多不信任）&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cm"&gt;/*&lt;/span&gt;
&lt;span class="cm"&gt; * Java POST Example&lt;/span&gt;
&lt;span class="cm"&gt; */&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.IOException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.net.MalformedURLException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.net.ProtocolException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.net.URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.net.URLEncoder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.DataOutputStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.BufferedReader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.InputStreamReader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.util.logging.Level&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.util.logging.Logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.File&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.FileInputStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.InputStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.security.KeyStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.security.cert.CertificateException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.security.cert.X509Certificate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.HttpsURLConnection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.TrustManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.TrustManagerFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.X509TrustManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.SSLContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.SSLSocketFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**&lt;/span&gt;
&lt;span class="cm"&gt; *&lt;/span&gt;
&lt;span class="cm"&gt; * @author Greg （原链接：http://sigterm.sh/2009/10/simple-post-in-java/）&lt;/span&gt;
&lt;span class="cm"&gt; */&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/**&lt;/span&gt;
&lt;span class="cm"&gt;     * Pretend you&amp;#39;re a script...&lt;/span&gt;
&lt;span class="cm"&gt;     */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* 加载CNNIC证书开始 */&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;X509TrustManager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sunJSSEX509TrustManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// 加载 Keytool 生成的证书文件&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;changeit&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;java.cnnic.cacert&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Loading KeyStore &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;InputStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FileInputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;KeyStore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultType&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// 构造 javax.net.ssl.TrustManager 对象&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TrustManagerFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TrustManagerFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SunX509&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SunJSSE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TrustManager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTrustManagers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// 使用构造好的 TrustManager 访问相应的 https 站点&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;SSLContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SSLContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SSL&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SunJSSE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;SSLSocketFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ssf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSocketFactory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* 加载CNNIC证书结束 */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://demo.site.url/uri/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MalformedURLException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;HttpsURLConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// URL connection channel.&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpsURLConnection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Let the conn use SSL&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSSLSocketFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Let the run-time system (RTS) know that we want input.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDoInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Let the RTS know that we want to do output.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDoOutput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// No caching, we want the real thing.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUseCaches&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setRequestMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;POST&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProtocolException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;DataOutputStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;BufferedReader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DataOutputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOutputStream&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Specify the content type if needed.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;//urlConn.setRequestProperty(&amp;quot;Content-Type&amp;quot;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;//  &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Construct the POST data.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;username=&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;randomuser&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;amp;password=&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;wrongmd5string&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Send the request data.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Get response data.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BufferedReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InputStreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urlConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readLine&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleHttpPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SEVERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="misc"/><category term="java"/><category term="ssl"/><category term="http"/><category term="post"/></entry><entry><title>Java 使用自签证书访问https站点</title><link href="https://www.chenxiaosheng.com/posts/2013-12-26/java-use-self_signed_certificate.html" rel="alternate"/><published>2013-12-26T10:18:00+08:00</published><updated>2013-12-26T10:18:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2013-12-26:/posts/2013-12-26/java-use-self_signed_certificate.html</id><summary type="html">&lt;p&gt;最近被 Java 使用自签证书访问 https 的问题折腾得体无完肤，做为一名 Java 小白，
我也忍不住想感慨一下，不同 Java 程序员的水平差 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;最近被 Java 使用自签证书访问 https 的问题折腾得体无完肤，做为一名 Java 小白，
我也忍不住想感慨一下，不同 Java 程序员的水平差别真的好大，有些根本无法沟通，
都不知道他们怎么胜任日常的编码工作的=,=
同时做为一个 Java 黑，我觉得，好吧，更黑了。。&lt;/p&gt;
&lt;p&gt;anyway，经过各种折腾，至少总结出了以下两种使用自签证书访问 https 站点的办法。&lt;/p&gt;
&lt;h3&gt;方法一&lt;/h3&gt;
&lt;p&gt;拿到相应的 https 站点证书后（好吧，我这里是cnnic.crt，顺便吐槽一下万恶的 cnnic 证书，各种不信任）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;keytool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keystore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jre&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cacerts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&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&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;请替换为你想使用的名称，比如我这里是&lt;span class="w"&gt; &lt;/span&gt;CNNIC
&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;请替换为你自己的&lt;span class="w"&gt; &lt;/span&gt;JAVA_HOME&lt;span class="w"&gt; &lt;/span&gt;目录，比如我这里是&lt;span class="w"&gt; &lt;/span&gt;/usr/lib/jvm/java-6-openjdk
&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;请替换为你的ca证书路径，比如我这里就是当前目录的&lt;span class="w"&gt; &lt;/span&gt;cnnic.crt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;那么，我需要完整执行的命令就是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;keytool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;CNNIC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keystore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jvm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;openjdk&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jre&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cacerts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cnnic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;如果 keytool 要求你输入密码，在你没有变更过的情况下，该值默认为英文 &lt;code&gt;changeit&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;方法二&lt;/h3&gt;
&lt;p&gt;有些同学表示，他们的服务器不允许导入证书，或者他们是一个很大的集群，不适合每台都导入，那怎么办呢？
方法就是在每次访问的时候，程序自己加载相应的证书，但是在加载证书之前，你还是需要使用 keytool 工具将原ca转换成
Java 可以读取的证书格式，命令同方法一，只是 keystore 的位置你可以放在你的程序可以读取的任一路径，如我这里：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;keytool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;CNNIC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keystore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cnnic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cacert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cnnic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;之后在你的 Java 程序里，使用 keystore 加载该证书，并附加该证书创建 https 请求即可。&lt;/p&gt;
&lt;p&gt;从 Java 官方 docs 和 网上拼凑了小样例一枚，请君参照并自行完善：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.File&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.FileInputStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.InputStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.io.InputStreamReader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.security.KeyStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.security.cert.CertificateException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.security.cert.X509Certificate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.net.URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.HttpsURLConnection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.TrustManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.TrustManagerFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.X509TrustManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.SSLContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;javax.net.ssl.SSLSocketFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoadCert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;X509TrustManager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sunJSSEX509TrustManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// 加载 Keytool 生成的证书文件&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;changeit&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;java.cnnic.cacert&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Loading KeyStore &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;InputStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FileInputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;KeyStore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultType&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// 构造 javax.net.ssl.TrustManager 对象&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TrustManagerFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TrustManagerFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SunX509&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SunJSSE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TrustManager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTrustManagers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// 使用构造好的 TrustManager 访问相应的 https 站点&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;SSLContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SSLContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SSL&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SunJSSE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;SSLSocketFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ssf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSocketFactory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;myURL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://replace.to.your.site.real.url/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;HttpsURLConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;httpsConn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpsURLConnection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;myURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;httpsConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSSLSocketFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;InputStreamReader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;insr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InputStreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpsConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInputStream&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;respInt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;insr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;respInt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;respInt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;respInt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;insr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="misc"/><category term="java"/><category term="ssl"/></entry><entry><title>OpenID4Java 使用dumb模式（stateless）并禁用 discovery</title><link href="https://www.chenxiaosheng.com/posts/2013-12-25/openid4java-dumb_mode-and-disable_discovery.html" rel="alternate"/><published>2013-12-25T12:20:00+08:00</published><updated>2013-12-25T12:20:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2013-12-25:/posts/2013-12-25/openid4java-dumb_mode-and-disable_discovery.html</id><summary type="html">&lt;p&gt;使用无状态模式（dumb mode/stateless）发起 OpenID 认证请求：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConsumerManager&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRealmVerifier&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setEnforceRpId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 强制manager使用无状态模式&lt;/span&gt;
&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMaxAssocAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;禁用 discovery&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// 不要使用manager.discover构造discoveries&lt;/span&gt;
&lt;span class="c1"&gt;// List discoveries = manager.discover(userSuppliedString);&lt;/span&gt;
&lt;span class="c1"&gt;// 使用人肉构造 …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;使用无状态模式（dumb mode/stateless）发起 OpenID 认证请求：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConsumerManager&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRealmVerifier&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setEnforceRpId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 强制manager使用无状态模式&lt;/span&gt;
&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMaxAssocAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;禁用 discovery&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// 不要使用manager.discover构造discoveries&lt;/span&gt;
&lt;span class="c1"&gt;// List discoveries = manager.discover(userSuppliedString);&lt;/span&gt;
&lt;span class="c1"&gt;// 使用人肉构造discoveries&lt;/span&gt;
&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;discoveries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ArrayList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;discoveries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DiscoveryInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;http//real.openid.server.url&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="misc"/><category term="openid"/><category term="java"/></entry><entry><title>ssh client 通过 socks5 proxy 登录远程服务器</title><link href="https://www.chenxiaosheng.com/posts/2013-12-20/ssh-through-socks-proxy.html" rel="alternate"/><published>2013-12-20T12:20:00+08:00</published><updated>2013-12-20T12:20:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2013-12-20:/posts/2013-12-20/ssh-through-socks-proxy.html</id><summary type="html">&lt;p&gt;今天某同学需要登录某国家服务器（A），但从我朝过去网络延时非常大
发现从岛国过去的速度相当快，但因为岛国的服务 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;今天某同学需要登录某国家服务器（A），但从我朝过去网络延时非常大
发现从岛国过去的速度相当快，但因为岛国的服务器（B）不适合加该同学的帐号
因此做了一个 socks5 proxy ，然后本地 ssh client 通过该 proxy 登录A服务器&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;ProxyCommand=&amp;#39;nc&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;proxy_server&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;:&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;proxy_server_port&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;%h&lt;span class="w"&gt; &lt;/span&gt;%p&amp;#39;&lt;span class="w"&gt; &lt;/span&gt;xxx.xxx.xxx.xxx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ssh -o ProxyCommand=&amp;#39;nc -x 127.0.0.1:7070 %h %p&amp;#39; 8.8.8.8
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;需要注意的是，nc需要使用 OpenBSD 版本，非 Linux 默认版本（该版本不支持）
通常，类 Debian 的衍生版本，都可以通过如下命令直接安装：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-get install netcat-openbsd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;非OpenBSD 可能报如下错误：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/bin/nc: invalid option -- &amp;#39;x&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="misc"/><category term="ssh"/><category term="proxy"/></entry><entry><title>Mac OSX iTerm2 终端UTF-8和GBK编码自由切换</title><link href="https://www.chenxiaosheng.com/posts/2013-10-29/mac_osx_iterm2_utf8_gbk_switch.html" rel="alternate"/><published>2013-10-29T16:43:00+08:00</published><updated>2013-10-29T16:43:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-10-29:/posts/2013-10-29/mac_osx_iterm2_utf8_gbk_switch.html</id><summary type="html">&lt;p&gt;老树使用的是Mac OSX系统，平时终端都是使用iTerm2替代默认的Terminal进行使用。&lt;/p&gt;
&lt;p&gt;考虑到各种兼容性，个人一直使用的是UTF-8编码，但由于老树管理着大量服务器，并且可 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;老树使用的是Mac OSX系统，平时终端都是使用iTerm2替代默认的Terminal进行使用。&lt;/p&gt;
&lt;p&gt;考虑到各种兼容性，个人一直使用的是UTF-8编码，但由于老树管理着大量服务器，并且可能使用的是GBK或者其它编码，经常由于终端环境编码的不同，导致登录服务器出现乱码，或者需要处理GBK文件时，要使用iconv进行多次编码转换，相当麻烦。&lt;/p&gt;
&lt;p&gt;好在iTerm2使用了比较友好的Profile配置及切换方式，首先我的默认配置（Default Profile）使用了UTF-8编码：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/iterm2_default_profile.png" /&gt;&lt;/p&gt;
&lt;p&gt;我另外建立了一个Profile，叫GBK：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/iterm2_gbk_profile.png" /&gt;&lt;/p&gt;
&lt;p&gt;并编写了一个非常简单的切换脚本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# 使用GBK Profile&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\033]50;SetProfile=GBK\a&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# 环境编码切换为GBK&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zh_CN.GBK
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zh_CN.GBK
&lt;span class="c1"&gt;# 更改当前 iTerm2 tab title&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ne&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\033]0;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\007&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ne&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\033]0;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PWD&lt;/span&gt;&lt;span class="p"&gt;/#&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/~&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\007&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# GBK任务完成后，自动切换回默认编码（UTF-8）&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\033]50;SetProfile=Default\a&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zh_CN.UTF-8
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zh_CN.UTF-8
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;将以上内容保存为一个叫grun的文件，并赋予可执行权限，同时加到系统可执行目录（PATH）&lt;/p&gt;
&lt;p&gt;当我需要登录一台GBK编码的服务器时，只需要&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;grun&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;${&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="cp"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;即可&lt;/p&gt;</content><category term="misc"/><category term="mac"/><category term="osx"/><category term="iterm2"/></entry><entry><title>sshd无法使用secureFX传输文件</title><link href="https://www.chenxiaosheng.com/posts/2013-09-24/sshd-sftp-securefx.html" rel="alternate"/><published>2013-09-24T15:58:00+08:00</published><updated>2013-09-24T15:58:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2013-09-24:/posts/2013-09-24/sshd-sftp-securefx.html</id><summary type="html">&lt;p&gt;某同学表示，某些机器无法使用&lt;code&gt;secureFX&lt;/code&gt;进行文件传输。&lt;/p&gt;
&lt;p&gt;正常情况下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;RECV&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AUTH_SUCCESS&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;RECV&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt; &lt;span class="n"&gt;Sftp&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;SEND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RealPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;Resolved&lt;/span&gt; &lt;span class="n"&gt;RealPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;SEND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenDir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;而无 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;某同学表示，某些机器无法使用&lt;code&gt;secureFX&lt;/code&gt;进行文件传输。&lt;/p&gt;
&lt;p&gt;正常情况下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;RECV&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AUTH_SUCCESS&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;RECV&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt; &lt;span class="n"&gt;Sftp&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;SEND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RealPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;Resolved&lt;/span&gt; &lt;span class="n"&gt;RealPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;
&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;SEND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenDir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;demo&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&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;i RECV : AUTH_SUCCESS
i Changing state from STATE_CONNECTION to   STATE_TRANSPORT_STOPPING.
i Changing state from STATE_TRANSPORT_STOPPING to STATE_CLOSING.
i Changing state from STATE_CLOSING to STATE_CLOSED.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;查看&lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;配置，发现缺少&lt;code&gt;sftp&lt;/code&gt;的相关配置，在该配置文件中增加：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Subsystem   sftp    /usr/libexec/openssh/sftp-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;然后重启sshd服务，问题解决。&lt;/p&gt;</content><category term="misc"/><category term="sftp"/><category term="secureFX"/><category term="sshd"/></entry><entry><title>Django Admin 使用 filter_horizontal 不生效</title><link href="https://www.chenxiaosheng.com/posts/2013-09-23/django-filter_horizontal.html" rel="alternate"/><published>2013-09-23T15:25:00+08:00</published><updated>2013-09-23T15:25:00+08:00</updated><author><name>陈小生</name></author><id>tag:www.chenxiaosheng.com,2013-09-23:/posts/2013-09-23/django-filter_horizontal.html</id><summary type="html">&lt;p&gt;Django 1.2.7 admin在使用ManyToManyField的时候，默认使用垂直&lt;code&gt;filter_vertical&lt;/code&gt;方式进行显示与选择。此方式在选项比较多的时候，难以直观的看出哪些选项被选中，在尝试使用&lt;code&gt;filter_horizontal&lt;/code&gt;进行显示的时候，我们碰到了一些问题。&lt;/p&gt;</summary><content type="html">&lt;p&gt;考虑到Django 1.0.2 admin的功能实在太弱了，今天冒着大风险，将某系统的Django进行了升级（升级至Django 1.2.7）。&lt;/p&gt;
&lt;p&gt;Django 1.2.7 admin在使用&lt;code&gt;ManyToManyField&lt;/code&gt;的时候，默认使用垂直&lt;code&gt;filter_vertical&lt;/code&gt;方式进行显示与选择。此方式在选项比较多的时候，难以直观的看出哪些选项被选中，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/django_filter_vertical.png"&gt;&lt;/p&gt;
&lt;p&gt;决定使用更直观的方式，&lt;code&gt;filter_horizontal&lt;/code&gt;，设置比较简单，只要在&lt;code&gt;admin.py&lt;/code&gt;里对应的模块下，添加类似如下内容即可：
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;filter_horizontal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;example&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;filter_horizontal&lt;/code&gt;显示方式如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/django_filter_horizontal.png"&gt;&lt;/p&gt;
&lt;p&gt;但是，当我设置了此选择后，发现出现了奇怪的问题，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src="/static/django_filter_nothing.png"&gt;&lt;/p&gt;
&lt;p&gt;没有报错，但也没有出现设想中的选择框。经过研究，发现，原来是因为我对应的字段&lt;code&gt;ips&lt;/code&gt;，在&lt;code&gt;models.py&lt;/code&gt;里定义的时候，&lt;code&gt;verbose_name&lt;/code&gt;使用了字符串，而不是&lt;code&gt;unicode&lt;/code&gt;，如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ManyToManyField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;IPData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;使用IP&amp;quot;&lt;/span&gt; &lt;span class="p"&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&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ManyToManyField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;IPData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;使用IP&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;问题解决。使用&lt;code&gt;string&lt;/code&gt;是因为我们这些是一些老旧的系统，新项目必须果断的抛弃&lt;code&gt;string&lt;/code&gt;，拥抱&lt;code&gt;unicode&lt;/code&gt;吧！&lt;/p&gt;</content><category term="misc"/><category term="django"/><category term="python"/></entry><entry><title>pelican增加自定义jinja template filters</title><link href="https://www.chenxiaosheng.com/posts/2013-09-11/pelican-custom-jinja-filters.html" rel="alternate"/><published>2013-09-11T19:46:00+08:00</published><updated>2013-09-11T19:46:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-09-11:/posts/2013-09-11/pelican-custom-jinja-filters.html</id><summary type="html">&lt;p&gt;pelican官方文档没有显式的指明应该如何自定义jinja template filters.&lt;/p&gt;
&lt;p&gt;在制作标签云的时候，不想写复杂的javascript，更不想又import一份javascript进来，所以决定自己搞一个比较简单的标签云&lt;/p&gt;
&lt;p&gt;pelican默认的tag排序比较简单，为了能有比较“云”的 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;pelican官方文档没有显式的指明应该如何自定义jinja template filters.&lt;/p&gt;
&lt;p&gt;在制作标签云的时候，不想写复杂的javascript，更不想又import一份javascript进来，所以决定自己搞一个比较简单的标签云&lt;/p&gt;
&lt;p&gt;pelican默认的tag排序比较简单，为了能有比较“云”的感觉，决定对tag加个随机排序&lt;/p&gt;
&lt;p&gt;查了Jinja的文档，没有随机排序的filter。由于我使用了&lt;code&gt;virtualenv&lt;/code&gt;，所以最简单粗暴的解决方案是直接修改了Jinja的源码，添加了随机排序的功能&lt;/p&gt;
&lt;p&gt;不过对于稍微有些“洁癖”的好，这样改后，心理上非常难受，决定到pelican社区，看看是否有幸添加个patch&lt;/p&gt;
&lt;p&gt;当然，更幸运的是在&lt;code&gt;github&lt;/code&gt;找到了此commit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;https://github.com/getpelican/pelican/pull/96
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;原来有个&lt;code&gt;JINJA_FILTERS&lt;/code&gt;的设置，那一切就好办了, 增加自定义模块&lt;code&gt;jinja_filters.py&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot; Jinja template filter for shuffling list/tuple &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在&lt;code&gt;pelican&lt;/code&gt;配置文件里填加:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;jinja_filters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;

&lt;span class="n"&gt;JINJA_FILTERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;shuffle&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;尝试重新编译生成html文件，顺利完成。&lt;/p&gt;</content><category term="misc"/><category term="python"/><category term="pelican"/><category term="jinja"/></entry><entry><title>配置Bind使用MySQL dlz模式</title><link href="https://www.chenxiaosheng.com/posts/2013-09-09/install-bind-mysql-dlz.html" rel="alternate"/><published>2013-09-09T16:18:00+08:00</published><updated>2013-09-09T16:18:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-09-09:/posts/2013-09-09/install-bind-mysql-dlz.html</id><summary type="html">&lt;p&gt;** 安装MySQL/Bind with dlz **&lt;/p&gt;
&lt;p&gt;略过，bind dlz安装，只需在编译的时候增加 &lt;code&gt;--with-dlz-mysql&lt;/code&gt; 选项即可。&lt;/p&gt;
&lt;p&gt;** 创建 MySQL 数据库 **&lt;/p&gt;
&lt;p&gt;根据自己的需求创建即可，如使用如下命令创 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;** 安装MySQL/Bind with dlz **&lt;/p&gt;
&lt;p&gt;略过，bind dlz安装，只需在编译的时候增加 &lt;code&gt;--with-dlz-mysql&lt;/code&gt; 选项即可。&lt;/p&gt;
&lt;p&gt;** 创建 MySQL 数据库 **&lt;/p&gt;
&lt;p&gt;根据自己的需求创建即可，如使用如下命令创建一个名为 &lt;code&gt;dns&lt;/code&gt; 的数据库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;CREATE DATABASE dns DEFAULT CHARSET UTF8;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;使用 &lt;code&gt;use dns&lt;/code&gt; 切换进 &lt;code&gt;dns&lt;/code&gt; 数据库后，使用如下命令创建一张 &lt;code&gt;dns_records&lt;/code&gt; 表：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n n-Quoted"&gt;`dns_records`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n n-Quoted"&gt;`zone`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n n-Quoted"&gt;`host`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n n-Quoted"&gt;`type`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n n-Quoted"&gt;`data`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n n-Quoted"&gt;`ttl`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;600&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n n-Quoted"&gt;`id`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n n-Quoted"&gt;`id`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CHARSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;这里以域 &lt;code&gt;example.com&lt;/code&gt; 为例，说明如上字段意义，假设我需要有一条 &lt;code&gt;www.example.com&lt;/code&gt; 的记录指向 &lt;code&gt;1.1.1.1&lt;/code&gt; 这个IP，应该增加哪些内容：&lt;/p&gt;
&lt;p&gt;zone:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;域，这里指 example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;主机名（名字），这里为 www
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;type:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;记录类型，这里为A
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;域名资源数据，这里为IP地址 1.1.1.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;ttl:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;域名TTL，为任意整数（建议不要低于120，即2分钟）
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;将该数据库授权给某用户（后续bind-dlz配置需要用到）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;grant&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dns&lt;/span&gt;&lt;span class="nv"&gt;@localhost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;identified&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dns&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;用户名为 &lt;code&gt;dns&lt;/code&gt; ，密码也为 &lt;code&gt;dns&lt;/code&gt; ，只允许本地主机 &lt;code&gt;localhost&lt;/code&gt; 访问，并只给予了 &lt;code&gt;select&lt;/code&gt; 权限。&lt;/p&gt;
&lt;p&gt;** Bind-dlz配置 **&lt;/p&gt;
&lt;p&gt;这里只涉及dlz的配置，其它named.conf的配置不涉及&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;dlz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;your custom description&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mysql&lt;/span&gt;
&lt;span class="s"&gt;        {host=localhost dbname=dns user=dns pass=dns}&lt;/span&gt;
&lt;span class="s"&gt;        {select zone from dns_records where zone = &amp;#39;$zone$&amp;#39;}&lt;/span&gt;
&lt;span class="s"&gt;        {select ttl, type, data from dns_records where host = &amp;#39;$record$&amp;#39; and zone=&amp;#39;$zone$&amp;#39;}&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;your custom description:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;管理员自行添加的描述
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;配置完成。&lt;/p&gt;</content><category term="misc"/><category term="bind"/></entry><entry><title>Django multiple select option with title</title><link href="https://www.chenxiaosheng.com/posts/2013-09-09/django-multiple-select-option-with-title.html" rel="alternate"/><published>2013-09-09T15:02:00+08:00</published><updated>2013-09-09T15:02:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-09-09:/posts/2013-09-09/django-multiple-select-option-with-title.html</id><summary type="html">&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.utils.encoding&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;force_unicode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.utils.html&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conditional_escape&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SelectMultipleWithTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectMultiple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot; multiple select optihon with title &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected_choices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;option_label&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;option_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;selected_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;selected_choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; selected=&amp;quot;selected&amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;option value=&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.utils.encoding&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;force_unicode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.utils.html&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conditional_escape&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SelectMultipleWithTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SelectMultiple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot; multiple select optihon with title &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected_choices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;option_label&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;option_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;selected_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;selected_choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; selected=&amp;quot;selected&amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;option value=&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt; title=&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/option&amp;gt;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;selected_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;conditional_escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;force_unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_label&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;conditional_escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;force_unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_label&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="misc"/><category term="python"/><category term="django"/></entry><entry><title>install Facebook scribe on Debian Squeeze</title><link href="https://www.chenxiaosheng.com/posts/2013-09-08/install-facebook-scribe-on-debian-squeeze.html" rel="alternate"/><published>2013-09-08T13:03:00+08:00</published><updated>2013-09-08T13:03:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-09-08:/posts/2013-09-08/install-facebook-scribe-on-debian-squeeze.html</id><summary type="html">&lt;p&gt;系统: &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Debian Squeeze 6.0 amd64&lt;/p&gt;
&lt;p&gt;thrift: 0.8.0&lt;/p&gt;
&lt;p&gt;hadoop: 0.20.2 cdh3 hadoop-0.20_0.20.2+923.142&lt;/p&gt;
&lt;p&gt;scribe: git current version&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先安装各种库，包括但不局限于:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;python-dev maven2 ant sun-java6-jre sun-java6-jdk bison flex gcc make autoconf …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;系统: &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Debian Squeeze 6.0 amd64&lt;/p&gt;
&lt;p&gt;thrift: 0.8.0&lt;/p&gt;
&lt;p&gt;hadoop: 0.20.2 cdh3 hadoop-0.20_0.20.2+923.142&lt;/p&gt;
&lt;p&gt;scribe: git current version&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先安装各种库，包括但不局限于:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;python-dev maven2 ant sun-java6-jre sun-java6-jdk bison flex gcc make autoconf libevent-dev libboost-all-dev git-core&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sudo update-alternatives --set java /usr/lib/jvm/java-6-sun/jre/bin/java #确保使用的不是openjdk&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装thrift:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tar -zxvf thrift-0.8.0.tar.gz&lt;/p&gt;
&lt;p&gt;cd thrift-0.8.0&lt;/p&gt;
&lt;p&gt;./configure --prefix=/usr/local/thrift-0.8.0&lt;/p&gt;
&lt;p&gt;make&lt;/p&gt;
&lt;p&gt;sudo make install&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装fb303:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;cd thrift-0.8.0/contrib/fb303&lt;/p&gt;
&lt;p&gt;sh boostrap.sh&lt;/p&gt;
&lt;p&gt;./configure --prefix=/usr/local/fb303-0.8.0 --with-thriftpath=/usr/local/thrift-0.8.0 CPPFLAGS="-DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H"&lt;/p&gt;
&lt;p&gt;make&lt;/p&gt;
&lt;p&gt;sudo make install&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装hadoop:&lt;/p&gt;
&lt;p&gt;使用apache官方下载的版本编译各种不成功，其中一个错误（编译scribe时出现）:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HdfsFile.cpp:255: error: ‘hdfsConnectNewInstance’ was not declared in this scope&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ConnectNewInstance是在hadoop 0.21之后的版本才出现的，由于我这边hadoop的服务器是0.20.2，所以只能使用cloudarea版本&lt;/p&gt;
&lt;p&gt;由于我在在编译时，cloudarea页面无法下载对应版本hadoop(或者跳转到apache页面)&lt;/p&gt;
&lt;p&gt;只好使用如下方式进行下载，首先，修改/etc/apt/source.list，增加&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;deb http://archive.cloudera.com/debian squeeze-cdh3u2 contrib&lt;/p&gt;
&lt;p&gt;deb-src http://archive.cloudera.com/debian squeeze-cdh3u2 contrib&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后使用apt-get source hadoop-0.20进行下载&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tar -zxvf hadoop-0.20.2.tar.gz&lt;/p&gt;
&lt;p&gt;mv hadoop-0.20.2 /usr/local&lt;/p&gt;
&lt;p&gt;cd /usr/local/hadoop-0.20.2&lt;/p&gt;
&lt;p&gt;sudo ant compile-c++-libhdfs -Dislibhdfs=true #由于cdh3/apache提供的hadoop包内的libhdfs库不一定能使用，重新编译c++的libhdfs库&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;安装scribe:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;git clone https://github.com/facebook/scribe.git&lt;/p&gt;
&lt;p&gt;cd scribe&lt;/p&gt;
&lt;p&gt;sh bootstrap.sh&lt;/p&gt;
&lt;p&gt;./configure --prefix=/usr/local/scribe-2.2 --with-fb303path=/usr/local/fb303-0.8.0 --with-thriftpath=/usr/local/thrift-0.8.0 --with-hadooppath=/usr/local/hadoop-0.20.2 --enable-hdfs CPPFLAGS="-DHAVE_NETDB_H=1 -fpermissive -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -I/usr/local/hadoop-0.20.2/src/c++/libhdfs -I/usr/lib/jvm/java-6-sun-1.6.0.26/include -I/usr/lib/jvm/java-6-sun-1.6.0.26/include/linux" LDFLAGS="-L/usr/lib/jvm/java-6-sun-1.6.0.26/jre/lib/amd64 -L/usr/lib/jvm/java-6-sun-1.6.0.26/jre/lib/amd64/server -L/usr/local/hadoop-0.20.2/build/c++/Linux-amd64-64/lib"&lt;/p&gt;
&lt;p&gt;make&lt;/p&gt;
&lt;p&gt;sudo make install&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其它配置:&lt;/p&gt;
&lt;p&gt;由于我这里各软件包都是自定义安装路径，需要配置一下ld加载路径，编辑(增加) /etc/ld.so.conf.d/scribe.conf&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;/usr/local/thrift-0.8.0/lib/&lt;/p&gt;
&lt;p&gt;/usr/local/hadoop-0.20.2/build/c++/Linux-amd64-64/lib/&lt;/p&gt;
&lt;p&gt;/usr/lib/jvm/java-6-sun-1.6.0.26/jre/lib/amd64/server/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;保存退出后&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sudo ldconfig&lt;/p&gt;
&lt;/blockquote&gt;</content><category term="misc"/><category term="debian"/><category term="scribe"/></entry><entry><title>Windows OpenVPN GUI记住用户名和密码</title><link href="https://www.chenxiaosheng.com/posts/2013-09-08/windows-openvpn-remember-password.html" rel="alternate"/><published>2013-09-08T12:51:00+08:00</published><updated>2013-09-08T12:51:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-09-08:/posts/2013-09-08/windows-openvpn-remember-password.html</id><summary type="html">&lt;p&gt;日常工作中经常使用VPN，由于Server端要求进行用户名和密码校验，在第一次链接的时候，OpenVPN Client会弹出输入用户名和密码的窗口。 如果网络比较稳定的情况下，这个还没什么问题，网络不稳定的时候，每次弹开，都会弹出重新验证的窗口，加上我自己的密码比较长，不胜其烦&lt;/p&gt;</summary><content type="html">&lt;p&gt;日常工作中经常使用VPN，由于Server端要求进行用户名和密码校验，在第一次链接的时候，OpenVPN Client会弹出输入用户名和密码的窗口。&lt;/p&gt;
&lt;p&gt;如果网络比较稳定的情况下，这个还没什么问题，网络不稳定的时候，每次弹开，都会弹出重新验证的窗口，加上我自己的密码比较长，不胜其烦&lt;/p&gt;
&lt;p&gt;我使用的版本（2.3.1），实际上已经支持保存密码的功能（旧版本可能需要重新编译），在ovpn配置文件里增加:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;auth-user-pass pass.txt&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后在ovpn配置文件所在目录建立一个pass.txt的文件，第一行为用户名，第二行为密码即可。&lt;/p&gt;
&lt;p&gt;PS：这样对自己电脑的安全性要有足够的保证，否则密码泄露就得不偿失了。&lt;/p&gt;</content><category term="misc"/><category term="windows"/><category term="openvpn"/></entry><entry><title>Debian系统添加全局根证书（CNNIC）</title><link href="https://www.chenxiaosheng.com/posts/2013-09-08/debian-add-cnnic-ca.html" rel="alternate"/><published>2013-09-08T12:25:00+08:00</published><updated>2013-09-08T12:25:00+08:00</updated><author><name>老树</name></author><id>tag:www.chenxiaosheng.com,2013-09-08:/posts/2013-09-08/debian-add-cnnic-ca.html</id><summary type="html">&lt;p&gt;鉴于CNNIC的证书默认被不信任，导致了应用在访问某些使用cnnic证书的ssl站点时，请求失败。本篇文章介绍了如何将CNNIC CA添加至Debian操作系统全局根证书。&lt;/p&gt;</summary><content type="html">&lt;p&gt;鉴于CNNIC的证书默认被不信任，导致了应用在访问某些使用cnnic证书的ssl站点时，请求失败。&lt;/p&gt;
&lt;p&gt;虽然我也不喜欢cnnic，但没办法，这是反抗不了的事情。&lt;/p&gt;
&lt;p&gt;过程如下（需要root权限）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;apt-get install ca-certificates #一般情况下应该是已经有安装的了&lt;/p&gt;
&lt;p&gt;mkdir -p /usr/share/ca-certificates/local #建立local目录是为了方便区分这是自己填加的证书，可以自定义，但一定要在/usr/share/ca-certificates目录下&lt;/p&gt;
&lt;p&gt;将cnnic的证书复制至/usr/share/ca-certificates/local目录，请注意，证书的后缀必须修改为.crt&lt;/p&gt;
&lt;p&gt;dpkg-reconfigure ca-certificates #选中刚才添加的local/cnnic.crt证中，确定，即可&lt;/p&gt;
&lt;/blockquote&gt;</content><category term="misc"/><category term="debian"/></entry></feed>